@fragments-sdk/mcp 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/bin.js +43 -21
- package/dist/bin.js.map +1 -1
- package/dist/chunk-HGGAXLRO.js +4501 -0
- package/dist/chunk-HGGAXLRO.js.map +1 -0
- package/dist/{chunk-2W7DAUUS.js → chunk-VV2PJ75X.js} +11 -6
- package/dist/chunk-VV2PJ75X.js.map +1 -0
- package/dist/chunk-YSRGQDEB.js +93 -0
- package/dist/chunk-YSRGQDEB.js.map +1 -0
- package/dist/index.js +80 -18
- package/dist/index.js.map +1 -1
- package/dist/rules-CKBRD3UL.js +8 -0
- package/dist/server.js +2 -2
- package/package.json +5 -4
- package/dist/chunk-2W7DAUUS.js.map +0 -1
- package/dist/chunk-FGIBLPSU.js +0 -29
- package/dist/chunk-FGIBLPSU.js.map +0 -1
- package/dist/chunk-NVHGG7GW.js +0 -630
- package/dist/chunk-NVHGG7GW.js.map +0 -1
- package/dist/chunk-WBOVO43F.js +0 -2481
- package/dist/chunk-WBOVO43F.js.map +0 -1
- package/dist/config-TUFA5J2S.js +0 -7
- package/dist/rules-WGBCECAK.js +0 -7
- package/dist/rules-WGBCECAK.js.map +0 -1
- /package/dist/{config-TUFA5J2S.js.map → rules-CKBRD3UL.js.map} +0 -0
|
@@ -0,0 +1,4501 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRAND,
|
|
3
|
+
DEFAULTS
|
|
4
|
+
} from "./chunk-4SVS3AA3.js";
|
|
5
|
+
import {
|
|
6
|
+
componentNames,
|
|
7
|
+
findComponent,
|
|
8
|
+
findComponentByName,
|
|
9
|
+
getGuidanceWhen,
|
|
10
|
+
getGuidanceWhenNot,
|
|
11
|
+
listBlocks,
|
|
12
|
+
listComponents
|
|
13
|
+
} from "./chunk-YSRGQDEB.js";
|
|
14
|
+
|
|
15
|
+
// src/server.ts
|
|
16
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import {
|
|
19
|
+
CallToolRequestSchema,
|
|
20
|
+
ListToolsRequestSchema
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
import { existsSync as existsSync9 } from "fs";
|
|
23
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
24
|
+
import { join as join8 } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
import { readFileSync, existsSync } from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
function loadConfigFile(projectRoot) {
|
|
31
|
+
const configPath = join(projectRoot, "ds-mcp.config.json");
|
|
32
|
+
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 result = results[rank];
|
|
474
|
+
const key = `${result.kind}:${result.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: result.kind, name: result.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 result of topComponents) {
|
|
525
|
+
const neighbors = engine.neighbors(result.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 result of results) {
|
|
613
|
+
const count = freq.get(result.name.toLowerCase()) ?? 0;
|
|
614
|
+
if (count > 0) {
|
|
615
|
+
result.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((result) => allowedNames.has(result.name)).map((result) => result.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((result) => {
|
|
902
|
+
const component = componentsByName.get(result.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
|
+
result.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((result) => {
|
|
923
|
+
const component = componentsByName.get(result.name.toLowerCase());
|
|
924
|
+
if (!component) return null;
|
|
925
|
+
return {
|
|
926
|
+
component: component.name,
|
|
927
|
+
category: component.category,
|
|
928
|
+
description: component.description,
|
|
929
|
+
confidence: assignConfidence(result.score, maxScore),
|
|
930
|
+
reasons: [`Matched via hybrid search (score: ${result.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
|
+
(result) => result.score >= topBlockScore * 0.3
|
|
1013
|
+
);
|
|
1014
|
+
if (relevantBlockResults.length > 0) {
|
|
1015
|
+
fullBlocks = (await Promise.all(
|
|
1016
|
+
relevantBlockResults.slice(0, 5).map(async (result) => {
|
|
1017
|
+
const block = allBlocks.find(
|
|
1018
|
+
(entry) => entry.name.toLowerCase() === result.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 result of tokenSearchResults) {
|
|
1047
|
+
const cat = tokensByName.get(result.name);
|
|
1048
|
+
if (cat) {
|
|
1049
|
+
if (!fullTokens[cat]) fullTokens[cat] = [];
|
|
1050
|
+
fullTokens[cat].push(result.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((result) => {
|
|
1176
|
+
const component = filteredComponents.find(
|
|
1177
|
+
(entry) => entry.name.toLowerCase() === result.name.toLowerCase()
|
|
1178
|
+
);
|
|
1179
|
+
if (!component) return null;
|
|
1180
|
+
return {
|
|
1181
|
+
component,
|
|
1182
|
+
score: result.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 result = {};
|
|
1252
|
+
for (const field of fields) {
|
|
1253
|
+
const parts = field.split(".");
|
|
1254
|
+
let source = obj;
|
|
1255
|
+
let target = result;
|
|
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 result;
|
|
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 result;
|
|
1445
|
+
if (verbosity === "compact" && !resolvedFields?.length) {
|
|
1446
|
+
result = {
|
|
1447
|
+
meta: fullResult.meta,
|
|
1448
|
+
propsSummary: component.propsSummary,
|
|
1449
|
+
metadata: component.metadata
|
|
1450
|
+
};
|
|
1451
|
+
} else if (verbosity === "full") {
|
|
1452
|
+
result = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
|
|
1453
|
+
} else if (resolvedFields && resolvedFields.length > 0) {
|
|
1454
|
+
result = projectFields(fullResult, resolvedFields);
|
|
1455
|
+
} else {
|
|
1456
|
+
const { source: _source, ...withoutSource } = fullResult;
|
|
1457
|
+
result = withoutSource;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
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.`);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
1868
|
+
if (!viewerUrl) {
|
|
1869
|
+
return {
|
|
1870
|
+
content: [{
|
|
1871
|
+
type: "text",
|
|
1872
|
+
text: NO_VIEWER_MSG
|
|
1873
|
+
}],
|
|
1874
|
+
isError: true
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
if (figmaUrl) {
|
|
1878
|
+
try {
|
|
1879
|
+
const result = await compareComponent(viewerUrl, {
|
|
1880
|
+
component: componentName,
|
|
1881
|
+
variant: variantName,
|
|
1882
|
+
props,
|
|
1883
|
+
figmaUrl,
|
|
1884
|
+
threshold
|
|
1885
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1886
|
+
if (result.error) {
|
|
1887
|
+
return {
|
|
1888
|
+
content: [{
|
|
1889
|
+
type: "text",
|
|
1890
|
+
text: `Compare error: ${result.error}${result.suggestion ? `
|
|
1891
|
+
Suggestion: ${result.suggestion}` : ""}`
|
|
1892
|
+
}],
|
|
1893
|
+
isError: true
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
const content = [];
|
|
1897
|
+
const summaryText = result.match ? `MATCH: ${componentName} matches Figma design (${result.diffPercentage}% diff, threshold: ${result.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result.diffPercentage}% (threshold: ${result.threshold}%)`;
|
|
1898
|
+
content.push({ type: "text", text: summaryText });
|
|
1899
|
+
if (result.diff && !result.match) {
|
|
1900
|
+
content.push({
|
|
1901
|
+
type: "image",
|
|
1902
|
+
data: result.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: ${result.changedRegions?.length ?? 0}`
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
content.push({
|
|
1911
|
+
type: "text",
|
|
1912
|
+
text: JSON.stringify({
|
|
1913
|
+
match: result.match,
|
|
1914
|
+
diffPercentage: result.diffPercentage,
|
|
1915
|
+
threshold: result.threshold,
|
|
1916
|
+
figmaUrl: result.figmaUrl,
|
|
1917
|
+
changedRegions: result.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
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
const result = await renderComponent(viewerUrl, {
|
|
1933
|
+
component: componentName,
|
|
1934
|
+
props,
|
|
1935
|
+
variant: variantName,
|
|
1936
|
+
viewport: viewport ?? { width: 800, height: 600 }
|
|
1937
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1938
|
+
if (result.error) {
|
|
1939
|
+
return {
|
|
1940
|
+
content: [{ type: "text", text: `Render error: ${result.error}` }],
|
|
1941
|
+
isError: true
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
return {
|
|
1945
|
+
content: [
|
|
1946
|
+
{
|
|
1947
|
+
type: "image",
|
|
1948
|
+
data: result.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
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
// src/tools/fix.ts
|
|
1969
|
+
var NO_VIEWER_MSG2 = "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.";
|
|
1970
|
+
var fixHandler = async (args, ctx) => {
|
|
1971
|
+
const componentName = args?.component;
|
|
1972
|
+
const variantName = args?.variant ?? void 0;
|
|
1973
|
+
const fixType = args?.fixType ?? "all";
|
|
1974
|
+
if (!componentName) {
|
|
1975
|
+
throw new Error("component is required");
|
|
1976
|
+
}
|
|
1977
|
+
const fragment = findComponentByName(ctx.data.snapshot, componentName);
|
|
1978
|
+
if (!fragment) {
|
|
1979
|
+
const allNames = componentNames(ctx.data.snapshot);
|
|
1980
|
+
const closest = findClosestMatch(componentName, allNames);
|
|
1981
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1982
|
+
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
1983
|
+
}
|
|
1984
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
1985
|
+
if (!viewerUrl) {
|
|
1986
|
+
return {
|
|
1987
|
+
content: [{
|
|
1988
|
+
type: "text",
|
|
1989
|
+
text: NO_VIEWER_MSG2
|
|
1990
|
+
}],
|
|
1991
|
+
isError: true
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
try {
|
|
1995
|
+
const result = await fixComponent(viewerUrl, {
|
|
1996
|
+
component: componentName,
|
|
1997
|
+
variant: variantName,
|
|
1998
|
+
fixType
|
|
1999
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
2000
|
+
if (result.error) {
|
|
2001
|
+
return {
|
|
2002
|
+
content: [{
|
|
2003
|
+
type: "text",
|
|
2004
|
+
text: `Fix generation error: ${result.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: result.patches,
|
|
2017
|
+
summary: result.summary,
|
|
2018
|
+
patchCount: result.patches.length,
|
|
2019
|
+
nextStep: result.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
|
+
};
|
|
2031
|
+
}
|
|
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
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
2054
|
+
if (!viewerUrl) {
|
|
2055
|
+
return {
|
|
2056
|
+
content: [{
|
|
2057
|
+
type: "text",
|
|
2058
|
+
text: NO_VIEWER_MSG3
|
|
2059
|
+
}],
|
|
2060
|
+
isError: true
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
const result = await auditComponent(viewerUrl, {
|
|
2065
|
+
component: componentName,
|
|
2066
|
+
variant: variantName,
|
|
2067
|
+
standard,
|
|
2068
|
+
includeFixPatches
|
|
2069
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
2070
|
+
if (result.error) {
|
|
2071
|
+
return {
|
|
2072
|
+
content: [{
|
|
2073
|
+
type: "text",
|
|
2074
|
+
text: `A11y audit error: ${result.error}`
|
|
2075
|
+
}],
|
|
2076
|
+
isError: true
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
let nextStep;
|
|
2080
|
+
if (result.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 (result.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
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
content: [{
|
|
2089
|
+
type: "text",
|
|
2090
|
+
text: JSON.stringify({
|
|
2091
|
+
component: componentName,
|
|
2092
|
+
variant: variantName ?? "all",
|
|
2093
|
+
standard,
|
|
2094
|
+
totalViolations: result.results.reduce((sum, r) => sum + r.summary.total, 0),
|
|
2095
|
+
variantsPassingAA: `${result.aaPercent}%`,
|
|
2096
|
+
variantsPassingAAA: `${result.aaaPercent}%`,
|
|
2097
|
+
passed: result.passed,
|
|
2098
|
+
...result.emptyAudit && { emptyAudit: true },
|
|
2099
|
+
results: result.results,
|
|
2100
|
+
nextStep
|
|
2101
|
+
})
|
|
2102
|
+
}]
|
|
2103
|
+
};
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
return {
|
|
2106
|
+
content: [{
|
|
2107
|
+
type: "text",
|
|
2108
|
+
text: `Failed to audit component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
2109
|
+
}],
|
|
2110
|
+
isError: true
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
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) {
|
|
2122
|
+
return {
|
|
2123
|
+
text: JSON.stringify({
|
|
2124
|
+
error: "No graph data available. Run `fragments build` to generate the component graph.",
|
|
2125
|
+
hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
|
|
2126
|
+
}),
|
|
2127
|
+
isError: true
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
const graph = deserializeGraph(serializedGraph);
|
|
2131
|
+
const blockData = blocks ? Object.fromEntries(
|
|
2132
|
+
Object.entries(blocks).map(([k, v]) => [k, { components: v.components }])
|
|
2133
|
+
) : void 0;
|
|
2134
|
+
const engine = new ComponentGraphEngine(graph, blockData);
|
|
2135
|
+
const edgeTypes = args.edgeTypes;
|
|
2136
|
+
switch (args.mode) {
|
|
2137
|
+
case "health": {
|
|
2138
|
+
const health = engine.getHealth();
|
|
2139
|
+
const blockCount = blocks ? Object.keys(blocks).length : 0;
|
|
2140
|
+
return {
|
|
2141
|
+
text: JSON.stringify({
|
|
2142
|
+
mode: "health",
|
|
2143
|
+
...health,
|
|
2144
|
+
...health.compositionCoverage === 0 && blockCount === 0 && {
|
|
2145
|
+
compositionNote: "No composition blocks defined yet \u2014 compositionCoverage will increase as blocks are added"
|
|
2146
|
+
},
|
|
2147
|
+
summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
|
|
2148
|
+
})
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
case "dependencies": {
|
|
2152
|
+
if (!args.component) {
|
|
2153
|
+
return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
|
|
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 result = engine.impact(args.component, args.maxDepth ?? 3);
|
|
2211
|
+
return {
|
|
2212
|
+
text: JSON.stringify({
|
|
2213
|
+
mode: "impact",
|
|
2214
|
+
...result,
|
|
2215
|
+
summary: `Changing ${args.component} affects ${result.totalAffected} component(s) and ${result.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 result = engine.path(args.component, args.target);
|
|
2224
|
+
return {
|
|
2225
|
+
text: JSON.stringify({
|
|
2226
|
+
mode: "path",
|
|
2227
|
+
from: args.component,
|
|
2228
|
+
to: args.target,
|
|
2229
|
+
...result,
|
|
2230
|
+
edges: result.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 };
|
|
2263
|
+
}
|
|
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
|
+
}
|
|
2288
|
+
default:
|
|
2289
|
+
return {
|
|
2290
|
+
text: JSON.stringify({
|
|
2291
|
+
error: `Unknown mode: "${args.mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`
|
|
2292
|
+
}),
|
|
2293
|
+
isError: true
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/tools/graph.ts
|
|
2299
|
+
var graphHandler = async (args, ctx) => {
|
|
2300
|
+
const graphArgs = {
|
|
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 result = handleGraphTool(
|
|
2310
|
+
graphArgs,
|
|
2311
|
+
data.graph,
|
|
2312
|
+
data.blocks,
|
|
2313
|
+
allNames
|
|
2314
|
+
);
|
|
2315
|
+
if (result.isError) {
|
|
2316
|
+
return {
|
|
2317
|
+
content: [{ type: "text", text: result.text }],
|
|
2318
|
+
isError: true
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
return {
|
|
2322
|
+
content: [{ type: "text", text: result.text }]
|
|
2323
|
+
};
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
// src/tools/perf.ts
|
|
2327
|
+
var perfHandler = async (args, ctx) => {
|
|
2328
|
+
const componentName = args?.component ?? void 0;
|
|
2329
|
+
const sort = args?.sort ?? "size";
|
|
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) {
|
|
2336
|
+
return {
|
|
2337
|
+
content: [
|
|
2338
|
+
{
|
|
2339
|
+
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
|
+
})
|
|
2345
|
+
}
|
|
2346
|
+
]
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
if (componentName) {
|
|
2350
|
+
entries = entries.filter(
|
|
2351
|
+
(entry) => entry.name.toLowerCase() === componentName.toLowerCase()
|
|
2352
|
+
);
|
|
2353
|
+
if (entries.length === 0) {
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
`No performance data for "${componentName}". Run \`${BRAND.cliCommand} perf --component ${componentName}\` first.`
|
|
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
|
+
}
|
|
2378
|
+
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
|
+
]
|
|
2389
|
+
};
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
// src/tools/govern.ts
|
|
2393
|
+
import {
|
|
2394
|
+
handleGovernTool,
|
|
2395
|
+
formatVerdict,
|
|
2396
|
+
universal,
|
|
2397
|
+
fragments as fragmentsPreset
|
|
2398
|
+
} from "@fragments-sdk/govern";
|
|
2399
|
+
var governHandler = async (args, ctx) => {
|
|
2400
|
+
const spec = args?.spec;
|
|
2401
|
+
if (!spec || typeof spec !== "object") {
|
|
2402
|
+
return {
|
|
2403
|
+
content: [
|
|
2404
|
+
{
|
|
2405
|
+
type: "text",
|
|
2406
|
+
text: JSON.stringify({
|
|
2407
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
2408
|
+
})
|
|
2409
|
+
}
|
|
2410
|
+
],
|
|
2411
|
+
isError: true
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
const policyOverrides = args?.policy;
|
|
2415
|
+
const format = args?.format ?? "json";
|
|
2416
|
+
const allowedComponents = Object.values(ctx.data.components).map(
|
|
2417
|
+
(component) => component.name
|
|
2418
|
+
);
|
|
2419
|
+
const tokenPrefix = ctx.data.tokens?.prefix;
|
|
2420
|
+
const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset().rules } : { rules: universal().rules };
|
|
2421
|
+
basePolicy.rules["components/allow"] = {
|
|
2422
|
+
enabled: true,
|
|
2423
|
+
severity: "serious",
|
|
2424
|
+
options: {
|
|
2425
|
+
components: allowedComponents
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
|
|
2429
|
+
const input = {
|
|
2430
|
+
spec,
|
|
2431
|
+
policy: policyOverrides,
|
|
2432
|
+
format
|
|
2433
|
+
};
|
|
2434
|
+
try {
|
|
2435
|
+
const verdict = await handleGovernTool(input, basePolicy, engineOptions);
|
|
2436
|
+
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
2437
|
+
return {
|
|
2438
|
+
content: [{ type: "text", text }],
|
|
2439
|
+
_meta: {
|
|
2440
|
+
score: verdict.score,
|
|
2441
|
+
passed: verdict.passed,
|
|
2442
|
+
violationCount: verdict.results.reduce(
|
|
2443
|
+
(sum, r) => sum + r.violations.length,
|
|
2444
|
+
0
|
|
2445
|
+
)
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
} catch (error) {
|
|
2449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2450
|
+
const isSpecError = message.includes("Expected") || message.includes("Required");
|
|
2451
|
+
return {
|
|
2452
|
+
content: [
|
|
2453
|
+
{
|
|
2454
|
+
type: "text",
|
|
2455
|
+
text: JSON.stringify({
|
|
2456
|
+
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
|
|
2457
|
+
})
|
|
2458
|
+
}
|
|
2459
|
+
],
|
|
2460
|
+
isError: true
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
|
|
2465
|
+
// src/tools/generate-ui.ts
|
|
2466
|
+
var generateUiHandler = async (args, ctx) => {
|
|
2467
|
+
const prompt = args?.prompt;
|
|
2468
|
+
if (!prompt) {
|
|
2469
|
+
throw new Error("prompt is required");
|
|
2470
|
+
}
|
|
2471
|
+
const currentTree = args?.currentTree;
|
|
2472
|
+
const playgroundUrl = ctx.config.playgroundUrl ?? "https://usefragments.com";
|
|
2473
|
+
const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
|
|
2474
|
+
method: "POST",
|
|
2475
|
+
headers: { "Content-Type": "application/json" },
|
|
2476
|
+
body: JSON.stringify({
|
|
2477
|
+
prompt,
|
|
2478
|
+
...currentTree && { currentSpec: currentTree }
|
|
2479
|
+
})
|
|
2480
|
+
});
|
|
2481
|
+
if (!response.ok) {
|
|
2482
|
+
const errorBody = await response.text();
|
|
2483
|
+
throw new Error(`Playground API error (${response.status}): ${errorBody}`);
|
|
2484
|
+
}
|
|
2485
|
+
const text = await response.text();
|
|
2486
|
+
return {
|
|
2487
|
+
content: [{
|
|
2488
|
+
type: "text",
|
|
2489
|
+
text
|
|
2490
|
+
}]
|
|
2491
|
+
};
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
// src/tools/index.ts
|
|
2495
|
+
var CORE_TOOLS = {
|
|
2496
|
+
discover: discoverHandler,
|
|
2497
|
+
inspect: inspectHandler,
|
|
2498
|
+
blocks: blocksHandler,
|
|
2499
|
+
tokens: tokensHandler,
|
|
2500
|
+
graph: graphHandler,
|
|
2501
|
+
perf: perfHandler,
|
|
2502
|
+
govern: governHandler
|
|
2503
|
+
};
|
|
2504
|
+
var VIEWER_TOOLS = {
|
|
2505
|
+
render: renderHandler,
|
|
2506
|
+
fix: fixHandler,
|
|
2507
|
+
a11y: a11yHandler
|
|
2508
|
+
};
|
|
2509
|
+
var INFRA_TOOLS = {
|
|
2510
|
+
generate_ui: generateUiHandler
|
|
2511
|
+
};
|
|
2512
|
+
var BUILTIN_TOOLS = {
|
|
2513
|
+
...CORE_TOOLS,
|
|
2514
|
+
...VIEWER_TOOLS,
|
|
2515
|
+
...INFRA_TOOLS
|
|
2516
|
+
};
|
|
2517
|
+
var TOOL_CAPABILITIES = {
|
|
2518
|
+
discover: ["components"],
|
|
2519
|
+
inspect: ["components"],
|
|
2520
|
+
blocks: ["blocks"],
|
|
2521
|
+
tokens: ["tokens"],
|
|
2522
|
+
graph: ["graph"],
|
|
2523
|
+
perf: ["performance"],
|
|
2524
|
+
render: ["components"],
|
|
2525
|
+
fix: ["components"],
|
|
2526
|
+
a11y: ["components"]
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
// src/registry.ts
|
|
2530
|
+
var ToolRegistry = class {
|
|
2531
|
+
tools = /* @__PURE__ */ new Map();
|
|
2532
|
+
prefix;
|
|
2533
|
+
onChanged;
|
|
2534
|
+
constructor(prefix, opts) {
|
|
2535
|
+
this.prefix = prefix;
|
|
2536
|
+
this.onChanged = opts?.onChanged;
|
|
2537
|
+
}
|
|
2538
|
+
/** Register a single tool */
|
|
2539
|
+
register(key, handler, definition, availability = "always", requiredCapabilities = []) {
|
|
2540
|
+
this.tools.set(key, {
|
|
2541
|
+
key,
|
|
2542
|
+
handler,
|
|
2543
|
+
definition,
|
|
2544
|
+
availability,
|
|
2545
|
+
requiredCapabilities
|
|
2546
|
+
});
|
|
2547
|
+
this.onChanged?.();
|
|
2548
|
+
}
|
|
2549
|
+
/** Unregister a tool by key */
|
|
2550
|
+
unregister(key) {
|
|
2551
|
+
if (this.tools.delete(key)) {
|
|
2552
|
+
this.onChanged?.();
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
/** Bulk register built-in tools with availability metadata */
|
|
2556
|
+
registerBuiltins(toolSets, definitions, capabilityByKey = {}) {
|
|
2557
|
+
const defMap = new Map(definitions.map((d) => [d.key, d]));
|
|
2558
|
+
for (const [key, handler] of Object.entries(toolSets.core)) {
|
|
2559
|
+
this.tools.set(key, {
|
|
2560
|
+
key,
|
|
2561
|
+
handler,
|
|
2562
|
+
definition: defMap.get(key),
|
|
2563
|
+
availability: "always",
|
|
2564
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
for (const [key, handler] of Object.entries(toolSets.viewer)) {
|
|
2568
|
+
this.tools.set(key, {
|
|
2569
|
+
key,
|
|
2570
|
+
handler,
|
|
2571
|
+
definition: defMap.get(key),
|
|
2572
|
+
availability: "viewer",
|
|
2573
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
for (const [key, handler] of Object.entries(toolSets.infra)) {
|
|
2577
|
+
this.tools.set(key, {
|
|
2578
|
+
key,
|
|
2579
|
+
handler,
|
|
2580
|
+
definition: defMap.get(key),
|
|
2581
|
+
availability: "playground",
|
|
2582
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
/** Get handler for a tool (by unprefixed key or prefixed name) */
|
|
2587
|
+
getHandler(nameOrKey) {
|
|
2588
|
+
const direct = this.tools.get(nameOrKey);
|
|
2589
|
+
if (direct) return direct.handler;
|
|
2590
|
+
if (!this.prefix) return void 0;
|
|
2591
|
+
const prefixStr = this.prefix + "_";
|
|
2592
|
+
if (nameOrKey.startsWith(prefixStr)) {
|
|
2593
|
+
const key = nameOrKey.slice(prefixStr.length);
|
|
2594
|
+
return this.tools.get(key)?.handler;
|
|
2595
|
+
}
|
|
2596
|
+
return void 0;
|
|
2597
|
+
}
|
|
2598
|
+
/** Resolve unprefixed key from a potentially prefixed tool name */
|
|
2599
|
+
resolveKey(name) {
|
|
2600
|
+
if (!this.prefix) return name;
|
|
2601
|
+
const prefixStr = this.prefix + "_";
|
|
2602
|
+
return name.startsWith(prefixStr) ? name.slice(prefixStr.length) : name;
|
|
2603
|
+
}
|
|
2604
|
+
/** List available tools as MCP Tool[] based on current availability context */
|
|
2605
|
+
listTools(ctx, allToolSchemas) {
|
|
2606
|
+
const availableKeys = /* @__PURE__ */ new Set();
|
|
2607
|
+
for (const [key, tool] of this.tools) {
|
|
2608
|
+
const hasCapabilities = tool.requiredCapabilities.every(
|
|
2609
|
+
(capability) => ctx.capabilities.has(capability)
|
|
2610
|
+
);
|
|
2611
|
+
if (!hasCapabilities) continue;
|
|
2612
|
+
if (tool.availability === "always") {
|
|
2613
|
+
availableKeys.add(key);
|
|
2614
|
+
} else if (tool.availability === "viewer" && ctx.hasViewer) {
|
|
2615
|
+
availableKeys.add(key);
|
|
2616
|
+
} else if (tool.availability === "playground" && ctx.hasPlayground) {
|
|
2617
|
+
availableKeys.add(key);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (!this.prefix) {
|
|
2621
|
+
return allToolSchemas.filter((t) => availableKeys.has(t.name));
|
|
2622
|
+
}
|
|
2623
|
+
const prefixStr = this.prefix + "_";
|
|
2624
|
+
return allToolSchemas.filter((t) => {
|
|
2625
|
+
const key = t.name.startsWith(prefixStr) ? t.name.slice(prefixStr.length) : t.name;
|
|
2626
|
+
return availableKeys.has(key);
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
/** Execute a tool by name, dispatching to its handler */
|
|
2630
|
+
async execute(name, args, ctx) {
|
|
2631
|
+
const key = this.resolveKey(name);
|
|
2632
|
+
const registered = this.tools.get(key);
|
|
2633
|
+
if (!registered) {
|
|
2634
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2635
|
+
}
|
|
2636
|
+
return registered.handler(args, ctx);
|
|
2637
|
+
}
|
|
2638
|
+
/** Get the number of registered tools */
|
|
2639
|
+
get size() {
|
|
2640
|
+
return this.tools.size;
|
|
2641
|
+
}
|
|
2642
|
+
/** Get all registered keys */
|
|
2643
|
+
keys() {
|
|
2644
|
+
return Array.from(this.tools.keys());
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
// src/middleware.ts
|
|
2649
|
+
function executeWithMiddleware(middlewares, mCtx, handler) {
|
|
2650
|
+
const chain = [...middlewares].reverse().reduce(
|
|
2651
|
+
(next, mw) => () => mw(mCtx, next),
|
|
2652
|
+
handler
|
|
2653
|
+
);
|
|
2654
|
+
return chain();
|
|
2655
|
+
}
|
|
2656
|
+
function telemetryMiddleware(logger) {
|
|
2657
|
+
return async (mCtx, next) => {
|
|
2658
|
+
const start = Date.now();
|
|
2659
|
+
const result = await next();
|
|
2660
|
+
logger(`${mCtx.toolKey}: ${Date.now() - start}ms`);
|
|
2661
|
+
return result;
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/source-selection.ts
|
|
2666
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2667
|
+
import { join as join7 } from "path";
|
|
2668
|
+
|
|
2669
|
+
// src/adapters/fragments-json.ts
|
|
2670
|
+
import { readFile } from "fs/promises";
|
|
2671
|
+
|
|
2672
|
+
// src/discovery.ts
|
|
2673
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
2674
|
+
import { join as join3, dirname, resolve } from "path";
|
|
2675
|
+
import { createRequire } from "module";
|
|
2676
|
+
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
2677
|
+
const parts = pattern.split("/");
|
|
2678
|
+
let dirs = [baseDir];
|
|
2679
|
+
for (const part of parts) {
|
|
2680
|
+
if (part === "**") continue;
|
|
2681
|
+
const next = [];
|
|
2682
|
+
for (const d of dirs) {
|
|
2683
|
+
if (part === "*") {
|
|
2684
|
+
try {
|
|
2685
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
2686
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2687
|
+
next.push(join3(d, entry.name));
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
} else {
|
|
2693
|
+
const candidate = join3(d, part);
|
|
2694
|
+
if (existsSync3(candidate)) next.push(candidate);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
dirs = next;
|
|
2698
|
+
}
|
|
2699
|
+
return dirs;
|
|
2700
|
+
}
|
|
2701
|
+
function getWorkspaceDirs(rootDir) {
|
|
2702
|
+
const dirs = [];
|
|
2703
|
+
const rootPkgPath = join3(rootDir, "package.json");
|
|
2704
|
+
if (existsSync3(rootPkgPath)) {
|
|
2705
|
+
try {
|
|
2706
|
+
const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
|
|
2707
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
2708
|
+
if (Array.isArray(workspaces)) {
|
|
2709
|
+
for (const pattern of workspaces) {
|
|
2710
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
|
|
2711
|
+
}
|
|
2712
|
+
return dirs;
|
|
2713
|
+
}
|
|
2714
|
+
} catch {
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
const pnpmWsPath = join3(rootDir, "pnpm-workspace.yaml");
|
|
2718
|
+
if (existsSync3(pnpmWsPath)) {
|
|
2719
|
+
try {
|
|
2720
|
+
const content = readFileSync3(pnpmWsPath, "utf-8");
|
|
2721
|
+
const lines = content.split("\n");
|
|
2722
|
+
let inPackages = false;
|
|
2723
|
+
for (const line of lines) {
|
|
2724
|
+
if (/^packages\s*:/.test(line)) {
|
|
2725
|
+
inPackages = true;
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
if (inPackages) {
|
|
2729
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
|
|
2730
|
+
if (match) {
|
|
2731
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
|
|
2732
|
+
} else if (/^\S/.test(line) && line.trim()) {
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return dirs;
|
|
2741
|
+
}
|
|
2742
|
+
function resolveDepPackageJson(localRequire, depName) {
|
|
2743
|
+
try {
|
|
2744
|
+
return localRequire.resolve(`${depName}/package.json`);
|
|
2745
|
+
} catch {
|
|
2746
|
+
}
|
|
2747
|
+
try {
|
|
2748
|
+
const mainPath = localRequire.resolve(depName);
|
|
2749
|
+
let dir = dirname(mainPath);
|
|
2750
|
+
while (true) {
|
|
2751
|
+
const candidate = join3(dir, "package.json");
|
|
2752
|
+
if (existsSync3(candidate)) {
|
|
2753
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
2754
|
+
if (pkg.name === depName) return candidate;
|
|
2755
|
+
}
|
|
2756
|
+
const parent = dirname(dir);
|
|
2757
|
+
if (parent === dir) break;
|
|
2758
|
+
dir = parent;
|
|
2759
|
+
}
|
|
2760
|
+
} catch {
|
|
2761
|
+
}
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
function findFragmentsInDeps(dir, found, depField) {
|
|
2765
|
+
const pkgJsonPath = join3(dir, "package.json");
|
|
2766
|
+
if (!existsSync3(pkgJsonPath)) return;
|
|
2767
|
+
try {
|
|
2768
|
+
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
|
|
2769
|
+
const allDeps = {
|
|
2770
|
+
...pkgJson.dependencies,
|
|
2771
|
+
...pkgJson.devDependencies
|
|
2772
|
+
};
|
|
2773
|
+
const localRequire = createRequire(join3(dir, "noop.js"));
|
|
2774
|
+
for (const depName of Object.keys(allDeps)) {
|
|
2775
|
+
try {
|
|
2776
|
+
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
2777
|
+
if (!depPkgPath) continue;
|
|
2778
|
+
const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
|
|
2779
|
+
if (depPkg[depField]) {
|
|
2780
|
+
const fragmentsPath = join3(dirname(depPkgPath), depPkg[depField]);
|
|
2781
|
+
if (existsSync3(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
2782
|
+
found.push(fragmentsPath);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
} catch {
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
} catch {
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
function findDesignSystemJson(startDir, outFile, depField) {
|
|
2792
|
+
const found = [];
|
|
2793
|
+
const resolvedStart = resolve(startDir);
|
|
2794
|
+
let dir = resolvedStart;
|
|
2795
|
+
while (true) {
|
|
2796
|
+
const candidate = join3(dir, outFile);
|
|
2797
|
+
if (existsSync3(candidate)) {
|
|
2798
|
+
found.push(candidate);
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
const parent = dirname(dir);
|
|
2802
|
+
if (parent === dir) break;
|
|
2803
|
+
dir = parent;
|
|
2804
|
+
}
|
|
2805
|
+
findFragmentsInDeps(resolvedStart, found, depField);
|
|
2806
|
+
if (found.length === 0 || existsSync3(join3(resolvedStart, "pnpm-workspace.yaml"))) {
|
|
2807
|
+
const workspaceDirs = getWorkspaceDirs(resolvedStart);
|
|
2808
|
+
for (const wsDir of workspaceDirs) {
|
|
2809
|
+
findFragmentsInDeps(wsDir, found, depField);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return found;
|
|
2813
|
+
}
|
|
2814
|
+
function findFragmentsJson(startDir) {
|
|
2815
|
+
return findDesignSystemJson(startDir, BRAND.outFile, "fragments");
|
|
2816
|
+
}
|
|
2817
|
+
function findBundleManifest(startDir) {
|
|
2818
|
+
const found = [];
|
|
2819
|
+
let dir = resolve(startDir);
|
|
2820
|
+
while (true) {
|
|
2821
|
+
const candidate = join3(dir, BRAND.dataDir, BRAND.manifestFile);
|
|
2822
|
+
if (existsSync3(candidate)) {
|
|
2823
|
+
found.push(candidate);
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2826
|
+
const parent = dirname(dir);
|
|
2827
|
+
if (parent === dir) break;
|
|
2828
|
+
dir = parent;
|
|
2829
|
+
}
|
|
2830
|
+
return found;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// src/adapters/snapshot-converters.ts
|
|
2834
|
+
import { mcpSnapshotSchema } from "@fragments-sdk/core";
|
|
2835
|
+
function slugify(value) {
|
|
2836
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
2837
|
+
}
|
|
2838
|
+
function buildComponentId(args) {
|
|
2839
|
+
const base = args.filePath ?? args.packageName ?? args.name;
|
|
2840
|
+
return `${slugify(args.name)}:${base}`;
|
|
2841
|
+
}
|
|
2842
|
+
function valueToString(value) {
|
|
2843
|
+
if (value === void 0) return void 0;
|
|
2844
|
+
if (typeof value === "string") return value;
|
|
2845
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
2846
|
+
return String(value);
|
|
2847
|
+
}
|
|
2848
|
+
try {
|
|
2849
|
+
return JSON.stringify(value);
|
|
2850
|
+
} catch {
|
|
2851
|
+
return void 0;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
function buildCompoundChildren(contract, ai) {
|
|
2855
|
+
const contractChildren = Object.entries(contract?.compoundChildren ?? {});
|
|
2856
|
+
if (contractChildren.length > 0) {
|
|
2857
|
+
return contractChildren.map(([childName, child]) => ({
|
|
2858
|
+
name: childName,
|
|
2859
|
+
description: child.description,
|
|
2860
|
+
required: child.required,
|
|
2861
|
+
accepts: child.accepts,
|
|
2862
|
+
visibility: "public"
|
|
2863
|
+
}));
|
|
2864
|
+
}
|
|
2865
|
+
const subs = ai?.subComponents;
|
|
2866
|
+
if (!subs || subs.length === 0) return [];
|
|
2867
|
+
const requiredSet = new Set(ai?.requiredChildren ?? []);
|
|
2868
|
+
return subs.map((name) => ({
|
|
2869
|
+
name,
|
|
2870
|
+
required: requiredSet.has(name) || void 0,
|
|
2871
|
+
visibility: "public"
|
|
2872
|
+
}));
|
|
2873
|
+
}
|
|
2874
|
+
function componentFromCompiledFragment(args) {
|
|
2875
|
+
const { fragment, sourceType } = args;
|
|
2876
|
+
const name = fragment.meta.name;
|
|
2877
|
+
const description = fragment.meta.description ?? "";
|
|
2878
|
+
const usage = fragment.usage ?? { when: [], whenNot: [] };
|
|
2879
|
+
const contract = fragment.contract;
|
|
2880
|
+
const ai = fragment.ai;
|
|
2881
|
+
const guidancePatterns = (fragment._cloudPatterns ?? []).map((pattern) => ({
|
|
2882
|
+
name: pattern.name ?? "Pattern",
|
|
2883
|
+
description: pattern.description
|
|
2884
|
+
}));
|
|
2885
|
+
return {
|
|
2886
|
+
id: args.id ?? buildComponentId({
|
|
2887
|
+
name,
|
|
2888
|
+
filePath: fragment.sourcePath ?? fragment.filePath,
|
|
2889
|
+
packageName: args.packageName
|
|
2890
|
+
}),
|
|
2891
|
+
name,
|
|
2892
|
+
description,
|
|
2893
|
+
category: fragment.meta.category ?? "uncategorized",
|
|
2894
|
+
status: fragment.meta.status ?? "stable",
|
|
2895
|
+
tags: fragment.meta.tags ?? [],
|
|
2896
|
+
props: Object.fromEntries(
|
|
2897
|
+
Object.entries(fragment.props ?? {}).map(([propName, prop]) => [
|
|
2898
|
+
propName,
|
|
2899
|
+
{
|
|
2900
|
+
type: prop.type,
|
|
2901
|
+
description: prop.description ?? "",
|
|
2902
|
+
required: prop.required ?? false,
|
|
2903
|
+
default: prop.default,
|
|
2904
|
+
values: prop.values ? [...prop.values] : void 0,
|
|
2905
|
+
constraints: prop.constraints ? [...prop.constraints] : void 0
|
|
2906
|
+
}
|
|
2907
|
+
])
|
|
2908
|
+
),
|
|
2909
|
+
propsSummary: fragment.propsSummary ?? contract?.propsSummary ?? Object.entries(fragment.props ?? {}).map(
|
|
2910
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
|
|
2911
|
+
),
|
|
2912
|
+
examples: (fragment.variants ?? []).map((variant) => ({
|
|
2913
|
+
name: variant.name,
|
|
2914
|
+
description: variant.description,
|
|
2915
|
+
code: variant.code,
|
|
2916
|
+
kind: "example"
|
|
2917
|
+
})),
|
|
2918
|
+
relations: (fragment.relations ?? []).map((relation) => ({
|
|
2919
|
+
componentName: relation.component,
|
|
2920
|
+
relationship: relation.relationship,
|
|
2921
|
+
note: relation.note
|
|
2922
|
+
})),
|
|
2923
|
+
compoundChildren: buildCompoundChildren(contract, ai),
|
|
2924
|
+
guidance: {
|
|
2925
|
+
when: usage.when ?? [],
|
|
2926
|
+
whenNot: usage.whenNot ?? [],
|
|
2927
|
+
guidelines: usage.guidelines ?? [],
|
|
2928
|
+
accessibility: usage.accessibility ?? [],
|
|
2929
|
+
dos: usage.when ?? [],
|
|
2930
|
+
donts: usage.whenNot ?? [],
|
|
2931
|
+
patterns: guidancePatterns.length > 0 ? guidancePatterns : (ai?.commonPatterns ?? []).map((pattern) => ({
|
|
2932
|
+
name: pattern
|
|
2933
|
+
}))
|
|
2934
|
+
},
|
|
2935
|
+
sourceType,
|
|
2936
|
+
sourcePath: fragment.sourcePath ?? fragment.filePath,
|
|
2937
|
+
packageName: args.packageName,
|
|
2938
|
+
importPath: args.importPath ?? args.packageName,
|
|
2939
|
+
performance: fragment.performance ? {
|
|
2940
|
+
bundleSize: fragment.performance.bundleSize,
|
|
2941
|
+
rawSize: fragment.performance.rawSize,
|
|
2942
|
+
complexity: fragment.performance.complexity,
|
|
2943
|
+
budgetPercent: fragment.performance.budgetPercent,
|
|
2944
|
+
overBudget: fragment.performance.overBudget,
|
|
2945
|
+
measuredAt: fragment.performance.measuredAt,
|
|
2946
|
+
imports: fragment.performance.imports?.map((entry) => ({
|
|
2947
|
+
path: entry.path,
|
|
2948
|
+
bytes: entry.bytes,
|
|
2949
|
+
percent: entry.percent
|
|
2950
|
+
}))
|
|
2951
|
+
} : void 0,
|
|
2952
|
+
metadata: {
|
|
2953
|
+
a11yRules: contract?.a11yRules ?? [],
|
|
2954
|
+
scenarioTags: contract?.scenarioTags ?? []
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function blockFromCompiledBlock(key, block) {
|
|
2959
|
+
return {
|
|
2960
|
+
id: key,
|
|
2961
|
+
name: block.name,
|
|
2962
|
+
description: block.description ?? "",
|
|
2963
|
+
category: block.category ?? "uncategorized",
|
|
2964
|
+
components: block.components ?? [],
|
|
2965
|
+
tags: block.tags ?? [],
|
|
2966
|
+
code: block.code ?? ""
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function tokensFromCompiledTokenData(tokens) {
|
|
2970
|
+
const flat = [];
|
|
2971
|
+
const categories = {};
|
|
2972
|
+
for (const [category, entries] of Object.entries(tokens.categories)) {
|
|
2973
|
+
const normalized = entries.map((entry) => ({
|
|
2974
|
+
name: entry.name,
|
|
2975
|
+
category,
|
|
2976
|
+
value: valueToString(entry.value),
|
|
2977
|
+
description: entry.description
|
|
2978
|
+
}));
|
|
2979
|
+
categories[category] = normalized;
|
|
2980
|
+
flat.push(...normalized);
|
|
2981
|
+
}
|
|
2982
|
+
return {
|
|
2983
|
+
prefix: tokens.prefix,
|
|
2984
|
+
total: tokens.total,
|
|
2985
|
+
categories,
|
|
2986
|
+
flat
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
function buildCapabilities(args) {
|
|
2990
|
+
const capabilities = /* @__PURE__ */ new Set();
|
|
2991
|
+
if (Object.keys(args.components).length > 0) {
|
|
2992
|
+
capabilities.add("components");
|
|
2993
|
+
}
|
|
2994
|
+
if (args.tokens && args.tokens.total > 0) {
|
|
2995
|
+
capabilities.add("tokens");
|
|
2996
|
+
}
|
|
2997
|
+
if (args.blocks && Object.keys(args.blocks).length > 0) {
|
|
2998
|
+
capabilities.add("blocks");
|
|
2999
|
+
}
|
|
3000
|
+
if (args.graph) {
|
|
3001
|
+
capabilities.add("graph");
|
|
3002
|
+
}
|
|
3003
|
+
const hasPerformance = Boolean(args.performanceSummary) || Object.values(args.components).some((component) => Boolean(component.performance));
|
|
3004
|
+
if (hasPerformance) {
|
|
3005
|
+
capabilities.add("performance");
|
|
3006
|
+
}
|
|
3007
|
+
return Array.from(capabilities);
|
|
3008
|
+
}
|
|
3009
|
+
function validateSnapshot(snapshot) {
|
|
3010
|
+
return mcpSnapshotSchema.parse(snapshot);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// src/adapters/fragments-json.ts
|
|
3014
|
+
var FragmentsJsonAdapter = class {
|
|
3015
|
+
name = "fragments-json";
|
|
3016
|
+
discover(startDir) {
|
|
3017
|
+
return findFragmentsJson(startDir);
|
|
3018
|
+
}
|
|
3019
|
+
async load(projectRoot) {
|
|
3020
|
+
const paths = this.discover(projectRoot);
|
|
3021
|
+
if (paths.length === 0) {
|
|
3022
|
+
throw new Error(
|
|
3023
|
+
`No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
|
|
3024
|
+
|
|
3025
|
+
Fix: Add a project-level MCP config so the server runs from your workspace root:
|
|
3026
|
+
|
|
3027
|
+
Cursor: .cursor/mcp.json
|
|
3028
|
+
VS Code: .vscode/mcp.json
|
|
3029
|
+
Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
|
|
3030
|
+
Windsurf: .windsurf/mcp.json
|
|
3031
|
+
|
|
3032
|
+
Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
|
|
3033
|
+
|
|
3034
|
+
If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
const content = await readFile(paths[0], "utf-8");
|
|
3038
|
+
const primary = JSON.parse(content);
|
|
3039
|
+
if (!primary.blocks && primary.recipes) {
|
|
3040
|
+
primary.blocks = primary.recipes;
|
|
3041
|
+
}
|
|
3042
|
+
const packageMap = {};
|
|
3043
|
+
if (primary.packageName) {
|
|
3044
|
+
for (const name of Object.keys(primary.fragments)) {
|
|
3045
|
+
packageMap[name] = primary.packageName;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
for (let i = 1; i < paths.length; i++) {
|
|
3049
|
+
const extra = JSON.parse(await readFile(paths[i], "utf-8"));
|
|
3050
|
+
if (extra.packageName) {
|
|
3051
|
+
for (const name of Object.keys(extra.fragments)) {
|
|
3052
|
+
packageMap[name] = extra.packageName;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
Object.assign(primary.fragments, extra.fragments);
|
|
3056
|
+
const extraBlocks = extra.blocks ?? extra.recipes;
|
|
3057
|
+
if (extraBlocks) {
|
|
3058
|
+
primary.blocks = { ...primary.blocks, ...extraBlocks };
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const components = Object.fromEntries(
|
|
3062
|
+
Object.entries(primary.fragments).map(([key, fragment]) => [
|
|
3063
|
+
key,
|
|
3064
|
+
componentFromCompiledFragment({
|
|
3065
|
+
id: key,
|
|
3066
|
+
fragment,
|
|
3067
|
+
sourceType: "fragments-json",
|
|
3068
|
+
packageName: packageMap[fragment.meta.name] ?? primary.packageName,
|
|
3069
|
+
importPath: packageMap[fragment.meta.name] ?? primary.packageName
|
|
3070
|
+
})
|
|
3071
|
+
])
|
|
3072
|
+
);
|
|
3073
|
+
const blocks = primary.blocks ? Object.fromEntries(
|
|
3074
|
+
Object.entries(primary.blocks).map(([key, block]) => [
|
|
3075
|
+
key,
|
|
3076
|
+
blockFromCompiledBlock(key, block)
|
|
3077
|
+
])
|
|
3078
|
+
) : void 0;
|
|
3079
|
+
const tokens = primary.tokens ? tokensFromCompiledTokenData(primary.tokens) : void 0;
|
|
3080
|
+
const snapshot = validateSnapshot({
|
|
3081
|
+
schemaVersion: 1,
|
|
3082
|
+
sourceType: "fragments-json",
|
|
3083
|
+
sourceLabel: BRAND.outFile,
|
|
3084
|
+
capabilities: buildCapabilities({
|
|
3085
|
+
components,
|
|
3086
|
+
blocks,
|
|
3087
|
+
tokens,
|
|
3088
|
+
graph: primary.graph,
|
|
3089
|
+
performanceSummary: primary.performanceSummary
|
|
3090
|
+
}),
|
|
3091
|
+
metadata: {
|
|
3092
|
+
designSystemName: primary.packageName,
|
|
3093
|
+
packageName: primary.packageName,
|
|
3094
|
+
importPath: primary.packageName
|
|
3095
|
+
},
|
|
3096
|
+
components,
|
|
3097
|
+
blocks,
|
|
3098
|
+
tokens,
|
|
3099
|
+
graph: primary.graph,
|
|
3100
|
+
performanceSummary: primary.performanceSummary,
|
|
3101
|
+
packageMap,
|
|
3102
|
+
defaultPackageName: primary.packageName
|
|
3103
|
+
});
|
|
3104
|
+
return {
|
|
3105
|
+
snapshot,
|
|
3106
|
+
components: snapshot.components,
|
|
3107
|
+
blocks: snapshot.blocks,
|
|
3108
|
+
tokens: snapshot.tokens,
|
|
3109
|
+
graph: primary.graph,
|
|
3110
|
+
performanceSummary: primary.performanceSummary,
|
|
3111
|
+
packageMap: snapshot.packageMap,
|
|
3112
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
3113
|
+
capabilities: new Set(snapshot.capabilities)
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
|
|
3118
|
+
// src/adapters/auto-extract.ts
|
|
3119
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
3120
|
+
import { join as join6, relative, sep } from "path";
|
|
3121
|
+
|
|
3122
|
+
// src/adapters/discover-components.ts
|
|
3123
|
+
import { readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
|
|
3124
|
+
import { join as join4, extname, basename } from "path";
|
|
3125
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
3126
|
+
"node_modules",
|
|
3127
|
+
"dist",
|
|
3128
|
+
"build",
|
|
3129
|
+
".next",
|
|
3130
|
+
".nuxt",
|
|
3131
|
+
"coverage",
|
|
3132
|
+
"__tests__",
|
|
3133
|
+
"__mocks__",
|
|
3134
|
+
".git",
|
|
3135
|
+
".cache",
|
|
3136
|
+
".turbo",
|
|
3137
|
+
"out"
|
|
3138
|
+
]);
|
|
3139
|
+
var EXCLUDED_PATTERNS = [
|
|
3140
|
+
/\.test\./,
|
|
3141
|
+
/\.spec\./,
|
|
3142
|
+
/\.stories\./,
|
|
3143
|
+
/\.story\./,
|
|
3144
|
+
/\.fragment\./,
|
|
3145
|
+
/\.d\.ts$/,
|
|
3146
|
+
/\.config\./,
|
|
3147
|
+
/\.mock\./,
|
|
3148
|
+
/\.fixture\./
|
|
3149
|
+
];
|
|
3150
|
+
function discoverComponentFiles(projectRoot) {
|
|
3151
|
+
const results = [];
|
|
3152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3153
|
+
const scanDirs = [
|
|
3154
|
+
"src/components",
|
|
3155
|
+
"components",
|
|
3156
|
+
"lib/components",
|
|
3157
|
+
"src/ui",
|
|
3158
|
+
"lib/ui",
|
|
3159
|
+
"packages"
|
|
3160
|
+
].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
|
|
3161
|
+
if (scanDirs.length === 0) {
|
|
3162
|
+
const srcDir = join4(projectRoot, "src");
|
|
3163
|
+
if (existsSync4(srcDir)) scanDirs.push(srcDir);
|
|
3164
|
+
}
|
|
3165
|
+
for (const dir of scanDirs) {
|
|
3166
|
+
walkDir(dir, results, seen);
|
|
3167
|
+
}
|
|
3168
|
+
return results;
|
|
3169
|
+
}
|
|
3170
|
+
function walkDir(dir, results, seen, depth = 0) {
|
|
3171
|
+
if (depth > 6) return;
|
|
3172
|
+
let entries;
|
|
3173
|
+
try {
|
|
3174
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
3175
|
+
} catch {
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
for (const entry of entries) {
|
|
3179
|
+
if (entry.name.startsWith(".")) continue;
|
|
3180
|
+
if (entry.isDirectory()) {
|
|
3181
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
3182
|
+
walkDir(join4(dir, entry.name), results, seen, depth + 1);
|
|
3183
|
+
continue;
|
|
3184
|
+
}
|
|
3185
|
+
if (!entry.isFile()) continue;
|
|
3186
|
+
const ext = extname(entry.name);
|
|
3187
|
+
if (ext !== ".tsx" && ext !== ".jsx") continue;
|
|
3188
|
+
if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
|
|
3189
|
+
const filePath = join4(dir, entry.name);
|
|
3190
|
+
if (seen.has(filePath)) continue;
|
|
3191
|
+
seen.add(filePath);
|
|
3192
|
+
const name = inferComponentName(entry.name, dir);
|
|
3193
|
+
if (name) {
|
|
3194
|
+
results.push({ filePath, componentName: name });
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
function inferComponentName(fileName, dirPath) {
|
|
3199
|
+
const withoutExt = fileName.replace(/\.(tsx|jsx)$/, "");
|
|
3200
|
+
if (withoutExt === "index") {
|
|
3201
|
+
return basename(dirPath);
|
|
3202
|
+
}
|
|
3203
|
+
return withoutExt;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/adapters/scan-tokens.ts
|
|
3207
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
3208
|
+
import { join as join5, extname as extname2 } from "path";
|
|
3209
|
+
function scanTokens(projectRoot) {
|
|
3210
|
+
const cssFiles = discoverCssFiles(projectRoot);
|
|
3211
|
+
if (cssFiles.length === 0) return void 0;
|
|
3212
|
+
const allTokens = [];
|
|
3213
|
+
let prefix = "";
|
|
3214
|
+
for (const filePath of cssFiles) {
|
|
3215
|
+
try {
|
|
3216
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
3217
|
+
const tokens = extractCustomProperties(content);
|
|
3218
|
+
allTokens.push(...tokens);
|
|
3219
|
+
} catch {
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
if (allTokens.length === 0) return void 0;
|
|
3224
|
+
prefix = detectPrefix(allTokens.map((t) => t.name));
|
|
3225
|
+
const categories = {};
|
|
3226
|
+
for (const token of allTokens) {
|
|
3227
|
+
const category = inferTokenCategory(token.name);
|
|
3228
|
+
if (!categories[category]) categories[category] = [];
|
|
3229
|
+
if (!categories[category].some((t) => t.name === token.name)) {
|
|
3230
|
+
categories[category].push(token);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
return {
|
|
3234
|
+
prefix,
|
|
3235
|
+
total: Object.values(categories).reduce((sum, arr) => sum + arr.length, 0),
|
|
3236
|
+
categories
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
function discoverCssFiles(projectRoot) {
|
|
3240
|
+
const files = [];
|
|
3241
|
+
const searchDirs = [
|
|
3242
|
+
"src",
|
|
3243
|
+
"styles",
|
|
3244
|
+
"css",
|
|
3245
|
+
"app"
|
|
3246
|
+
].map((d) => join5(projectRoot, d)).filter((d) => existsSync5(d));
|
|
3247
|
+
searchDirs.push(projectRoot);
|
|
3248
|
+
for (const dir of searchDirs) {
|
|
3249
|
+
try {
|
|
3250
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
3251
|
+
for (const entry of entries) {
|
|
3252
|
+
if (!entry.isFile()) continue;
|
|
3253
|
+
const ext = extname2(entry.name);
|
|
3254
|
+
if (ext === ".css" || ext === ".scss") {
|
|
3255
|
+
files.push(join5(dir, entry.name));
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
} catch {
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
const srcDir = join5(projectRoot, "src");
|
|
3263
|
+
if (existsSync5(srcDir)) {
|
|
3264
|
+
try {
|
|
3265
|
+
for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
3266
|
+
if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
|
|
3267
|
+
const subDir = join5(srcDir, subEntry.name);
|
|
3268
|
+
for (const file of readdirSync3(subDir, { withFileTypes: true })) {
|
|
3269
|
+
if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
|
|
3270
|
+
files.push(join5(subDir, file.name));
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
} catch {
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
return [...new Set(files)];
|
|
3279
|
+
}
|
|
3280
|
+
function extractCustomProperties(content) {
|
|
3281
|
+
const tokens = [];
|
|
3282
|
+
let inRelevantBlock = false;
|
|
3283
|
+
let braceDepth = 0;
|
|
3284
|
+
const relevantSelectors = [":root", ".dark", "[data-theme", "@theme"];
|
|
3285
|
+
const lines = content.split("\n");
|
|
3286
|
+
for (const line of lines) {
|
|
3287
|
+
const trimmed = line.trim();
|
|
3288
|
+
if (!inRelevantBlock && braceDepth === 0) {
|
|
3289
|
+
if (relevantSelectors.some((sel) => trimmed.startsWith(sel) || trimmed.includes(sel))) {
|
|
3290
|
+
if (trimmed.includes("{")) {
|
|
3291
|
+
inRelevantBlock = true;
|
|
3292
|
+
braceDepth = 1;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (inRelevantBlock) {
|
|
3297
|
+
for (const ch of trimmed) {
|
|
3298
|
+
if (ch === "{") braceDepth++;
|
|
3299
|
+
else if (ch === "}") {
|
|
3300
|
+
braceDepth--;
|
|
3301
|
+
if (braceDepth <= 0) {
|
|
3302
|
+
inRelevantBlock = false;
|
|
3303
|
+
braceDepth = 0;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (!inRelevantBlock && braceDepth === 0) continue;
|
|
3309
|
+
const match = trimmed.match(/^(--[\w-]+)\s*:\s*(.+?)\s*;/);
|
|
3310
|
+
if (match) {
|
|
3311
|
+
const [, name, value] = match;
|
|
3312
|
+
const commentMatch = value.match(/\/\*\s*(.+?)\s*\*\//);
|
|
3313
|
+
const cleanValue = value.replace(/\/\*.*?\*\//, "").trim();
|
|
3314
|
+
tokens.push({
|
|
3315
|
+
name,
|
|
3316
|
+
value: cleanValue,
|
|
3317
|
+
description: commentMatch?.[1]
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
return tokens;
|
|
3322
|
+
}
|
|
3323
|
+
function detectPrefix(names) {
|
|
3324
|
+
if (names.length === 0) return "--";
|
|
3325
|
+
const stripped = names.map((n) => n.slice(2));
|
|
3326
|
+
let prefix = "";
|
|
3327
|
+
const first = stripped[0];
|
|
3328
|
+
for (let i = 0; i < first.length; i++) {
|
|
3329
|
+
const ch = first[i];
|
|
3330
|
+
if (stripped.every((n) => n[i] === ch)) {
|
|
3331
|
+
prefix += ch;
|
|
3332
|
+
} else {
|
|
3333
|
+
break;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
const lastDash = prefix.lastIndexOf("-");
|
|
3337
|
+
if (lastDash > 0) {
|
|
3338
|
+
return "--" + prefix.slice(0, lastDash + 1);
|
|
3339
|
+
}
|
|
3340
|
+
return "--";
|
|
3341
|
+
}
|
|
3342
|
+
function inferTokenCategory(name) {
|
|
3343
|
+
const n = name.toLowerCase();
|
|
3344
|
+
if (n.includes("color") || n.includes("background") || n.includes("foreground") || n.includes("primary") || n.includes("secondary") || n.includes("accent") || n.includes("muted") || n.includes("destructive") || n.includes("popover") || n.includes("card") && !n.includes("card-") || n.includes("chart")) {
|
|
3345
|
+
return "colors";
|
|
3346
|
+
}
|
|
3347
|
+
if (n.includes("font") || n.includes("text") || n.includes("letter") || n.includes("line-height")) {
|
|
3348
|
+
return "typography";
|
|
3349
|
+
}
|
|
3350
|
+
if (n.includes("space") || n.includes("gap") || n.includes("padding") || n.includes("margin")) {
|
|
3351
|
+
return "spacing";
|
|
3352
|
+
}
|
|
3353
|
+
if (n.includes("radius")) {
|
|
3354
|
+
return "radius";
|
|
3355
|
+
}
|
|
3356
|
+
if (n.includes("shadow")) {
|
|
3357
|
+
return "shadows";
|
|
3358
|
+
}
|
|
3359
|
+
if (n.includes("border")) {
|
|
3360
|
+
return "borders";
|
|
3361
|
+
}
|
|
3362
|
+
if (n.includes("ring")) {
|
|
3363
|
+
return "focus";
|
|
3364
|
+
}
|
|
3365
|
+
if (n.includes("sidebar")) {
|
|
3366
|
+
return "sidebar";
|
|
3367
|
+
}
|
|
3368
|
+
if (n.includes("input")) {
|
|
3369
|
+
return "forms";
|
|
3370
|
+
}
|
|
3371
|
+
return "other";
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/adapters/auto-extract.ts
|
|
3375
|
+
var AutoExtractionAdapter = class {
|
|
3376
|
+
name = "auto-extract";
|
|
3377
|
+
discover(startDir) {
|
|
3378
|
+
return discoverComponentFiles(startDir).map((f) => f.filePath);
|
|
3379
|
+
}
|
|
3380
|
+
async load(projectRoot) {
|
|
3381
|
+
let extractMod;
|
|
3382
|
+
try {
|
|
3383
|
+
extractMod = await loadExtractorModule();
|
|
3384
|
+
} catch (e) {
|
|
3385
|
+
throw new Error(
|
|
3386
|
+
"Auto-extraction requires @fragments-sdk/extract and TypeScript.\n\nIf you see this error, the MCP server may not be installed correctly.\nAlternative: pre-build your design system with `npx fragments build`"
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
const tsconfigPath = findTsConfig(projectRoot);
|
|
3390
|
+
const extractor = extractMod.createComponentExtractor(tsconfigPath ?? void 0);
|
|
3391
|
+
try {
|
|
3392
|
+
const discovered = discoverComponentFiles(projectRoot);
|
|
3393
|
+
if (discovered.length === 0) {
|
|
3394
|
+
throw new Error(
|
|
3395
|
+
`No component files found in ${projectRoot}.
|
|
3396
|
+
Searched: src/components/, components/, lib/components/, src/ui/
|
|
3397
|
+
|
|
3398
|
+
If your components are elsewhere, create a fragments.json with:
|
|
3399
|
+
npx fragments build`
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
const components = {};
|
|
3403
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
3404
|
+
const defaultPackageName = readPackageName(projectRoot);
|
|
3405
|
+
let extractedCount = 0;
|
|
3406
|
+
const fileToComponents = /* @__PURE__ */ new Map();
|
|
3407
|
+
for (const { filePath, componentName } of discovered) {
|
|
3408
|
+
try {
|
|
3409
|
+
const metas = extractor.extractAll(filePath);
|
|
3410
|
+
for (const meta of metas) {
|
|
3411
|
+
const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
|
|
3412
|
+
components[meta.name] = fragment;
|
|
3413
|
+
if (defaultPackageName) {
|
|
3414
|
+
packageMap.set(meta.name, defaultPackageName);
|
|
3415
|
+
}
|
|
3416
|
+
extractedCount++;
|
|
3417
|
+
const relPath = relative(projectRoot, filePath);
|
|
3418
|
+
if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
|
|
3419
|
+
fileToComponents.get(relPath).push(meta.name);
|
|
3420
|
+
}
|
|
3421
|
+
if (metas.length === 0) {
|
|
3422
|
+
const meta = extractor.extract(filePath, componentName);
|
|
3423
|
+
if (meta) {
|
|
3424
|
+
const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
|
|
3425
|
+
components[meta.name] = fragment;
|
|
3426
|
+
if (defaultPackageName) {
|
|
3427
|
+
packageMap.set(meta.name, defaultPackageName);
|
|
3428
|
+
}
|
|
3429
|
+
extractedCount++;
|
|
3430
|
+
const relPath = relative(projectRoot, filePath);
|
|
3431
|
+
if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
|
|
3432
|
+
fileToComponents.get(relPath).push(meta.name);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
} catch {
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
inferRelations(components, fileToComponents);
|
|
3440
|
+
console.error(`[fragments-mcp] Extracted ${extractedCount} components from ${discovered.length} files.`);
|
|
3441
|
+
if (extractedCount === 0) {
|
|
3442
|
+
throw new Error(
|
|
3443
|
+
`Found ${discovered.length} component files but could not extract any props.
|
|
3444
|
+
This usually means TypeScript cannot parse the files.
|
|
3445
|
+
Check that your tsconfig.json includes the component directories.`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
const tokens = scanTokens(projectRoot);
|
|
3449
|
+
if (tokens) {
|
|
3450
|
+
console.error(`[fragments-mcp] Found ${tokens.total} design tokens across ${Object.keys(tokens.categories).length} categories.`);
|
|
3451
|
+
}
|
|
3452
|
+
const snapshotComponents = Object.fromEntries(
|
|
3453
|
+
Object.entries(components).map(([key, fragment]) => [
|
|
3454
|
+
key,
|
|
3455
|
+
componentFromCompiledFragment({
|
|
3456
|
+
id: key,
|
|
3457
|
+
fragment,
|
|
3458
|
+
sourceType: "auto-extract",
|
|
3459
|
+
packageName: packageMap.get(fragment.meta.name) ?? defaultPackageName,
|
|
3460
|
+
importPath: packageMap.get(fragment.meta.name) ?? defaultPackageName
|
|
3461
|
+
})
|
|
3462
|
+
])
|
|
3463
|
+
);
|
|
3464
|
+
const snapshotTokens = tokens ? tokensFromCompiledTokenData(tokens) : void 0;
|
|
3465
|
+
const packageMapRecord = Object.fromEntries(packageMap.entries());
|
|
3466
|
+
const snapshot = validateSnapshot({
|
|
3467
|
+
schemaVersion: 1,
|
|
3468
|
+
sourceType: "auto-extract",
|
|
3469
|
+
sourceLabel: "Auto-extracted source files",
|
|
3470
|
+
capabilities: buildCapabilities({
|
|
3471
|
+
components: snapshotComponents,
|
|
3472
|
+
tokens: snapshotTokens
|
|
3473
|
+
}),
|
|
3474
|
+
metadata: {
|
|
3475
|
+
packageName: defaultPackageName,
|
|
3476
|
+
importPath: defaultPackageName
|
|
3477
|
+
},
|
|
3478
|
+
components: snapshotComponents,
|
|
3479
|
+
tokens: snapshotTokens,
|
|
3480
|
+
packageMap: packageMapRecord,
|
|
3481
|
+
defaultPackageName
|
|
3482
|
+
});
|
|
3483
|
+
return {
|
|
3484
|
+
snapshot,
|
|
3485
|
+
components: snapshot.components,
|
|
3486
|
+
blocks: snapshot.blocks,
|
|
3487
|
+
tokens: snapshot.tokens,
|
|
3488
|
+
graph: void 0,
|
|
3489
|
+
performanceSummary: void 0,
|
|
3490
|
+
packageMap: snapshot.packageMap,
|
|
3491
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
3492
|
+
capabilities: new Set(snapshot.capabilities)
|
|
3493
|
+
};
|
|
3494
|
+
} finally {
|
|
3495
|
+
extractor.dispose();
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
var extractorModulePromise = null;
|
|
3500
|
+
async function loadExtractorModule() {
|
|
3501
|
+
if (!extractorModulePromise) {
|
|
3502
|
+
extractorModulePromise = import("./dist-V7D67NXS.js");
|
|
3503
|
+
}
|
|
3504
|
+
return extractorModulePromise;
|
|
3505
|
+
}
|
|
3506
|
+
var UNIVERSAL_INHERITED = /* @__PURE__ */ new Set(["children", "className", "id", "disabled"]);
|
|
3507
|
+
var FORM_INPUT_INHERITED = /* @__PURE__ */ new Set([
|
|
3508
|
+
"placeholder",
|
|
3509
|
+
"value",
|
|
3510
|
+
"defaultValue",
|
|
3511
|
+
"onChange",
|
|
3512
|
+
"name",
|
|
3513
|
+
"required",
|
|
3514
|
+
"autoFocus",
|
|
3515
|
+
"autoComplete",
|
|
3516
|
+
"checked",
|
|
3517
|
+
"defaultChecked",
|
|
3518
|
+
"type"
|
|
3519
|
+
]);
|
|
3520
|
+
var LABEL_INHERITED = /* @__PURE__ */ new Set(["htmlFor"]);
|
|
3521
|
+
var INPUT_LIKE_NAMES = /input|select|textarea|checkbox|radio|switch|slider|combobox/i;
|
|
3522
|
+
var LABEL_LIKE_NAMES = /label/i;
|
|
3523
|
+
function getRelevantInheritedProps(componentName) {
|
|
3524
|
+
const allowed = new Set(UNIVERSAL_INHERITED);
|
|
3525
|
+
if (INPUT_LIKE_NAMES.test(componentName)) {
|
|
3526
|
+
for (const p of FORM_INPUT_INHERITED) allowed.add(p);
|
|
3527
|
+
}
|
|
3528
|
+
if (LABEL_LIKE_NAMES.test(componentName)) {
|
|
3529
|
+
for (const p of LABEL_INHERITED) allowed.add(p);
|
|
3530
|
+
}
|
|
3531
|
+
return allowed;
|
|
3532
|
+
}
|
|
3533
|
+
function mapToCompiledFragment(meta, filePath, projectRoot) {
|
|
3534
|
+
const relevantInherited = getRelevantInheritedProps(meta.name);
|
|
3535
|
+
const props = {};
|
|
3536
|
+
for (const [name, propMeta] of Object.entries(meta.props)) {
|
|
3537
|
+
if (propMeta.source === "inherited" && !relevantInherited.has(name)) continue;
|
|
3538
|
+
props[name] = {
|
|
3539
|
+
type: propMeta.type,
|
|
3540
|
+
description: propMeta.description ?? "",
|
|
3541
|
+
required: propMeta.required,
|
|
3542
|
+
...propMeta.values && { values: propMeta.values },
|
|
3543
|
+
...propMeta.default !== void 0 && { default: propMeta.default }
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
const relativePath = relative(projectRoot, filePath);
|
|
3547
|
+
const category = inferCategory(relativePath);
|
|
3548
|
+
const importPath = buildImportPath(relativePath);
|
|
3549
|
+
const description = buildDescription(meta, props);
|
|
3550
|
+
const propsSummary = buildPropsSummary(props);
|
|
3551
|
+
const codeExample = buildCodeExample(meta.name, props);
|
|
3552
|
+
const fragmentMeta = {
|
|
3553
|
+
name: meta.name,
|
|
3554
|
+
description,
|
|
3555
|
+
category
|
|
3556
|
+
};
|
|
3557
|
+
return {
|
|
3558
|
+
filePath: relativePath,
|
|
3559
|
+
meta: fragmentMeta,
|
|
3560
|
+
usage: {
|
|
3561
|
+
when: [`Use ${meta.name} when you need a ${category} ${meta.name.toLowerCase()} element.`],
|
|
3562
|
+
whenNot: [],
|
|
3563
|
+
guidelines: importPath ? [`import { ${meta.name} } from "${importPath}"`] : []
|
|
3564
|
+
},
|
|
3565
|
+
props,
|
|
3566
|
+
propsSummary,
|
|
3567
|
+
variants: codeExample ? [{ name: "Default", description: `Basic ${meta.name} usage`, code: codeExample }] : [],
|
|
3568
|
+
sourcePath: relativePath,
|
|
3569
|
+
exportName: meta.name,
|
|
3570
|
+
_generated: {
|
|
3571
|
+
source: "extracted",
|
|
3572
|
+
verified: false,
|
|
3573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3574
|
+
},
|
|
3575
|
+
...meta.composition && {
|
|
3576
|
+
ai: {
|
|
3577
|
+
compositionPattern: meta.composition.pattern
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function buildImportPath(relativePath) {
|
|
3583
|
+
const withoutExt = relativePath.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
3584
|
+
if (withoutExt.startsWith(`src${sep}`)) {
|
|
3585
|
+
return "@/" + withoutExt.slice(4).split(sep).join("/");
|
|
3586
|
+
}
|
|
3587
|
+
return "./" + withoutExt.split(sep).join("/");
|
|
3588
|
+
}
|
|
3589
|
+
function buildDescription(meta, localProps) {
|
|
3590
|
+
if (meta.description && meta.description !== meta.name && !meta.description.endsWith(" component")) {
|
|
3591
|
+
return meta.description;
|
|
3592
|
+
}
|
|
3593
|
+
const propEntries = Object.entries(localProps);
|
|
3594
|
+
if (propEntries.length === 0) {
|
|
3595
|
+
return `${meta.name} component`;
|
|
3596
|
+
}
|
|
3597
|
+
const details = [];
|
|
3598
|
+
for (const [name, prop] of propEntries) {
|
|
3599
|
+
if (prop.values && prop.values.length > 0) {
|
|
3600
|
+
details.push(`${prop.values.length} ${name} options`);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
if (details.length > 0) {
|
|
3604
|
+
return `${meta.name} component with ${details.join(" and ")}.`;
|
|
3605
|
+
}
|
|
3606
|
+
return `${meta.name} component with ${propEntries.length} configurable prop${propEntries.length === 1 ? "" : "s"}.`;
|
|
3607
|
+
}
|
|
3608
|
+
function buildPropsSummary(props) {
|
|
3609
|
+
const summaries = [];
|
|
3610
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
3611
|
+
let summary = `${name}`;
|
|
3612
|
+
if (prop.values && prop.values.length > 0) {
|
|
3613
|
+
summary += `: ${prop.values.map((v) => `"${v}"`).join(" | ")}`;
|
|
3614
|
+
} else {
|
|
3615
|
+
summary += `: ${prop.type}`;
|
|
3616
|
+
}
|
|
3617
|
+
if (prop.default !== void 0) {
|
|
3618
|
+
summary += ` (default: ${JSON.stringify(prop.default)})`;
|
|
3619
|
+
}
|
|
3620
|
+
if (prop.required) {
|
|
3621
|
+
summary += " (required)";
|
|
3622
|
+
}
|
|
3623
|
+
summaries.push(summary);
|
|
3624
|
+
}
|
|
3625
|
+
return summaries;
|
|
3626
|
+
}
|
|
3627
|
+
function buildCodeExample(name, props) {
|
|
3628
|
+
const propsStr = [];
|
|
3629
|
+
for (const [pName, prop] of Object.entries(props)) {
|
|
3630
|
+
if (pName === "children" || pName === "asChild" || pName === "className") continue;
|
|
3631
|
+
if (pName === "onChange" || pName === "onSubmit" || pName === "value" || pName === "defaultValue" || pName === "checked" || pName === "defaultChecked" || pName === "autoFocus" || pName === "autoComplete" || pName === "required" || pName === "disabled" || pName === "name") continue;
|
|
3632
|
+
if (prop.default !== void 0) {
|
|
3633
|
+
if (prop.values && prop.values.length > 1) {
|
|
3634
|
+
const nonDefault = prop.values.find((v) => v !== String(prop.default));
|
|
3635
|
+
if (nonDefault) {
|
|
3636
|
+
propsStr.push(`${pName}="${nonDefault}"`);
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
if (pName === "placeholder" && INPUT_LIKE_NAMES.test(name)) {
|
|
3642
|
+
propsStr.push(`placeholder="Enter ${name.toLowerCase()}..."`);
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
if (pName === "htmlFor" && LABEL_LIKE_NAMES.test(name)) {
|
|
3646
|
+
propsStr.push(`htmlFor="field-id"`);
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3649
|
+
if (pName === "id" || pName === "className" || pName === "children") continue;
|
|
3650
|
+
if (prop.required) {
|
|
3651
|
+
if (prop.values && prop.values.length > 0) {
|
|
3652
|
+
propsStr.push(`${pName}="${prop.values[0]}"`);
|
|
3653
|
+
} else if (prop.type === "string") {
|
|
3654
|
+
propsStr.push(`${pName}="..."`);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
const propsJsx = propsStr.length > 0 ? " " + propsStr.join(" ") : "";
|
|
3659
|
+
const nameLower = name.toLowerCase();
|
|
3660
|
+
const selfClosing = nameLower.includes("input") || nameLower.includes("separator") || nameLower.includes("slider") || nameLower.includes("switch");
|
|
3661
|
+
if (selfClosing) {
|
|
3662
|
+
return `<${name}${propsJsx} />`;
|
|
3663
|
+
}
|
|
3664
|
+
return `<${name}${propsJsx}>${name}</${name}>`;
|
|
3665
|
+
}
|
|
3666
|
+
function buildCompositionExample(root, subs) {
|
|
3667
|
+
const header = subs.filter((s) => s.includes("Header"));
|
|
3668
|
+
const title = subs.filter((s) => s.includes("Title"));
|
|
3669
|
+
const description = subs.filter((s) => s.includes("Description"));
|
|
3670
|
+
const content = subs.filter((s) => s.includes("Content") || s.includes("Body"));
|
|
3671
|
+
const footer = subs.filter((s) => s.includes("Footer"));
|
|
3672
|
+
const action = subs.filter((s) => s.includes("Action"));
|
|
3673
|
+
const other = subs.filter(
|
|
3674
|
+
(s) => !header.includes(s) && !title.includes(s) && !description.includes(s) && !content.includes(s) && !footer.includes(s) && !action.includes(s)
|
|
3675
|
+
);
|
|
3676
|
+
const lines = [`<${root}>`];
|
|
3677
|
+
if (header.length > 0) {
|
|
3678
|
+
lines.push(` <${header[0]}>`);
|
|
3679
|
+
for (const t of title) lines.push(` <${t}>Title</${t}>`);
|
|
3680
|
+
for (const d of description) lines.push(` <${d}>Description</${d}>`);
|
|
3681
|
+
lines.push(` </${header[0]}>`);
|
|
3682
|
+
} else {
|
|
3683
|
+
for (const t of title) lines.push(` <${t}>Title</${t}>`);
|
|
3684
|
+
for (const d of description) lines.push(` <${d}>Description</${d}>`);
|
|
3685
|
+
}
|
|
3686
|
+
for (const c of content) lines.push(` <${c}>Content</${c}>`);
|
|
3687
|
+
for (const o of other) lines.push(` <${o}>...</${o}>`);
|
|
3688
|
+
if (footer.length > 0) {
|
|
3689
|
+
lines.push(` <${footer[0]}>`);
|
|
3690
|
+
for (const a of action) lines.push(` <${a}>Action</${a}>`);
|
|
3691
|
+
lines.push(` </${footer[0]}>`);
|
|
3692
|
+
} else {
|
|
3693
|
+
for (const a of action) lines.push(` <${a}>Action</${a}>`);
|
|
3694
|
+
}
|
|
3695
|
+
lines.push(`</${root}>`);
|
|
3696
|
+
return lines.join("\n");
|
|
3697
|
+
}
|
|
3698
|
+
function inferRelations(components, fileToComponents) {
|
|
3699
|
+
for (const [_file, names] of fileToComponents) {
|
|
3700
|
+
if (names.length <= 1) continue;
|
|
3701
|
+
const sorted = [...names].sort((a, b) => a.length - b.length);
|
|
3702
|
+
const root = sorted[0];
|
|
3703
|
+
const subs = sorted.slice(1).filter((n) => n.startsWith(root));
|
|
3704
|
+
if (subs.length === 0) continue;
|
|
3705
|
+
const rootComp = components[root];
|
|
3706
|
+
if (rootComp) {
|
|
3707
|
+
rootComp.ai = {
|
|
3708
|
+
...rootComp.ai,
|
|
3709
|
+
compositionPattern: "compound",
|
|
3710
|
+
subComponents: subs
|
|
3711
|
+
};
|
|
3712
|
+
rootComp.relations = subs.map((sub) => ({
|
|
3713
|
+
component: sub,
|
|
3714
|
+
relationship: "parent-of",
|
|
3715
|
+
note: `${sub} is a sub-component of ${root}`
|
|
3716
|
+
}));
|
|
3717
|
+
const compositionCode = buildCompositionExample(root, subs);
|
|
3718
|
+
rootComp.variants = [
|
|
3719
|
+
...rootComp.variants ?? [],
|
|
3720
|
+
{ name: "Composition", description: `${root} with all sub-components`, code: compositionCode }
|
|
3721
|
+
];
|
|
3722
|
+
}
|
|
3723
|
+
const rootImportPath = rootComp?.usage?.guidelines?.[0];
|
|
3724
|
+
for (const sub of subs) {
|
|
3725
|
+
const subComp = components[sub];
|
|
3726
|
+
if (subComp) {
|
|
3727
|
+
subComp.relations = [{
|
|
3728
|
+
component: root,
|
|
3729
|
+
relationship: "child-of",
|
|
3730
|
+
note: `Use inside <${root}>`
|
|
3731
|
+
}];
|
|
3732
|
+
if (rootImportPath) {
|
|
3733
|
+
const allNames = [root, ...subs].join(", ");
|
|
3734
|
+
const fromPath = rootImportPath.match(/from\s+"([^"]+)"/)?.[1] ?? "";
|
|
3735
|
+
if (fromPath) {
|
|
3736
|
+
subComp.usage = {
|
|
3737
|
+
...subComp.usage,
|
|
3738
|
+
guidelines: [`import { ${allNames} } from "${fromPath}"`]
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
if (rootComp && rootComp.usage?.guidelines?.[0]) {
|
|
3745
|
+
const fromPath = rootComp.usage.guidelines[0].match(/from\s+"([^"]+)"/)?.[1] ?? "";
|
|
3746
|
+
if (fromPath) {
|
|
3747
|
+
const allNames = [root, ...subs].join(", ");
|
|
3748
|
+
rootComp.usage.guidelines = [`import { ${allNames} } from "${fromPath}"`];
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
function inferCategory(relativePath) {
|
|
3754
|
+
const parts = relativePath.split(sep);
|
|
3755
|
+
const componentsIdx = parts.findIndex(
|
|
3756
|
+
(p) => p === "components" || p === "ui"
|
|
3757
|
+
);
|
|
3758
|
+
if (componentsIdx >= 0 && componentsIdx + 1 < parts.length - 1) {
|
|
3759
|
+
const nextPart = parts[componentsIdx + 1];
|
|
3760
|
+
if (/^[A-Z]/.test(nextPart)) return parts[componentsIdx] || "uncategorized";
|
|
3761
|
+
return nextPart;
|
|
3762
|
+
}
|
|
3763
|
+
return "uncategorized";
|
|
3764
|
+
}
|
|
3765
|
+
function findTsConfig(projectRoot) {
|
|
3766
|
+
const candidates = ["tsconfig.json", "tsconfig.app.json"];
|
|
3767
|
+
for (const name of candidates) {
|
|
3768
|
+
const p = join6(projectRoot, name);
|
|
3769
|
+
if (existsSync6(p)) return p;
|
|
3770
|
+
}
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
function readPackageName(projectRoot) {
|
|
3774
|
+
try {
|
|
3775
|
+
const pkgPath = join6(projectRoot, "package.json");
|
|
3776
|
+
if (!existsSync6(pkgPath)) return void 0;
|
|
3777
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
3778
|
+
return pkg.name;
|
|
3779
|
+
} catch {
|
|
3780
|
+
return void 0;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// src/adapters/cloud-catalog.ts
|
|
3785
|
+
var DEFAULT_CLOUD_URL = "https://app.usefragments.com/api/catalog";
|
|
3786
|
+
var TOKEN_CATEGORY_ALIASES2 = {
|
|
3787
|
+
color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
|
|
3788
|
+
spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
|
|
3789
|
+
typography: ["typography", "font", "text", "copy", "line-height", "letter"],
|
|
3790
|
+
border: ["border", "borders", "stroke", "outline"],
|
|
3791
|
+
radius: ["radius", "radii", "corner", "corners", "rounded"],
|
|
3792
|
+
shadow: ["shadow", "shadows", "elevation"],
|
|
3793
|
+
layout: ["layout", "grid", "container", "breakpoint"],
|
|
3794
|
+
focus: ["focus", "ring", "focus-ring"],
|
|
3795
|
+
surface: ["surface", "surfaces", "canvas", "card", "background"]
|
|
3796
|
+
};
|
|
3797
|
+
function normalizeCatalogUrl(url) {
|
|
3798
|
+
if (!url) return DEFAULT_CLOUD_URL;
|
|
3799
|
+
if (url.endsWith("/api/catalog")) return url;
|
|
3800
|
+
return `${url.replace(/\/+$/, "")}/api/catalog`;
|
|
3801
|
+
}
|
|
3802
|
+
function normalizeValue(value) {
|
|
3803
|
+
return value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() ?? "";
|
|
3804
|
+
}
|
|
3805
|
+
function canonicalizeTokenCategory(token) {
|
|
3806
|
+
const candidates = [
|
|
3807
|
+
token.category,
|
|
3808
|
+
token.path?.[0],
|
|
3809
|
+
token.name.split(/[.:/-]/)[0]
|
|
3810
|
+
].map(normalizeValue).filter(Boolean);
|
|
3811
|
+
for (const candidate of candidates) {
|
|
3812
|
+
for (const [canonical, aliases] of Object.entries(
|
|
3813
|
+
TOKEN_CATEGORY_ALIASES2
|
|
3814
|
+
)) {
|
|
3815
|
+
if (candidate === canonical || aliases.some(
|
|
3816
|
+
(alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
|
|
3817
|
+
)) {
|
|
3818
|
+
return canonical;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return candidates[0] || "other";
|
|
3823
|
+
}
|
|
3824
|
+
function groupTokens(flat) {
|
|
3825
|
+
const categories = {};
|
|
3826
|
+
const normalizedFlat = (flat ?? []).map((token) => {
|
|
3827
|
+
const category = canonicalizeTokenCategory(token);
|
|
3828
|
+
const normalized = {
|
|
3829
|
+
name: token.name,
|
|
3830
|
+
category,
|
|
3831
|
+
value: token.value,
|
|
3832
|
+
description: token.description,
|
|
3833
|
+
path: token.path,
|
|
3834
|
+
type: token.type
|
|
3835
|
+
};
|
|
3836
|
+
if (!categories[category]) {
|
|
3837
|
+
categories[category] = [];
|
|
3838
|
+
}
|
|
3839
|
+
categories[category].push(normalized);
|
|
3840
|
+
return normalized;
|
|
3841
|
+
});
|
|
3842
|
+
return {
|
|
3843
|
+
prefix: "",
|
|
3844
|
+
total: normalizedFlat.length,
|
|
3845
|
+
categories,
|
|
3846
|
+
flat: normalizedFlat
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
function mapComponent(component, designSystem) {
|
|
3850
|
+
return {
|
|
3851
|
+
id: component.componentKey,
|
|
3852
|
+
name: component.name,
|
|
3853
|
+
description: component.description ?? "",
|
|
3854
|
+
category: component.category ?? "uncategorized",
|
|
3855
|
+
status: component.status ?? "stable",
|
|
3856
|
+
tags: [],
|
|
3857
|
+
props: Object.fromEntries(
|
|
3858
|
+
Object.entries(component.props ?? {}).map(([propName, prop]) => [
|
|
3859
|
+
propName,
|
|
3860
|
+
{
|
|
3861
|
+
type: prop.type ?? "unknown",
|
|
3862
|
+
description: prop.description ?? "",
|
|
3863
|
+
required: prop.required ?? false,
|
|
3864
|
+
default: prop.default,
|
|
3865
|
+
values: prop.values
|
|
3866
|
+
}
|
|
3867
|
+
])
|
|
3868
|
+
),
|
|
3869
|
+
propsSummary: Object.entries(component.props ?? {}).map(
|
|
3870
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type ?? "unknown"}`
|
|
3871
|
+
),
|
|
3872
|
+
examples: (component.examples ?? []).map((example) => ({
|
|
3873
|
+
name: example.title ?? "Example",
|
|
3874
|
+
description: example.kind,
|
|
3875
|
+
code: example.code,
|
|
3876
|
+
kind: example.kind
|
|
3877
|
+
})),
|
|
3878
|
+
relations: (component.relations ?? []).map((relation) => ({
|
|
3879
|
+
componentName: relation.component ?? relation.componentKey ?? "Unknown",
|
|
3880
|
+
componentId: relation.componentKey,
|
|
3881
|
+
relationship: relation.relationship ?? relation.type ?? "related",
|
|
3882
|
+
note: relation.note,
|
|
3883
|
+
description: relation.description
|
|
3884
|
+
})),
|
|
3885
|
+
compoundChildren: (component.compoundChildren ?? []).filter((child) => child.subcomponentVisibility !== "internal").map((child) => ({
|
|
3886
|
+
name: child.name,
|
|
3887
|
+
componentId: child.componentKey,
|
|
3888
|
+
description: child.description,
|
|
3889
|
+
required: child.required,
|
|
3890
|
+
accepts: child.accepts,
|
|
3891
|
+
visibility: child.subcomponentVisibility === "internal" ? "internal" : "public"
|
|
3892
|
+
})),
|
|
3893
|
+
guidance: {
|
|
3894
|
+
when: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3895
|
+
whenNot: component.donts ?? [],
|
|
3896
|
+
guidelines: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3897
|
+
accessibility: [],
|
|
3898
|
+
usageGuidance: component.usageGuidance,
|
|
3899
|
+
dos: component.dos ?? [],
|
|
3900
|
+
donts: component.donts ?? [],
|
|
3901
|
+
patterns: []
|
|
3902
|
+
},
|
|
3903
|
+
sourceType: "cloud",
|
|
3904
|
+
sourcePath: component.sourcePath,
|
|
3905
|
+
sourceRepoFullName: component.sourceRepoFullName ?? void 0,
|
|
3906
|
+
packageName: designSystem?.packageName ?? void 0,
|
|
3907
|
+
importPath: designSystem?.importPath ?? designSystem?.packageName ?? void 0,
|
|
3908
|
+
publicRef: component.publicRef,
|
|
3909
|
+
publicSlug: component.publicSlug ?? null,
|
|
3910
|
+
isCanonical: component.isCanonical ?? false,
|
|
3911
|
+
tier: component.tier,
|
|
3912
|
+
parentComponentId: component.parentComponentKey,
|
|
3913
|
+
parentComponentName: component.parentComponentName,
|
|
3914
|
+
metadata: {
|
|
3915
|
+
a11yRules: [],
|
|
3916
|
+
scenarioTags: []
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
var CloudCatalogAdapter = class {
|
|
3921
|
+
constructor(options) {
|
|
3922
|
+
this.options = options;
|
|
3923
|
+
}
|
|
3924
|
+
name = "cloud";
|
|
3925
|
+
async load(_projectRoot) {
|
|
3926
|
+
const response = await fetch(normalizeCatalogUrl(this.options.url), {
|
|
3927
|
+
headers: {
|
|
3928
|
+
"X-API-Key": this.options.apiKey
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
if (!response.ok) {
|
|
3932
|
+
throw new Error(
|
|
3933
|
+
`Failed to load Cloud catalog (${response.status} ${response.statusText}).`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
const raw = await response.json();
|
|
3937
|
+
const components = Object.fromEntries(
|
|
3938
|
+
(raw.components ?? []).map((component) => [
|
|
3939
|
+
component.componentKey,
|
|
3940
|
+
mapComponent(component, raw.designSystem)
|
|
3941
|
+
])
|
|
3942
|
+
);
|
|
3943
|
+
const tokens = raw.tokens?.flat ? groupTokens(raw.tokens.flat) : void 0;
|
|
3944
|
+
const packageName = raw.designSystem?.packageName ?? void 0;
|
|
3945
|
+
const importPath = raw.designSystem?.importPath ?? raw.designSystem?.packageName ?? void 0;
|
|
3946
|
+
const packageMap = packageName ? Object.fromEntries(
|
|
3947
|
+
Object.values(components).map((component) => [
|
|
3948
|
+
component.name,
|
|
3949
|
+
packageName
|
|
3950
|
+
])
|
|
3951
|
+
) : {};
|
|
3952
|
+
const snapshot = validateSnapshot({
|
|
3953
|
+
schemaVersion: 1,
|
|
3954
|
+
sourceType: "cloud",
|
|
3955
|
+
sourceLabel: "Fragments Cloud catalog",
|
|
3956
|
+
capabilities: buildCapabilities({
|
|
3957
|
+
components,
|
|
3958
|
+
tokens
|
|
3959
|
+
}),
|
|
3960
|
+
metadata: {
|
|
3961
|
+
designSystemName: raw.designSystem?.name ?? raw.org.name,
|
|
3962
|
+
packageName,
|
|
3963
|
+
importPath,
|
|
3964
|
+
revision: raw.revision,
|
|
3965
|
+
updatedAt: typeof raw.updatedAt === "number" ? new Date(raw.updatedAt).toISOString() : void 0
|
|
3966
|
+
},
|
|
3967
|
+
components,
|
|
3968
|
+
tokens,
|
|
3969
|
+
packageMap,
|
|
3970
|
+
defaultPackageName: packageName
|
|
3971
|
+
});
|
|
3972
|
+
const hydratedComponents = Object.fromEntries(
|
|
3973
|
+
Object.entries(snapshot.components).map(([componentId, component]) => [
|
|
3974
|
+
componentId,
|
|
3975
|
+
{
|
|
3976
|
+
...component,
|
|
3977
|
+
isCanonical: components[componentId]?.isCanonical ?? false
|
|
3978
|
+
}
|
|
3979
|
+
])
|
|
3980
|
+
);
|
|
3981
|
+
return {
|
|
3982
|
+
snapshot: {
|
|
3983
|
+
...snapshot,
|
|
3984
|
+
components: hydratedComponents
|
|
3985
|
+
},
|
|
3986
|
+
components: hydratedComponents,
|
|
3987
|
+
blocks: snapshot.blocks,
|
|
3988
|
+
tokens: snapshot.tokens,
|
|
3989
|
+
graph: void 0,
|
|
3990
|
+
performanceSummary: void 0,
|
|
3991
|
+
packageMap: snapshot.packageMap,
|
|
3992
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
3993
|
+
capabilities: new Set(snapshot.capabilities)
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
|
|
3998
|
+
// src/adapters/bundle.ts
|
|
3999
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4000
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
4001
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
4002
|
+
import {
|
|
4003
|
+
bundleComponentShardSchema,
|
|
4004
|
+
bundleManifestSchema,
|
|
4005
|
+
bundleTokenFileSchema
|
|
4006
|
+
} from "@fragments-sdk/core";
|
|
4007
|
+
async function readJsonFile(path, parser, label) {
|
|
4008
|
+
const content = await readFile2(path, "utf-8");
|
|
4009
|
+
try {
|
|
4010
|
+
return parser.parse(JSON.parse(content));
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
throw new Error(
|
|
4013
|
+
`Invalid ${label} at ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
function normalizeProps(props) {
|
|
4018
|
+
return Object.fromEntries(
|
|
4019
|
+
Object.entries(props ?? {}).map(([propName, value]) => {
|
|
4020
|
+
const prop = value && typeof value === "object" ? value : {};
|
|
4021
|
+
return [
|
|
4022
|
+
propName,
|
|
4023
|
+
{
|
|
4024
|
+
type: typeof prop.type === "string" && prop.type.length > 0 ? prop.type : "unknown",
|
|
4025
|
+
description: typeof prop.description === "string" ? prop.description : "",
|
|
4026
|
+
required: Boolean(prop.required),
|
|
4027
|
+
default: prop.default,
|
|
4028
|
+
values: Array.isArray(prop.values) ? prop.values.filter((entry) => typeof entry === "string") : void 0,
|
|
4029
|
+
constraints: Array.isArray(prop.constraints) ? prop.constraints.filter(
|
|
4030
|
+
(entry) => typeof entry === "string"
|
|
4031
|
+
) : void 0
|
|
4032
|
+
}
|
|
4033
|
+
];
|
|
4034
|
+
})
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
function normalizeExamples(examples) {
|
|
4038
|
+
return examples.map((example) => {
|
|
4039
|
+
const record = example && typeof example === "object" ? example : {};
|
|
4040
|
+
return {
|
|
4041
|
+
name: typeof record.title === "string" && record.title || typeof record.name === "string" && record.name || "Example",
|
|
4042
|
+
description: typeof record.description === "string" ? record.description : void 0,
|
|
4043
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
4044
|
+
kind: typeof record.kind === "string" ? record.kind : void 0
|
|
4045
|
+
};
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
function normalizeRelations(relations, fallback) {
|
|
4049
|
+
if (relations.length > 0) {
|
|
4050
|
+
return relations.map((relation) => {
|
|
4051
|
+
const record = relation && typeof relation === "object" ? relation : {};
|
|
4052
|
+
return {
|
|
4053
|
+
componentName: typeof record.component === "string" && record.component || typeof record.name === "string" && record.name || typeof record.componentName === "string" && record.componentName || typeof record.componentId === "string" && record.componentId || "Unknown",
|
|
4054
|
+
componentId: typeof record.componentId === "string" ? record.componentId : void 0,
|
|
4055
|
+
relationship: typeof record.relationship === "string" && record.relationship || typeof record.type === "string" && record.type || "related",
|
|
4056
|
+
note: typeof record.note === "string" ? record.note : void 0,
|
|
4057
|
+
description: typeof record.description === "string" ? record.description : void 0
|
|
4058
|
+
};
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
return fallback.map((relation) => ({
|
|
4062
|
+
componentName: relation.name,
|
|
4063
|
+
componentId: relation.componentId,
|
|
4064
|
+
relationship: relation.type
|
|
4065
|
+
}));
|
|
4066
|
+
}
|
|
4067
|
+
function buildComponent(manifest, entry, shard) {
|
|
4068
|
+
const sourcePackage = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
|
|
4069
|
+
const component = shard.component;
|
|
4070
|
+
const componentRecord = component;
|
|
4071
|
+
const props = normalizeProps(component.props);
|
|
4072
|
+
return {
|
|
4073
|
+
id: shard.componentId,
|
|
4074
|
+
name: component.name,
|
|
4075
|
+
description: component.description,
|
|
4076
|
+
category: component.category,
|
|
4077
|
+
status: component.status,
|
|
4078
|
+
tags: [],
|
|
4079
|
+
props,
|
|
4080
|
+
propsSummary: Object.entries(props).map(
|
|
4081
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
|
|
4082
|
+
),
|
|
4083
|
+
examples: normalizeExamples(component.examples),
|
|
4084
|
+
relations: normalizeRelations(component.relations, entry.relations),
|
|
4085
|
+
compoundChildren: entry.compoundChildren.map((child) => ({
|
|
4086
|
+
name: child.name,
|
|
4087
|
+
componentId: child.componentId,
|
|
4088
|
+
visibility: "public"
|
|
4089
|
+
})),
|
|
4090
|
+
guidance: {
|
|
4091
|
+
when: component.usageGuidance ? [component.usageGuidance] : [],
|
|
4092
|
+
whenNot: component.donts,
|
|
4093
|
+
guidelines: component.usageGuidance ? [component.usageGuidance] : [],
|
|
4094
|
+
accessibility: [],
|
|
4095
|
+
usageGuidance: component.usageGuidance || void 0,
|
|
4096
|
+
dos: component.dos,
|
|
4097
|
+
donts: component.donts,
|
|
4098
|
+
patterns: []
|
|
4099
|
+
},
|
|
4100
|
+
sourceType: "bundle",
|
|
4101
|
+
sourcePath: component.sourcePath,
|
|
4102
|
+
sourceRepoFullName: component.sourceRepoFullName,
|
|
4103
|
+
packageName: manifest.designSystem.packageName ?? void 0,
|
|
4104
|
+
importPath: sourcePackage,
|
|
4105
|
+
publicRef: component.publicRef,
|
|
4106
|
+
publicSlug: component.publicSlug,
|
|
4107
|
+
isCanonical: componentRecord.isCanonical ?? component.tier === "core",
|
|
4108
|
+
tier: component.tier,
|
|
4109
|
+
parentComponentName: component.parentComponentName,
|
|
4110
|
+
metadata: {
|
|
4111
|
+
a11yRules: [],
|
|
4112
|
+
scenarioTags: []
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
var BundleAdapter = class {
|
|
4117
|
+
name = "bundle";
|
|
4118
|
+
discover(startDir) {
|
|
4119
|
+
return findBundleManifest(startDir);
|
|
4120
|
+
}
|
|
4121
|
+
async load(projectRoot) {
|
|
4122
|
+
const manifests = this.discover(projectRoot);
|
|
4123
|
+
if (manifests.length === 0) {
|
|
4124
|
+
throw new Error(
|
|
4125
|
+
`No ${BRAND.dataDir}/${BRAND.manifestFile} found. Run \`${BRAND.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
|
|
4126
|
+
);
|
|
4127
|
+
}
|
|
4128
|
+
const manifestPath = manifests[0];
|
|
4129
|
+
const manifest = await readJsonFile(
|
|
4130
|
+
manifestPath,
|
|
4131
|
+
bundleManifestSchema,
|
|
4132
|
+
"bundle manifest"
|
|
4133
|
+
);
|
|
4134
|
+
const bundleDir = dirname3(manifestPath);
|
|
4135
|
+
const repoRoot = dirname3(bundleDir);
|
|
4136
|
+
const tokensPath = resolve2(bundleDir, "tokens.json");
|
|
4137
|
+
const tokensFile = existsSync7(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
|
|
4138
|
+
const components = Object.fromEntries(
|
|
4139
|
+
await Promise.all(
|
|
4140
|
+
Object.values(manifest.components).map(async (entry) => {
|
|
4141
|
+
const shardPath = resolve2(repoRoot, entry.file);
|
|
4142
|
+
const shard = await readJsonFile(
|
|
4143
|
+
shardPath,
|
|
4144
|
+
bundleComponentShardSchema,
|
|
4145
|
+
`bundle component shard (${entry.name})`
|
|
4146
|
+
);
|
|
4147
|
+
return [entry.componentId, buildComponent(manifest, entry, shard)];
|
|
4148
|
+
})
|
|
4149
|
+
)
|
|
4150
|
+
);
|
|
4151
|
+
const packageName = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
|
|
4152
|
+
const packageMap = packageName ? Object.fromEntries(
|
|
4153
|
+
Object.values(components).map((component) => [component.name, packageName])
|
|
4154
|
+
) : {};
|
|
4155
|
+
const tokens = tokensFile ? {
|
|
4156
|
+
prefix: "",
|
|
4157
|
+
total: tokensFile.flat.length,
|
|
4158
|
+
categories: Object.fromEntries(
|
|
4159
|
+
Object.entries(tokensFile.categories).map(([category, value]) => [
|
|
4160
|
+
category,
|
|
4161
|
+
value.tokens.map((token) => ({
|
|
4162
|
+
name: token.name,
|
|
4163
|
+
category: token.category,
|
|
4164
|
+
value: token.value,
|
|
4165
|
+
description: token.description,
|
|
4166
|
+
path: token.path,
|
|
4167
|
+
type: token.type
|
|
4168
|
+
}))
|
|
4169
|
+
])
|
|
4170
|
+
),
|
|
4171
|
+
flat: tokensFile.flat.map((token) => ({
|
|
4172
|
+
name: token.name,
|
|
4173
|
+
category: token.category,
|
|
4174
|
+
value: token.value,
|
|
4175
|
+
description: token.description,
|
|
4176
|
+
path: token.path,
|
|
4177
|
+
type: token.type
|
|
4178
|
+
}))
|
|
4179
|
+
} : void 0;
|
|
4180
|
+
const snapshot = validateSnapshot({
|
|
4181
|
+
schemaVersion: 1,
|
|
4182
|
+
sourceType: "bundle",
|
|
4183
|
+
sourceLabel: `${BRAND.dataDir}/${BRAND.manifestFile}`,
|
|
4184
|
+
capabilities: buildCapabilities({
|
|
4185
|
+
components,
|
|
4186
|
+
tokens
|
|
4187
|
+
}),
|
|
4188
|
+
metadata: {
|
|
4189
|
+
designSystemName: manifest.designSystem.name,
|
|
4190
|
+
packageName: manifest.designSystem.packageName ?? void 0,
|
|
4191
|
+
importPath: manifest.designSystem.importPath ?? void 0,
|
|
4192
|
+
revision: manifest.catalogRevision,
|
|
4193
|
+
updatedAt: manifest.catalogUpdatedAt
|
|
4194
|
+
},
|
|
4195
|
+
components,
|
|
4196
|
+
tokens,
|
|
4197
|
+
packageMap,
|
|
4198
|
+
defaultPackageName: packageName
|
|
4199
|
+
});
|
|
4200
|
+
const hydratedComponents = Object.fromEntries(
|
|
4201
|
+
Object.entries(snapshot.components).map(([componentId, component]) => [
|
|
4202
|
+
componentId,
|
|
4203
|
+
{
|
|
4204
|
+
...component,
|
|
4205
|
+
isCanonical: components[componentId]?.isCanonical ?? component.tier === "core"
|
|
4206
|
+
}
|
|
4207
|
+
])
|
|
4208
|
+
);
|
|
4209
|
+
return {
|
|
4210
|
+
snapshot: {
|
|
4211
|
+
...snapshot,
|
|
4212
|
+
components: hydratedComponents
|
|
4213
|
+
},
|
|
4214
|
+
components: hydratedComponents,
|
|
4215
|
+
blocks: snapshot.blocks,
|
|
4216
|
+
tokens: snapshot.tokens,
|
|
4217
|
+
graph: snapshot.graph,
|
|
4218
|
+
performanceSummary: snapshot.performanceSummary,
|
|
4219
|
+
packageMap: snapshot.packageMap,
|
|
4220
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
4221
|
+
capabilities: new Set(snapshot.capabilities)
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
|
|
4226
|
+
// src/source-selection.ts
|
|
4227
|
+
function resolveCloudApiKey(config, fileConfig) {
|
|
4228
|
+
return config.cloudApiKey ?? fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
4229
|
+
}
|
|
4230
|
+
function resolveCloudUrl(fileConfig) {
|
|
4231
|
+
return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
|
|
4232
|
+
}
|
|
4233
|
+
function hasTsProject(projectRoot) {
|
|
4234
|
+
return existsSync8(join7(projectRoot, "tsconfig.json")) || existsSync8(join7(projectRoot, "tsconfig.app.json"));
|
|
4235
|
+
}
|
|
4236
|
+
function resolveDataAdapter(config, fileConfig) {
|
|
4237
|
+
const source = config.source ?? fileConfig?.source ?? "auto";
|
|
4238
|
+
const fragmentsJsonPaths = findFragmentsJson(config.projectRoot);
|
|
4239
|
+
const bundleManifestPaths = findBundleManifest(config.projectRoot);
|
|
4240
|
+
const cloudApiKey = resolveCloudApiKey(config, fileConfig);
|
|
4241
|
+
const cloudUrl = resolveCloudUrl(fileConfig);
|
|
4242
|
+
switch (source) {
|
|
4243
|
+
case "fragments-json":
|
|
4244
|
+
return { adapter: new FragmentsJsonAdapter(), mode: "fragments-json" };
|
|
4245
|
+
case "cloud":
|
|
4246
|
+
if (!cloudApiKey) {
|
|
4247
|
+
throw new Error(
|
|
4248
|
+
"Cloud source requires a Cloud API key. Set FRAGMENTS_API_KEY or pass cloudApiKey."
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
return {
|
|
4252
|
+
adapter: new CloudCatalogAdapter({ apiKey: cloudApiKey, url: cloudUrl }),
|
|
4253
|
+
mode: "cloud"
|
|
4254
|
+
};
|
|
4255
|
+
case "bundle":
|
|
4256
|
+
return { adapter: new BundleAdapter(), mode: "bundle" };
|
|
4257
|
+
case "extract":
|
|
4258
|
+
return { adapter: new AutoExtractionAdapter(), mode: "extract" };
|
|
4259
|
+
case "auto":
|
|
4260
|
+
default:
|
|
4261
|
+
if (fragmentsJsonPaths.length > 0) {
|
|
4262
|
+
return {
|
|
4263
|
+
adapter: new FragmentsJsonAdapter(),
|
|
4264
|
+
mode: "fragments-json"
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
if (cloudApiKey) {
|
|
4268
|
+
return {
|
|
4269
|
+
adapter: new CloudCatalogAdapter({
|
|
4270
|
+
apiKey: cloudApiKey,
|
|
4271
|
+
url: cloudUrl
|
|
4272
|
+
}),
|
|
4273
|
+
mode: "cloud"
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
if (bundleManifestPaths.length > 0) {
|
|
4277
|
+
return {
|
|
4278
|
+
adapter: new BundleAdapter(),
|
|
4279
|
+
mode: "bundle"
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
if (hasTsProject(config.projectRoot)) {
|
|
4283
|
+
return { adapter: new AutoExtractionAdapter(), mode: "extract" };
|
|
4284
|
+
}
|
|
4285
|
+
return {
|
|
4286
|
+
adapter: new FragmentsJsonAdapter(),
|
|
4287
|
+
mode: "fragments-json"
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
function resolveSearchApiKey(config, fileConfig) {
|
|
4292
|
+
return config.searchApiKey ?? config.apiKey ?? fileConfig?.vectorSearch?.apiKey;
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
// src/server.ts
|
|
4296
|
+
var TOOL_NAMES = buildToolNames();
|
|
4297
|
+
var TOOLS = buildMcpTools();
|
|
4298
|
+
var TOOL_DEFINITION_BY_KEY = new Map(
|
|
4299
|
+
MCP_TOOL_DEFINITIONS.map((definition) => [definition.key, definition])
|
|
4300
|
+
);
|
|
4301
|
+
function createMcpServer(config) {
|
|
4302
|
+
const server = new Server(
|
|
4303
|
+
{
|
|
4304
|
+
name: `${BRAND.nameLower}-mcp`,
|
|
4305
|
+
version: MCP_SERVER_VERSION
|
|
4306
|
+
},
|
|
4307
|
+
{
|
|
4308
|
+
capabilities: {
|
|
4309
|
+
tools: { listChanged: true }
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
);
|
|
4313
|
+
const registry = new ToolRegistry("", {
|
|
4314
|
+
onChanged: () => {
|
|
4315
|
+
server.notification({ method: "notifications/tools/list_changed", params: {} });
|
|
4316
|
+
}
|
|
4317
|
+
});
|
|
4318
|
+
registry.registerBuiltins(
|
|
4319
|
+
{ core: CORE_TOOLS, viewer: VIEWER_TOOLS, infra: INFRA_TOOLS },
|
|
4320
|
+
MCP_TOOL_DEFINITIONS,
|
|
4321
|
+
TOOL_CAPABILITIES
|
|
4322
|
+
);
|
|
4323
|
+
const fileConfig = config.fileConfig ?? loadConfigFile(config.projectRoot) ?? void 0;
|
|
4324
|
+
const mergedConfig = {
|
|
4325
|
+
...fileConfig ? { ...config, fileConfig } : config,
|
|
4326
|
+
searchApiKey: resolveSearchApiKey(config, fileConfig)
|
|
4327
|
+
};
|
|
4328
|
+
const adapter = config.adapter ?? resolveDataAdapter(mergedConfig, fileConfig).adapter;
|
|
4329
|
+
config.onRegistry?.(registry);
|
|
4330
|
+
if (fileConfig?.tools?.exclude) {
|
|
4331
|
+
for (const key of fileConfig.tools.exclude) {
|
|
4332
|
+
registry.unregister(key);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
let cachedData = null;
|
|
4336
|
+
let loadDataPromise = null;
|
|
4337
|
+
let resolvedRoot = null;
|
|
4338
|
+
let resolveProjectRootPromise = null;
|
|
4339
|
+
let componentIndex = null;
|
|
4340
|
+
let blockIndex = null;
|
|
4341
|
+
let tokenIndex = null;
|
|
4342
|
+
async function resolveProjectRoot() {
|
|
4343
|
+
if (resolvedRoot) return resolvedRoot;
|
|
4344
|
+
if (resolveProjectRootPromise) return resolveProjectRootPromise;
|
|
4345
|
+
resolveProjectRootPromise = (async () => {
|
|
4346
|
+
try {
|
|
4347
|
+
const result = await server.listRoots();
|
|
4348
|
+
if (result.roots?.length > 0) {
|
|
4349
|
+
const rootUri = result.roots[0].uri;
|
|
4350
|
+
resolvedRoot = fileURLToPath(rootUri);
|
|
4351
|
+
return resolvedRoot;
|
|
4352
|
+
}
|
|
4353
|
+
} catch {
|
|
4354
|
+
}
|
|
4355
|
+
resolvedRoot = config.projectRoot;
|
|
4356
|
+
return resolvedRoot;
|
|
4357
|
+
})();
|
|
4358
|
+
try {
|
|
4359
|
+
return await resolveProjectRootPromise;
|
|
4360
|
+
} finally {
|
|
4361
|
+
resolveProjectRootPromise = null;
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
async function loadData() {
|
|
4365
|
+
if (cachedData) return cachedData;
|
|
4366
|
+
if (loadDataPromise) return loadDataPromise;
|
|
4367
|
+
loadDataPromise = (async () => {
|
|
4368
|
+
const projectRoot = await resolveProjectRoot();
|
|
4369
|
+
const loaded = await adapter.load(projectRoot);
|
|
4370
|
+
const allFragments = Object.values(loaded.components);
|
|
4371
|
+
const allBlocks = Object.values(loaded.blocks ?? {});
|
|
4372
|
+
componentIndex = buildComponentIndex(allFragments);
|
|
4373
|
+
blockIndex = allBlocks.length > 0 ? buildBlockIndex(allBlocks) : null;
|
|
4374
|
+
tokenIndex = loaded.tokens && loaded.tokens.total > 0 ? buildTokenIndex(loaded.tokens) : null;
|
|
4375
|
+
cachedData = loaded;
|
|
4376
|
+
return loaded;
|
|
4377
|
+
})();
|
|
4378
|
+
try {
|
|
4379
|
+
return await loadDataPromise;
|
|
4380
|
+
} finally {
|
|
4381
|
+
loadDataPromise = null;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4385
|
+
const data = await loadData();
|
|
4386
|
+
return {
|
|
4387
|
+
tools: registry.listTools(
|
|
4388
|
+
{
|
|
4389
|
+
hasViewer: !!config.viewerUrl,
|
|
4390
|
+
hasPlayground: !!(config.playgroundUrl ?? fileConfig?.playgroundUrl),
|
|
4391
|
+
capabilities: data.capabilities
|
|
4392
|
+
},
|
|
4393
|
+
TOOLS
|
|
4394
|
+
)
|
|
4395
|
+
};
|
|
4396
|
+
});
|
|
4397
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4398
|
+
const { name, arguments: args } = request.params;
|
|
4399
|
+
const data = await loadData();
|
|
4400
|
+
const toolContext = {
|
|
4401
|
+
data,
|
|
4402
|
+
config: mergedConfig,
|
|
4403
|
+
indexes: { componentIndex, blockIndex, tokenIndex },
|
|
4404
|
+
resolvePackageName: (name2) => {
|
|
4405
|
+
if (name2) {
|
|
4406
|
+
const pkg = data.packageMap[name2];
|
|
4407
|
+
if (pkg) return pkg;
|
|
4408
|
+
}
|
|
4409
|
+
if (data.defaultPackageName) return data.defaultPackageName;
|
|
4410
|
+
const root = resolvedRoot ?? config.projectRoot;
|
|
4411
|
+
const packageJsonPath = join8(root, "package.json");
|
|
4412
|
+
if (existsSync9(packageJsonPath)) {
|
|
4413
|
+
try {
|
|
4414
|
+
const content = readFileSync6(packageJsonPath, "utf-8");
|
|
4415
|
+
const pkg = JSON.parse(content);
|
|
4416
|
+
if (pkg.name) {
|
|
4417
|
+
return pkg.name;
|
|
4418
|
+
}
|
|
4419
|
+
} catch {
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
return "your-component-library";
|
|
4423
|
+
},
|
|
4424
|
+
toolNames: TOOL_NAMES
|
|
4425
|
+
};
|
|
4426
|
+
try {
|
|
4427
|
+
const toolKey = registry.resolveKey(name);
|
|
4428
|
+
const definition = TOOL_DEFINITION_BY_KEY.get(toolKey);
|
|
4429
|
+
const argumentKeys = Object.keys(args ?? {});
|
|
4430
|
+
const allowedKeys = new Set(Object.keys(definition?.params ?? {}));
|
|
4431
|
+
const unknownKeys = definition ? argumentKeys.filter((key) => !allowedKeys.has(key)) : [];
|
|
4432
|
+
if (unknownKeys.length > 0) {
|
|
4433
|
+
return {
|
|
4434
|
+
content: [
|
|
4435
|
+
{
|
|
4436
|
+
type: "text",
|
|
4437
|
+
text: JSON.stringify({
|
|
4438
|
+
error: `Unknown argument(s) for ${toolKey}: ${unknownKeys.join(", ")}`
|
|
4439
|
+
})
|
|
4440
|
+
}
|
|
4441
|
+
],
|
|
4442
|
+
isError: true
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
const mCtx = {
|
|
4446
|
+
toolName: name,
|
|
4447
|
+
toolKey,
|
|
4448
|
+
args: args ?? {},
|
|
4449
|
+
ctx: toolContext
|
|
4450
|
+
};
|
|
4451
|
+
return await executeWithMiddleware(
|
|
4452
|
+
config.middleware ?? [],
|
|
4453
|
+
mCtx,
|
|
4454
|
+
() => registry.execute(name, args ?? {}, toolContext)
|
|
4455
|
+
);
|
|
4456
|
+
} catch (error) {
|
|
4457
|
+
return {
|
|
4458
|
+
content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }],
|
|
4459
|
+
isError: true
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
});
|
|
4463
|
+
return server;
|
|
4464
|
+
}
|
|
4465
|
+
async function startMcpServer(config) {
|
|
4466
|
+
const server = createMcpServer(config);
|
|
4467
|
+
const transport = new StdioServerTransport();
|
|
4468
|
+
await server.connect(transport);
|
|
4469
|
+
}
|
|
4470
|
+
function createSandboxServer() {
|
|
4471
|
+
return createMcpServer({ projectRoot: process.cwd() });
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
export {
|
|
4475
|
+
loadConfigFile,
|
|
4476
|
+
SYNONYM_MAP,
|
|
4477
|
+
USE_CASE_TOKEN_CATEGORIES,
|
|
4478
|
+
MINIMUM_SCORE_THRESHOLD,
|
|
4479
|
+
BLOCK_BOOST_PER_OCCURRENCE,
|
|
4480
|
+
DEFAULT_ENDPOINTS,
|
|
4481
|
+
CORE_TOOLS,
|
|
4482
|
+
VIEWER_TOOLS,
|
|
4483
|
+
INFRA_TOOLS,
|
|
4484
|
+
BUILTIN_TOOLS,
|
|
4485
|
+
ToolRegistry,
|
|
4486
|
+
executeWithMiddleware,
|
|
4487
|
+
telemetryMiddleware,
|
|
4488
|
+
componentFromCompiledFragment,
|
|
4489
|
+
blockFromCompiledBlock,
|
|
4490
|
+
tokensFromCompiledTokenData,
|
|
4491
|
+
buildCapabilities,
|
|
4492
|
+
validateSnapshot,
|
|
4493
|
+
FragmentsJsonAdapter,
|
|
4494
|
+
AutoExtractionAdapter,
|
|
4495
|
+
resolveDataAdapter,
|
|
4496
|
+
resolveSearchApiKey,
|
|
4497
|
+
createMcpServer,
|
|
4498
|
+
startMcpServer,
|
|
4499
|
+
createSandboxServer
|
|
4500
|
+
};
|
|
4501
|
+
//# sourceMappingURL=chunk-HGGAXLRO.js.map
|