@fragments-sdk/mcp 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -17
- package/dist/bin.js +5 -20
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-VV2PJ75X.js → chunk-WDQPNHZ2.js} +37 -6
- package/dist/chunk-WDQPNHZ2.js.map +1 -0
- package/dist/{chunk-YSNIGHNU.js → chunk-YJTMK4JY.js} +1129 -2519
- package/dist/chunk-YJTMK4JY.js.map +1 -0
- package/dist/{dist-BDWAHJ4K.js → dist-TTCI6TME.js} +726 -22
- package/dist/dist-TTCI6TME.js.map +1 -0
- package/dist/index.js +74 -11
- 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 -2
- package/package.json +5 -4
- 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/dist-BDWAHJ4K.js.map +0 -1
- package/dist/rules-TN4KHFFG.js +0 -9
- /package/dist/{rules-TN4KHFFG.js.map → rules-JUZ3RABB.js.map} +0 -0
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BRAND
|
|
3
|
-
DEFAULTS
|
|
2
|
+
BRAND
|
|
4
3
|
} 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
4
|
|
|
15
5
|
// src/server.ts
|
|
16
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
8
|
import {
|
|
19
9
|
CallToolRequestSchema,
|
|
20
|
-
|
|
10
|
+
ErrorCode,
|
|
11
|
+
ListResourcesRequestSchema,
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
McpError,
|
|
14
|
+
ReadResourceRequestSchema
|
|
21
15
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
-
import { existsSync as
|
|
16
|
+
import { existsSync as existsSync8 } from "fs";
|
|
23
17
|
import { readFileSync as readFileSync6 } from "fs";
|
|
24
|
-
import { join as
|
|
18
|
+
import { join as join7 } from "path";
|
|
25
19
|
import { fileURLToPath } from "url";
|
|
26
20
|
|
|
27
21
|
// src/config.ts
|
|
@@ -30,2362 +24,289 @@ import { join } from "path";
|
|
|
30
24
|
function loadConfigFile(projectRoot) {
|
|
31
25
|
const configPath = join(projectRoot, "ds-mcp.config.json");
|
|
32
26
|
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.`);
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync(configPath, "utf-8");
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1865
32
|
}
|
|
1866
33
|
}
|
|
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) {
|
|
34
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
35
|
+
if (existsSync(pkgPath)) {
|
|
1878
36
|
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
|
-
};
|
|
37
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
38
|
+
const pkg = JSON.parse(content);
|
|
39
|
+
if (pkg.dsMcp) return pkg.dsMcp;
|
|
40
|
+
} catch {
|
|
1929
41
|
}
|
|
1930
42
|
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/server.ts
|
|
47
|
+
import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
|
|
48
|
+
|
|
49
|
+
// src/version.ts
|
|
50
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
51
|
+
function readPackageVersion() {
|
|
1931
52
|
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
|
-
};
|
|
53
|
+
const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
|
|
54
|
+
const pkg = JSON.parse(raw);
|
|
55
|
+
return pkg.version ?? "0.0.0";
|
|
56
|
+
} catch {
|
|
57
|
+
return "0.0.0";
|
|
1965
58
|
}
|
|
1966
|
-
}
|
|
59
|
+
}
|
|
60
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
1967
61
|
|
|
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
|
-
};
|
|
62
|
+
// src/catalog-meta.ts
|
|
63
|
+
function getCatalogMeta(data) {
|
|
64
|
+
const rawUpdatedAt = data.validateFixContext?.updatedAt ?? data.snapshot.metadata.updatedAt;
|
|
65
|
+
const updatedAt = typeof rawUpdatedAt === "number" ? new Date(rawUpdatedAt).toISOString() : rawUpdatedAt;
|
|
66
|
+
return {
|
|
67
|
+
catalogRevision: data.validateFixContext?.catalogRevision ?? data.snapshot.metadata.revision,
|
|
68
|
+
updatedAt
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/token-suggestions.ts
|
|
73
|
+
function propertyFamilyFor(property, value) {
|
|
74
|
+
const prop = property.toLowerCase().trim();
|
|
75
|
+
const normalizedValue = value?.toLowerCase().trim();
|
|
76
|
+
if (prop.includes("shadow")) return "shadow";
|
|
77
|
+
if (prop.includes("z-index")) return "z-index";
|
|
78
|
+
if (prop.includes("transition") || prop.includes("duration") || prop.includes("animation")) {
|
|
79
|
+
return "duration";
|
|
80
|
+
}
|
|
81
|
+
if (prop.includes("font") || prop.includes("line-height") || prop === "letter-spacing") {
|
|
82
|
+
return "typography";
|
|
1993
83
|
}
|
|
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
|
-
};
|
|
84
|
+
if (prop.includes("radius")) return "radius";
|
|
85
|
+
if (prop.endsWith("border-width") || prop === "border-width" || prop === "outline-width" || prop === "stroke-width") {
|
|
86
|
+
return "border-width";
|
|
2031
87
|
}
|
|
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
|
-
}
|
|
88
|
+
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)) {
|
|
89
|
+
return "color";
|
|
2052
90
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
return {
|
|
2056
|
-
content: [{
|
|
2057
|
-
type: "text",
|
|
2058
|
-
text: NO_VIEWER_MSG3
|
|
2059
|
-
}],
|
|
2060
|
-
isError: true
|
|
2061
|
-
};
|
|
91
|
+
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")) {
|
|
92
|
+
return "spacing";
|
|
2062
93
|
}
|
|
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
|
-
}
|
|
94
|
+
return "other";
|
|
95
|
+
}
|
|
96
|
+
function suggestToken(input) {
|
|
97
|
+
const family = propertyFamilyFor(input.property, input.value);
|
|
98
|
+
const limit = Math.min(Math.max(input.limit ?? 5, 1), 10);
|
|
99
|
+
const candidates = input.tokens ? scoreCandidates(input.tokens, family, input) : [];
|
|
100
|
+
const top = candidates.slice(0, limit).map(
|
|
101
|
+
(candidate) => presentCandidate(candidate, input.tokens)
|
|
102
|
+
);
|
|
103
|
+
const meta = {
|
|
104
|
+
propertyFamily: family,
|
|
105
|
+
catalogRevision: input.catalogRevision,
|
|
106
|
+
updatedAt: input.updatedAt,
|
|
107
|
+
candidateCount: candidates.length
|
|
108
|
+
};
|
|
109
|
+
if (family === "other") {
|
|
2087
110
|
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
|
-
}]
|
|
111
|
+
alternatives: [],
|
|
112
|
+
noSuggestion: true,
|
|
113
|
+
noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
|
|
114
|
+
_meta: meta
|
|
2103
115
|
};
|
|
2104
|
-
}
|
|
116
|
+
}
|
|
117
|
+
if (!input.tokens || input.tokens.total === 0) {
|
|
2105
118
|
return {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
isError: true
|
|
119
|
+
alternatives: [],
|
|
120
|
+
noSuggestion: true,
|
|
121
|
+
noSuggestionReason: "No design tokens are available in the active catalog.",
|
|
122
|
+
_meta: meta
|
|
2111
123
|
};
|
|
2112
124
|
}
|
|
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) {
|
|
125
|
+
if (top.length === 0) {
|
|
2122
126
|
return {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
isError: true
|
|
127
|
+
alternatives: [],
|
|
128
|
+
noSuggestion: true,
|
|
129
|
+
noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
|
|
130
|
+
_meta: meta
|
|
2128
131
|
};
|
|
2129
132
|
}
|
|
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 };
|
|
133
|
+
const [recommended, ...alternatives] = top;
|
|
134
|
+
return {
|
|
135
|
+
recommended,
|
|
136
|
+
alternatives,
|
|
137
|
+
_meta: meta
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function scoreCandidates(tokenData, family, input) {
|
|
141
|
+
if (family === "other") return [];
|
|
142
|
+
const value = normalizeComparableValue(input.value);
|
|
143
|
+
const flatTokens = tokenData.flat.length > 0 ? tokenData.flat : Object.values(tokenData.categories).flat();
|
|
144
|
+
const familyTokens = flatTokens.filter((token) => !isGarbageToken(token)).map((token) => ({ token, family: tokenFamily(token) })).filter((entry) => entry.family === family);
|
|
145
|
+
const scored = familyTokens.map(({ token }) => {
|
|
146
|
+
const tokenValue = normalizeComparableValue(token.value);
|
|
147
|
+
let score = 50;
|
|
148
|
+
let reason = "family-match";
|
|
149
|
+
if (value && tokenValue && value === tokenValue) {
|
|
150
|
+
score += 60;
|
|
151
|
+
reason = "exact-value-match";
|
|
152
|
+
} else if (value && tokenValue && family !== "color") {
|
|
153
|
+
const distanceScore = lengthDistanceScore(value, tokenValue);
|
|
154
|
+
if (distanceScore > 0) {
|
|
155
|
+
score += distanceScore;
|
|
156
|
+
reason = "nearest-neighbor";
|
|
2263
157
|
}
|
|
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
158
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
}),
|
|
2293
|
-
isError: true
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
159
|
+
score += nameRelevanceScore(token, input.property, input.context);
|
|
160
|
+
return { token, family, score, reason };
|
|
161
|
+
});
|
|
162
|
+
return scored.filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.token.name.localeCompare(b.token.name));
|
|
2296
163
|
}
|
|
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
|
-
}
|
|
164
|
+
function presentCandidate(candidate, tokenData) {
|
|
165
|
+
const cssVar = cssVarForToken(candidate.token);
|
|
166
|
+
const resolvedValue = resolvedValueForToken(candidate.token);
|
|
167
|
+
const confidence = candidate.score >= 105 ? "high" : candidate.score >= 65 ? "medium" : "low";
|
|
2321
168
|
return {
|
|
2322
|
-
|
|
169
|
+
name: dottedNameForToken(candidate.token, tokenData),
|
|
170
|
+
...cssVar && { cssVar },
|
|
171
|
+
...cssVar && {
|
|
172
|
+
cssValue: resolvedValue ? `var(${cssVar}, ${resolvedValue})` : `var(${cssVar})`
|
|
173
|
+
},
|
|
174
|
+
...resolvedValue && { resolvedValue },
|
|
175
|
+
confidence,
|
|
176
|
+
reason: candidate.reason
|
|
2323
177
|
};
|
|
2324
|
-
}
|
|
178
|
+
}
|
|
179
|
+
function isGarbageToken(token) {
|
|
180
|
+
const value = token.value?.trim();
|
|
181
|
+
if (!value) return false;
|
|
182
|
+
if (value.includes("#{") || value.includes("$")) return true;
|
|
183
|
+
if (/^\$[\w-]+/.test(token.name)) return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function tokenFamily(token) {
|
|
187
|
+
const haystack = [
|
|
188
|
+
token.type,
|
|
189
|
+
token.category,
|
|
190
|
+
...token.path ?? [],
|
|
191
|
+
token.name
|
|
192
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
193
|
+
if (/\b(color|colour|background|foreground|surface|palette)\b/.test(haystack)) {
|
|
194
|
+
return "color";
|
|
195
|
+
}
|
|
196
|
+
if (/\b(radius|radii|rounded|corner)\b/.test(haystack)) return "radius";
|
|
197
|
+
if (/\b(border-width|border width|stroke-width|stroke width)\b/.test(haystack)) {
|
|
198
|
+
return "border-width";
|
|
199
|
+
}
|
|
200
|
+
if (/\b(shadow|elevation)\b/.test(haystack)) return "shadow";
|
|
201
|
+
if (/\b(font|type|typography|line-height|letter-spacing)\b/.test(haystack)) {
|
|
202
|
+
return "typography";
|
|
203
|
+
}
|
|
204
|
+
if (/\b(duration|transition|animation)\b/.test(haystack)) return "duration";
|
|
205
|
+
if (/\b(z-index|zindex)\b/.test(haystack)) return "z-index";
|
|
206
|
+
if (/\b(space|spacing|size|sizing|width|height|gap|padding|margin|inset)\b/.test(haystack)) {
|
|
207
|
+
return "spacing";
|
|
208
|
+
}
|
|
209
|
+
if (/\bborder\b/.test(haystack)) return "border-width";
|
|
210
|
+
return "other";
|
|
211
|
+
}
|
|
212
|
+
function cssVarForToken(token) {
|
|
213
|
+
if (token.name.startsWith("--")) return token.name;
|
|
214
|
+
const match = token.value?.match(/var\((--[\w-]+)/);
|
|
215
|
+
return match?.[1];
|
|
216
|
+
}
|
|
217
|
+
function dottedNameForToken(token, tokenData) {
|
|
218
|
+
if (!token.name.startsWith("--")) return token.name;
|
|
219
|
+
let name = token.name.slice(2);
|
|
220
|
+
const prefix = tokenData?.prefix?.replace(/^--/, "").replace(/-$/, "");
|
|
221
|
+
if (prefix && name.startsWith(`${prefix}-`)) {
|
|
222
|
+
name = name.slice(prefix.length + 1);
|
|
223
|
+
}
|
|
224
|
+
return name.replace(/-/g, ".");
|
|
225
|
+
}
|
|
226
|
+
function resolvedValueForToken(token) {
|
|
227
|
+
const value = token.value?.trim();
|
|
228
|
+
if (!value) return void 0;
|
|
229
|
+
const fallback = value.match(/var\(--[\w-]+,\s*([^)]+)\)/)?.[1]?.trim();
|
|
230
|
+
if (fallback) return fallback;
|
|
231
|
+
if (value.startsWith("var(")) return void 0;
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
function normalizeComparableValue(value) {
|
|
235
|
+
if (!value) return void 0;
|
|
236
|
+
const trimmed = value.trim().toLowerCase();
|
|
237
|
+
const color = normalizeColor(trimmed);
|
|
238
|
+
if (color) return color;
|
|
239
|
+
const length = parseLength(trimmed);
|
|
240
|
+
if (length) return `${length.value}${length.unit}`;
|
|
241
|
+
return trimmed.replace(/\s+/g, " ");
|
|
242
|
+
}
|
|
243
|
+
function looksLikeColor(value) {
|
|
244
|
+
return /^#([\da-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
|
|
245
|
+
}
|
|
246
|
+
function normalizeColor(value) {
|
|
247
|
+
const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i);
|
|
248
|
+
if (!hex) return void 0;
|
|
249
|
+
const body = hex[1].toLowerCase();
|
|
250
|
+
if (body.length === 3) {
|
|
251
|
+
return `#${body[0]}${body[0]}${body[1]}${body[1]}${body[2]}${body[2]}`;
|
|
252
|
+
}
|
|
253
|
+
return `#${body}`;
|
|
254
|
+
}
|
|
255
|
+
function parseLength(value) {
|
|
256
|
+
const match = value.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%)$/);
|
|
257
|
+
if (!match) return void 0;
|
|
258
|
+
return { value: Number(match[1]), unit: match[2] };
|
|
259
|
+
}
|
|
260
|
+
function lengthDistanceScore(inputValue, tokenValue) {
|
|
261
|
+
const input = parseLength(inputValue);
|
|
262
|
+
const token = parseLength(tokenValue);
|
|
263
|
+
if (!input || !token || input.unit !== token.unit) return 0;
|
|
264
|
+
const distance = Math.abs(input.value - token.value);
|
|
265
|
+
if (distance === 0) return 60;
|
|
266
|
+
if (distance <= 2) return 35;
|
|
267
|
+
if (distance <= 4) return 20;
|
|
268
|
+
if (distance <= 8) return 10;
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
function nameRelevanceScore(token, property, context) {
|
|
272
|
+
const haystack = [token.name, token.category, ...token.path ?? []].join(" ").toLowerCase();
|
|
273
|
+
const prop = property.toLowerCase();
|
|
274
|
+
let score = 0;
|
|
275
|
+
for (const part of prop.split(/[^a-z0-9]+/).filter((part2) => part2.length > 2)) {
|
|
276
|
+
if (haystack.includes(part)) score += 4;
|
|
277
|
+
}
|
|
278
|
+
if (context === "component" && haystack.includes("component")) score += 3;
|
|
279
|
+
if (context === "global" && haystack.includes("global")) score += 3;
|
|
280
|
+
return score;
|
|
281
|
+
}
|
|
2325
282
|
|
|
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) {
|
|
283
|
+
// src/tools/tokens-suggest.ts
|
|
284
|
+
var tokensSuggestHandler = async (args, ctx) => {
|
|
285
|
+
const property = args.property;
|
|
286
|
+
if (!property || typeof property !== "string") {
|
|
2336
287
|
return {
|
|
2337
288
|
content: [
|
|
2338
289
|
{
|
|
2339
290
|
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
|
-
})
|
|
291
|
+
text: JSON.stringify({ error: "property is required." })
|
|
2345
292
|
}
|
|
2346
|
-
]
|
|
293
|
+
],
|
|
294
|
+
isError: true
|
|
2347
295
|
};
|
|
2348
296
|
}
|
|
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
|
-
}
|
|
297
|
+
const context = args.context;
|
|
298
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
299
|
+
const result2 = suggestToken({
|
|
300
|
+
tokens: ctx.data.tokens,
|
|
301
|
+
property,
|
|
302
|
+
value: typeof args.value === "string" ? args.value : void 0,
|
|
303
|
+
context: context === "component" || context === "block" || context === "global" ? context : void 0,
|
|
304
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
305
|
+
updatedAt: catalogMeta.updatedAt
|
|
306
|
+
});
|
|
2378
307
|
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
|
-
]
|
|
308
|
+
content: [{ type: "text", text: JSON.stringify(result2) }],
|
|
309
|
+
_meta: result2._meta
|
|
2389
310
|
};
|
|
2390
311
|
};
|
|
2391
312
|
|
|
@@ -2448,12 +369,25 @@ function result(validator, violations) {
|
|
|
2448
369
|
violations
|
|
2449
370
|
};
|
|
2450
371
|
}
|
|
372
|
+
var SEVERITY_SCORE_CAPS = {
|
|
373
|
+
critical: 25,
|
|
374
|
+
serious: 50,
|
|
375
|
+
moderate: 80,
|
|
376
|
+
minor: 95
|
|
377
|
+
};
|
|
378
|
+
function verdictFor(violations) {
|
|
379
|
+
if (violations.length === 0) return "pass";
|
|
380
|
+
const worst = worstSeverity(violations);
|
|
381
|
+
return worst === "critical" || worst === "serious" ? "fail" : "warn";
|
|
382
|
+
}
|
|
2451
383
|
function computeScore(violations) {
|
|
384
|
+
if (violations.length === 0) return 100;
|
|
2452
385
|
const penalty = violations.reduce(
|
|
2453
386
|
(sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
|
|
2454
387
|
0
|
|
2455
388
|
);
|
|
2456
|
-
|
|
389
|
+
const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
|
|
390
|
+
return Math.min(cap, Math.max(0, 100 - penalty));
|
|
2457
391
|
}
|
|
2458
392
|
function validateComponents(nodes, options) {
|
|
2459
393
|
const rules = options.policy?.rules ?? {};
|
|
@@ -2524,7 +458,76 @@ function validateSafety(nodes) {
|
|
|
2524
458
|
}
|
|
2525
459
|
}
|
|
2526
460
|
}
|
|
2527
|
-
return result("safety", violations);
|
|
461
|
+
return result("safety", violations);
|
|
462
|
+
}
|
|
463
|
+
function editDistance(a, b) {
|
|
464
|
+
const rows = a.length + 1;
|
|
465
|
+
const cols = b.length + 1;
|
|
466
|
+
const distances = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
467
|
+
for (let i = 0; i < rows; i++) distances[i][0] = i;
|
|
468
|
+
for (let j = 0; j < cols; j++) distances[0][j] = j;
|
|
469
|
+
for (let i = 1; i < rows; i++) {
|
|
470
|
+
for (let j = 1; j < cols; j++) {
|
|
471
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
472
|
+
distances[i][j] = Math.min(
|
|
473
|
+
distances[i - 1][j] + 1,
|
|
474
|
+
distances[i][j - 1] + 1,
|
|
475
|
+
distances[i - 1][j - 1] + cost
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return distances[a.length][b.length];
|
|
480
|
+
}
|
|
481
|
+
function normalizePropValue(value) {
|
|
482
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
483
|
+
}
|
|
484
|
+
function closestAllowedValue(rawValue, allowedValues) {
|
|
485
|
+
const normalizedRaw = normalizePropValue(rawValue);
|
|
486
|
+
const ranked = allowedValues.map((value) => {
|
|
487
|
+
const normalizedValue = normalizePropValue(value);
|
|
488
|
+
const distance = editDistance(normalizedRaw, normalizedValue);
|
|
489
|
+
const prefixMatch = normalizedRaw.length >= 4 && (normalizedValue.startsWith(normalizedRaw) || normalizedRaw.startsWith(normalizedValue));
|
|
490
|
+
return { value, distance, prefixMatch };
|
|
491
|
+
}).sort((a, b) => {
|
|
492
|
+
if (a.prefixMatch !== b.prefixMatch) return a.prefixMatch ? -1 : 1;
|
|
493
|
+
return a.distance - b.distance || a.value.localeCompare(b.value);
|
|
494
|
+
});
|
|
495
|
+
const best = ranked[0];
|
|
496
|
+
if (!best) return void 0;
|
|
497
|
+
if (best.prefixMatch || best.distance <= 2) return best.value;
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
function validateProps(nodes, options) {
|
|
501
|
+
const rules = options.policy?.rules ?? {};
|
|
502
|
+
const propRule = rules["props/valid-values"];
|
|
503
|
+
const componentProps = options.componentProps ?? {};
|
|
504
|
+
const violations = [];
|
|
505
|
+
if (!isRuleEnabled(propRule)) {
|
|
506
|
+
return result("props", violations);
|
|
507
|
+
}
|
|
508
|
+
for (const [index, node] of nodes.entries()) {
|
|
509
|
+
const type = nodeType(node);
|
|
510
|
+
const propSchema = componentProps[type] ?? componentProps[parentType(type)];
|
|
511
|
+
if (!propSchema) continue;
|
|
512
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
513
|
+
const allowedValues = propSchema[prop]?.values?.filter(Boolean) ?? [];
|
|
514
|
+
if (allowedValues.length === 0) continue;
|
|
515
|
+
const rawValue = typeof value === "string" ? value : void 0;
|
|
516
|
+
if (!rawValue || allowedValues.includes(rawValue)) continue;
|
|
517
|
+
const closest = closestAllowedValue(rawValue, allowedValues);
|
|
518
|
+
violations.push({
|
|
519
|
+
nodeId: nodeId(node, index),
|
|
520
|
+
nodeType: type,
|
|
521
|
+
rule: "props/invalid-value",
|
|
522
|
+
severity: ruleSeverity(propRule, "moderate"),
|
|
523
|
+
message: `Prop "${prop}" on ${type} has invalid value "${rawValue}"`,
|
|
524
|
+
suggestion: closest ? `Use "${closest}" for prop "${prop}" on ${type}.` : `Use one of: ${allowedValues.join(", ")}`,
|
|
525
|
+
prop,
|
|
526
|
+
rawValue
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return result("props", violations);
|
|
2528
531
|
}
|
|
2529
532
|
function hasHardcodedCssValue(value) {
|
|
2530
533
|
if (value.includes("var(")) return false;
|
|
@@ -2576,14 +579,29 @@ function validateTokens(nodes, options) {
|
|
|
2576
579
|
}
|
|
2577
580
|
return result("tokens", violations);
|
|
2578
581
|
}
|
|
582
|
+
function textFromUnknown(value) {
|
|
583
|
+
if (typeof value === "string") return value.trim();
|
|
584
|
+
if (Array.isArray(value)) return textFromChildren(value);
|
|
585
|
+
if (typeof value === "object" && value !== null) {
|
|
586
|
+
return textFromNode(value);
|
|
587
|
+
}
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
function textFromNode(node) {
|
|
591
|
+
const type = nodeType(node).toLowerCase();
|
|
592
|
+
const props = node.props ?? {};
|
|
593
|
+
const propText = [
|
|
594
|
+
textFromUnknown(props.children),
|
|
595
|
+
type === "text" ? textFromUnknown(props.value) : ""
|
|
596
|
+
].filter(Boolean);
|
|
597
|
+
const childText = textFromChildren(node.children);
|
|
598
|
+
return [...propText, childText].join(" ").trim();
|
|
599
|
+
}
|
|
2579
600
|
function textFromChildren(children) {
|
|
601
|
+
if (typeof children === "string") return children.trim();
|
|
2580
602
|
if (!Array.isArray(children)) return "";
|
|
2581
603
|
return children.map((child) => {
|
|
2582
|
-
|
|
2583
|
-
if (typeof child === "object" && child !== null && !Array.isArray(child)) {
|
|
2584
|
-
return textFromChildren(child.children);
|
|
2585
|
-
}
|
|
2586
|
-
return "";
|
|
604
|
+
return textFromUnknown(child);
|
|
2587
605
|
}).join(" ").trim();
|
|
2588
606
|
}
|
|
2589
607
|
function validateA11y(nodes) {
|
|
@@ -2593,7 +611,7 @@ function validateA11y(nodes) {
|
|
|
2593
611
|
if (!/button/i.test(type)) continue;
|
|
2594
612
|
const props = node.props ?? {};
|
|
2595
613
|
const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
|
|
2596
|
-
const childText =
|
|
614
|
+
const childText = textFromNode(node);
|
|
2597
615
|
if (typeof label === "string" && label.trim().length > 0) continue;
|
|
2598
616
|
if (childText.length > 0) continue;
|
|
2599
617
|
violations.push({
|
|
@@ -2613,6 +631,7 @@ function runSpecGovern(spec, options) {
|
|
|
2613
631
|
const results = [
|
|
2614
632
|
validateSafety(nodes),
|
|
2615
633
|
validateComponents(nodes, options),
|
|
634
|
+
validateProps(nodes, options),
|
|
2616
635
|
validateTokens(nodes, options),
|
|
2617
636
|
validateA11y(nodes)
|
|
2618
637
|
];
|
|
@@ -2621,6 +640,7 @@ function runSpecGovern(spec, options) {
|
|
|
2621
640
|
new Set(nodes.map((node) => nodeType(node)))
|
|
2622
641
|
);
|
|
2623
642
|
return {
|
|
643
|
+
verdict: verdictFor(violations),
|
|
2624
644
|
passed: results.every((entry) => entry.passed),
|
|
2625
645
|
score: computeScore(violations),
|
|
2626
646
|
results,
|
|
@@ -2638,7 +658,7 @@ function formatVerdict(verdict, format = "summary") {
|
|
|
2638
658
|
}
|
|
2639
659
|
const lines = [];
|
|
2640
660
|
const icon = verdict.passed ? "ok" : "fail";
|
|
2641
|
-
lines.push(`${icon} Governance check: score ${verdict.score}/100`);
|
|
661
|
+
lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
|
|
2642
662
|
lines.push("");
|
|
2643
663
|
for (const entry of verdict.results) {
|
|
2644
664
|
const resultIcon = entry.passed ? "ok" : "fail";
|
|
@@ -2657,6 +677,14 @@ function formatVerdict(verdict, format = "summary") {
|
|
|
2657
677
|
}
|
|
2658
678
|
|
|
2659
679
|
// src/tools/govern.ts
|
|
680
|
+
function buildComponentProps(ctx) {
|
|
681
|
+
return Object.fromEntries(
|
|
682
|
+
Object.values(ctx.data.components).map((component) => [
|
|
683
|
+
component.name,
|
|
684
|
+
component.props
|
|
685
|
+
])
|
|
686
|
+
);
|
|
687
|
+
}
|
|
2660
688
|
var governHandler = async (args, ctx) => {
|
|
2661
689
|
const spec = args?.spec;
|
|
2662
690
|
if (!spec || typeof spec !== "object") {
|
|
@@ -2665,7 +693,7 @@ var governHandler = async (args, ctx) => {
|
|
|
2665
693
|
{
|
|
2666
694
|
type: "text",
|
|
2667
695
|
text: JSON.stringify({
|
|
2668
|
-
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
696
|
+
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
697
|
})
|
|
2670
698
|
}
|
|
2671
699
|
],
|
|
@@ -2681,12 +709,15 @@ var governHandler = async (args, ctx) => {
|
|
|
2681
709
|
const verdict = runSpecGovern(spec, {
|
|
2682
710
|
allowedComponents,
|
|
2683
711
|
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2684
|
-
policy: policyOverrides
|
|
712
|
+
policy: policyOverrides,
|
|
713
|
+
componentProps: buildComponentProps(ctx)
|
|
2685
714
|
});
|
|
2686
715
|
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
2687
716
|
return {
|
|
2688
717
|
content: [{ type: "text", text }],
|
|
2689
718
|
_meta: {
|
|
719
|
+
...getCatalogMeta(ctx.data),
|
|
720
|
+
verdict: verdict.verdict,
|
|
2690
721
|
score: verdict.score,
|
|
2691
722
|
passed: verdict.passed,
|
|
2692
723
|
violationCount: verdict.results.reduce(
|
|
@@ -2703,7 +734,7 @@ var governHandler = async (args, ctx) => {
|
|
|
2703
734
|
{
|
|
2704
735
|
type: "text",
|
|
2705
736
|
text: JSON.stringify({
|
|
2706
|
-
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id
|
|
737
|
+
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
738
|
})
|
|
2708
739
|
}
|
|
2709
740
|
],
|
|
@@ -2752,8 +783,11 @@ function validateNodeShape(node, path) {
|
|
|
2752
783
|
return `${path}.props must be an object`;
|
|
2753
784
|
}
|
|
2754
785
|
if ("children" in node && node.children !== void 0) {
|
|
786
|
+
if (typeof node.children === "string") {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
2755
789
|
if (!Array.isArray(node.children)) {
|
|
2756
|
-
return `${path}.children must be an array`;
|
|
790
|
+
return `${path}.children must be an array or string`;
|
|
2757
791
|
}
|
|
2758
792
|
for (const [index, child] of node.children.entries()) {
|
|
2759
793
|
if (!isPlainObject(child)) continue;
|
|
@@ -2898,6 +932,59 @@ function getNextAction(status, replacements, unresolvedAmbiguityCount) {
|
|
|
2898
932
|
if (replacements.length > 0) return "review_partial_fix";
|
|
2899
933
|
return "revise_input";
|
|
2900
934
|
}
|
|
935
|
+
function authorizationForAmbiguity(args) {
|
|
936
|
+
const required = [];
|
|
937
|
+
if (!args.applyFixes) {
|
|
938
|
+
required.push("applyFixes");
|
|
939
|
+
}
|
|
940
|
+
if (!args.allowElicitation) {
|
|
941
|
+
required.push("allowElicitation");
|
|
942
|
+
} else if (!args.supportsElicitation) {
|
|
943
|
+
required.push("clientCapabilities.elicitation.form");
|
|
944
|
+
}
|
|
945
|
+
if (!args.allowSampling) {
|
|
946
|
+
required.push("allowSampling");
|
|
947
|
+
} else if (!args.supportsSampling) {
|
|
948
|
+
required.push("clientCapabilities.sampling");
|
|
949
|
+
}
|
|
950
|
+
return required;
|
|
951
|
+
}
|
|
952
|
+
function buildWouldFixIfAuthorized(args) {
|
|
953
|
+
const entries = [];
|
|
954
|
+
if (!args.applyFixes) {
|
|
955
|
+
entries.push(
|
|
956
|
+
...args.deterministicPreview.map((replacement) => ({
|
|
957
|
+
action: "replace_component",
|
|
958
|
+
nodeId: replacement.nodeId,
|
|
959
|
+
from: replacement.from,
|
|
960
|
+
to: replacement.to,
|
|
961
|
+
reason: replacement.reason,
|
|
962
|
+
requiredAuthorization: ["applyFixes"]
|
|
963
|
+
}))
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const requiredAuthorization = authorizationForAmbiguity({
|
|
967
|
+
applyFixes: args.applyFixes,
|
|
968
|
+
allowElicitation: args.allowElicitation,
|
|
969
|
+
allowSampling: args.allowSampling,
|
|
970
|
+
supportsElicitation: args.supportsElicitation,
|
|
971
|
+
supportsSampling: args.supportsSampling
|
|
972
|
+
});
|
|
973
|
+
if (requiredAuthorization.length > 0) {
|
|
974
|
+
entries.push(
|
|
975
|
+
...args.unresolvedAmbiguities.map((ambiguity) => ({
|
|
976
|
+
action: "resolve_ambiguous_component",
|
|
977
|
+
nodeId: ambiguity.nodeId,
|
|
978
|
+
from: ambiguity.from,
|
|
979
|
+
candidates: ambiguity.candidates,
|
|
980
|
+
rankedCandidates: ambiguity.rankedCandidates,
|
|
981
|
+
reason: ambiguity.reason,
|
|
982
|
+
requiredAuthorization
|
|
983
|
+
}))
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return entries;
|
|
987
|
+
}
|
|
2901
988
|
async function emitValidateAndFixTelemetry(args) {
|
|
2902
989
|
if (!args.ctx.mcp?.server) return;
|
|
2903
990
|
try {
|
|
@@ -2919,11 +1006,20 @@ async function emitValidateAndFixTelemetry(args) {
|
|
|
2919
1006
|
function buildAllowedComponents(ctx) {
|
|
2920
1007
|
return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
|
|
2921
1008
|
}
|
|
1009
|
+
function buildComponentProps2(ctx) {
|
|
1010
|
+
return Object.fromEntries(
|
|
1011
|
+
Object.values(ctx.data.components).map((component) => [
|
|
1012
|
+
component.name,
|
|
1013
|
+
component.props
|
|
1014
|
+
])
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
2922
1017
|
function runGovern(spec, ctx, policyOverrides) {
|
|
2923
1018
|
return runSpecGovern(spec, {
|
|
2924
1019
|
allowedComponents: buildAllowedComponents(ctx),
|
|
2925
1020
|
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2926
|
-
policy: policyOverrides
|
|
1021
|
+
policy: policyOverrides,
|
|
1022
|
+
componentProps: buildComponentProps2(ctx)
|
|
2927
1023
|
});
|
|
2928
1024
|
}
|
|
2929
1025
|
function walkNodes2(nodes, visitor, path = "nodes") {
|
|
@@ -3237,11 +1333,14 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3237
1333
|
let replacements = [];
|
|
3238
1334
|
let ambiguities = [];
|
|
3239
1335
|
const resolutionPath = [];
|
|
1336
|
+
const supportsElicitation = supportsFormElicitation(ctx);
|
|
1337
|
+
const supportsModelSampling = supportsSampling(ctx);
|
|
1338
|
+
const preview = !originalVerdict.passed ? applyDeterministicReplacements(spec, ctx) : void 0;
|
|
1339
|
+
if (!applyFixes && preview) {
|
|
1340
|
+
ambiguities = preview.ambiguities;
|
|
1341
|
+
}
|
|
3240
1342
|
if (!originalVerdict.passed && applyFixes) {
|
|
3241
|
-
const result2 = applyDeterministicReplacements(
|
|
3242
|
-
spec,
|
|
3243
|
-
ctx
|
|
3244
|
-
);
|
|
1343
|
+
const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
|
|
3245
1344
|
replacements = result2.replacements;
|
|
3246
1345
|
ambiguities = result2.ambiguities;
|
|
3247
1346
|
if (replacements.length > 0) {
|
|
@@ -3251,7 +1350,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3251
1350
|
const unresolvedAmbiguities2 = ambiguities.filter(
|
|
3252
1351
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3253
1352
|
);
|
|
3254
|
-
if (unresolvedAmbiguities2.length > 0 && allowElicitation &&
|
|
1353
|
+
if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
|
|
3255
1354
|
workingSpec ??= cloneSpec(spec);
|
|
3256
1355
|
const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
|
|
3257
1356
|
unresolvedAmbiguities2,
|
|
@@ -3266,7 +1365,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3266
1365
|
const remainingAmbiguities = ambiguities.filter(
|
|
3267
1366
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3268
1367
|
);
|
|
3269
|
-
if (remainingAmbiguities.length > 0 && allowSampling &&
|
|
1368
|
+
if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
|
|
3270
1369
|
workingSpec ??= cloneSpec(spec);
|
|
3271
1370
|
const sampledReplacements = await resolveAmbiguitiesWithSampling(
|
|
3272
1371
|
remainingAmbiguities,
|
|
@@ -3287,6 +1386,18 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3287
1386
|
const unresolvedAmbiguities = ambiguities.filter(
|
|
3288
1387
|
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3289
1388
|
).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
|
|
1389
|
+
const unresolvedAmbiguitiesWithRefs = ambiguities.filter(
|
|
1390
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1391
|
+
);
|
|
1392
|
+
const wouldFixIfAuthorized = buildWouldFixIfAuthorized({
|
|
1393
|
+
deterministicPreview: preview?.replacements ?? [],
|
|
1394
|
+
unresolvedAmbiguities: unresolvedAmbiguitiesWithRefs,
|
|
1395
|
+
applyFixes,
|
|
1396
|
+
allowElicitation,
|
|
1397
|
+
allowSampling,
|
|
1398
|
+
supportsElicitation,
|
|
1399
|
+
supportsSampling: supportsModelSampling
|
|
1400
|
+
});
|
|
3290
1401
|
const attestation = {
|
|
3291
1402
|
sourceType: ctx.data.snapshot.sourceType,
|
|
3292
1403
|
sourceLabel: ctx.data.snapshot.sourceLabel,
|
|
@@ -3301,9 +1412,9 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3301
1412
|
overrideApplied: Boolean(policyOverrides)
|
|
3302
1413
|
},
|
|
3303
1414
|
clientCapabilities: {
|
|
3304
|
-
sampling:
|
|
1415
|
+
sampling: supportsModelSampling,
|
|
3305
1416
|
samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
|
|
3306
|
-
elicitationForm:
|
|
1417
|
+
elicitationForm: supportsElicitation,
|
|
3307
1418
|
roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
|
|
3308
1419
|
},
|
|
3309
1420
|
capabilitiesUsed: {
|
|
@@ -3333,6 +1444,7 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3333
1444
|
replacements,
|
|
3334
1445
|
unresolvedAmbiguities.length
|
|
3335
1446
|
);
|
|
1447
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3336
1448
|
const payload = {
|
|
3337
1449
|
status,
|
|
3338
1450
|
nextAction,
|
|
@@ -3358,7 +1470,8 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3358
1470
|
effectiveComponents,
|
|
3359
1471
|
"forbidden"
|
|
3360
1472
|
),
|
|
3361
|
-
unresolvedAmbiguities
|
|
1473
|
+
unresolvedAmbiguities,
|
|
1474
|
+
wouldFixIfAuthorized
|
|
3362
1475
|
};
|
|
3363
1476
|
await emitValidateAndFixTelemetry({
|
|
3364
1477
|
ctx,
|
|
@@ -3381,13 +1494,14 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3381
1494
|
}
|
|
3382
1495
|
],
|
|
3383
1496
|
_meta: {
|
|
1497
|
+
...catalogMeta,
|
|
3384
1498
|
status,
|
|
3385
1499
|
nextAction,
|
|
3386
1500
|
replacementCount: replacements.length,
|
|
3387
1501
|
passed: finalVerdict.passed,
|
|
3388
1502
|
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
3389
|
-
|
|
3390
|
-
|
|
1503
|
+
wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
|
|
1504
|
+
resolutionPath
|
|
3391
1505
|
}
|
|
3392
1506
|
};
|
|
3393
1507
|
} catch (error) {
|
|
@@ -3406,52 +1520,22 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
3406
1520
|
}
|
|
3407
1521
|
};
|
|
3408
1522
|
|
|
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
|
|
1523
|
+
// src/cloud-http.ts
|
|
3439
1524
|
var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
|
|
3440
1525
|
function normalizeCloudUrl(url) {
|
|
3441
1526
|
if (!url) return DEFAULT_CLOUD_URL;
|
|
3442
1527
|
return url.replace(/\/+$/, "");
|
|
3443
1528
|
}
|
|
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));
|
|
1529
|
+
async function cloudFetchJson(args) {
|
|
1530
|
+
const base = normalizeCloudUrl(args.cloudUrl);
|
|
1531
|
+
const url = new URL(`${base}${args.path}`);
|
|
1532
|
+
if (args.query) {
|
|
1533
|
+
for (const [key, value] of Object.entries(args.query)) {
|
|
1534
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
3453
1537
|
const response = await fetch(url.toString(), {
|
|
3454
|
-
headers: { "X-API-Key": apiKey }
|
|
1538
|
+
headers: { "X-API-Key": args.apiKey }
|
|
3455
1539
|
});
|
|
3456
1540
|
if (!response.ok) {
|
|
3457
1541
|
const body = await response.text();
|
|
@@ -3463,11 +1547,29 @@ async function fetchFindings(apiKey, params, cloudUrl) {
|
|
|
3463
1547
|
message = body;
|
|
3464
1548
|
}
|
|
3465
1549
|
throw new Error(
|
|
3466
|
-
`Cloud
|
|
1550
|
+
`Cloud ${args.resource} API error (${response.status}): ${message}`
|
|
3467
1551
|
);
|
|
3468
1552
|
}
|
|
3469
1553
|
return await response.json();
|
|
3470
1554
|
}
|
|
1555
|
+
|
|
1556
|
+
// src/findings-service.ts
|
|
1557
|
+
async function fetchFindings(apiKey, params, cloudUrl) {
|
|
1558
|
+
return cloudFetchJson({
|
|
1559
|
+
apiKey,
|
|
1560
|
+
cloudUrl,
|
|
1561
|
+
path: "/api/findings",
|
|
1562
|
+
resource: "findings",
|
|
1563
|
+
query: {
|
|
1564
|
+
status: params.status,
|
|
1565
|
+
severity: params.severity,
|
|
1566
|
+
category: params.category,
|
|
1567
|
+
ruleId: params.ruleId,
|
|
1568
|
+
filePath: params.filePath,
|
|
1569
|
+
limit: params.limit
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
3471
1573
|
async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
|
|
3472
1574
|
return fetchFindings(
|
|
3473
1575
|
apiKey,
|
|
@@ -3475,8 +1577,59 @@ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
|
|
|
3475
1577
|
cloudUrl
|
|
3476
1578
|
);
|
|
3477
1579
|
}
|
|
1580
|
+
function buildFindingSourceUrl(finding) {
|
|
1581
|
+
const { repoFullName, commitSha, filePath, line } = finding;
|
|
1582
|
+
if (!repoFullName || !commitSha || !filePath) return void 0;
|
|
1583
|
+
if (!/^[-\w.]+\/[-\w.]+$/.test(repoFullName)) return void 0;
|
|
1584
|
+
if (!/^[a-fA-F0-9]{7,64}$/.test(commitSha)) return void 0;
|
|
1585
|
+
const normalizedPath = filePath.replace(/^\/+/, "");
|
|
1586
|
+
if (!normalizedPath) return void 0;
|
|
1587
|
+
const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1588
|
+
const lineSuffix = line != null && line > 0 ? `#L${Math.floor(line)}` : "";
|
|
1589
|
+
return `https://github.com/${repoFullName}/blob/${commitSha}/${encodedPath}${lineSuffix}`;
|
|
1590
|
+
}
|
|
1591
|
+
async function fetchFindingsSummary(apiKey, params, cloudUrl) {
|
|
1592
|
+
const { findings } = await fetchFindings(
|
|
1593
|
+
apiKey,
|
|
1594
|
+
{ ...params, limit: params.limit ?? 200 },
|
|
1595
|
+
cloudUrl
|
|
1596
|
+
);
|
|
1597
|
+
return summarizeFindings(findings, params);
|
|
1598
|
+
}
|
|
1599
|
+
function summarizeFindings(findings, params) {
|
|
1600
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
1601
|
+
const byStatus = { open: 0, resolved: 0, ignored: 0 };
|
|
1602
|
+
const byCategory = {};
|
|
1603
|
+
const byRuleId = {};
|
|
1604
|
+
const byFilePath = {};
|
|
1605
|
+
for (const finding of findings) {
|
|
1606
|
+
bySeverity[finding.severity] += 1;
|
|
1607
|
+
byStatus[finding.status] += 1;
|
|
1608
|
+
increment(byRuleId, finding.ruleId);
|
|
1609
|
+
if (finding.category) increment(byCategory, finding.category);
|
|
1610
|
+
if (finding.filePath) increment(byFilePath, finding.filePath);
|
|
1611
|
+
}
|
|
1612
|
+
const { limit: _limit, ...filters } = params;
|
|
1613
|
+
return {
|
|
1614
|
+
total: findings.length,
|
|
1615
|
+
filters,
|
|
1616
|
+
bySeverity,
|
|
1617
|
+
byStatus,
|
|
1618
|
+
byCategory,
|
|
1619
|
+
byRuleId,
|
|
1620
|
+
byFilePath,
|
|
1621
|
+
topFiles: topEntries(byFilePath, "filePath"),
|
|
1622
|
+
topRules: topEntries(byRuleId, "ruleId")
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function increment(counts, key) {
|
|
1626
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1627
|
+
}
|
|
1628
|
+
function topEntries(counts, keyName) {
|
|
1629
|
+
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 }));
|
|
1630
|
+
}
|
|
3478
1631
|
|
|
3479
|
-
// src/tools/
|
|
1632
|
+
// src/tools/cloud-auth.ts
|
|
3480
1633
|
function resolveCloudApiKey(ctx) {
|
|
3481
1634
|
return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
3482
1635
|
}
|
|
@@ -3496,6 +1649,38 @@ function missingKeyError() {
|
|
|
3496
1649
|
isError: true
|
|
3497
1650
|
};
|
|
3498
1651
|
}
|
|
1652
|
+
|
|
1653
|
+
// src/tools/findings.ts
|
|
1654
|
+
function enrichFindings(findings, ctx) {
|
|
1655
|
+
return findings.map((finding) => {
|
|
1656
|
+
const sourceUrl = finding.sourceUrl ?? buildFindingSourceUrl(finding);
|
|
1657
|
+
const withSourceUrl = sourceUrl ? { ...finding, sourceUrl } : finding;
|
|
1658
|
+
if (!finding.prop || !finding.rawValue || !ctx.data.tokens) {
|
|
1659
|
+
return withSourceUrl;
|
|
1660
|
+
}
|
|
1661
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1662
|
+
const suggestion = suggestToken({
|
|
1663
|
+
tokens: ctx.data.tokens,
|
|
1664
|
+
property: finding.prop,
|
|
1665
|
+
value: finding.rawValue,
|
|
1666
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
1667
|
+
updatedAt: catalogMeta.updatedAt
|
|
1668
|
+
});
|
|
1669
|
+
if (!suggestion.recommended) {
|
|
1670
|
+
const {
|
|
1671
|
+
suggestedToken: _discarded,
|
|
1672
|
+
suggestedTokenDetails: _details,
|
|
1673
|
+
...rest
|
|
1674
|
+
} = withSourceUrl;
|
|
1675
|
+
return rest;
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
...withSourceUrl,
|
|
1679
|
+
suggestedToken: suggestion.recommended.cssVar ?? suggestion.recommended.name,
|
|
1680
|
+
suggestedTokenDetails: suggestion.recommended
|
|
1681
|
+
};
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
3499
1684
|
var findingsListHandler = async (args, ctx) => {
|
|
3500
1685
|
const apiKey = resolveCloudApiKey(ctx);
|
|
3501
1686
|
if (!apiKey) return missingKeyError();
|
|
@@ -3510,9 +1695,65 @@ var findingsListHandler = async (args, ctx) => {
|
|
|
3510
1695
|
if (args.limit != null) params.limit = Number(args.limit);
|
|
3511
1696
|
try {
|
|
3512
1697
|
const result2 = await fetchFindings(apiKey, params, cloudUrl);
|
|
1698
|
+
const findings = enrichFindings(result2.findings, ctx);
|
|
1699
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1700
|
+
return {
|
|
1701
|
+
content: [
|
|
1702
|
+
{
|
|
1703
|
+
type: "text",
|
|
1704
|
+
text: JSON.stringify({ ...result2, findings })
|
|
1705
|
+
}
|
|
1706
|
+
],
|
|
1707
|
+
_meta: {
|
|
1708
|
+
...catalogMeta,
|
|
1709
|
+
count: findings.length,
|
|
1710
|
+
tokenSuggestionCount: findings.filter(
|
|
1711
|
+
(finding) => finding.suggestedTokenDetails
|
|
1712
|
+
).length
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
return {
|
|
1717
|
+
content: [
|
|
1718
|
+
{
|
|
1719
|
+
type: "text",
|
|
1720
|
+
text: JSON.stringify({
|
|
1721
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1722
|
+
})
|
|
1723
|
+
}
|
|
1724
|
+
],
|
|
1725
|
+
isError: true
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
var findingsSummaryHandler = async (args, ctx) => {
|
|
1730
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1731
|
+
if (!apiKey) return missingKeyError();
|
|
1732
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1733
|
+
const params = {};
|
|
1734
|
+
if (args.status) params.status = args.status;
|
|
1735
|
+
if (args.severity)
|
|
1736
|
+
params.severity = args.severity;
|
|
1737
|
+
if (args.category) params.category = String(args.category);
|
|
1738
|
+
if (args.ruleId) params.ruleId = String(args.ruleId);
|
|
1739
|
+
if (args.filePath) params.filePath = String(args.filePath);
|
|
1740
|
+
if (args.limit != null) params.limit = Number(args.limit);
|
|
1741
|
+
try {
|
|
1742
|
+
const summary = await fetchFindingsSummary(apiKey, params, cloudUrl);
|
|
1743
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3513
1744
|
return {
|
|
3514
|
-
content: [
|
|
3515
|
-
|
|
1745
|
+
content: [
|
|
1746
|
+
{
|
|
1747
|
+
type: "text",
|
|
1748
|
+
text: JSON.stringify(summary)
|
|
1749
|
+
}
|
|
1750
|
+
],
|
|
1751
|
+
_meta: {
|
|
1752
|
+
...catalogMeta,
|
|
1753
|
+
total: summary.total,
|
|
1754
|
+
topFileCount: summary.topFiles.length,
|
|
1755
|
+
topRuleCount: summary.topRules.length
|
|
1756
|
+
}
|
|
3516
1757
|
};
|
|
3517
1758
|
} catch (error) {
|
|
3518
1759
|
return {
|
|
@@ -3546,7 +1787,11 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3546
1787
|
const cloudUrl = resolveCloudUrl(ctx);
|
|
3547
1788
|
try {
|
|
3548
1789
|
const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
|
|
3549
|
-
const findings =
|
|
1790
|
+
const findings = enrichFindings(
|
|
1791
|
+
result2.findings.filter((f) => f.filePath === filePath),
|
|
1792
|
+
ctx
|
|
1793
|
+
);
|
|
1794
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
3550
1795
|
return {
|
|
3551
1796
|
content: [
|
|
3552
1797
|
{
|
|
@@ -3554,7 +1799,14 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3554
1799
|
text: JSON.stringify({ findings, filePath })
|
|
3555
1800
|
}
|
|
3556
1801
|
],
|
|
3557
|
-
_meta: {
|
|
1802
|
+
_meta: {
|
|
1803
|
+
...catalogMeta,
|
|
1804
|
+
count: findings.length,
|
|
1805
|
+
filePath,
|
|
1806
|
+
tokenSuggestionCount: findings.filter(
|
|
1807
|
+
(finding) => finding.suggestedTokenDetails
|
|
1808
|
+
).length
|
|
1809
|
+
}
|
|
3558
1810
|
};
|
|
3559
1811
|
} catch (error) {
|
|
3560
1812
|
return {
|
|
@@ -3571,43 +1823,211 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
3571
1823
|
}
|
|
3572
1824
|
};
|
|
3573
1825
|
|
|
1826
|
+
// src/tools/swap-to-canonical.ts
|
|
1827
|
+
import * as ts from "typescript";
|
|
1828
|
+
import {
|
|
1829
|
+
resolveCanonicalForHtmlElement,
|
|
1830
|
+
formatRawHtmlElement
|
|
1831
|
+
} from "@fragments-sdk/classifier";
|
|
1832
|
+
|
|
1833
|
+
// src/canonical-mappings-service.ts
|
|
1834
|
+
async function fetchCanonicalMappings(apiKey, cloudUrl) {
|
|
1835
|
+
const catalog = await cloudFetchJson({
|
|
1836
|
+
apiKey,
|
|
1837
|
+
cloudUrl,
|
|
1838
|
+
path: "/api/catalog",
|
|
1839
|
+
resource: "catalog"
|
|
1840
|
+
});
|
|
1841
|
+
return catalog.canonicalMappings ?? [];
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/tools/swap-to-canonical.ts
|
|
1845
|
+
function errorResult(message) {
|
|
1846
|
+
return {
|
|
1847
|
+
content: [
|
|
1848
|
+
{
|
|
1849
|
+
type: "text",
|
|
1850
|
+
text: JSON.stringify({ error: message })
|
|
1851
|
+
}
|
|
1852
|
+
],
|
|
1853
|
+
isError: true
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
var resolveCanonicalForElement = resolveCanonicalForHtmlElement;
|
|
1857
|
+
function walkRawJsxElements(source) {
|
|
1858
|
+
const results = [];
|
|
1859
|
+
const visit = (node) => {
|
|
1860
|
+
let opening;
|
|
1861
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
1862
|
+
opening = node;
|
|
1863
|
+
} else if (ts.isJsxElement(node)) {
|
|
1864
|
+
opening = node.openingElement;
|
|
1865
|
+
}
|
|
1866
|
+
if (opening) {
|
|
1867
|
+
const tagNameNode = opening.tagName;
|
|
1868
|
+
if (ts.isIdentifier(tagNameNode)) {
|
|
1869
|
+
const tagName = tagNameNode.text;
|
|
1870
|
+
if (/^[a-z]/.test(tagName)) {
|
|
1871
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
1872
|
+
for (const attr of opening.attributes.properties) {
|
|
1873
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1874
|
+
if (!attr.name || !ts.isIdentifier(attr.name)) continue;
|
|
1875
|
+
const name = attr.name.text;
|
|
1876
|
+
if (!attr.initializer) {
|
|
1877
|
+
attrs.set(name, { value: "", dynamic: false });
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
1881
|
+
attrs.set(name, {
|
|
1882
|
+
value: attr.initializer.text,
|
|
1883
|
+
dynamic: false
|
|
1884
|
+
});
|
|
1885
|
+
} else if (ts.isJsxExpression(attr.initializer)) {
|
|
1886
|
+
attrs.set(name, { value: "", dynamic: true });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const start = opening.getStart(source);
|
|
1890
|
+
const { line } = source.getLineAndCharacterOfPosition(start);
|
|
1891
|
+
results.push({ tagName, attrs, line: line + 1 });
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
ts.forEachChild(node, visit);
|
|
1896
|
+
};
|
|
1897
|
+
visit(source);
|
|
1898
|
+
return results;
|
|
1899
|
+
}
|
|
1900
|
+
var formatRawElement = formatRawHtmlElement;
|
|
1901
|
+
function buildPropMapping(attrs, rowMapping) {
|
|
1902
|
+
const transforms = new Map(
|
|
1903
|
+
rowMapping.map((m) => [m.rawProp, m])
|
|
1904
|
+
);
|
|
1905
|
+
const mapping = [];
|
|
1906
|
+
for (const rawProp of attrs.keys()) {
|
|
1907
|
+
if (rawProp === "type") continue;
|
|
1908
|
+
mapping.push(
|
|
1909
|
+
transforms.get(rawProp) ?? { rawProp, canonicalProp: rawProp }
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
return mapping;
|
|
1913
|
+
}
|
|
1914
|
+
function groupMappingsByCanonical(mappings) {
|
|
1915
|
+
const out = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const m of mappings) {
|
|
1917
|
+
if (!m.importPath) continue;
|
|
1918
|
+
const list = out.get(m.canonical) ?? [];
|
|
1919
|
+
list.push(m);
|
|
1920
|
+
out.set(m.canonical, list);
|
|
1921
|
+
}
|
|
1922
|
+
for (const list of out.values()) {
|
|
1923
|
+
list.sort((a, b) => {
|
|
1924
|
+
const ac = a.confidence ?? 0;
|
|
1925
|
+
const bc = b.confidence ?? 0;
|
|
1926
|
+
if (ac !== bc) return bc - ac;
|
|
1927
|
+
return a.name.localeCompare(b.name);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
return out;
|
|
1931
|
+
}
|
|
1932
|
+
function buildSwapSuggestions(args) {
|
|
1933
|
+
const eligible = args.mappings.filter(
|
|
1934
|
+
(m) => m.canonicalConfidence === "auto" || m.canonicalConfidence === "overridden"
|
|
1935
|
+
);
|
|
1936
|
+
if (eligible.length === 0) return [];
|
|
1937
|
+
const byCanonical = groupMappingsByCanonical(eligible);
|
|
1938
|
+
if (byCanonical.size === 0) return [];
|
|
1939
|
+
const source = ts.createSourceFile(
|
|
1940
|
+
args.filePath,
|
|
1941
|
+
args.fileContent,
|
|
1942
|
+
ts.ScriptTarget.Latest,
|
|
1943
|
+
/* setParentNodes */
|
|
1944
|
+
true,
|
|
1945
|
+
ts.ScriptKind.TSX
|
|
1946
|
+
);
|
|
1947
|
+
const elements = walkRawJsxElements(source);
|
|
1948
|
+
const suggestions = [];
|
|
1949
|
+
for (const el of elements) {
|
|
1950
|
+
const canonical = resolveCanonicalForElement(el.tagName, el.attrs);
|
|
1951
|
+
if (!canonical) continue;
|
|
1952
|
+
const candidates = byCanonical.get(canonical);
|
|
1953
|
+
if (!candidates || candidates.length === 0) continue;
|
|
1954
|
+
const primary = candidates[0];
|
|
1955
|
+
const importPath = primary.importPath;
|
|
1956
|
+
if (!importPath) continue;
|
|
1957
|
+
const suggestion = {
|
|
1958
|
+
rawElement: formatRawElement(el.tagName, el.attrs),
|
|
1959
|
+
canonical,
|
|
1960
|
+
componentName: primary.name,
|
|
1961
|
+
importPath,
|
|
1962
|
+
propMapping: buildPropMapping(el.attrs, primary.propMapping),
|
|
1963
|
+
line: el.line
|
|
1964
|
+
};
|
|
1965
|
+
if (candidates.length > 1) {
|
|
1966
|
+
suggestion.alternates = candidates.slice(1).filter((c) => c.importPath).map((c) => ({
|
|
1967
|
+
name: c.name,
|
|
1968
|
+
importPath: c.importPath,
|
|
1969
|
+
confidence: c.confidence ?? 0
|
|
1970
|
+
}));
|
|
1971
|
+
}
|
|
1972
|
+
suggestions.push(suggestion);
|
|
1973
|
+
}
|
|
1974
|
+
return suggestions;
|
|
1975
|
+
}
|
|
1976
|
+
var swapToCanonicalHandler = async (args, ctx) => {
|
|
1977
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1978
|
+
if (!apiKey) return missingKeyError();
|
|
1979
|
+
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
1980
|
+
const fileContent = typeof args.fileContent === "string" ? args.fileContent : "";
|
|
1981
|
+
if (!filePath || !fileContent) {
|
|
1982
|
+
return errorResult("filePath and fileContent are required.");
|
|
1983
|
+
}
|
|
1984
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1985
|
+
let mappings;
|
|
1986
|
+
try {
|
|
1987
|
+
mappings = await fetchCanonicalMappings(apiKey, cloudUrl);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1990
|
+
}
|
|
1991
|
+
const suggestions = buildSwapSuggestions({
|
|
1992
|
+
filePath,
|
|
1993
|
+
fileContent,
|
|
1994
|
+
mappings
|
|
1995
|
+
});
|
|
1996
|
+
return {
|
|
1997
|
+
content: [
|
|
1998
|
+
{
|
|
1999
|
+
type: "text",
|
|
2000
|
+
text: JSON.stringify({ suggestions, filePath })
|
|
2001
|
+
}
|
|
2002
|
+
],
|
|
2003
|
+
_meta: {
|
|
2004
|
+
count: suggestions.length,
|
|
2005
|
+
mappingCount: mappings.length,
|
|
2006
|
+
filePath
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
};
|
|
2010
|
+
|
|
3574
2011
|
// src/tools/index.ts
|
|
3575
2012
|
var CORE_TOOLS = {
|
|
3576
|
-
|
|
3577
|
-
inspect: inspectHandler,
|
|
3578
|
-
blocks: blocksHandler,
|
|
3579
|
-
tokens: tokensHandler,
|
|
3580
|
-
graph: graphHandler,
|
|
3581
|
-
perf: perfHandler,
|
|
2013
|
+
"tokens.suggest": tokensSuggestHandler,
|
|
3582
2014
|
govern: governHandler,
|
|
3583
2015
|
validate_and_fix: validateAndFixHandler,
|
|
3584
2016
|
findings_list: findingsListHandler,
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
render: renderHandler,
|
|
3589
|
-
fix: fixHandler,
|
|
3590
|
-
a11y: a11yHandler
|
|
3591
|
-
};
|
|
3592
|
-
var INFRA_TOOLS = {
|
|
3593
|
-
generate_ui: generateUiHandler
|
|
2017
|
+
findings_summary: findingsSummaryHandler,
|
|
2018
|
+
findings_for_file: findingsForFileHandler,
|
|
2019
|
+
swap_to_canonical: swapToCanonicalHandler
|
|
3594
2020
|
};
|
|
2021
|
+
var VIEWER_TOOLS = {};
|
|
2022
|
+
var INFRA_TOOLS = {};
|
|
3595
2023
|
var BUILTIN_TOOLS = {
|
|
3596
2024
|
...CORE_TOOLS,
|
|
3597
2025
|
...VIEWER_TOOLS,
|
|
3598
2026
|
...INFRA_TOOLS
|
|
3599
2027
|
};
|
|
3600
2028
|
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"]
|
|
2029
|
+
"tokens.suggest": ["tokens"],
|
|
2030
|
+
validate_and_fix: ["components"]
|
|
3611
2031
|
};
|
|
3612
2032
|
|
|
3613
2033
|
// src/registry.ts
|
|
@@ -3747,15 +2167,15 @@ function telemetryMiddleware(logger) {
|
|
|
3747
2167
|
}
|
|
3748
2168
|
|
|
3749
2169
|
// src/source-selection.ts
|
|
3750
|
-
import { existsSync as
|
|
3751
|
-
import { join as
|
|
2170
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2171
|
+
import { join as join6 } from "path";
|
|
3752
2172
|
|
|
3753
2173
|
// src/adapters/fragments-json.ts
|
|
3754
2174
|
import { readFile } from "fs/promises";
|
|
3755
2175
|
|
|
3756
2176
|
// src/discovery.ts
|
|
3757
|
-
import { existsSync as
|
|
3758
|
-
import { join as
|
|
2177
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
2178
|
+
import { join as join2, dirname, resolve } from "path";
|
|
3759
2179
|
import { createRequire } from "module";
|
|
3760
2180
|
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
3761
2181
|
const parts = pattern.split("/");
|
|
@@ -3768,14 +2188,14 @@ function resolveWorkspaceGlob(baseDir, pattern) {
|
|
|
3768
2188
|
try {
|
|
3769
2189
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
3770
2190
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
3771
|
-
next.push(
|
|
2191
|
+
next.push(join2(d, entry.name));
|
|
3772
2192
|
}
|
|
3773
2193
|
}
|
|
3774
2194
|
} catch {
|
|
3775
2195
|
}
|
|
3776
2196
|
} else {
|
|
3777
|
-
const candidate =
|
|
3778
|
-
if (
|
|
2197
|
+
const candidate = join2(d, part);
|
|
2198
|
+
if (existsSync2(candidate)) next.push(candidate);
|
|
3779
2199
|
}
|
|
3780
2200
|
}
|
|
3781
2201
|
dirs = next;
|
|
@@ -3784,8 +2204,8 @@ function resolveWorkspaceGlob(baseDir, pattern) {
|
|
|
3784
2204
|
}
|
|
3785
2205
|
function getWorkspaceDirs(rootDir) {
|
|
3786
2206
|
const dirs = [];
|
|
3787
|
-
const rootPkgPath =
|
|
3788
|
-
if (
|
|
2207
|
+
const rootPkgPath = join2(rootDir, "package.json");
|
|
2208
|
+
if (existsSync2(rootPkgPath)) {
|
|
3789
2209
|
try {
|
|
3790
2210
|
const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
|
|
3791
2211
|
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
@@ -3798,8 +2218,8 @@ function getWorkspaceDirs(rootDir) {
|
|
|
3798
2218
|
} catch {
|
|
3799
2219
|
}
|
|
3800
2220
|
}
|
|
3801
|
-
const pnpmWsPath =
|
|
3802
|
-
if (
|
|
2221
|
+
const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
|
|
2222
|
+
if (existsSync2(pnpmWsPath)) {
|
|
3803
2223
|
try {
|
|
3804
2224
|
const content = readFileSync3(pnpmWsPath, "utf-8");
|
|
3805
2225
|
const lines = content.split("\n");
|
|
@@ -3832,8 +2252,8 @@ function resolveDepPackageJson(localRequire, depName) {
|
|
|
3832
2252
|
const mainPath = localRequire.resolve(depName);
|
|
3833
2253
|
let dir = dirname(mainPath);
|
|
3834
2254
|
while (true) {
|
|
3835
|
-
const candidate =
|
|
3836
|
-
if (
|
|
2255
|
+
const candidate = join2(dir, "package.json");
|
|
2256
|
+
if (existsSync2(candidate)) {
|
|
3837
2257
|
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
3838
2258
|
if (pkg.name === depName) return candidate;
|
|
3839
2259
|
}
|
|
@@ -3846,23 +2266,23 @@ function resolveDepPackageJson(localRequire, depName) {
|
|
|
3846
2266
|
return null;
|
|
3847
2267
|
}
|
|
3848
2268
|
function findFragmentsInDeps(dir, found, depField) {
|
|
3849
|
-
const pkgJsonPath =
|
|
3850
|
-
if (!
|
|
2269
|
+
const pkgJsonPath = join2(dir, "package.json");
|
|
2270
|
+
if (!existsSync2(pkgJsonPath)) return;
|
|
3851
2271
|
try {
|
|
3852
2272
|
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
|
|
3853
2273
|
const allDeps = {
|
|
3854
2274
|
...pkgJson.dependencies,
|
|
3855
2275
|
...pkgJson.devDependencies
|
|
3856
2276
|
};
|
|
3857
|
-
const localRequire = createRequire(
|
|
2277
|
+
const localRequire = createRequire(join2(dir, "noop.js"));
|
|
3858
2278
|
for (const depName of Object.keys(allDeps)) {
|
|
3859
2279
|
try {
|
|
3860
2280
|
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
3861
2281
|
if (!depPkgPath) continue;
|
|
3862
2282
|
const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
|
|
3863
2283
|
if (depPkg[depField]) {
|
|
3864
|
-
const fragmentsPath =
|
|
3865
|
-
if (
|
|
2284
|
+
const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
|
|
2285
|
+
if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
3866
2286
|
found.push(fragmentsPath);
|
|
3867
2287
|
}
|
|
3868
2288
|
}
|
|
@@ -3877,8 +2297,8 @@ function findDesignSystemJson(startDir, outFile, depField) {
|
|
|
3877
2297
|
const resolvedStart = resolve(startDir);
|
|
3878
2298
|
let dir = resolvedStart;
|
|
3879
2299
|
while (true) {
|
|
3880
|
-
const candidate =
|
|
3881
|
-
if (
|
|
2300
|
+
const candidate = join2(dir, outFile);
|
|
2301
|
+
if (existsSync2(candidate)) {
|
|
3882
2302
|
found.push(candidate);
|
|
3883
2303
|
break;
|
|
3884
2304
|
}
|
|
@@ -3887,7 +2307,7 @@ function findDesignSystemJson(startDir, outFile, depField) {
|
|
|
3887
2307
|
dir = parent;
|
|
3888
2308
|
}
|
|
3889
2309
|
findFragmentsInDeps(resolvedStart, found, depField);
|
|
3890
|
-
if (found.length === 0 ||
|
|
2310
|
+
if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
|
|
3891
2311
|
const workspaceDirs = getWorkspaceDirs(resolvedStart);
|
|
3892
2312
|
for (const wsDir of workspaceDirs) {
|
|
3893
2313
|
findFragmentsInDeps(wsDir, found, depField);
|
|
@@ -3902,8 +2322,8 @@ function findBundleManifest(startDir) {
|
|
|
3902
2322
|
const found = [];
|
|
3903
2323
|
let dir = resolve(startDir);
|
|
3904
2324
|
while (true) {
|
|
3905
|
-
const candidate =
|
|
3906
|
-
if (
|
|
2325
|
+
const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
|
|
2326
|
+
if (existsSync2(candidate)) {
|
|
3907
2327
|
found.push(candidate);
|
|
3908
2328
|
break;
|
|
3909
2329
|
}
|
|
@@ -4059,13 +2479,15 @@ function tokensFromCompiledTokenData(tokens) {
|
|
|
4059
2479
|
category,
|
|
4060
2480
|
value: valueToString(entry.value),
|
|
4061
2481
|
description: entry.description
|
|
4062
|
-
}));
|
|
4063
|
-
|
|
4064
|
-
|
|
2482
|
+
})).filter((token) => !isGarbageToken(token));
|
|
2483
|
+
if (normalized.length > 0) {
|
|
2484
|
+
categories[category] = normalized;
|
|
2485
|
+
flat.push(...normalized);
|
|
2486
|
+
}
|
|
4065
2487
|
}
|
|
4066
2488
|
return {
|
|
4067
2489
|
prefix: tokens.prefix,
|
|
4068
|
-
total:
|
|
2490
|
+
total: flat.length,
|
|
4069
2491
|
categories,
|
|
4070
2492
|
flat
|
|
4071
2493
|
};
|
|
@@ -4200,12 +2622,12 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
|
4200
2622
|
};
|
|
4201
2623
|
|
|
4202
2624
|
// src/adapters/auto-extract.ts
|
|
4203
|
-
import { existsSync as
|
|
4204
|
-
import { join as
|
|
2625
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2626
|
+
import { join as join5, relative, sep } from "path";
|
|
4205
2627
|
|
|
4206
2628
|
// src/adapters/discover-components.ts
|
|
4207
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
4208
|
-
import { join as
|
|
2629
|
+
import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
|
|
2630
|
+
import { join as join3, extname, basename } from "path";
|
|
4209
2631
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
4210
2632
|
"node_modules",
|
|
4211
2633
|
"dist",
|
|
@@ -4241,10 +2663,10 @@ function discoverComponentFiles(projectRoot) {
|
|
|
4241
2663
|
"src/ui",
|
|
4242
2664
|
"lib/ui",
|
|
4243
2665
|
"packages"
|
|
4244
|
-
].map((d) =>
|
|
2666
|
+
].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
|
|
4245
2667
|
if (scanDirs.length === 0) {
|
|
4246
|
-
const srcDir =
|
|
4247
|
-
if (
|
|
2668
|
+
const srcDir = join3(projectRoot, "src");
|
|
2669
|
+
if (existsSync3(srcDir)) scanDirs.push(srcDir);
|
|
4248
2670
|
}
|
|
4249
2671
|
for (const dir of scanDirs) {
|
|
4250
2672
|
walkDir(dir, results, seen);
|
|
@@ -4263,14 +2685,14 @@ function walkDir(dir, results, seen, depth = 0) {
|
|
|
4263
2685
|
if (entry.name.startsWith(".")) continue;
|
|
4264
2686
|
if (entry.isDirectory()) {
|
|
4265
2687
|
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
4266
|
-
walkDir(
|
|
2688
|
+
walkDir(join3(dir, entry.name), results, seen, depth + 1);
|
|
4267
2689
|
continue;
|
|
4268
2690
|
}
|
|
4269
2691
|
if (!entry.isFile()) continue;
|
|
4270
2692
|
const ext = extname(entry.name);
|
|
4271
2693
|
if (ext !== ".tsx" && ext !== ".jsx") continue;
|
|
4272
2694
|
if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
|
|
4273
|
-
const filePath =
|
|
2695
|
+
const filePath = join3(dir, entry.name);
|
|
4274
2696
|
if (seen.has(filePath)) continue;
|
|
4275
2697
|
seen.add(filePath);
|
|
4276
2698
|
const name = inferComponentName(entry.name, dir);
|
|
@@ -4288,8 +2710,8 @@ function inferComponentName(fileName, dirPath) {
|
|
|
4288
2710
|
}
|
|
4289
2711
|
|
|
4290
2712
|
// src/adapters/scan-tokens.ts
|
|
4291
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as
|
|
4292
|
-
import { join as
|
|
2713
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
2714
|
+
import { join as join4, extname as extname2 } from "path";
|
|
4293
2715
|
function scanTokens(projectRoot) {
|
|
4294
2716
|
const cssFiles = discoverCssFiles(projectRoot);
|
|
4295
2717
|
if (cssFiles.length === 0) return void 0;
|
|
@@ -4327,7 +2749,7 @@ function discoverCssFiles(projectRoot) {
|
|
|
4327
2749
|
"styles",
|
|
4328
2750
|
"css",
|
|
4329
2751
|
"app"
|
|
4330
|
-
].map((d) =>
|
|
2752
|
+
].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
|
|
4331
2753
|
searchDirs.push(projectRoot);
|
|
4332
2754
|
for (const dir of searchDirs) {
|
|
4333
2755
|
try {
|
|
@@ -4336,22 +2758,22 @@ function discoverCssFiles(projectRoot) {
|
|
|
4336
2758
|
if (!entry.isFile()) continue;
|
|
4337
2759
|
const ext = extname2(entry.name);
|
|
4338
2760
|
if (ext === ".css" || ext === ".scss") {
|
|
4339
|
-
files.push(
|
|
2761
|
+
files.push(join4(dir, entry.name));
|
|
4340
2762
|
}
|
|
4341
2763
|
}
|
|
4342
2764
|
} catch {
|
|
4343
2765
|
continue;
|
|
4344
2766
|
}
|
|
4345
2767
|
}
|
|
4346
|
-
const srcDir =
|
|
4347
|
-
if (
|
|
2768
|
+
const srcDir = join4(projectRoot, "src");
|
|
2769
|
+
if (existsSync4(srcDir)) {
|
|
4348
2770
|
try {
|
|
4349
2771
|
for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
4350
2772
|
if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
|
|
4351
|
-
const subDir =
|
|
2773
|
+
const subDir = join4(srcDir, subEntry.name);
|
|
4352
2774
|
for (const file of readdirSync3(subDir, { withFileTypes: true })) {
|
|
4353
2775
|
if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
|
|
4354
|
-
files.push(
|
|
2776
|
+
files.push(join4(subDir, file.name));
|
|
4355
2777
|
}
|
|
4356
2778
|
}
|
|
4357
2779
|
}
|
|
@@ -4583,7 +3005,7 @@ Check that your tsconfig.json includes the component directories.`
|
|
|
4583
3005
|
var extractorModulePromise = null;
|
|
4584
3006
|
async function loadExtractorModule() {
|
|
4585
3007
|
if (!extractorModulePromise) {
|
|
4586
|
-
extractorModulePromise = import("./dist-
|
|
3008
|
+
extractorModulePromise = import("./dist-TTCI6TME.js");
|
|
4587
3009
|
}
|
|
4588
3010
|
return extractorModulePromise;
|
|
4589
3011
|
}
|
|
@@ -4849,15 +3271,15 @@ function inferCategory(relativePath) {
|
|
|
4849
3271
|
function findTsConfig(projectRoot) {
|
|
4850
3272
|
const candidates = ["tsconfig.json", "tsconfig.app.json"];
|
|
4851
3273
|
for (const name of candidates) {
|
|
4852
|
-
const p =
|
|
4853
|
-
if (
|
|
3274
|
+
const p = join5(projectRoot, name);
|
|
3275
|
+
if (existsSync5(p)) return p;
|
|
4854
3276
|
}
|
|
4855
3277
|
return null;
|
|
4856
3278
|
}
|
|
4857
3279
|
function readPackageName(projectRoot) {
|
|
4858
3280
|
try {
|
|
4859
|
-
const pkgPath =
|
|
4860
|
-
if (!
|
|
3281
|
+
const pkgPath = join5(projectRoot, "package.json");
|
|
3282
|
+
if (!existsSync5(pkgPath)) return void 0;
|
|
4861
3283
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
4862
3284
|
return pkg.name;
|
|
4863
3285
|
} catch {
|
|
@@ -4880,7 +3302,36 @@ function chooseComponentSource(args) {
|
|
|
4880
3302
|
}
|
|
4881
3303
|
return args.catalogComponents;
|
|
4882
3304
|
}
|
|
4883
|
-
|
|
3305
|
+
function dominantString(values) {
|
|
3306
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3307
|
+
for (const value of values) {
|
|
3308
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
3309
|
+
if (!trimmed) continue;
|
|
3310
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
3311
|
+
}
|
|
3312
|
+
const ranked = [...counts.entries()].sort(
|
|
3313
|
+
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
|
|
3314
|
+
);
|
|
3315
|
+
if (ranked.length === 0) return void 0;
|
|
3316
|
+
if (ranked[1] && ranked[1][1] === ranked[0][1]) return void 0;
|
|
3317
|
+
return ranked[0][0];
|
|
3318
|
+
}
|
|
3319
|
+
function inferDesignSystemMetadataFromComponents(components) {
|
|
3320
|
+
const packageName = dominantString(
|
|
3321
|
+
components.map((component) => component.packageName)
|
|
3322
|
+
);
|
|
3323
|
+
const packageComponents = packageName ? components.filter((component) => component.packageName === packageName) : components;
|
|
3324
|
+
const importPath = dominantString(
|
|
3325
|
+
packageComponents.map(
|
|
3326
|
+
(component) => component.importPath ?? component.packageName
|
|
3327
|
+
)
|
|
3328
|
+
);
|
|
3329
|
+
return {
|
|
3330
|
+
packageName: packageName ?? null,
|
|
3331
|
+
importPath: importPath ?? packageName ?? null
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
var TOKEN_CATEGORY_ALIASES = {
|
|
4884
3335
|
color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
|
|
4885
3336
|
spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
|
|
4886
3337
|
typography: ["typography", "font", "text", "copy", "line-height", "letter"],
|
|
@@ -4911,7 +3362,7 @@ function canonicalizeTokenCategory(token) {
|
|
|
4911
3362
|
].map(normalizeValue).filter(Boolean);
|
|
4912
3363
|
for (const candidate of candidates) {
|
|
4913
3364
|
for (const [canonical, aliases] of Object.entries(
|
|
4914
|
-
|
|
3365
|
+
TOKEN_CATEGORY_ALIASES
|
|
4915
3366
|
)) {
|
|
4916
3367
|
if (candidate === canonical || aliases.some(
|
|
4917
3368
|
(alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
|
|
@@ -4924,7 +3375,7 @@ function canonicalizeTokenCategory(token) {
|
|
|
4924
3375
|
}
|
|
4925
3376
|
function groupTokens(flat) {
|
|
4926
3377
|
const categories = {};
|
|
4927
|
-
const normalizedFlat = (flat ?? []).map((token) => {
|
|
3378
|
+
const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
|
|
4928
3379
|
const category = canonicalizeTokenCategory(token);
|
|
4929
3380
|
const normalized = {
|
|
4930
3381
|
name: token.name,
|
|
@@ -5055,6 +3506,7 @@ var CloudCatalogAdapter = class {
|
|
|
5055
3506
|
constructor(options) {
|
|
5056
3507
|
this.options = options;
|
|
5057
3508
|
}
|
|
3509
|
+
options;
|
|
5058
3510
|
name = "cloud";
|
|
5059
3511
|
async load(_projectRoot) {
|
|
5060
3512
|
const headers = {
|
|
@@ -5073,14 +3525,19 @@ var CloudCatalogAdapter = class {
|
|
|
5073
3525
|
}
|
|
5074
3526
|
const raw = await response.json();
|
|
5075
3527
|
const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
|
|
5076
|
-
const designSystem = mergeDesignSystemMetadata(
|
|
5077
|
-
raw.designSystem,
|
|
5078
|
-
validateFixRaw?.content?.designSystem
|
|
5079
|
-
);
|
|
5080
3528
|
const sourceComponents = chooseComponentSource({
|
|
5081
3529
|
catalogComponents: raw.components ?? [],
|
|
5082
3530
|
contextComponents: validateFixRaw?.content?.components ?? []
|
|
5083
3531
|
});
|
|
3532
|
+
const inferredDesignSystem = inferDesignSystemMetadataFromComponents(sourceComponents);
|
|
3533
|
+
const designSystem = mergeDesignSystemMetadata(
|
|
3534
|
+
{
|
|
3535
|
+
...raw.designSystem,
|
|
3536
|
+
packageName: raw.designSystem?.packageName ?? inferredDesignSystem.packageName,
|
|
3537
|
+
importPath: raw.designSystem?.importPath ?? inferredDesignSystem.importPath
|
|
3538
|
+
},
|
|
3539
|
+
validateFixRaw?.content?.designSystem
|
|
3540
|
+
);
|
|
5084
3541
|
const components = Object.fromEntries(
|
|
5085
3542
|
sourceComponents.map((component) => [
|
|
5086
3543
|
component.componentKey,
|
|
@@ -5147,7 +3604,7 @@ var CloudCatalogAdapter = class {
|
|
|
5147
3604
|
};
|
|
5148
3605
|
|
|
5149
3606
|
// src/adapters/bundle.ts
|
|
5150
|
-
import { existsSync as
|
|
3607
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5151
3608
|
import { readFile as readFile2 } from "fs/promises";
|
|
5152
3609
|
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
5153
3610
|
import {
|
|
@@ -5285,7 +3742,7 @@ var BundleAdapter = class {
|
|
|
5285
3742
|
const bundleDir = dirname3(manifestPath);
|
|
5286
3743
|
const repoRoot = dirname3(bundleDir);
|
|
5287
3744
|
const tokensPath = resolve2(bundleDir, "tokens.json");
|
|
5288
|
-
const tokensFile =
|
|
3745
|
+
const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
|
|
5289
3746
|
const components = Object.fromEntries(
|
|
5290
3747
|
await Promise.all(
|
|
5291
3748
|
Object.values(manifest.components).map(async (entry) => {
|
|
@@ -5382,7 +3839,7 @@ function resolveCloudUrl2(fileConfig) {
|
|
|
5382
3839
|
return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
|
|
5383
3840
|
}
|
|
5384
3841
|
function hasTsProject(projectRoot) {
|
|
5385
|
-
return
|
|
3842
|
+
return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
|
|
5386
3843
|
}
|
|
5387
3844
|
function resolveDataAdapter(config, fileConfig) {
|
|
5388
3845
|
const source = config.source ?? fileConfig?.source ?? "auto";
|
|
@@ -5396,7 +3853,7 @@ function resolveDataAdapter(config, fileConfig) {
|
|
|
5396
3853
|
case "cloud":
|
|
5397
3854
|
if (!cloudApiKey) {
|
|
5398
3855
|
throw new Error(
|
|
5399
|
-
"Cloud source requires a Cloud API key.
|
|
3856
|
+
"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
3857
|
);
|
|
5401
3858
|
}
|
|
5402
3859
|
return {
|
|
@@ -5440,7 +3897,142 @@ function resolveDataAdapter(config, fileConfig) {
|
|
|
5440
3897
|
}
|
|
5441
3898
|
}
|
|
5442
3899
|
function resolveSearchApiKey(config, fileConfig) {
|
|
5443
|
-
return config.searchApiKey ??
|
|
3900
|
+
return config.searchApiKey ?? fileConfig?.vectorSearch?.apiKey;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/spec-schema.ts
|
|
3904
|
+
var UI_SPEC_SCHEMA_URI = "fragments://schemas/govern.schema";
|
|
3905
|
+
var UI_SPEC_SCHEMA_NAME = "govern.schema";
|
|
3906
|
+
var UI_SPEC_SCHEMA_MIME_TYPE = "application/schema+json";
|
|
3907
|
+
var UI_SPEC_SCHEMA_RESOURCE = {
|
|
3908
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
3909
|
+
$id: UI_SPEC_SCHEMA_URI,
|
|
3910
|
+
title: "Fragments UI Spec",
|
|
3911
|
+
description: "Input shape accepted by the Fragments govern and validate_and_fix MCP tools.",
|
|
3912
|
+
type: "object",
|
|
3913
|
+
required: ["nodes"],
|
|
3914
|
+
additionalProperties: false,
|
|
3915
|
+
properties: {
|
|
3916
|
+
root: {
|
|
3917
|
+
type: "string",
|
|
3918
|
+
description: "Optional node id to treat as the root. When omitted, top-level nodes are evaluated in order."
|
|
3919
|
+
},
|
|
3920
|
+
metadata: {
|
|
3921
|
+
type: "object",
|
|
3922
|
+
description: "Optional caller metadata. The validator stores and echoes only fields that downstream tools understand.",
|
|
3923
|
+
additionalProperties: true
|
|
3924
|
+
},
|
|
3925
|
+
nodes: {
|
|
3926
|
+
type: "array",
|
|
3927
|
+
description: "Top-level UI nodes. Each node names a component type and may carry props plus nested children.",
|
|
3928
|
+
items: { $ref: "#/$defs/node" }
|
|
3929
|
+
}
|
|
3930
|
+
},
|
|
3931
|
+
$defs: {
|
|
3932
|
+
node: {
|
|
3933
|
+
type: "object",
|
|
3934
|
+
required: ["type"],
|
|
3935
|
+
additionalProperties: false,
|
|
3936
|
+
properties: {
|
|
3937
|
+
id: {
|
|
3938
|
+
type: "string",
|
|
3939
|
+
description: "Stable caller-provided id. Used in violations and fixedSpec patches."
|
|
3940
|
+
},
|
|
3941
|
+
type: {
|
|
3942
|
+
type: "string",
|
|
3943
|
+
description: 'Design-system component name, including compound names such as "DatePicker.Trigger".'
|
|
3944
|
+
},
|
|
3945
|
+
props: {
|
|
3946
|
+
type: "object",
|
|
3947
|
+
description: 'Component props. Put simple text labels in props.children, e.g. { "children": "Save" }.',
|
|
3948
|
+
additionalProperties: true
|
|
3949
|
+
},
|
|
3950
|
+
children: {
|
|
3951
|
+
description: "Nested child nodes, or a string text child for leaf content. Prefer props.children for simple button labels.",
|
|
3952
|
+
oneOf: [
|
|
3953
|
+
{ type: "string" },
|
|
3954
|
+
{
|
|
3955
|
+
type: "array",
|
|
3956
|
+
items: {
|
|
3957
|
+
oneOf: [{ type: "string" }, { $ref: "#/$defs/node" }]
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
]
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
},
|
|
3965
|
+
examples: {
|
|
3966
|
+
valid: [
|
|
3967
|
+
{
|
|
3968
|
+
nodes: [
|
|
3969
|
+
{
|
|
3970
|
+
id: "save",
|
|
3971
|
+
type: "Button",
|
|
3972
|
+
props: { variant: "primary", children: "Save" }
|
|
3973
|
+
}
|
|
3974
|
+
]
|
|
3975
|
+
},
|
|
3976
|
+
{
|
|
3977
|
+
root: "card",
|
|
3978
|
+
metadata: { source: "agent-draft" },
|
|
3979
|
+
nodes: [
|
|
3980
|
+
{
|
|
3981
|
+
id: "card",
|
|
3982
|
+
type: "Card",
|
|
3983
|
+
props: {},
|
|
3984
|
+
children: [
|
|
3985
|
+
{
|
|
3986
|
+
id: "title",
|
|
3987
|
+
type: "Heading",
|
|
3988
|
+
props: { level: 2, children: "Billing" }
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
id: "submit",
|
|
3992
|
+
type: "Button",
|
|
3993
|
+
props: { children: "Update plan" }
|
|
3994
|
+
}
|
|
3995
|
+
]
|
|
3996
|
+
}
|
|
3997
|
+
]
|
|
3998
|
+
}
|
|
3999
|
+
],
|
|
4000
|
+
invalid: [
|
|
4001
|
+
{
|
|
4002
|
+
reason: "String event handlers are blocked by safety/no-string-handlers.",
|
|
4003
|
+
spec: {
|
|
4004
|
+
nodes: [
|
|
4005
|
+
{
|
|
4006
|
+
id: "danger",
|
|
4007
|
+
type: "Button",
|
|
4008
|
+
props: { children: "Save", onClick: 'alert("saved")' }
|
|
4009
|
+
}
|
|
4010
|
+
]
|
|
4011
|
+
}
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
reason: "Raw CSS values should use design tokens; call tokens.suggest before writing them.",
|
|
4015
|
+
spec: {
|
|
4016
|
+
nodes: [
|
|
4017
|
+
{
|
|
4018
|
+
id: "panel",
|
|
4019
|
+
type: "Box",
|
|
4020
|
+
props: { style: { padding: "6px" } }
|
|
4021
|
+
}
|
|
4022
|
+
]
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
]
|
|
4026
|
+
},
|
|
4027
|
+
notes: [
|
|
4028
|
+
"Use component names from the active catalog; unknown components fail components/allow.",
|
|
4029
|
+
"Use props.children for simple text labels, especially Button text.",
|
|
4030
|
+
"Do not put JavaScript source strings in event handler props.",
|
|
4031
|
+
"Run validate_and_fix after govern when the verdict asks for revision or deterministic repair."
|
|
4032
|
+
]
|
|
4033
|
+
};
|
|
4034
|
+
function serializeUiSpecSchema() {
|
|
4035
|
+
return JSON.stringify(UI_SPEC_SCHEMA_RESOURCE, null, 2);
|
|
5444
4036
|
}
|
|
5445
4037
|
|
|
5446
4038
|
// src/server.ts
|
|
@@ -5458,6 +4050,7 @@ function createMcpServer(config) {
|
|
|
5458
4050
|
{
|
|
5459
4051
|
capabilities: {
|
|
5460
4052
|
tools: { listChanged: true },
|
|
4053
|
+
resources: { listChanged: false },
|
|
5461
4054
|
logging: {}
|
|
5462
4055
|
}
|
|
5463
4056
|
}
|
|
@@ -5488,21 +4081,20 @@ function createMcpServer(config) {
|
|
|
5488
4081
|
let loadDataPromise = null;
|
|
5489
4082
|
let resolvedRoot = null;
|
|
5490
4083
|
let resolveProjectRootPromise = null;
|
|
5491
|
-
let componentIndex = null;
|
|
5492
|
-
let blockIndex = null;
|
|
5493
|
-
let tokenIndex = null;
|
|
5494
4084
|
async function resolveProjectRoot() {
|
|
5495
4085
|
if (resolvedRoot) return resolvedRoot;
|
|
5496
4086
|
if (resolveProjectRootPromise) return resolveProjectRootPromise;
|
|
5497
4087
|
resolveProjectRootPromise = (async () => {
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
4088
|
+
if (server.getClientCapabilities()?.roots) {
|
|
4089
|
+
try {
|
|
4090
|
+
const result2 = await server.listRoots();
|
|
4091
|
+
if (result2.roots?.length > 0) {
|
|
4092
|
+
const rootUri = result2.roots[0].uri;
|
|
4093
|
+
resolvedRoot = fileURLToPath(rootUri);
|
|
4094
|
+
return resolvedRoot;
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
5504
4097
|
}
|
|
5505
|
-
} catch {
|
|
5506
4098
|
}
|
|
5507
4099
|
resolvedRoot = config.projectRoot;
|
|
5508
4100
|
return resolvedRoot;
|
|
@@ -5519,11 +4111,6 @@ function createMcpServer(config) {
|
|
|
5519
4111
|
loadDataPromise = (async () => {
|
|
5520
4112
|
const projectRoot = await resolveProjectRoot();
|
|
5521
4113
|
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
4114
|
cachedData = loaded;
|
|
5528
4115
|
return loaded;
|
|
5529
4116
|
})();
|
|
@@ -5538,21 +4125,49 @@ function createMcpServer(config) {
|
|
|
5538
4125
|
return {
|
|
5539
4126
|
tools: registry.listTools(
|
|
5540
4127
|
{
|
|
5541
|
-
hasViewer:
|
|
5542
|
-
hasPlayground:
|
|
4128
|
+
hasViewer: false,
|
|
4129
|
+
hasPlayground: false,
|
|
5543
4130
|
capabilities: data.capabilities
|
|
5544
4131
|
},
|
|
5545
4132
|
TOOLS
|
|
5546
4133
|
)
|
|
5547
4134
|
};
|
|
5548
4135
|
});
|
|
4136
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
4137
|
+
resources: [
|
|
4138
|
+
{
|
|
4139
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4140
|
+
name: UI_SPEC_SCHEMA_NAME,
|
|
4141
|
+
title: "Fragments govern UI spec schema",
|
|
4142
|
+
description: "JSON schema and examples for the spec argument accepted by govern and validate_and_fix.",
|
|
4143
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE
|
|
4144
|
+
}
|
|
4145
|
+
]
|
|
4146
|
+
}));
|
|
4147
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4148
|
+
if (request.params.uri !== UI_SPEC_SCHEMA_URI) {
|
|
4149
|
+
throw new McpError(
|
|
4150
|
+
ErrorCode.InvalidParams,
|
|
4151
|
+
`Unknown resource URI: ${request.params.uri}`
|
|
4152
|
+
);
|
|
4153
|
+
}
|
|
4154
|
+
return {
|
|
4155
|
+
contents: [
|
|
4156
|
+
{
|
|
4157
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4158
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE,
|
|
4159
|
+
text: serializeUiSpecSchema()
|
|
4160
|
+
}
|
|
4161
|
+
]
|
|
4162
|
+
};
|
|
4163
|
+
});
|
|
5549
4164
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5550
4165
|
const { name, arguments: args } = request.params;
|
|
5551
4166
|
const data = await loadData();
|
|
5552
4167
|
const toolContext = {
|
|
5553
4168
|
data,
|
|
5554
4169
|
config: mergedConfig,
|
|
5555
|
-
indexes: { componentIndex, blockIndex, tokenIndex },
|
|
4170
|
+
indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
|
|
5556
4171
|
mcp: {
|
|
5557
4172
|
server,
|
|
5558
4173
|
clientCapabilities: server.getClientCapabilities()
|
|
@@ -5567,8 +4182,8 @@ function createMcpServer(config) {
|
|
|
5567
4182
|
return "your-component-library";
|
|
5568
4183
|
}
|
|
5569
4184
|
const root = resolvedRoot ?? config.projectRoot;
|
|
5570
|
-
const packageJsonPath =
|
|
5571
|
-
if (
|
|
4185
|
+
const packageJsonPath = join7(root, "package.json");
|
|
4186
|
+
if (existsSync8(packageJsonPath)) {
|
|
5572
4187
|
try {
|
|
5573
4188
|
const content = readFileSync6(packageJsonPath, "utf-8");
|
|
5574
4189
|
const pkg = JSON.parse(content);
|
|
@@ -5632,11 +4247,6 @@ function createSandboxServer() {
|
|
|
5632
4247
|
|
|
5633
4248
|
export {
|
|
5634
4249
|
loadConfigFile,
|
|
5635
|
-
SYNONYM_MAP,
|
|
5636
|
-
USE_CASE_TOKEN_CATEGORIES,
|
|
5637
|
-
MINIMUM_SCORE_THRESHOLD,
|
|
5638
|
-
BLOCK_BOOST_PER_OCCURRENCE,
|
|
5639
|
-
DEFAULT_ENDPOINTS,
|
|
5640
4250
|
CORE_TOOLS,
|
|
5641
4251
|
VIEWER_TOOLS,
|
|
5642
4252
|
INFRA_TOOLS,
|
|
@@ -5657,4 +4267,4 @@ export {
|
|
|
5657
4267
|
startMcpServer,
|
|
5658
4268
|
createSandboxServer
|
|
5659
4269
|
};
|
|
5660
|
-
//# sourceMappingURL=chunk-
|
|
4270
|
+
//# sourceMappingURL=chunk-YJTMK4JY.js.map
|