@fragments-sdk/mcp 0.6.0 → 0.6.2
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/dist/bin.js +122 -21
- package/dist/bin.js.map +1 -1
- package/dist/chunk-2W7DAUUS.js +107 -0
- package/dist/chunk-2W7DAUUS.js.map +1 -0
- package/dist/chunk-4SVS3AA3.js +78 -0
- package/dist/chunk-4SVS3AA3.js.map +1 -0
- package/dist/chunk-FGIBLPSU.js +29 -0
- package/dist/chunk-FGIBLPSU.js.map +1 -0
- package/dist/chunk-NVHGG7GW.js +630 -0
- package/dist/chunk-NVHGG7GW.js.map +1 -0
- package/dist/chunk-VRPDT3Y6.js +52 -0
- package/dist/chunk-VRPDT3Y6.js.map +1 -0
- package/dist/chunk-WBOVO43F.js +2481 -0
- package/dist/chunk-WBOVO43F.js.map +1 -0
- package/dist/config-TUFA5J2S.js +7 -0
- package/dist/config-TUFA5J2S.js.map +1 -0
- package/dist/constants-YXOTMY3I.js +9 -0
- package/dist/constants-YXOTMY3I.js.map +1 -0
- package/dist/dist-V7D67NXS.js +1093 -0
- package/dist/dist-V7D67NXS.js.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/init.js +139 -0
- package/dist/init.js.map +1 -0
- package/dist/rules-WGBCECAK.js +7 -0
- package/dist/rules-WGBCECAK.js.map +1 -0
- package/dist/sass.node-4XJK6YBF-2NJM7G64.js +130796 -0
- package/dist/sass.node-4XJK6YBF-2NJM7G64.js.map +1 -0
- package/dist/server.js +3 -1
- package/package.json +22 -4
- package/dist/chunk-VHHXL3YB.js +0 -2274
- package/dist/chunk-VHHXL3YB.js.map +0 -1
|
@@ -0,0 +1,2481 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRAND,
|
|
3
|
+
DEFAULTS
|
|
4
|
+
} from "./chunk-4SVS3AA3.js";
|
|
5
|
+
import {
|
|
6
|
+
loadConfigFile
|
|
7
|
+
} from "./chunk-FGIBLPSU.js";
|
|
8
|
+
|
|
9
|
+
// src/server.ts
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import {
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ListToolsRequestSchema
|
|
15
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { existsSync as existsSync3 } from "fs";
|
|
17
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
18
|
+
import { join as join3 } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
|
|
21
|
+
|
|
22
|
+
// src/orama-index.ts
|
|
23
|
+
import { create, insertMultiple, search } from "@orama/orama";
|
|
24
|
+
var SYNONYM_MAP = {
|
|
25
|
+
"form": ["input", "field", "submit", "validation"],
|
|
26
|
+
"input": ["form", "field", "text", "entry"],
|
|
27
|
+
"button": ["action", "click", "submit", "trigger"],
|
|
28
|
+
"action": ["button", "click", "trigger"],
|
|
29
|
+
"submit": ["button", "form", "action", "send"],
|
|
30
|
+
"alert": ["notification", "message", "warning", "error", "feedback"],
|
|
31
|
+
"notification": ["alert", "message", "toast"],
|
|
32
|
+
"feedback": ["form", "comment", "review", "rating"],
|
|
33
|
+
"card": ["container", "panel", "box", "content"],
|
|
34
|
+
"toggle": ["switch", "checkbox", "boolean", "on/off"],
|
|
35
|
+
"switch": ["toggle", "checkbox", "boolean"],
|
|
36
|
+
"badge": ["tag", "label", "status", "indicator"],
|
|
37
|
+
"status": ["badge", "indicator", "state"],
|
|
38
|
+
"login": ["auth", "signin", "authentication", "form"],
|
|
39
|
+
"auth": ["login", "signin", "authentication"],
|
|
40
|
+
"chat": ["message", "conversation", "ai"],
|
|
41
|
+
"table": ["data", "grid", "list", "rows"],
|
|
42
|
+
"textarea": ["text", "input", "multiline", "area", "comment"],
|
|
43
|
+
"area": ["textarea", "multiline", "text"],
|
|
44
|
+
"landing": ["page", "hero", "marketing", "section", "layout"],
|
|
45
|
+
"hero": ["landing", "marketing", "banner", "headline", "section"],
|
|
46
|
+
"marketing": ["landing", "hero", "pricing", "testimonial", "cta"],
|
|
47
|
+
"cta": ["marketing", "banner", "action", "button"],
|
|
48
|
+
"testimonial": ["marketing", "review", "quote", "feedback"],
|
|
49
|
+
"layout": ["stack", "grid", "box", "container", "page"],
|
|
50
|
+
"page": ["layout", "landing", "section", "container"],
|
|
51
|
+
"section": ["hero", "feature", "testimonial", "cta", "faq"],
|
|
52
|
+
"pricing": ["card", "plan", "tier", "marketing"],
|
|
53
|
+
"plan": ["pricing", "card", "tier", "subscription"],
|
|
54
|
+
"dashboard": ["metrics", "stats", "chart", "card", "grid"],
|
|
55
|
+
"metrics": ["dashboard", "stats", "progress", "number"],
|
|
56
|
+
"stats": ["metrics", "dashboard", "progress", "badge"],
|
|
57
|
+
"chart": ["dashboard", "metrics", "data", "graph"]
|
|
58
|
+
};
|
|
59
|
+
function expandQuery(query) {
|
|
60
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
61
|
+
const expanded = new Set(terms);
|
|
62
|
+
for (const term of terms) {
|
|
63
|
+
const synonyms = SYNONYM_MAP[term];
|
|
64
|
+
if (synonyms) {
|
|
65
|
+
for (const syn of synonyms) expanded.add(syn);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Array.from(expanded).join(" ");
|
|
69
|
+
}
|
|
70
|
+
function twoPassSearch(config) {
|
|
71
|
+
const { index, query, properties, boost, limit, kind } = config;
|
|
72
|
+
const baseConfig = {
|
|
73
|
+
mode: "fulltext",
|
|
74
|
+
properties,
|
|
75
|
+
boost,
|
|
76
|
+
limit
|
|
77
|
+
};
|
|
78
|
+
const originalTermsQuery = query.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
|
|
79
|
+
const expandedQuery = expandQuery(query);
|
|
80
|
+
const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });
|
|
81
|
+
const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });
|
|
82
|
+
const origHits = originalResults.hits;
|
|
83
|
+
const expHits = expandedResults.hits;
|
|
84
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const hit of origHits) {
|
|
86
|
+
scoreMap.set(hit.document.name, (hit.score || 0) * 2);
|
|
87
|
+
}
|
|
88
|
+
for (const hit of expHits) {
|
|
89
|
+
const name = hit.document.name;
|
|
90
|
+
const existing = scoreMap.get(name) ?? 0;
|
|
91
|
+
scoreMap.set(name, existing + (hit.score || 0));
|
|
92
|
+
}
|
|
93
|
+
const scored = [];
|
|
94
|
+
for (const [name, score] of scoreMap) {
|
|
95
|
+
if (score > 0) {
|
|
96
|
+
scored.push({ name, kind, rank: scored.length, score });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
scored.sort((a, b) => b.score - a.score);
|
|
100
|
+
scored.forEach((s, i) => {
|
|
101
|
+
s.rank = i;
|
|
102
|
+
});
|
|
103
|
+
return scored;
|
|
104
|
+
}
|
|
105
|
+
var componentSchema = {
|
|
106
|
+
name: "string",
|
|
107
|
+
description: "string",
|
|
108
|
+
category: "string",
|
|
109
|
+
tags: "string",
|
|
110
|
+
whenUsed: "string",
|
|
111
|
+
variants: "string",
|
|
112
|
+
status: "string"
|
|
113
|
+
};
|
|
114
|
+
function buildComponentIndex(fragments) {
|
|
115
|
+
const db = create({ schema: componentSchema, language: "english" });
|
|
116
|
+
const docs = fragments.map((f) => ({
|
|
117
|
+
name: f.meta.name,
|
|
118
|
+
description: f.meta.description ?? "",
|
|
119
|
+
category: f.meta.category ?? "",
|
|
120
|
+
tags: (f.meta.tags ?? []).join(" "),
|
|
121
|
+
whenUsed: (f.usage?.when ?? []).join(" "),
|
|
122
|
+
variants: f.variants.map((v) => `${v.name} ${v.description || ""}`).join(" "),
|
|
123
|
+
status: f.meta.status ?? "stable"
|
|
124
|
+
}));
|
|
125
|
+
insertMultiple(db, docs);
|
|
126
|
+
return db;
|
|
127
|
+
}
|
|
128
|
+
function searchComponents(query, index, fragments, limit = 50) {
|
|
129
|
+
const boostConfig = {
|
|
130
|
+
mode: "fulltext",
|
|
131
|
+
properties: ["name", "whenUsed", "description", "category", "tags", "variants"],
|
|
132
|
+
boost: {
|
|
133
|
+
name: 3,
|
|
134
|
+
whenUsed: 2.5,
|
|
135
|
+
description: 2,
|
|
136
|
+
category: 1.5,
|
|
137
|
+
tags: 1.5,
|
|
138
|
+
variants: 1
|
|
139
|
+
},
|
|
140
|
+
limit
|
|
141
|
+
};
|
|
142
|
+
const originalTermsList = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
143
|
+
const originalTermsQuery = originalTermsList.join(" ");
|
|
144
|
+
const expandedQuery = expandQuery(query);
|
|
145
|
+
const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });
|
|
146
|
+
const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });
|
|
147
|
+
const origHits = originalResults.hits;
|
|
148
|
+
const expHits = expandedResults.hits;
|
|
149
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const hit of origHits) {
|
|
151
|
+
scoreMap.set(hit.document.name, (hit.score || 0) * 2);
|
|
152
|
+
}
|
|
153
|
+
for (const hit of expHits) {
|
|
154
|
+
const name = hit.document.name;
|
|
155
|
+
const existing = scoreMap.get(name) ?? 0;
|
|
156
|
+
scoreMap.set(name, existing + (hit.score || 0));
|
|
157
|
+
}
|
|
158
|
+
const fragmentMap = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const f of fragments) {
|
|
160
|
+
fragmentMap.set(f.meta.name.toLowerCase(), f);
|
|
161
|
+
}
|
|
162
|
+
const originalTermsSet = new Set(originalTermsList);
|
|
163
|
+
const scored = [];
|
|
164
|
+
for (const [name, rawScore] of scoreMap) {
|
|
165
|
+
let score = rawScore;
|
|
166
|
+
const nameLower = name.toLowerCase();
|
|
167
|
+
const fragment = fragmentMap.get(nameLower);
|
|
168
|
+
if (originalTermsSet.has(nameLower)) {
|
|
169
|
+
score += 25;
|
|
170
|
+
}
|
|
171
|
+
if (fragment) {
|
|
172
|
+
if (fragment.meta.status === "stable") score += 5;
|
|
173
|
+
else if (fragment.meta.status === "beta") score += 2;
|
|
174
|
+
if (fragment.meta.status === "deprecated") score -= 25;
|
|
175
|
+
}
|
|
176
|
+
if (score > 0) {
|
|
177
|
+
scored.push({ name, kind: "component", rank: scored.length, score });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
scored.sort((a, b) => b.score - a.score);
|
|
181
|
+
scored.forEach((s, i) => {
|
|
182
|
+
s.rank = i;
|
|
183
|
+
});
|
|
184
|
+
return scored;
|
|
185
|
+
}
|
|
186
|
+
var blockSchema = {
|
|
187
|
+
name: "string",
|
|
188
|
+
description: "string",
|
|
189
|
+
category: "string",
|
|
190
|
+
tags: "string",
|
|
191
|
+
components: "string"
|
|
192
|
+
};
|
|
193
|
+
function buildBlockIndex(blocks) {
|
|
194
|
+
const db = create({ schema: blockSchema, language: "english" });
|
|
195
|
+
const docs = blocks.map((b) => ({
|
|
196
|
+
name: b.name,
|
|
197
|
+
description: b.description ?? "",
|
|
198
|
+
category: b.category ?? "",
|
|
199
|
+
tags: (b.tags ?? []).join(" "),
|
|
200
|
+
components: b.components.join(" ")
|
|
201
|
+
}));
|
|
202
|
+
insertMultiple(db, docs);
|
|
203
|
+
return db;
|
|
204
|
+
}
|
|
205
|
+
function searchBlocks(query, index, limit = 50) {
|
|
206
|
+
return twoPassSearch({
|
|
207
|
+
index,
|
|
208
|
+
query,
|
|
209
|
+
properties: ["name", "description", "components", "tags", "category"],
|
|
210
|
+
boost: {
|
|
211
|
+
name: 3,
|
|
212
|
+
description: 2,
|
|
213
|
+
components: 1.5,
|
|
214
|
+
tags: 1.5,
|
|
215
|
+
category: 1.5
|
|
216
|
+
},
|
|
217
|
+
limit,
|
|
218
|
+
kind: "block"
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
var tokenSchema = {
|
|
222
|
+
name: "string",
|
|
223
|
+
category: "string",
|
|
224
|
+
description: "string"
|
|
225
|
+
};
|
|
226
|
+
function buildTokenIndex(tokenData) {
|
|
227
|
+
const db = create({ schema: tokenSchema, language: "english" });
|
|
228
|
+
const docs = [];
|
|
229
|
+
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
230
|
+
for (const token of tokens) {
|
|
231
|
+
docs.push({
|
|
232
|
+
name: token.name,
|
|
233
|
+
category: cat,
|
|
234
|
+
description: token.description ?? ""
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
insertMultiple(db, docs);
|
|
239
|
+
return db;
|
|
240
|
+
}
|
|
241
|
+
function searchTokens(query, index, limit = 50) {
|
|
242
|
+
return twoPassSearch({
|
|
243
|
+
index,
|
|
244
|
+
query,
|
|
245
|
+
properties: ["name", "category", "description"],
|
|
246
|
+
boost: {
|
|
247
|
+
name: 2.5,
|
|
248
|
+
category: 2,
|
|
249
|
+
description: 1.5
|
|
250
|
+
},
|
|
251
|
+
limit,
|
|
252
|
+
kind: "token"
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
var USE_CASE_TOKEN_CATEGORIES = {
|
|
256
|
+
"table": ["spacing", "borders", "surfaces", "text"],
|
|
257
|
+
"data": ["spacing", "borders", "surfaces"],
|
|
258
|
+
"grid": ["spacing", "layout"],
|
|
259
|
+
"form": ["spacing", "borders", "radius", "focus"],
|
|
260
|
+
"input": ["spacing", "borders", "radius", "focus"],
|
|
261
|
+
"card": ["surfaces", "shadows", "radius", "borders", "spacing"],
|
|
262
|
+
"button": ["colors", "radius", "spacing", "focus"],
|
|
263
|
+
"layout": ["spacing", "layout", "surfaces"],
|
|
264
|
+
"dashboard": ["spacing", "surfaces", "borders", "shadows"],
|
|
265
|
+
"chat": ["spacing", "surfaces", "radius", "shadows"],
|
|
266
|
+
"modal": ["shadows", "surfaces", "radius", "spacing"],
|
|
267
|
+
"dialog": ["shadows", "surfaces", "radius", "spacing"],
|
|
268
|
+
"navigation": ["spacing", "surfaces", "borders"],
|
|
269
|
+
"sidebar": ["spacing", "surfaces", "borders"],
|
|
270
|
+
"hero": ["spacing", "typography", "colors"],
|
|
271
|
+
"landing": ["spacing", "typography", "colors"],
|
|
272
|
+
"pricing": ["spacing", "surfaces", "borders", "radius"],
|
|
273
|
+
"auth": ["spacing", "borders", "radius", "focus"],
|
|
274
|
+
"login": ["spacing", "borders", "radius", "focus"],
|
|
275
|
+
"dark": ["colors", "surfaces"],
|
|
276
|
+
"theme": ["colors", "surfaces", "text"]
|
|
277
|
+
};
|
|
278
|
+
function extractTokenCategories(query) {
|
|
279
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
280
|
+
const categories = /* @__PURE__ */ new Set();
|
|
281
|
+
for (const term of terms) {
|
|
282
|
+
const cats = USE_CASE_TOKEN_CATEGORIES[term];
|
|
283
|
+
if (cats) {
|
|
284
|
+
for (const cat of cats) categories.add(cat);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (categories.size === 0) {
|
|
288
|
+
return ["spacing", "colors", "surfaces"];
|
|
289
|
+
}
|
|
290
|
+
return Array.from(categories);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/adapters/fragments-json.ts
|
|
294
|
+
import { readFile } from "fs/promises";
|
|
295
|
+
|
|
296
|
+
// src/discovery.ts
|
|
297
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
298
|
+
import { join, dirname, resolve } from "path";
|
|
299
|
+
import { createRequire } from "module";
|
|
300
|
+
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
301
|
+
const parts = pattern.split("/");
|
|
302
|
+
let dirs = [baseDir];
|
|
303
|
+
for (const part of parts) {
|
|
304
|
+
if (part === "**") continue;
|
|
305
|
+
const next = [];
|
|
306
|
+
for (const d of dirs) {
|
|
307
|
+
if (part === "*") {
|
|
308
|
+
try {
|
|
309
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
310
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
311
|
+
next.push(join(d, entry.name));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
const candidate = join(d, part);
|
|
318
|
+
if (existsSync(candidate)) next.push(candidate);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
dirs = next;
|
|
322
|
+
}
|
|
323
|
+
return dirs;
|
|
324
|
+
}
|
|
325
|
+
function getWorkspaceDirs(rootDir) {
|
|
326
|
+
const dirs = [];
|
|
327
|
+
const rootPkgPath = join(rootDir, "package.json");
|
|
328
|
+
if (existsSync(rootPkgPath)) {
|
|
329
|
+
try {
|
|
330
|
+
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
|
331
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
332
|
+
if (Array.isArray(workspaces)) {
|
|
333
|
+
for (const pattern of workspaces) {
|
|
334
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
|
|
335
|
+
}
|
|
336
|
+
return dirs;
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const pnpmWsPath = join(rootDir, "pnpm-workspace.yaml");
|
|
342
|
+
if (existsSync(pnpmWsPath)) {
|
|
343
|
+
try {
|
|
344
|
+
const content = readFileSync(pnpmWsPath, "utf-8");
|
|
345
|
+
const lines = content.split("\n");
|
|
346
|
+
let inPackages = false;
|
|
347
|
+
for (const line of lines) {
|
|
348
|
+
if (/^packages\s*:/.test(line)) {
|
|
349
|
+
inPackages = true;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (inPackages) {
|
|
353
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
|
|
354
|
+
if (match) {
|
|
355
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
|
|
356
|
+
} else if (/^\S/.test(line) && line.trim()) {
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return dirs;
|
|
365
|
+
}
|
|
366
|
+
function resolveDepPackageJson(localRequire, depName) {
|
|
367
|
+
try {
|
|
368
|
+
return localRequire.resolve(`${depName}/package.json`);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const mainPath = localRequire.resolve(depName);
|
|
373
|
+
let dir = dirname(mainPath);
|
|
374
|
+
while (true) {
|
|
375
|
+
const candidate = join(dir, "package.json");
|
|
376
|
+
if (existsSync(candidate)) {
|
|
377
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
378
|
+
if (pkg.name === depName) return candidate;
|
|
379
|
+
}
|
|
380
|
+
const parent = dirname(dir);
|
|
381
|
+
if (parent === dir) break;
|
|
382
|
+
dir = parent;
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
function findFragmentsInDeps(dir, found, depField) {
|
|
389
|
+
const pkgJsonPath = join(dir, "package.json");
|
|
390
|
+
if (!existsSync(pkgJsonPath)) return;
|
|
391
|
+
try {
|
|
392
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
393
|
+
const allDeps = {
|
|
394
|
+
...pkgJson.dependencies,
|
|
395
|
+
...pkgJson.devDependencies
|
|
396
|
+
};
|
|
397
|
+
const localRequire = createRequire(join(dir, "noop.js"));
|
|
398
|
+
for (const depName of Object.keys(allDeps)) {
|
|
399
|
+
try {
|
|
400
|
+
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
401
|
+
if (!depPkgPath) continue;
|
|
402
|
+
const depPkg = JSON.parse(readFileSync(depPkgPath, "utf-8"));
|
|
403
|
+
if (depPkg[depField]) {
|
|
404
|
+
const fragmentsPath = join(dirname(depPkgPath), depPkg[depField]);
|
|
405
|
+
if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
406
|
+
found.push(fragmentsPath);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function findDesignSystemJson(startDir, outFile, depField) {
|
|
416
|
+
const found = [];
|
|
417
|
+
const resolvedStart = resolve(startDir);
|
|
418
|
+
let dir = resolvedStart;
|
|
419
|
+
while (true) {
|
|
420
|
+
const candidate = join(dir, outFile);
|
|
421
|
+
if (existsSync(candidate)) {
|
|
422
|
+
found.push(candidate);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const parent = dirname(dir);
|
|
426
|
+
if (parent === dir) break;
|
|
427
|
+
dir = parent;
|
|
428
|
+
}
|
|
429
|
+
findFragmentsInDeps(resolvedStart, found, depField);
|
|
430
|
+
if (found.length === 0 || existsSync(join(resolvedStart, "pnpm-workspace.yaml"))) {
|
|
431
|
+
const workspaceDirs = getWorkspaceDirs(resolvedStart);
|
|
432
|
+
for (const wsDir of workspaceDirs) {
|
|
433
|
+
findFragmentsInDeps(wsDir, found, depField);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return found;
|
|
437
|
+
}
|
|
438
|
+
function findFragmentsJson(startDir) {
|
|
439
|
+
return findDesignSystemJson(startDir, BRAND.outFile, "fragments");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/adapters/fragments-json.ts
|
|
443
|
+
var FragmentsJsonAdapter = class {
|
|
444
|
+
name = "fragments-json";
|
|
445
|
+
discover(startDir) {
|
|
446
|
+
return findFragmentsJson(startDir);
|
|
447
|
+
}
|
|
448
|
+
async load(projectRoot) {
|
|
449
|
+
const paths = this.discover(projectRoot);
|
|
450
|
+
if (paths.length === 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
|
|
453
|
+
|
|
454
|
+
Fix: Add a project-level MCP config so the server runs from your workspace root:
|
|
455
|
+
|
|
456
|
+
Cursor: .cursor/mcp.json
|
|
457
|
+
VS Code: .vscode/mcp.json
|
|
458
|
+
Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
|
|
459
|
+
Windsurf: .windsurf/mcp.json
|
|
460
|
+
|
|
461
|
+
Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
|
|
462
|
+
|
|
463
|
+
If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const content = await readFile(paths[0], "utf-8");
|
|
467
|
+
const primary = JSON.parse(content);
|
|
468
|
+
if (!primary.blocks && primary.recipes) {
|
|
469
|
+
primary.blocks = primary.recipes;
|
|
470
|
+
}
|
|
471
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
472
|
+
if (primary.packageName) {
|
|
473
|
+
for (const name of Object.keys(primary.fragments)) {
|
|
474
|
+
packageMap.set(name, primary.packageName);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (let i = 1; i < paths.length; i++) {
|
|
478
|
+
const extra = JSON.parse(await readFile(paths[i], "utf-8"));
|
|
479
|
+
if (extra.packageName) {
|
|
480
|
+
for (const name of Object.keys(extra.fragments)) {
|
|
481
|
+
packageMap.set(name, extra.packageName);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
Object.assign(primary.fragments, extra.fragments);
|
|
485
|
+
const extraBlocks = extra.blocks ?? extra.recipes;
|
|
486
|
+
if (extraBlocks) {
|
|
487
|
+
primary.blocks = { ...primary.blocks, ...extraBlocks };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
components: primary.fragments,
|
|
492
|
+
blocks: primary.blocks,
|
|
493
|
+
tokens: primary.tokens,
|
|
494
|
+
graph: primary.graph,
|
|
495
|
+
performanceSummary: primary.performanceSummary,
|
|
496
|
+
packageMap,
|
|
497
|
+
defaultPackageName: primary.packageName
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/version.ts
|
|
503
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
504
|
+
function readPackageVersion() {
|
|
505
|
+
try {
|
|
506
|
+
const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
|
|
507
|
+
const pkg = JSON.parse(raw);
|
|
508
|
+
return pkg.version ?? "0.0.0";
|
|
509
|
+
} catch {
|
|
510
|
+
return "0.0.0";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
514
|
+
|
|
515
|
+
// src/tools/discover.ts
|
|
516
|
+
import { generateContext, filterPlaceholders } from "@fragments-sdk/context/generate";
|
|
517
|
+
|
|
518
|
+
// src/search.ts
|
|
519
|
+
var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
|
|
520
|
+
var CONVEX_TIMEOUT_MS = 3e3;
|
|
521
|
+
async function searchConvex(query, apiKey, limit = 10, kind) {
|
|
522
|
+
try {
|
|
523
|
+
const controller = new AbortController();
|
|
524
|
+
const timeout = setTimeout(() => controller.abort(), CONVEX_TIMEOUT_MS);
|
|
525
|
+
const response = await fetch(CONVEX_SEARCH_URL, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: {
|
|
528
|
+
"Content-Type": "application/json",
|
|
529
|
+
"Authorization": `Bearer ${apiKey}`
|
|
530
|
+
},
|
|
531
|
+
body: JSON.stringify({ query, limit, ...kind && { kind } }),
|
|
532
|
+
signal: controller.signal
|
|
533
|
+
});
|
|
534
|
+
clearTimeout(timeout);
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
const data = await response.json();
|
|
539
|
+
return data.results.map((r, i) => ({
|
|
540
|
+
name: r.name,
|
|
541
|
+
kind: r.kind ?? "component",
|
|
542
|
+
rank: i,
|
|
543
|
+
score: r.score
|
|
544
|
+
}));
|
|
545
|
+
} catch {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function keywordScoreComponents(query, fragments, componentIndex) {
|
|
550
|
+
const index = componentIndex ?? buildComponentIndex(fragments);
|
|
551
|
+
return searchComponents(query, index, fragments);
|
|
552
|
+
}
|
|
553
|
+
function keywordScoreBlocks(query, blocks, blockIndex) {
|
|
554
|
+
const index = blockIndex ?? buildBlockIndex(blocks);
|
|
555
|
+
return searchBlocks(query, index);
|
|
556
|
+
}
|
|
557
|
+
function keywordScoreTokens(query, tokenData, tokenIndex) {
|
|
558
|
+
const index = tokenIndex ?? buildTokenIndex(tokenData);
|
|
559
|
+
return searchTokens(query, index);
|
|
560
|
+
}
|
|
561
|
+
function reciprocalRankFusion(resultSets, k = 60) {
|
|
562
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
563
|
+
for (const { results } of resultSets) {
|
|
564
|
+
for (let rank = 0; rank < results.length; rank++) {
|
|
565
|
+
const result = results[rank];
|
|
566
|
+
const key = `${result.kind}:${result.name}`;
|
|
567
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
568
|
+
const existing = scoreMap.get(key);
|
|
569
|
+
if (existing) {
|
|
570
|
+
existing.score += rrfScore;
|
|
571
|
+
} else {
|
|
572
|
+
scoreMap.set(key, { score: rrfScore, kind: result.kind, name: result.name });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const fused = [];
|
|
577
|
+
for (const [, { score, kind, name }] of scoreMap) {
|
|
578
|
+
fused.push({ name, kind, rank: 0, score });
|
|
579
|
+
}
|
|
580
|
+
fused.sort((a, b) => b.score - a.score);
|
|
581
|
+
fused.forEach((r, i) => {
|
|
582
|
+
r.rank = i;
|
|
583
|
+
});
|
|
584
|
+
return fused;
|
|
585
|
+
}
|
|
586
|
+
async function hybridSearch(query, data, limit = 10, kind, apiKey) {
|
|
587
|
+
const keywordResults = [];
|
|
588
|
+
if (!kind || kind === "component") {
|
|
589
|
+
keywordResults.push(...keywordScoreComponents(query, data.fragments, data.componentIndex));
|
|
590
|
+
}
|
|
591
|
+
if ((!kind || kind === "block") && data.blocks) {
|
|
592
|
+
keywordResults.push(...keywordScoreBlocks(query, data.blocks, data.blockIndex));
|
|
593
|
+
}
|
|
594
|
+
if ((!kind || kind === "token") && data.tokenData) {
|
|
595
|
+
keywordResults.push(...keywordScoreTokens(query, data.tokenData, data.tokenIndex));
|
|
596
|
+
}
|
|
597
|
+
keywordResults.sort((a, b) => b.score - a.score);
|
|
598
|
+
keywordResults.forEach((r, i) => {
|
|
599
|
+
r.rank = i;
|
|
600
|
+
});
|
|
601
|
+
if (!apiKey) {
|
|
602
|
+
return keywordResults.slice(0, limit);
|
|
603
|
+
}
|
|
604
|
+
const vectorResults = await searchConvex(query, apiKey, limit, kind);
|
|
605
|
+
if (vectorResults.length === 0) {
|
|
606
|
+
return keywordResults.slice(0, limit);
|
|
607
|
+
}
|
|
608
|
+
const graphBoostResults = [];
|
|
609
|
+
if (data.graph) {
|
|
610
|
+
try {
|
|
611
|
+
const { ComponentGraphEngine: ComponentGraphEngine2, deserializeGraph: deserializeGraph2 } = await import("@fragments-sdk/context/graph");
|
|
612
|
+
const graph = deserializeGraph2(data.graph);
|
|
613
|
+
const engine = new ComponentGraphEngine2(graph);
|
|
614
|
+
const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
|
|
615
|
+
const neighborSet = /* @__PURE__ */ new Set();
|
|
616
|
+
for (const result of topComponents) {
|
|
617
|
+
const neighbors = engine.neighbors(result.name, 1);
|
|
618
|
+
for (const n of neighbors.neighbors) {
|
|
619
|
+
if (!neighborSet.has(n.component)) {
|
|
620
|
+
neighborSet.add(n.component);
|
|
621
|
+
graphBoostResults.push({
|
|
622
|
+
name: n.component,
|
|
623
|
+
kind: "component",
|
|
624
|
+
rank: graphBoostResults.length,
|
|
625
|
+
score: 1
|
|
626
|
+
// Will be normalized through RRF
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const resultSets = [
|
|
635
|
+
{ label: "vector", results: vectorResults },
|
|
636
|
+
{ label: "keyword", results: keywordResults }
|
|
637
|
+
];
|
|
638
|
+
if (graphBoostResults.length > 0) {
|
|
639
|
+
resultSets.push({ label: "graph", results: graphBoostResults });
|
|
640
|
+
}
|
|
641
|
+
const fused = reciprocalRankFusion(resultSets);
|
|
642
|
+
return fused.slice(0, limit);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/scoring.ts
|
|
646
|
+
var MINIMUM_SCORE_THRESHOLD = 5;
|
|
647
|
+
function assignConfidence(score, maxScore) {
|
|
648
|
+
if (maxScore <= 0) return "low";
|
|
649
|
+
const ratio = score / maxScore;
|
|
650
|
+
if (ratio >= 0.7) return "high";
|
|
651
|
+
if (ratio >= 0.4) return "medium";
|
|
652
|
+
return "low";
|
|
653
|
+
}
|
|
654
|
+
function meetsMinimumThreshold(maxScore) {
|
|
655
|
+
return maxScore >= MINIMUM_SCORE_THRESHOLD;
|
|
656
|
+
}
|
|
657
|
+
function levenshtein(a, b) {
|
|
658
|
+
const la = a.length;
|
|
659
|
+
const lb = b.length;
|
|
660
|
+
const dp = Array.from({ length: lb + 1 }, (_, i) => i);
|
|
661
|
+
for (let i = 1; i <= la; i++) {
|
|
662
|
+
let prev = i - 1;
|
|
663
|
+
dp[0] = i;
|
|
664
|
+
for (let j = 1; j <= lb; j++) {
|
|
665
|
+
const temp = dp[j];
|
|
666
|
+
dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
667
|
+
prev = temp;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return dp[lb];
|
|
671
|
+
}
|
|
672
|
+
function findClosestMatch(input, candidates, maxDistance = 3) {
|
|
673
|
+
const inputLower = input.toLowerCase();
|
|
674
|
+
let bestMatch = null;
|
|
675
|
+
let bestDist = maxDistance + 1;
|
|
676
|
+
for (const candidate of candidates) {
|
|
677
|
+
const candidateLower = candidate.toLowerCase();
|
|
678
|
+
const dist = levenshtein(inputLower, candidateLower);
|
|
679
|
+
if (dist < bestDist) {
|
|
680
|
+
bestDist = dist;
|
|
681
|
+
bestMatch = candidate;
|
|
682
|
+
} else if (dist === bestDist && bestMatch) {
|
|
683
|
+
const currentLenDiff = Math.abs(bestMatch.length - input.length);
|
|
684
|
+
const newLenDiff = Math.abs(candidate.length - input.length);
|
|
685
|
+
if (newLenDiff < currentLenDiff) {
|
|
686
|
+
bestMatch = candidate;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return bestDist <= maxDistance ? bestMatch : null;
|
|
691
|
+
}
|
|
692
|
+
var BLOCK_BOOST_PER_OCCURRENCE = 5;
|
|
693
|
+
function buildBlockComponentFrequency(blocks) {
|
|
694
|
+
const freq = /* @__PURE__ */ new Map();
|
|
695
|
+
for (const block of blocks) {
|
|
696
|
+
for (const comp of block.components) {
|
|
697
|
+
const key = comp.toLowerCase();
|
|
698
|
+
freq.set(key, (freq.get(key) ?? 0) + 1);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return freq;
|
|
702
|
+
}
|
|
703
|
+
function boostByBlockFrequency(results, freq) {
|
|
704
|
+
for (const result of results) {
|
|
705
|
+
const count = freq.get(result.name.toLowerCase()) ?? 0;
|
|
706
|
+
if (count > 0) {
|
|
707
|
+
result.score += count * BLOCK_BOOST_PER_OCCURRENCE;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
results.sort((a, b) => b.score - a.score);
|
|
711
|
+
results.forEach((r, i) => {
|
|
712
|
+
r.rank = i;
|
|
713
|
+
});
|
|
714
|
+
return results;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/server-helpers.ts
|
|
718
|
+
function normalizeFilter(value) {
|
|
719
|
+
const normalized = value?.trim().toLowerCase();
|
|
720
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
721
|
+
}
|
|
722
|
+
function categoryMatches(category, categoryFilter) {
|
|
723
|
+
if (!categoryFilter) return true;
|
|
724
|
+
return normalizeFilter(category) === categoryFilter;
|
|
725
|
+
}
|
|
726
|
+
function buildLocalSearchData(data, indexes) {
|
|
727
|
+
const allFragments = Object.values(data.fragments);
|
|
728
|
+
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
729
|
+
const localData = {
|
|
730
|
+
fragments: allFragments,
|
|
731
|
+
blocks: allBlocks,
|
|
732
|
+
tokenData: data.tokens,
|
|
733
|
+
graph: data.graph,
|
|
734
|
+
componentIndex: indexes.componentIndex ?? void 0,
|
|
735
|
+
blockIndex: indexes.blockIndex ?? void 0,
|
|
736
|
+
tokenIndex: indexes.tokenIndex ?? void 0
|
|
737
|
+
};
|
|
738
|
+
return { allFragments, allBlocks, localData };
|
|
739
|
+
}
|
|
740
|
+
async function buildImportStatements(components, resolvePackageName) {
|
|
741
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
742
|
+
for (const component of components) {
|
|
743
|
+
if (!component) continue;
|
|
744
|
+
const packageName = await resolvePackageName(component);
|
|
745
|
+
const existing = grouped.get(packageName);
|
|
746
|
+
if (!existing) {
|
|
747
|
+
grouped.set(packageName, [component]);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (!existing.includes(component)) {
|
|
751
|
+
existing.push(component);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return Array.from(grouped.entries()).map(
|
|
755
|
+
([packageName, componentNames]) => `import { ${componentNames.join(", ")} } from '${packageName}';`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
function limitTokensPerCategory(categories, limit) {
|
|
759
|
+
if (limit === void 0) {
|
|
760
|
+
return {
|
|
761
|
+
categories,
|
|
762
|
+
total: Object.values(categories).reduce((sum, tokens) => sum + tokens.length, 0)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const limited = {};
|
|
766
|
+
let total = 0;
|
|
767
|
+
for (const [category, tokens] of Object.entries(categories)) {
|
|
768
|
+
const sliced = tokens.slice(0, limit);
|
|
769
|
+
if (sliced.length === 0) continue;
|
|
770
|
+
limited[category] = sliced;
|
|
771
|
+
total += sliced.length;
|
|
772
|
+
}
|
|
773
|
+
return { categories: limited, total };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/tools/discover.ts
|
|
777
|
+
var discoverHandler = async (args, ctx) => {
|
|
778
|
+
const data = ctx.data;
|
|
779
|
+
const useCase = args?.useCase ?? void 0;
|
|
780
|
+
const componentForAlts = args?.component ?? void 0;
|
|
781
|
+
const category = normalizeFilter(args?.category);
|
|
782
|
+
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
783
|
+
const status = args?.status ?? void 0;
|
|
784
|
+
const format = args?.format ?? "markdown";
|
|
785
|
+
const compact = args?.compact ?? false;
|
|
786
|
+
const includeCode = args?.includeCode ?? false;
|
|
787
|
+
const includeRelations = args?.includeRelations ?? false;
|
|
788
|
+
const limit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
|
|
789
|
+
const verbosity = args?.verbosity ?? "standard";
|
|
790
|
+
if (compact || args?.format && !useCase && !componentForAlts) {
|
|
791
|
+
let fragments2 = Object.values(data.components);
|
|
792
|
+
const allBlocks = Object.values(data.blocks ?? {});
|
|
793
|
+
if (category) {
|
|
794
|
+
fragments2 = fragments2.filter((f) => categoryMatches(f.meta.category, category));
|
|
795
|
+
}
|
|
796
|
+
if (search2) {
|
|
797
|
+
fragments2 = fragments2.filter(
|
|
798
|
+
(f) => f.meta.name.toLowerCase().includes(search2) || f.meta.description?.toLowerCase().includes(search2) || f.meta.tags?.some((t) => t.toLowerCase().includes(search2))
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (status) {
|
|
802
|
+
fragments2 = fragments2.filter((f) => f.meta.status === status);
|
|
803
|
+
}
|
|
804
|
+
const { content: ctxContent, tokenEstimate } = generateContext(fragments2, {
|
|
805
|
+
format,
|
|
806
|
+
compact,
|
|
807
|
+
include: {
|
|
808
|
+
code: includeCode,
|
|
809
|
+
relations: includeRelations
|
|
810
|
+
}
|
|
811
|
+
}, allBlocks);
|
|
812
|
+
return {
|
|
813
|
+
content: [{ type: "text", text: ctxContent }],
|
|
814
|
+
_meta: { tokenEstimate }
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (useCase) {
|
|
818
|
+
const { allFragments, allBlocks, localData } = buildLocalSearchData(
|
|
819
|
+
{
|
|
820
|
+
fragments: data.components,
|
|
821
|
+
blocks: data.blocks,
|
|
822
|
+
tokens: data.tokens,
|
|
823
|
+
graph: data.graph
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
componentIndex: ctx.indexes.componentIndex,
|
|
827
|
+
blockIndex: ctx.indexes.blockIndex,
|
|
828
|
+
tokenIndex: ctx.indexes.tokenIndex
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
const context = args?.context?.toLowerCase() ?? "";
|
|
832
|
+
const fullQuery = context ? `${useCase} ${context}` : useCase;
|
|
833
|
+
const searchResults = await hybridSearch(fullQuery, localData, limit, "component", ctx.config.apiKey);
|
|
834
|
+
const blockMatches = keywordScoreBlocks(fullQuery, allBlocks, ctx.indexes.blockIndex ?? void 0).slice(0, 5);
|
|
835
|
+
if (blockMatches.length > 0) {
|
|
836
|
+
const matchedBlocks = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean);
|
|
837
|
+
const blockFreq = buildBlockComponentFrequency(matchedBlocks);
|
|
838
|
+
boostByBlockFrequency(searchResults, blockFreq);
|
|
839
|
+
}
|
|
840
|
+
const maxScore = searchResults.length > 0 ? searchResults[0].score : 0;
|
|
841
|
+
const scored = searchResults.map((result) => {
|
|
842
|
+
const fragment = allFragments.find(
|
|
843
|
+
(s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
|
|
844
|
+
);
|
|
845
|
+
if (!fragment) return null;
|
|
846
|
+
const filteredWhen = filterPlaceholders(fragment.usage?.when).slice(0, 3);
|
|
847
|
+
const filteredWhenNot = filterPlaceholders(fragment.usage?.whenNot).slice(0, 2);
|
|
848
|
+
return {
|
|
849
|
+
component: fragment.meta.name,
|
|
850
|
+
category: fragment.meta.category,
|
|
851
|
+
description: fragment.meta.description,
|
|
852
|
+
confidence: assignConfidence(result.score, maxScore),
|
|
853
|
+
reasons: [`Matched via hybrid search (score: ${result.score.toFixed(4)})`],
|
|
854
|
+
usage: { when: filteredWhen, whenNot: filteredWhenNot },
|
|
855
|
+
variantCount: fragment.variants.length,
|
|
856
|
+
status: fragment.meta.status
|
|
857
|
+
};
|
|
858
|
+
}).filter(Boolean);
|
|
859
|
+
const suggestions = [];
|
|
860
|
+
const categoryCount = {};
|
|
861
|
+
for (const item of scored) {
|
|
862
|
+
if (!item) continue;
|
|
863
|
+
const cat = item.category || "uncategorized";
|
|
864
|
+
const count = categoryCount[cat] || 0;
|
|
865
|
+
if (count < 2 || suggestions.length < 3) {
|
|
866
|
+
suggestions.push(item);
|
|
867
|
+
categoryCount[cat] = count + 1;
|
|
868
|
+
if (suggestions.length >= limit) break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((s) => s.component).join(" and ")}.` : void 0;
|
|
872
|
+
const useCaseLower = useCase.toLowerCase();
|
|
873
|
+
const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
|
|
874
|
+
const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
|
|
875
|
+
const noMatch = suggestions.length === 0;
|
|
876
|
+
const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
|
|
877
|
+
const weakMatch = !noMatch && (belowThreshold || suggestions.every((s) => s.confidence === "low"));
|
|
878
|
+
let recommendation;
|
|
879
|
+
let nextStep;
|
|
880
|
+
if (noMatch) {
|
|
881
|
+
recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${ctx.toolNames.tokens} to find CSS custom properties.` : `No matching components found. Try different keywords or browse all components with ${ctx.toolNames.discover}.`;
|
|
882
|
+
nextStep = isStyleQuery ? `Use ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}") to find design tokens.` : void 0;
|
|
883
|
+
} else if (weakMatch) {
|
|
884
|
+
recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${ctx.toolNames.tokens}.` : ""}`;
|
|
885
|
+
nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
|
|
886
|
+
} else {
|
|
887
|
+
recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
|
|
888
|
+
nextStep = `Use ${ctx.toolNames.inspect}("${suggestions[0].component}") for full details.`;
|
|
889
|
+
}
|
|
890
|
+
const tokenHint = isStyleQuery && !noMatch ? `Your query includes styling terms. For CSS custom properties, also try ${ctx.toolNames.tokens}(search: "${useCaseLower.split(/\s+/)[0]}").` : void 0;
|
|
891
|
+
const blockNames = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean).slice(0, 3).map((b) => b.name);
|
|
892
|
+
const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${ctx.toolNames.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
|
|
893
|
+
const suggestResponse = verbosity === "compact" ? {
|
|
894
|
+
useCase,
|
|
895
|
+
suggestions: suggestions.map((s) => ({
|
|
896
|
+
component: s.component,
|
|
897
|
+
description: s.description,
|
|
898
|
+
confidence: s.confidence
|
|
899
|
+
})),
|
|
900
|
+
recommendation
|
|
901
|
+
} : {
|
|
902
|
+
useCase,
|
|
903
|
+
context: context || void 0,
|
|
904
|
+
suggestions,
|
|
905
|
+
noMatch,
|
|
906
|
+
weakMatch,
|
|
907
|
+
recommendation,
|
|
908
|
+
compositionHint,
|
|
909
|
+
...tokenHint && { tokenHint },
|
|
910
|
+
...blockHint && { blockHint },
|
|
911
|
+
nextStep
|
|
912
|
+
};
|
|
913
|
+
return {
|
|
914
|
+
content: [{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: JSON.stringify(suggestResponse)
|
|
917
|
+
}]
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (componentForAlts) {
|
|
921
|
+
const fragment = Object.values(data.components).find(
|
|
922
|
+
(s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
|
|
923
|
+
);
|
|
924
|
+
if (!fragment) {
|
|
925
|
+
const allNames = Object.values(data.components).map((s) => s.meta.name);
|
|
926
|
+
const closest = findClosestMatch(componentForAlts, allNames);
|
|
927
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
928
|
+
throw new Error(`Component "${componentForAlts}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
929
|
+
}
|
|
930
|
+
const relations = fragment.relations ?? [];
|
|
931
|
+
const referencedBy = Object.values(data.components).filter(
|
|
932
|
+
(s) => s.relations?.some((r) => r.component.toLowerCase() === componentForAlts.toLowerCase())
|
|
933
|
+
).map((s) => ({
|
|
934
|
+
component: s.meta.name,
|
|
935
|
+
relationship: s.relations?.find(
|
|
936
|
+
(r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
|
|
937
|
+
)?.relationship,
|
|
938
|
+
note: s.relations?.find(
|
|
939
|
+
(r) => r.component.toLowerCase() === componentForAlts.toLowerCase()
|
|
940
|
+
)?.note
|
|
941
|
+
}));
|
|
942
|
+
const sameCategory = Object.values(data.components).filter(
|
|
943
|
+
(s) => s.meta.category === fragment.meta.category && s.meta.name.toLowerCase() !== componentForAlts.toLowerCase()
|
|
944
|
+
).map((s) => ({
|
|
945
|
+
component: s.meta.name,
|
|
946
|
+
description: s.meta.description
|
|
947
|
+
}));
|
|
948
|
+
return {
|
|
949
|
+
content: [{
|
|
950
|
+
type: "text",
|
|
951
|
+
text: JSON.stringify({
|
|
952
|
+
component: fragment.meta.name,
|
|
953
|
+
category: fragment.meta.category,
|
|
954
|
+
directRelations: relations,
|
|
955
|
+
referencedBy,
|
|
956
|
+
sameCategory,
|
|
957
|
+
suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
|
|
958
|
+
})
|
|
959
|
+
}]
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const fragments = Object.values(data.components).filter((s) => {
|
|
963
|
+
if (category && !categoryMatches(s.meta.category, category)) return false;
|
|
964
|
+
if (status && (s.meta.status ?? "stable") !== status) return false;
|
|
965
|
+
if (search2) {
|
|
966
|
+
const nameMatch = s.meta.name.toLowerCase().includes(search2);
|
|
967
|
+
const descMatch = s.meta.description?.toLowerCase().includes(search2);
|
|
968
|
+
const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search2));
|
|
969
|
+
if (!nameMatch && !descMatch && !tagMatch) return false;
|
|
970
|
+
}
|
|
971
|
+
return true;
|
|
972
|
+
}).map((s) => {
|
|
973
|
+
if (verbosity === "compact") {
|
|
974
|
+
return {
|
|
975
|
+
name: s.meta.name,
|
|
976
|
+
category: s.meta.category,
|
|
977
|
+
...s.propsSummary?.length && { propsSummary: s.propsSummary },
|
|
978
|
+
...s.contract?.propsSummary?.length && !s.propsSummary?.length && { propsSummary: s.contract.propsSummary }
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
name: s.meta.name,
|
|
983
|
+
category: s.meta.category,
|
|
984
|
+
description: s.meta.description,
|
|
985
|
+
status: s.meta.status ?? "stable",
|
|
986
|
+
variantCount: s.variants.length,
|
|
987
|
+
tags: s.meta.tags ?? [],
|
|
988
|
+
...(includeCode || verbosity === "full") && s.variants[0]?.code && {
|
|
989
|
+
example: s.variants[0].code
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
content: [{
|
|
995
|
+
type: "text",
|
|
996
|
+
text: JSON.stringify({
|
|
997
|
+
total: fragments.length,
|
|
998
|
+
fragments,
|
|
999
|
+
categories: [...new Set(fragments.map((s) => s.category))],
|
|
1000
|
+
hint: fragments.length === 0 ? "No components found. Try broader search terms or check available categories." : fragments.length > 5 ? `Use ${ctx.toolNames.discover} with useCase for recommendations, or ${ctx.toolNames.inspect} for details on a specific component.` : void 0
|
|
1001
|
+
})
|
|
1002
|
+
}]
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// src/tools/inspect.ts
|
|
1007
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
1008
|
+
import { join as join2 } from "path";
|
|
1009
|
+
|
|
1010
|
+
// src/utils.ts
|
|
1011
|
+
function projectFields(obj, fields) {
|
|
1012
|
+
if (!fields || fields.length === 0) {
|
|
1013
|
+
return obj;
|
|
1014
|
+
}
|
|
1015
|
+
const result = {};
|
|
1016
|
+
for (const field of fields) {
|
|
1017
|
+
const parts = field.split(".");
|
|
1018
|
+
let source = obj;
|
|
1019
|
+
let target = result;
|
|
1020
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1021
|
+
const part = parts[i];
|
|
1022
|
+
const isLast = i === parts.length - 1;
|
|
1023
|
+
if (source === null || source === void 0 || typeof source !== "object") {
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
const sourceObj = source;
|
|
1027
|
+
const value = sourceObj[part];
|
|
1028
|
+
if (isLast) {
|
|
1029
|
+
target[part] = value;
|
|
1030
|
+
} else {
|
|
1031
|
+
if (!(part in target)) {
|
|
1032
|
+
target[part] = {};
|
|
1033
|
+
}
|
|
1034
|
+
target = target[part];
|
|
1035
|
+
source = value;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return result;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/tools/inspect.ts
|
|
1043
|
+
import { filterPlaceholders as filterPlaceholders2 } from "@fragments-sdk/context/generate";
|
|
1044
|
+
function getSourceCode(fragment, projectRoot) {
|
|
1045
|
+
const sourcePath = fragment.sourcePath;
|
|
1046
|
+
if (!sourcePath) return void 0;
|
|
1047
|
+
const fullPath = join2(projectRoot, sourcePath);
|
|
1048
|
+
if (!existsSync2(fullPath)) return { path: sourcePath, code: null };
|
|
1049
|
+
try {
|
|
1050
|
+
const code = readFileSync3(fullPath, "utf-8");
|
|
1051
|
+
return { path: sourcePath, code };
|
|
1052
|
+
} catch {
|
|
1053
|
+
return { path: sourcePath, code: null };
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var inspectHandler = async (args, ctx) => {
|
|
1057
|
+
const componentName = args?.component;
|
|
1058
|
+
const fields = args?.fields;
|
|
1059
|
+
const variantName = args?.variant ?? void 0;
|
|
1060
|
+
const maxExamples = args?.maxExamples;
|
|
1061
|
+
const maxLines = args?.maxLines;
|
|
1062
|
+
const verbosity = args?.verbosity ?? "standard";
|
|
1063
|
+
if (!componentName) {
|
|
1064
|
+
throw new Error("component is required");
|
|
1065
|
+
}
|
|
1066
|
+
const fragment = Object.values(ctx.data.components).find(
|
|
1067
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1068
|
+
);
|
|
1069
|
+
if (!fragment) {
|
|
1070
|
+
const allNames = Object.values(ctx.data.components).map((s) => s.meta.name);
|
|
1071
|
+
const closest = findClosestMatch(componentName, allNames);
|
|
1072
|
+
const suggestion = closest ? ` Did you mean "${closest}"? Use ${ctx.toolNames.inspect}("${closest}") to inspect it.` : "";
|
|
1073
|
+
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
1074
|
+
}
|
|
1075
|
+
const pkgName = ctx.resolvePackageName(fragment.meta.name);
|
|
1076
|
+
let variants = fragment.variants;
|
|
1077
|
+
if (variantName) {
|
|
1078
|
+
const query = variantName.toLowerCase();
|
|
1079
|
+
let filtered = variants.filter((v) => v.name.toLowerCase() === query);
|
|
1080
|
+
if (filtered.length === 0) {
|
|
1081
|
+
filtered = variants.filter((v) => v.name.toLowerCase().startsWith(query));
|
|
1082
|
+
}
|
|
1083
|
+
if (filtered.length === 0) {
|
|
1084
|
+
filtered = variants.filter((v) => v.name.toLowerCase().includes(query));
|
|
1085
|
+
}
|
|
1086
|
+
if (filtered.length > 0) {
|
|
1087
|
+
variants = filtered;
|
|
1088
|
+
} else {
|
|
1089
|
+
throw new Error(
|
|
1090
|
+
`Variant "${variantName}" not found for ${componentName}. Available: ${fragment.variants.map((v) => v.name).join(", ")}`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (maxExamples && maxExamples > 0) {
|
|
1095
|
+
variants = variants.slice(0, maxExamples);
|
|
1096
|
+
}
|
|
1097
|
+
const truncateCode = (code) => {
|
|
1098
|
+
if (!maxLines || maxLines <= 0) return code;
|
|
1099
|
+
const lines = code.split("\n");
|
|
1100
|
+
if (lines.length <= maxLines) return code;
|
|
1101
|
+
return lines.slice(0, maxLines).join("\n") + "\n// ... truncated";
|
|
1102
|
+
};
|
|
1103
|
+
const examples = variants.map((variant) => {
|
|
1104
|
+
if (variant.code) {
|
|
1105
|
+
return {
|
|
1106
|
+
variant: variant.name,
|
|
1107
|
+
description: variant.description,
|
|
1108
|
+
code: truncateCode(variant.code)
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
variant: variant.name,
|
|
1113
|
+
description: variant.description,
|
|
1114
|
+
code: `<${fragment.meta.name} />`,
|
|
1115
|
+
note: "No code example provided in fragment. Refer to props for customization."
|
|
1116
|
+
};
|
|
1117
|
+
});
|
|
1118
|
+
const propsReference = Object.entries(fragment.props ?? {}).map(([propName, prop]) => ({
|
|
1119
|
+
name: propName,
|
|
1120
|
+
type: prop.type,
|
|
1121
|
+
required: prop.required,
|
|
1122
|
+
default: prop.default,
|
|
1123
|
+
description: prop.description
|
|
1124
|
+
}));
|
|
1125
|
+
const propConstraints = Object.entries(fragment.props ?? {}).filter(([, prop]) => prop.constraints && prop.constraints.length > 0).map(([pName, prop]) => ({
|
|
1126
|
+
prop: pName,
|
|
1127
|
+
constraints: prop.constraints
|
|
1128
|
+
}));
|
|
1129
|
+
const fullResult = {
|
|
1130
|
+
meta: fragment.meta,
|
|
1131
|
+
props: fragment.props,
|
|
1132
|
+
variants: fragment.variants,
|
|
1133
|
+
relations: fragment.relations,
|
|
1134
|
+
contract: fragment.contract,
|
|
1135
|
+
generated: fragment._generated,
|
|
1136
|
+
guidelines: {
|
|
1137
|
+
when: filterPlaceholders2(fragment.usage?.when),
|
|
1138
|
+
whenNot: filterPlaceholders2(fragment.usage?.whenNot),
|
|
1139
|
+
guidelines: fragment.usage?.guidelines ?? [],
|
|
1140
|
+
accessibility: fragment.usage?.accessibility ?? [],
|
|
1141
|
+
propConstraints,
|
|
1142
|
+
alternatives: fragment.relations?.filter((r) => r.relationship === "alternative").map((r) => ({
|
|
1143
|
+
component: r.component,
|
|
1144
|
+
note: r.note
|
|
1145
|
+
})) ?? []
|
|
1146
|
+
},
|
|
1147
|
+
examples: {
|
|
1148
|
+
import: `import { ${fragment.meta.name} } from '${pkgName}';`,
|
|
1149
|
+
code: examples,
|
|
1150
|
+
propsReference
|
|
1151
|
+
},
|
|
1152
|
+
source: getSourceCode(fragment, ctx.config.projectRoot)
|
|
1153
|
+
};
|
|
1154
|
+
const aliasMap = { "usage": "guidelines" };
|
|
1155
|
+
const resolvedFields = fields?.map((f) => {
|
|
1156
|
+
const parts = f.split(".");
|
|
1157
|
+
if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
|
|
1158
|
+
return parts.join(".");
|
|
1159
|
+
});
|
|
1160
|
+
let result;
|
|
1161
|
+
if (verbosity === "compact" && !resolvedFields?.length) {
|
|
1162
|
+
result = {
|
|
1163
|
+
meta: fullResult.meta,
|
|
1164
|
+
propsSummary: fragment.propsSummary ?? fragment.contract?.propsSummary ?? Object.keys(fragment.props ?? {}),
|
|
1165
|
+
...fragment.provenance && { provenance: fragment.provenance }
|
|
1166
|
+
};
|
|
1167
|
+
} else if (verbosity === "full") {
|
|
1168
|
+
result = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
|
|
1169
|
+
} else {
|
|
1170
|
+
if (resolvedFields && resolvedFields.length > 0) {
|
|
1171
|
+
result = projectFields(fullResult, resolvedFields);
|
|
1172
|
+
} else {
|
|
1173
|
+
const { source: _source, ...withoutSource } = fullResult;
|
|
1174
|
+
result = {
|
|
1175
|
+
...withoutSource,
|
|
1176
|
+
...fragment.provenance && { provenance: fragment.provenance },
|
|
1177
|
+
...fragment.ai && { ai: fragment.ai }
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1183
|
+
};
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/tools/blocks.ts
|
|
1187
|
+
var blocksHandler = async (args, ctx) => {
|
|
1188
|
+
const blockName = args?.name;
|
|
1189
|
+
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
1190
|
+
const component = args?.component?.toLowerCase() ?? void 0;
|
|
1191
|
+
const category = args?.category?.toLowerCase() ?? void 0;
|
|
1192
|
+
const blocksLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 50) : search2 ? 10 : void 0;
|
|
1193
|
+
const allBlocks = Object.values(ctx.data.blocks ?? {});
|
|
1194
|
+
if (allBlocks.length === 0) {
|
|
1195
|
+
return {
|
|
1196
|
+
content: [{
|
|
1197
|
+
type: "text",
|
|
1198
|
+
text: JSON.stringify({
|
|
1199
|
+
total: 0,
|
|
1200
|
+
blocks: [],
|
|
1201
|
+
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\`.`
|
|
1202
|
+
})
|
|
1203
|
+
}]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
let filtered = allBlocks;
|
|
1207
|
+
if (blockName) {
|
|
1208
|
+
filtered = filtered.filter(
|
|
1209
|
+
(b) => b.name.toLowerCase() === blockName.toLowerCase()
|
|
1210
|
+
);
|
|
1211
|
+
if (filtered.length === 0) {
|
|
1212
|
+
const allBlockNames = allBlocks.map((b) => b.name);
|
|
1213
|
+
const closest = findClosestMatch(blockName, allBlockNames);
|
|
1214
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1215
|
+
throw new Error(`Block "${blockName}" not found.${suggestion} Use ${ctx.toolNames.blocks} to see available blocks.`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (search2) {
|
|
1219
|
+
if (ctx.indexes.blockIndex) {
|
|
1220
|
+
const ranked = searchBlocks(search2, ctx.indexes.blockIndex, 50);
|
|
1221
|
+
const rankedNames = new Set(ranked.map((r) => r.name.toLowerCase()));
|
|
1222
|
+
filtered = filtered.filter((b) => rankedNames.has(b.name.toLowerCase()));
|
|
1223
|
+
filtered.sort((a, b) => {
|
|
1224
|
+
const aIdx = ranked.findIndex((r) => r.name.toLowerCase() === a.name.toLowerCase());
|
|
1225
|
+
const bIdx = ranked.findIndex((r) => r.name.toLowerCase() === b.name.toLowerCase());
|
|
1226
|
+
return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
|
|
1227
|
+
});
|
|
1228
|
+
} else {
|
|
1229
|
+
filtered = filtered.filter((b) => {
|
|
1230
|
+
const haystack = [
|
|
1231
|
+
b.name,
|
|
1232
|
+
b.description,
|
|
1233
|
+
...b.tags ?? [],
|
|
1234
|
+
...b.components,
|
|
1235
|
+
b.category
|
|
1236
|
+
].join(" ").toLowerCase();
|
|
1237
|
+
return haystack.includes(search2);
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (component) {
|
|
1242
|
+
filtered = filtered.filter(
|
|
1243
|
+
(b) => b.components.some((c) => c.toLowerCase() === component)
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
if (category) {
|
|
1247
|
+
filtered = filtered.filter(
|
|
1248
|
+
(b) => b.category.toLowerCase() === category
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const blocksUseIcons = filtered.some(
|
|
1252
|
+
(b) => b.components.some((c) => c === "Icon") || b.code && /\bIcon\b/.test(b.code)
|
|
1253
|
+
);
|
|
1254
|
+
const iconHint = blocksUseIcons ? "Icon components in block code are from @phosphor-icons/react. Import them as: import { IconName } from '@phosphor-icons/react';" : void 0;
|
|
1255
|
+
if (blocksLimit !== void 0) {
|
|
1256
|
+
filtered = filtered.slice(0, blocksLimit);
|
|
1257
|
+
}
|
|
1258
|
+
const verbosity = args?.verbosity ?? "standard";
|
|
1259
|
+
const blocksWithImports = await Promise.all(filtered.map(async (b) => {
|
|
1260
|
+
const imports = await buildImportStatements(
|
|
1261
|
+
b.components,
|
|
1262
|
+
async (componentName) => ctx.resolvePackageName(componentName)
|
|
1263
|
+
);
|
|
1264
|
+
const base = {
|
|
1265
|
+
name: b.name,
|
|
1266
|
+
description: b.description,
|
|
1267
|
+
category: b.category,
|
|
1268
|
+
components: b.components,
|
|
1269
|
+
tags: b.tags,
|
|
1270
|
+
import: imports.join("\n"),
|
|
1271
|
+
imports
|
|
1272
|
+
};
|
|
1273
|
+
if (verbosity === "compact") return base;
|
|
1274
|
+
if (verbosity === "full") return { ...base, code: b.code };
|
|
1275
|
+
const codeLines = b.code.split("\n");
|
|
1276
|
+
const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : b.code;
|
|
1277
|
+
return { ...base, code };
|
|
1278
|
+
}));
|
|
1279
|
+
return {
|
|
1280
|
+
content: [{
|
|
1281
|
+
type: "text",
|
|
1282
|
+
text: JSON.stringify({
|
|
1283
|
+
total: blocksWithImports.length,
|
|
1284
|
+
blocks: blocksWithImports,
|
|
1285
|
+
...iconHint && { iconHint },
|
|
1286
|
+
...blocksWithImports.length === 0 && allBlocks.length > 0 && {
|
|
1287
|
+
hint: "No blocks matching your query. Try broader search terms."
|
|
1288
|
+
}
|
|
1289
|
+
})
|
|
1290
|
+
}]
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// src/tools/tokens.ts
|
|
1295
|
+
var tokensHandler = async (args, ctx) => {
|
|
1296
|
+
const category = args?.category?.toLowerCase() ?? void 0;
|
|
1297
|
+
const search2 = args?.search?.toLowerCase() ?? void 0;
|
|
1298
|
+
const tokensLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 100) : search2 ? 25 : void 0;
|
|
1299
|
+
const tokenData = ctx.data.tokens;
|
|
1300
|
+
if (!tokenData || tokenData.total === 0) {
|
|
1301
|
+
return {
|
|
1302
|
+
content: [{
|
|
1303
|
+
type: "text",
|
|
1304
|
+
text: JSON.stringify({
|
|
1305
|
+
total: 0,
|
|
1306
|
+
categories: {},
|
|
1307
|
+
hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
|
|
1308
|
+
})
|
|
1309
|
+
}]
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
let filteredCategories = {};
|
|
1313
|
+
let filteredTotal = 0;
|
|
1314
|
+
const searchMatchesCategory = search2 ? Object.keys(tokenData.categories).find((cat) => cat.toLowerCase() === search2) : void 0;
|
|
1315
|
+
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
1316
|
+
if (category && cat !== category) continue;
|
|
1317
|
+
let filtered = tokens;
|
|
1318
|
+
if (search2) {
|
|
1319
|
+
if (searchMatchesCategory && cat.toLowerCase() === search2) {
|
|
1320
|
+
filtered = tokens;
|
|
1321
|
+
} else {
|
|
1322
|
+
filtered = tokens.filter(
|
|
1323
|
+
(t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || cat.toLowerCase().includes(search2)
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (filtered.length > 0) {
|
|
1328
|
+
filteredCategories[cat] = filtered;
|
|
1329
|
+
filteredTotal += filtered.length;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (tokensLimit !== void 0) {
|
|
1333
|
+
const limited = limitTokensPerCategory(filteredCategories, tokensLimit);
|
|
1334
|
+
filteredCategories = limited.categories;
|
|
1335
|
+
filteredTotal = limited.total;
|
|
1336
|
+
}
|
|
1337
|
+
let hint;
|
|
1338
|
+
if (filteredTotal === 0) {
|
|
1339
|
+
const availableCategories = Object.keys(tokenData.categories);
|
|
1340
|
+
if (category && search2) {
|
|
1341
|
+
const categoryTotal = tokenData.categories[category]?.length ?? 0;
|
|
1342
|
+
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. Available: ${availableCategories.join(", ")}`;
|
|
1343
|
+
} else if (search2) {
|
|
1344
|
+
hint = `No tokens matching "${search2}". Available categories: ${availableCategories.join(", ")}`;
|
|
1345
|
+
} else if (category) {
|
|
1346
|
+
hint = `Category "${category}" not found. Available: ${availableCategories.join(", ")}`;
|
|
1347
|
+
}
|
|
1348
|
+
} else if (!category && !search2) {
|
|
1349
|
+
hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
content: [{
|
|
1353
|
+
type: "text",
|
|
1354
|
+
text: JSON.stringify({
|
|
1355
|
+
prefix: tokenData.prefix,
|
|
1356
|
+
total: filteredTotal,
|
|
1357
|
+
totalAvailable: tokenData.total,
|
|
1358
|
+
categories: filteredCategories,
|
|
1359
|
+
...hint && { hint },
|
|
1360
|
+
...!category && !search2 && {
|
|
1361
|
+
availableCategories: Object.entries(tokenData.categories).map(
|
|
1362
|
+
([cat, tokens]) => ({ category: cat, count: tokens.length })
|
|
1363
|
+
)
|
|
1364
|
+
}
|
|
1365
|
+
})
|
|
1366
|
+
}]
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// src/tools/implement.ts
|
|
1371
|
+
import { filterPlaceholders as filterPlaceholders3 } from "@fragments-sdk/context/generate";
|
|
1372
|
+
var implementHandler = async (args, ctx) => {
|
|
1373
|
+
const useCase = args?.useCase;
|
|
1374
|
+
if (!useCase) {
|
|
1375
|
+
throw new Error("useCase is required");
|
|
1376
|
+
}
|
|
1377
|
+
const verbosity = args?.verbosity ?? "standard";
|
|
1378
|
+
const { allFragments, allBlocks, localData } = buildLocalSearchData(
|
|
1379
|
+
{
|
|
1380
|
+
fragments: ctx.data.components,
|
|
1381
|
+
blocks: ctx.data.blocks,
|
|
1382
|
+
tokens: ctx.data.tokens,
|
|
1383
|
+
graph: ctx.data.graph
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
componentIndex: ctx.indexes.componentIndex,
|
|
1387
|
+
blockIndex: ctx.indexes.blockIndex,
|
|
1388
|
+
tokenIndex: ctx.indexes.tokenIndex
|
|
1389
|
+
}
|
|
1390
|
+
);
|
|
1391
|
+
const tokenData = ctx.data.tokens;
|
|
1392
|
+
const implLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 15) : 5;
|
|
1393
|
+
const [componentResults, blockResults, tokenResults] = await Promise.all([
|
|
1394
|
+
hybridSearch(useCase, localData, implLimit * 3, "component", ctx.config.apiKey),
|
|
1395
|
+
hybridSearch(useCase, localData, implLimit, "block", ctx.config.apiKey),
|
|
1396
|
+
hybridSearch(useCase, localData, implLimit, "token", ctx.config.apiKey)
|
|
1397
|
+
]);
|
|
1398
|
+
const topBlockScore = blockResults.length > 0 ? blockResults[0].score : 0;
|
|
1399
|
+
const filteredBlockResults = blockResults.filter((r) => r.score >= topBlockScore * 0.3);
|
|
1400
|
+
if (filteredBlockResults.length > 0) {
|
|
1401
|
+
const matchedBlocks = filteredBlockResults.map((r) => allBlocks.find((b) => b.name.toLowerCase() === r.name.toLowerCase())).filter(Boolean);
|
|
1402
|
+
const blockFreq = buildBlockComponentFrequency(matchedBlocks);
|
|
1403
|
+
boostByBlockFrequency(componentResults, blockFreq);
|
|
1404
|
+
}
|
|
1405
|
+
const topComponentResults = componentResults.slice(0, implLimit);
|
|
1406
|
+
const maxCompScore = topComponentResults.length > 0 ? topComponentResults[0].score : 0;
|
|
1407
|
+
const topMatches = topComponentResults.map((result) => {
|
|
1408
|
+
const fragment = allFragments.find(
|
|
1409
|
+
(s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
|
|
1410
|
+
);
|
|
1411
|
+
return fragment ? { fragment, score: result.score } : null;
|
|
1412
|
+
}).filter(Boolean);
|
|
1413
|
+
const components = await Promise.all(
|
|
1414
|
+
topMatches.map(async ({ fragment: s, score }) => {
|
|
1415
|
+
const pkgName = ctx.resolvePackageName(s.meta.name);
|
|
1416
|
+
if (verbosity === "compact") {
|
|
1417
|
+
return {
|
|
1418
|
+
name: s.meta.name,
|
|
1419
|
+
description: s.meta.description,
|
|
1420
|
+
confidence: assignConfidence(score, maxCompScore),
|
|
1421
|
+
import: `import { ${s.meta.name} } from '${pkgName}';`
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
const exampleLimit = verbosity === "full" ? s.variants.length : 2;
|
|
1425
|
+
const propsLimit = verbosity === "full" ? Object.keys(s.props ?? {}).length : 5;
|
|
1426
|
+
const examples = s.variants.slice(0, exampleLimit).map((v) => ({
|
|
1427
|
+
variant: v.name,
|
|
1428
|
+
code: v.code ?? `<${s.meta.name} />`
|
|
1429
|
+
}));
|
|
1430
|
+
const propsSummary = Object.entries(s.props ?? {}).slice(0, propsLimit).map(
|
|
1431
|
+
([pName, p]) => `${pName}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
|
|
1432
|
+
);
|
|
1433
|
+
return {
|
|
1434
|
+
name: s.meta.name,
|
|
1435
|
+
category: s.meta.category,
|
|
1436
|
+
description: s.meta.description,
|
|
1437
|
+
confidence: assignConfidence(score, maxCompScore),
|
|
1438
|
+
import: `import { ${s.meta.name} } from '${pkgName}';`,
|
|
1439
|
+
props: propsSummary,
|
|
1440
|
+
examples,
|
|
1441
|
+
guidelines: filterPlaceholders3(s.usage?.when).slice(0, 3),
|
|
1442
|
+
accessibility: s.usage?.accessibility?.slice(0, 2) ?? []
|
|
1443
|
+
};
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
const matchingBlocks = (await Promise.all(filteredBlockResults.slice(0, 5).map(async (result) => {
|
|
1447
|
+
const block = allBlocks.find(
|
|
1448
|
+
(b) => b.name.toLowerCase() === result.name.toLowerCase()
|
|
1449
|
+
);
|
|
1450
|
+
if (!block) return null;
|
|
1451
|
+
const imports = await buildImportStatements(
|
|
1452
|
+
block.components,
|
|
1453
|
+
async (componentName) => ctx.resolvePackageName(componentName)
|
|
1454
|
+
);
|
|
1455
|
+
const codeLines = block.code.split("\n");
|
|
1456
|
+
const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : block.code;
|
|
1457
|
+
return {
|
|
1458
|
+
name: block.name,
|
|
1459
|
+
description: block.description,
|
|
1460
|
+
components: block.components,
|
|
1461
|
+
code,
|
|
1462
|
+
import: imports.join("\n"),
|
|
1463
|
+
imports
|
|
1464
|
+
};
|
|
1465
|
+
}))).filter(Boolean);
|
|
1466
|
+
let relevantTokens;
|
|
1467
|
+
if (tokenResults.length > 0 && tokenData) {
|
|
1468
|
+
relevantTokens = {};
|
|
1469
|
+
for (const result of tokenResults) {
|
|
1470
|
+
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
1471
|
+
if (tokens.some((t) => t.name === result.name)) {
|
|
1472
|
+
if (!relevantTokens[cat]) relevantTokens[cat] = [];
|
|
1473
|
+
relevantTokens[cat].push(result.name);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
|
|
1479
|
+
}
|
|
1480
|
+
if (!relevantTokens && tokenData) {
|
|
1481
|
+
const categories = extractTokenCategories(useCase);
|
|
1482
|
+
relevantTokens = {};
|
|
1483
|
+
for (const cat of categories) {
|
|
1484
|
+
const tokens = tokenData.categories[cat];
|
|
1485
|
+
if (tokens && tokens.length > 0) {
|
|
1486
|
+
relevantTokens[cat] = tokens.slice(0, 5).map((t) => t.name);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
content: [{
|
|
1493
|
+
type: "text",
|
|
1494
|
+
text: JSON.stringify({
|
|
1495
|
+
useCase,
|
|
1496
|
+
components,
|
|
1497
|
+
blocks: verbosity !== "compact" && matchingBlocks.length > 0 ? matchingBlocks : void 0,
|
|
1498
|
+
tokens: verbosity !== "compact" ? relevantTokens : void 0,
|
|
1499
|
+
noMatch: components.length === 0,
|
|
1500
|
+
summary: components.length > 0 ? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ""}` : `No components match "${useCase}". Try ${ctx.toolNames.discover} with different terms${tokenData ? ` or ${ctx.toolNames.tokens} for CSS variables` : ""}.`
|
|
1501
|
+
})
|
|
1502
|
+
}]
|
|
1503
|
+
};
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
// src/service.ts
|
|
1507
|
+
var DEFAULT_ENDPOINTS = {
|
|
1508
|
+
render: "/fragments/render",
|
|
1509
|
+
compare: "/fragments/compare",
|
|
1510
|
+
fix: "/fragments/fix",
|
|
1511
|
+
a11y: "/fragments/a11y"
|
|
1512
|
+
};
|
|
1513
|
+
async function renderComponent(viewerUrl, request, endpoints) {
|
|
1514
|
+
const renderUrl = `${viewerUrl}${endpoints?.render ?? DEFAULT_ENDPOINTS.render}`;
|
|
1515
|
+
const response = await fetch(renderUrl, {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
headers: { "Content-Type": "application/json" },
|
|
1518
|
+
body: JSON.stringify({
|
|
1519
|
+
component: request.component,
|
|
1520
|
+
props: request.props ?? {},
|
|
1521
|
+
variant: request.variant,
|
|
1522
|
+
viewport: request.viewport ?? { width: 800, height: 600 }
|
|
1523
|
+
})
|
|
1524
|
+
});
|
|
1525
|
+
return await response.json();
|
|
1526
|
+
}
|
|
1527
|
+
async function compareComponent(viewerUrl, request, endpoints) {
|
|
1528
|
+
const compareUrl = `${viewerUrl}${endpoints?.compare ?? DEFAULT_ENDPOINTS.compare}`;
|
|
1529
|
+
const response = await fetch(compareUrl, {
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
headers: { "Content-Type": "application/json" },
|
|
1532
|
+
body: JSON.stringify(request)
|
|
1533
|
+
});
|
|
1534
|
+
return await response.json();
|
|
1535
|
+
}
|
|
1536
|
+
async function fixComponent(viewerUrl, request, endpoints) {
|
|
1537
|
+
const fixUrl = `${viewerUrl}${endpoints?.fix ?? DEFAULT_ENDPOINTS.fix}`;
|
|
1538
|
+
const response = await fetch(fixUrl, {
|
|
1539
|
+
method: "POST",
|
|
1540
|
+
headers: { "Content-Type": "application/json" },
|
|
1541
|
+
body: JSON.stringify(request)
|
|
1542
|
+
});
|
|
1543
|
+
return await response.json();
|
|
1544
|
+
}
|
|
1545
|
+
async function auditComponent(viewerUrl, request, endpoints) {
|
|
1546
|
+
const a11yUrl = `${viewerUrl}${endpoints?.a11y ?? DEFAULT_ENDPOINTS.a11y}`;
|
|
1547
|
+
const response = await fetch(a11yUrl, {
|
|
1548
|
+
method: "POST",
|
|
1549
|
+
headers: { "Content-Type": "application/json" },
|
|
1550
|
+
body: JSON.stringify({
|
|
1551
|
+
component: request.component,
|
|
1552
|
+
variant: request.variant,
|
|
1553
|
+
standard: request.standard,
|
|
1554
|
+
includeFixPatches: request.includeFixPatches
|
|
1555
|
+
})
|
|
1556
|
+
});
|
|
1557
|
+
const raw = await response.json();
|
|
1558
|
+
if (raw.error) {
|
|
1559
|
+
return {
|
|
1560
|
+
component: request.component,
|
|
1561
|
+
results: [],
|
|
1562
|
+
score: 0,
|
|
1563
|
+
aaPercent: 0,
|
|
1564
|
+
aaaPercent: 0,
|
|
1565
|
+
passed: false,
|
|
1566
|
+
standard: request.standard ?? "AA",
|
|
1567
|
+
error: raw.error
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const results = raw.results ?? [];
|
|
1571
|
+
const standard = request.standard ?? "AA";
|
|
1572
|
+
let totalCritical = 0;
|
|
1573
|
+
let totalSerious = 0;
|
|
1574
|
+
let totalModerate = 0;
|
|
1575
|
+
let totalMinor = 0;
|
|
1576
|
+
for (const r of results) {
|
|
1577
|
+
totalCritical += r.summary.critical;
|
|
1578
|
+
totalSerious += r.summary.serious;
|
|
1579
|
+
totalModerate += r.summary.moderate;
|
|
1580
|
+
totalMinor += r.summary.minor;
|
|
1581
|
+
}
|
|
1582
|
+
const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
|
|
1583
|
+
const score = Math.max(0, 100 - deductions);
|
|
1584
|
+
const variantCount = results.length;
|
|
1585
|
+
const aaPassCount = results.filter((r) => {
|
|
1586
|
+
const critical = r.summary.critical;
|
|
1587
|
+
const serious = r.summary.serious;
|
|
1588
|
+
return critical === 0 && serious === 0;
|
|
1589
|
+
}).length;
|
|
1590
|
+
const aaaPassCount = results.filter((r) => {
|
|
1591
|
+
const total = r.summary.critical + r.summary.serious + r.summary.moderate + r.summary.minor;
|
|
1592
|
+
return total === 0;
|
|
1593
|
+
}).length;
|
|
1594
|
+
const totalPasses = results.reduce((sum, r) => sum + r.passes, 0);
|
|
1595
|
+
const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
|
|
1596
|
+
const emptyAudit = results.length > 0 && totalPasses === 0 && totalViolations === 0;
|
|
1597
|
+
const aaPercent = variantCount > 0 ? Math.round(aaPassCount / variantCount * 100) : 100;
|
|
1598
|
+
const aaaPercent = variantCount > 0 ? Math.round(aaaPassCount / variantCount * 100) : 100;
|
|
1599
|
+
const aaPass = !emptyAudit && totalCritical === 0 && totalSerious === 0;
|
|
1600
|
+
const aaaPass = !emptyAudit && totalViolations === 0;
|
|
1601
|
+
const passed = standard === "AAA" ? aaaPass : aaPass;
|
|
1602
|
+
return {
|
|
1603
|
+
component: request.component,
|
|
1604
|
+
results,
|
|
1605
|
+
score: emptyAudit ? 0 : score,
|
|
1606
|
+
aaPercent: emptyAudit ? 0 : aaPercent,
|
|
1607
|
+
aaaPercent: emptyAudit ? 0 : aaaPercent,
|
|
1608
|
+
...emptyAudit && { emptyAudit },
|
|
1609
|
+
passed,
|
|
1610
|
+
standard
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/tools/render.ts
|
|
1615
|
+
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.";
|
|
1616
|
+
var renderHandler = async (args, ctx) => {
|
|
1617
|
+
const componentName = args?.component;
|
|
1618
|
+
const variantName = args?.variant;
|
|
1619
|
+
const props = args?.props ?? {};
|
|
1620
|
+
const viewport = args?.viewport;
|
|
1621
|
+
const figmaUrl = args?.figmaUrl;
|
|
1622
|
+
const threshold = args?.threshold ?? (figmaUrl ? 1 : ctx.config.threshold ?? DEFAULTS.diffThreshold);
|
|
1623
|
+
if (!componentName) {
|
|
1624
|
+
return {
|
|
1625
|
+
content: [{ type: "text", text: "Error: component name is required" }],
|
|
1626
|
+
isError: true
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
{
|
|
1630
|
+
const fragment = Object.values(ctx.data.components).find(
|
|
1631
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1632
|
+
);
|
|
1633
|
+
if (!fragment) {
|
|
1634
|
+
const allNames = Object.values(ctx.data.components).map((s) => s.meta.name);
|
|
1635
|
+
const closest = findClosestMatch(componentName, allNames);
|
|
1636
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1637
|
+
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
1641
|
+
if (!viewerUrl) {
|
|
1642
|
+
return {
|
|
1643
|
+
content: [{
|
|
1644
|
+
type: "text",
|
|
1645
|
+
text: NO_VIEWER_MSG
|
|
1646
|
+
}],
|
|
1647
|
+
isError: true
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
if (figmaUrl) {
|
|
1651
|
+
try {
|
|
1652
|
+
const result = await compareComponent(viewerUrl, {
|
|
1653
|
+
component: componentName,
|
|
1654
|
+
variant: variantName,
|
|
1655
|
+
props,
|
|
1656
|
+
figmaUrl,
|
|
1657
|
+
threshold
|
|
1658
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1659
|
+
if (result.error) {
|
|
1660
|
+
return {
|
|
1661
|
+
content: [{
|
|
1662
|
+
type: "text",
|
|
1663
|
+
text: `Compare error: ${result.error}${result.suggestion ? `
|
|
1664
|
+
Suggestion: ${result.suggestion}` : ""}`
|
|
1665
|
+
}],
|
|
1666
|
+
isError: true
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const content = [];
|
|
1670
|
+
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}%)`;
|
|
1671
|
+
content.push({ type: "text", text: summaryText });
|
|
1672
|
+
if (result.diff && !result.match) {
|
|
1673
|
+
content.push({
|
|
1674
|
+
type: "image",
|
|
1675
|
+
data: result.diff.replace("data:image/png;base64,", ""),
|
|
1676
|
+
mimeType: "image/png"
|
|
1677
|
+
});
|
|
1678
|
+
content.push({
|
|
1679
|
+
type: "text",
|
|
1680
|
+
text: `Diff image above shows visual differences (red highlights). Changed regions: ${result.changedRegions?.length ?? 0}`
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
content.push({
|
|
1684
|
+
type: "text",
|
|
1685
|
+
text: JSON.stringify({
|
|
1686
|
+
match: result.match,
|
|
1687
|
+
diffPercentage: result.diffPercentage,
|
|
1688
|
+
threshold: result.threshold,
|
|
1689
|
+
figmaUrl: result.figmaUrl,
|
|
1690
|
+
changedRegions: result.changedRegions
|
|
1691
|
+
})
|
|
1692
|
+
});
|
|
1693
|
+
return { content };
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
return {
|
|
1696
|
+
content: [{
|
|
1697
|
+
type: "text",
|
|
1698
|
+
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.`
|
|
1699
|
+
}],
|
|
1700
|
+
isError: true
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
const result = await renderComponent(viewerUrl, {
|
|
1706
|
+
component: componentName,
|
|
1707
|
+
props,
|
|
1708
|
+
variant: variantName,
|
|
1709
|
+
viewport: viewport ?? { width: 800, height: 600 }
|
|
1710
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1711
|
+
if (result.error) {
|
|
1712
|
+
return {
|
|
1713
|
+
content: [{ type: "text", text: `Render error: ${result.error}` }],
|
|
1714
|
+
isError: true
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
content: [
|
|
1719
|
+
{
|
|
1720
|
+
type: "image",
|
|
1721
|
+
data: result.screenshot.replace("data:image/png;base64,", ""),
|
|
1722
|
+
mimeType: "image/png"
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
type: "text",
|
|
1726
|
+
text: `Successfully rendered ${componentName}${variantName ? ` (variant: "${variantName}")` : ""}${Object.keys(props).length > 0 ? ` with props: ${JSON.stringify(props)}` : ""}`
|
|
1727
|
+
}
|
|
1728
|
+
]
|
|
1729
|
+
};
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
return {
|
|
1732
|
+
content: [{
|
|
1733
|
+
type: "text",
|
|
1734
|
+
text: `Failed to render component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1735
|
+
}],
|
|
1736
|
+
isError: true
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
// src/tools/fix.ts
|
|
1742
|
+
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.";
|
|
1743
|
+
var fixHandler = async (args, ctx) => {
|
|
1744
|
+
const componentName = args?.component;
|
|
1745
|
+
const variantName = args?.variant ?? void 0;
|
|
1746
|
+
const fixType = args?.fixType ?? "all";
|
|
1747
|
+
if (!componentName) {
|
|
1748
|
+
throw new Error("component is required");
|
|
1749
|
+
}
|
|
1750
|
+
const fragment = Object.values(ctx.data.components).find(
|
|
1751
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1752
|
+
);
|
|
1753
|
+
if (!fragment) {
|
|
1754
|
+
const allNames = Object.values(ctx.data.components).map((s) => s.meta.name);
|
|
1755
|
+
const closest = findClosestMatch(componentName, allNames);
|
|
1756
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1757
|
+
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
1758
|
+
}
|
|
1759
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
1760
|
+
if (!viewerUrl) {
|
|
1761
|
+
return {
|
|
1762
|
+
content: [{
|
|
1763
|
+
type: "text",
|
|
1764
|
+
text: NO_VIEWER_MSG2
|
|
1765
|
+
}],
|
|
1766
|
+
isError: true
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
try {
|
|
1770
|
+
const result = await fixComponent(viewerUrl, {
|
|
1771
|
+
component: componentName,
|
|
1772
|
+
variant: variantName,
|
|
1773
|
+
fixType
|
|
1774
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1775
|
+
if (result.error) {
|
|
1776
|
+
return {
|
|
1777
|
+
content: [{
|
|
1778
|
+
type: "text",
|
|
1779
|
+
text: `Fix generation error: ${result.error}`
|
|
1780
|
+
}],
|
|
1781
|
+
isError: true
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
return {
|
|
1785
|
+
content: [{
|
|
1786
|
+
type: "text",
|
|
1787
|
+
text: JSON.stringify({
|
|
1788
|
+
component: componentName,
|
|
1789
|
+
variant: variantName ?? "all",
|
|
1790
|
+
fixType,
|
|
1791
|
+
patches: result.patches,
|
|
1792
|
+
summary: result.summary,
|
|
1793
|
+
patchCount: result.patches.length,
|
|
1794
|
+
nextStep: result.patches.length > 0 ? `Apply patches using your editor or \`patch\` command, then run ${ctx.toolNames.render} to confirm fixes.` : void 0
|
|
1795
|
+
})
|
|
1796
|
+
}]
|
|
1797
|
+
};
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
return {
|
|
1800
|
+
content: [{
|
|
1801
|
+
type: "text",
|
|
1802
|
+
text: `Failed to generate fixes: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1803
|
+
}],
|
|
1804
|
+
isError: true
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
// src/tools/a11y.ts
|
|
1810
|
+
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.";
|
|
1811
|
+
var a11yHandler = async (args, ctx) => {
|
|
1812
|
+
const componentName = args?.component;
|
|
1813
|
+
const variantName = args?.variant ?? void 0;
|
|
1814
|
+
const standard = args?.standard ?? "AA";
|
|
1815
|
+
const includeFixPatches = args?.includeFixPatches ?? false;
|
|
1816
|
+
if (!componentName) {
|
|
1817
|
+
throw new Error("component is required");
|
|
1818
|
+
}
|
|
1819
|
+
{
|
|
1820
|
+
const fragment = Object.values(ctx.data.components).find(
|
|
1821
|
+
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1822
|
+
);
|
|
1823
|
+
if (!fragment) {
|
|
1824
|
+
const allNames = Object.values(ctx.data.components).map((s) => s.meta.name);
|
|
1825
|
+
const closest = findClosestMatch(componentName, allNames);
|
|
1826
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1827
|
+
throw new Error(`Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const viewerUrl = ctx.config.viewerUrl;
|
|
1831
|
+
if (!viewerUrl) {
|
|
1832
|
+
return {
|
|
1833
|
+
content: [{
|
|
1834
|
+
type: "text",
|
|
1835
|
+
text: NO_VIEWER_MSG3
|
|
1836
|
+
}],
|
|
1837
|
+
isError: true
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
try {
|
|
1841
|
+
const result = await auditComponent(viewerUrl, {
|
|
1842
|
+
component: componentName,
|
|
1843
|
+
variant: variantName,
|
|
1844
|
+
standard,
|
|
1845
|
+
includeFixPatches
|
|
1846
|
+
}, ctx.config.fileConfig?.endpoints);
|
|
1847
|
+
if (result.error) {
|
|
1848
|
+
return {
|
|
1849
|
+
content: [{
|
|
1850
|
+
type: "text",
|
|
1851
|
+
text: `A11y audit error: ${result.error}`
|
|
1852
|
+
}],
|
|
1853
|
+
isError: true
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
let nextStep;
|
|
1857
|
+
if (result.emptyAudit) {
|
|
1858
|
+
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}").`;
|
|
1859
|
+
} else if (result.passed) {
|
|
1860
|
+
nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
|
|
1861
|
+
} else {
|
|
1862
|
+
nextStep = `Fix the violations above, then re-run ${ctx.toolNames.a11y} to verify. Use ${ctx.toolNames.fix} for automated fixes.`;
|
|
1863
|
+
}
|
|
1864
|
+
return {
|
|
1865
|
+
content: [{
|
|
1866
|
+
type: "text",
|
|
1867
|
+
text: JSON.stringify({
|
|
1868
|
+
component: componentName,
|
|
1869
|
+
variant: variantName ?? "all",
|
|
1870
|
+
standard,
|
|
1871
|
+
totalViolations: result.results.reduce((sum, r) => sum + r.summary.total, 0),
|
|
1872
|
+
variantsPassingAA: `${result.aaPercent}%`,
|
|
1873
|
+
variantsPassingAAA: `${result.aaaPercent}%`,
|
|
1874
|
+
passed: result.passed,
|
|
1875
|
+
...result.emptyAudit && { emptyAudit: true },
|
|
1876
|
+
results: result.results,
|
|
1877
|
+
nextStep
|
|
1878
|
+
})
|
|
1879
|
+
}]
|
|
1880
|
+
};
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
return {
|
|
1883
|
+
content: [{
|
|
1884
|
+
type: "text",
|
|
1885
|
+
text: `Failed to audit component: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1886
|
+
}],
|
|
1887
|
+
isError: true
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// src/graph-handler.ts
|
|
1893
|
+
import {
|
|
1894
|
+
ComponentGraphEngine,
|
|
1895
|
+
deserializeGraph
|
|
1896
|
+
} from "@fragments-sdk/context/graph";
|
|
1897
|
+
function handleGraphTool(args, serializedGraph, blocks, componentNames) {
|
|
1898
|
+
if (!serializedGraph) {
|
|
1899
|
+
return {
|
|
1900
|
+
text: JSON.stringify({
|
|
1901
|
+
error: "No graph data available. Run `fragments build` to generate the component graph.",
|
|
1902
|
+
hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
|
|
1903
|
+
}),
|
|
1904
|
+
isError: true
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const graph = deserializeGraph(serializedGraph);
|
|
1908
|
+
const blockData = blocks ? Object.fromEntries(
|
|
1909
|
+
Object.entries(blocks).map(([k, v]) => [k, { components: v.components }])
|
|
1910
|
+
) : void 0;
|
|
1911
|
+
const engine = new ComponentGraphEngine(graph, blockData);
|
|
1912
|
+
const edgeTypes = args.edgeTypes;
|
|
1913
|
+
switch (args.mode) {
|
|
1914
|
+
case "health": {
|
|
1915
|
+
const health = engine.getHealth();
|
|
1916
|
+
const blockCount = blocks ? Object.keys(blocks).length : 0;
|
|
1917
|
+
return {
|
|
1918
|
+
text: JSON.stringify({
|
|
1919
|
+
mode: "health",
|
|
1920
|
+
...health,
|
|
1921
|
+
...health.compositionCoverage === 0 && blockCount === 0 && {
|
|
1922
|
+
compositionNote: "No composition blocks defined yet \u2014 compositionCoverage will increase as blocks are added"
|
|
1923
|
+
},
|
|
1924
|
+
summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
|
|
1925
|
+
})
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
case "dependencies": {
|
|
1929
|
+
if (!args.component) {
|
|
1930
|
+
return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
|
|
1931
|
+
}
|
|
1932
|
+
if (!engine.hasNode(args.component)) {
|
|
1933
|
+
const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
|
|
1934
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1935
|
+
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
1936
|
+
}
|
|
1937
|
+
const deps = engine.dependencies(args.component, edgeTypes);
|
|
1938
|
+
return {
|
|
1939
|
+
text: JSON.stringify({
|
|
1940
|
+
mode: "dependencies",
|
|
1941
|
+
component: args.component,
|
|
1942
|
+
count: deps.length,
|
|
1943
|
+
dependencies: deps.map((e) => ({
|
|
1944
|
+
component: e.target,
|
|
1945
|
+
type: e.type,
|
|
1946
|
+
weight: e.weight,
|
|
1947
|
+
note: e.note,
|
|
1948
|
+
provenance: e.provenance
|
|
1949
|
+
}))
|
|
1950
|
+
})
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
case "dependents": {
|
|
1954
|
+
if (!args.component) {
|
|
1955
|
+
return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
|
|
1956
|
+
}
|
|
1957
|
+
if (!engine.hasNode(args.component)) {
|
|
1958
|
+
const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
|
|
1959
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1960
|
+
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
1961
|
+
}
|
|
1962
|
+
const deps = engine.dependents(args.component, edgeTypes);
|
|
1963
|
+
return {
|
|
1964
|
+
text: JSON.stringify({
|
|
1965
|
+
mode: "dependents",
|
|
1966
|
+
component: args.component,
|
|
1967
|
+
count: deps.length,
|
|
1968
|
+
dependents: deps.map((e) => ({
|
|
1969
|
+
component: e.source,
|
|
1970
|
+
type: e.type,
|
|
1971
|
+
weight: e.weight,
|
|
1972
|
+
note: e.note,
|
|
1973
|
+
provenance: e.provenance
|
|
1974
|
+
}))
|
|
1975
|
+
})
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
case "impact": {
|
|
1979
|
+
if (!args.component) {
|
|
1980
|
+
return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
|
|
1981
|
+
}
|
|
1982
|
+
if (!engine.hasNode(args.component)) {
|
|
1983
|
+
const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
|
|
1984
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
1985
|
+
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
1986
|
+
}
|
|
1987
|
+
const result = engine.impact(args.component, args.maxDepth ?? 3);
|
|
1988
|
+
return {
|
|
1989
|
+
text: JSON.stringify({
|
|
1990
|
+
mode: "impact",
|
|
1991
|
+
...result,
|
|
1992
|
+
summary: `Changing ${args.component} affects ${result.totalAffected} component(s) and ${result.affectedBlocks.length} block(s)`
|
|
1993
|
+
})
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
case "path": {
|
|
1997
|
+
if (!args.component || !args.target) {
|
|
1998
|
+
return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
|
|
1999
|
+
}
|
|
2000
|
+
const result = engine.path(args.component, args.target);
|
|
2001
|
+
return {
|
|
2002
|
+
text: JSON.stringify({
|
|
2003
|
+
mode: "path",
|
|
2004
|
+
from: args.component,
|
|
2005
|
+
to: args.target,
|
|
2006
|
+
...result,
|
|
2007
|
+
edges: result.edges.map((e) => ({
|
|
2008
|
+
source: e.source,
|
|
2009
|
+
target: e.target,
|
|
2010
|
+
type: e.type
|
|
2011
|
+
}))
|
|
2012
|
+
})
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
case "composition": {
|
|
2016
|
+
if (!args.component) {
|
|
2017
|
+
return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
|
|
2018
|
+
}
|
|
2019
|
+
if (!engine.hasNode(args.component)) {
|
|
2020
|
+
const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
|
|
2021
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2022
|
+
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2023
|
+
}
|
|
2024
|
+
const tree = engine.composition(args.component);
|
|
2025
|
+
return {
|
|
2026
|
+
text: JSON.stringify({
|
|
2027
|
+
mode: "composition",
|
|
2028
|
+
...tree
|
|
2029
|
+
})
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
case "alternatives": {
|
|
2033
|
+
if (!args.component) {
|
|
2034
|
+
return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
|
|
2035
|
+
}
|
|
2036
|
+
if (!engine.hasNode(args.component)) {
|
|
2037
|
+
const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
|
|
2038
|
+
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2039
|
+
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2040
|
+
}
|
|
2041
|
+
const alts = engine.alternatives(args.component);
|
|
2042
|
+
return {
|
|
2043
|
+
text: JSON.stringify({
|
|
2044
|
+
mode: "alternatives",
|
|
2045
|
+
component: args.component,
|
|
2046
|
+
count: alts.length,
|
|
2047
|
+
alternatives: alts
|
|
2048
|
+
})
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
case "islands": {
|
|
2052
|
+
const islands = engine.islands();
|
|
2053
|
+
return {
|
|
2054
|
+
text: JSON.stringify({
|
|
2055
|
+
mode: "islands",
|
|
2056
|
+
count: islands.length,
|
|
2057
|
+
islands: islands.map((island, i) => ({
|
|
2058
|
+
id: i + 1,
|
|
2059
|
+
size: island.length,
|
|
2060
|
+
components: island
|
|
2061
|
+
}))
|
|
2062
|
+
})
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
default:
|
|
2066
|
+
return {
|
|
2067
|
+
text: JSON.stringify({
|
|
2068
|
+
error: `Unknown mode: "${args.mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`
|
|
2069
|
+
}),
|
|
2070
|
+
isError: true
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/tools/graph.ts
|
|
2076
|
+
var graphHandler = async (args, ctx) => {
|
|
2077
|
+
const graphArgs = {
|
|
2078
|
+
mode: args?.mode ?? "health",
|
|
2079
|
+
component: args?.component,
|
|
2080
|
+
target: args?.target,
|
|
2081
|
+
edgeTypes: args?.edgeTypes,
|
|
2082
|
+
maxDepth: args?.maxDepth
|
|
2083
|
+
};
|
|
2084
|
+
const data = ctx.data;
|
|
2085
|
+
const allNames = Object.values(data.components).map((s) => s.meta.name);
|
|
2086
|
+
const result = handleGraphTool(
|
|
2087
|
+
graphArgs,
|
|
2088
|
+
data.graph,
|
|
2089
|
+
data.blocks,
|
|
2090
|
+
allNames
|
|
2091
|
+
);
|
|
2092
|
+
if (result.isError) {
|
|
2093
|
+
return {
|
|
2094
|
+
content: [{ type: "text", text: result.text }],
|
|
2095
|
+
isError: true
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
content: [{ type: "text", text: result.text }]
|
|
2100
|
+
};
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// src/tools/govern.ts
|
|
2104
|
+
import {
|
|
2105
|
+
handleGovernTool,
|
|
2106
|
+
formatVerdict,
|
|
2107
|
+
universal,
|
|
2108
|
+
fragments as fragmentsPreset
|
|
2109
|
+
} from "@fragments-sdk/govern";
|
|
2110
|
+
var governHandler = async (args, ctx) => {
|
|
2111
|
+
const spec = args?.spec;
|
|
2112
|
+
if (!spec || typeof spec !== "object") {
|
|
2113
|
+
return {
|
|
2114
|
+
content: [
|
|
2115
|
+
{
|
|
2116
|
+
type: "text",
|
|
2117
|
+
text: JSON.stringify({
|
|
2118
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
2119
|
+
})
|
|
2120
|
+
}
|
|
2121
|
+
],
|
|
2122
|
+
isError: true
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
const policyOverrides = args?.policy;
|
|
2126
|
+
const format = args?.format ?? "json";
|
|
2127
|
+
const tokenPrefix = ctx.data.tokens?.prefix;
|
|
2128
|
+
const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset().rules } : { rules: universal().rules };
|
|
2129
|
+
const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
|
|
2130
|
+
const input = {
|
|
2131
|
+
spec,
|
|
2132
|
+
policy: policyOverrides,
|
|
2133
|
+
format
|
|
2134
|
+
};
|
|
2135
|
+
try {
|
|
2136
|
+
const verdict = await handleGovernTool(input, basePolicy, engineOptions);
|
|
2137
|
+
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
2138
|
+
return {
|
|
2139
|
+
content: [{ type: "text", text }],
|
|
2140
|
+
_meta: {
|
|
2141
|
+
score: verdict.score,
|
|
2142
|
+
passed: verdict.passed,
|
|
2143
|
+
violationCount: verdict.results.reduce(
|
|
2144
|
+
(sum, r) => sum + r.violations.length,
|
|
2145
|
+
0
|
|
2146
|
+
)
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2151
|
+
const isSpecError = message.includes("Expected") || message.includes("Required");
|
|
2152
|
+
return {
|
|
2153
|
+
content: [
|
|
2154
|
+
{
|
|
2155
|
+
type: "text",
|
|
2156
|
+
text: JSON.stringify({
|
|
2157
|
+
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
|
|
2158
|
+
})
|
|
2159
|
+
}
|
|
2160
|
+
],
|
|
2161
|
+
isError: true
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
// src/tools/generate-ui.ts
|
|
2167
|
+
var generateUiHandler = async (args, ctx) => {
|
|
2168
|
+
const prompt = args?.prompt;
|
|
2169
|
+
if (!prompt) {
|
|
2170
|
+
throw new Error("prompt is required");
|
|
2171
|
+
}
|
|
2172
|
+
const currentTree = args?.currentTree;
|
|
2173
|
+
const playgroundUrl = ctx.config.playgroundUrl ?? "https://usefragments.com";
|
|
2174
|
+
const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
|
|
2175
|
+
method: "POST",
|
|
2176
|
+
headers: { "Content-Type": "application/json" },
|
|
2177
|
+
body: JSON.stringify({
|
|
2178
|
+
prompt,
|
|
2179
|
+
...currentTree && { currentSpec: currentTree }
|
|
2180
|
+
})
|
|
2181
|
+
});
|
|
2182
|
+
if (!response.ok) {
|
|
2183
|
+
const errorBody = await response.text();
|
|
2184
|
+
throw new Error(`Playground API error (${response.status}): ${errorBody}`);
|
|
2185
|
+
}
|
|
2186
|
+
const text = await response.text();
|
|
2187
|
+
return {
|
|
2188
|
+
content: [{
|
|
2189
|
+
type: "text",
|
|
2190
|
+
text
|
|
2191
|
+
}]
|
|
2192
|
+
};
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
// src/tools/index.ts
|
|
2196
|
+
var CORE_TOOLS = {
|
|
2197
|
+
discover: discoverHandler,
|
|
2198
|
+
inspect: inspectHandler,
|
|
2199
|
+
blocks: blocksHandler,
|
|
2200
|
+
tokens: tokensHandler,
|
|
2201
|
+
implement: implementHandler,
|
|
2202
|
+
graph: graphHandler,
|
|
2203
|
+
govern: governHandler
|
|
2204
|
+
};
|
|
2205
|
+
var VIEWER_TOOLS = {
|
|
2206
|
+
render: renderHandler,
|
|
2207
|
+
fix: fixHandler,
|
|
2208
|
+
a11y: a11yHandler
|
|
2209
|
+
};
|
|
2210
|
+
var INFRA_TOOLS = {
|
|
2211
|
+
generate_ui: generateUiHandler
|
|
2212
|
+
};
|
|
2213
|
+
var BUILTIN_TOOLS = {
|
|
2214
|
+
...CORE_TOOLS,
|
|
2215
|
+
...VIEWER_TOOLS,
|
|
2216
|
+
...INFRA_TOOLS
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
// src/registry.ts
|
|
2220
|
+
var ToolRegistry = class {
|
|
2221
|
+
tools = /* @__PURE__ */ new Map();
|
|
2222
|
+
prefix;
|
|
2223
|
+
onChanged;
|
|
2224
|
+
constructor(prefix, opts) {
|
|
2225
|
+
this.prefix = prefix;
|
|
2226
|
+
this.onChanged = opts?.onChanged;
|
|
2227
|
+
}
|
|
2228
|
+
/** Register a single tool */
|
|
2229
|
+
register(key, handler, definition, availability = "always") {
|
|
2230
|
+
this.tools.set(key, { key, handler, definition, availability });
|
|
2231
|
+
this.onChanged?.();
|
|
2232
|
+
}
|
|
2233
|
+
/** Unregister a tool by key */
|
|
2234
|
+
unregister(key) {
|
|
2235
|
+
if (this.tools.delete(key)) {
|
|
2236
|
+
this.onChanged?.();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
/** Bulk register built-in tools with availability metadata */
|
|
2240
|
+
registerBuiltins(toolSets, definitions) {
|
|
2241
|
+
const defMap = new Map(definitions.map((d) => [d.key, d]));
|
|
2242
|
+
for (const [key, handler] of Object.entries(toolSets.core)) {
|
|
2243
|
+
this.tools.set(key, { key, handler, definition: defMap.get(key), availability: "always" });
|
|
2244
|
+
}
|
|
2245
|
+
for (const [key, handler] of Object.entries(toolSets.viewer)) {
|
|
2246
|
+
this.tools.set(key, { key, handler, definition: defMap.get(key), availability: "viewer" });
|
|
2247
|
+
}
|
|
2248
|
+
for (const [key, handler] of Object.entries(toolSets.infra)) {
|
|
2249
|
+
this.tools.set(key, { key, handler, definition: defMap.get(key), availability: "playground" });
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/** Get handler for a tool (by unprefixed key or prefixed name) */
|
|
2253
|
+
getHandler(nameOrKey) {
|
|
2254
|
+
const direct = this.tools.get(nameOrKey);
|
|
2255
|
+
if (direct) return direct.handler;
|
|
2256
|
+
if (!this.prefix) return void 0;
|
|
2257
|
+
const prefixStr = this.prefix + "_";
|
|
2258
|
+
if (nameOrKey.startsWith(prefixStr)) {
|
|
2259
|
+
const key = nameOrKey.slice(prefixStr.length);
|
|
2260
|
+
return this.tools.get(key)?.handler;
|
|
2261
|
+
}
|
|
2262
|
+
return void 0;
|
|
2263
|
+
}
|
|
2264
|
+
/** Resolve unprefixed key from a potentially prefixed tool name */
|
|
2265
|
+
resolveKey(name) {
|
|
2266
|
+
if (!this.prefix) return name;
|
|
2267
|
+
const prefixStr = this.prefix + "_";
|
|
2268
|
+
return name.startsWith(prefixStr) ? name.slice(prefixStr.length) : name;
|
|
2269
|
+
}
|
|
2270
|
+
/** List available tools as MCP Tool[] based on current availability context */
|
|
2271
|
+
listTools(ctx, allToolSchemas) {
|
|
2272
|
+
const availableKeys = /* @__PURE__ */ new Set();
|
|
2273
|
+
for (const [key, tool] of this.tools) {
|
|
2274
|
+
if (tool.availability === "always") {
|
|
2275
|
+
availableKeys.add(key);
|
|
2276
|
+
} else if (tool.availability === "viewer" && ctx.hasViewer) {
|
|
2277
|
+
availableKeys.add(key);
|
|
2278
|
+
} else if (tool.availability === "playground" && ctx.hasPlayground) {
|
|
2279
|
+
availableKeys.add(key);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (!this.prefix) {
|
|
2283
|
+
return allToolSchemas.filter((t) => availableKeys.has(t.name));
|
|
2284
|
+
}
|
|
2285
|
+
const prefixStr = this.prefix + "_";
|
|
2286
|
+
return allToolSchemas.filter((t) => {
|
|
2287
|
+
const key = t.name.startsWith(prefixStr) ? t.name.slice(prefixStr.length) : t.name;
|
|
2288
|
+
return availableKeys.has(key);
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
/** Execute a tool by name, dispatching to its handler */
|
|
2292
|
+
async execute(name, args, ctx) {
|
|
2293
|
+
const key = this.resolveKey(name);
|
|
2294
|
+
const registered = this.tools.get(key);
|
|
2295
|
+
if (!registered) {
|
|
2296
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2297
|
+
}
|
|
2298
|
+
return registered.handler(args, ctx);
|
|
2299
|
+
}
|
|
2300
|
+
/** Get the number of registered tools */
|
|
2301
|
+
get size() {
|
|
2302
|
+
return this.tools.size;
|
|
2303
|
+
}
|
|
2304
|
+
/** Get all registered keys */
|
|
2305
|
+
keys() {
|
|
2306
|
+
return Array.from(this.tools.keys());
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
// src/middleware.ts
|
|
2311
|
+
function executeWithMiddleware(middlewares, mCtx, handler) {
|
|
2312
|
+
const chain = [...middlewares].reverse().reduce(
|
|
2313
|
+
(next, mw) => () => mw(mCtx, next),
|
|
2314
|
+
handler
|
|
2315
|
+
);
|
|
2316
|
+
return chain();
|
|
2317
|
+
}
|
|
2318
|
+
function telemetryMiddleware(logger) {
|
|
2319
|
+
return async (mCtx, next) => {
|
|
2320
|
+
const start = Date.now();
|
|
2321
|
+
const result = await next();
|
|
2322
|
+
logger(`${mCtx.toolKey}: ${Date.now() - start}ms`);
|
|
2323
|
+
return result;
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/server.ts
|
|
2328
|
+
var TOOL_NAMES = buildToolNames();
|
|
2329
|
+
var TOOLS = buildMcpTools();
|
|
2330
|
+
function createMcpServer(config) {
|
|
2331
|
+
const server = new Server(
|
|
2332
|
+
{
|
|
2333
|
+
name: `${BRAND.nameLower}-mcp`,
|
|
2334
|
+
version: MCP_SERVER_VERSION
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
capabilities: {
|
|
2338
|
+
tools: { listChanged: true }
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
);
|
|
2342
|
+
const registry = new ToolRegistry("", {
|
|
2343
|
+
onChanged: () => {
|
|
2344
|
+
server.notification({ method: "notifications/tools/list_changed", params: {} });
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
registry.registerBuiltins(
|
|
2348
|
+
{ core: CORE_TOOLS, viewer: VIEWER_TOOLS, infra: INFRA_TOOLS },
|
|
2349
|
+
MCP_TOOL_DEFINITIONS
|
|
2350
|
+
);
|
|
2351
|
+
const adapter = config.adapter ?? new FragmentsJsonAdapter();
|
|
2352
|
+
const fileConfig = config.fileConfig ?? loadConfigFile(config.projectRoot) ?? void 0;
|
|
2353
|
+
const mergedConfig = fileConfig ? { ...config, fileConfig } : config;
|
|
2354
|
+
config.onRegistry?.(registry);
|
|
2355
|
+
if (fileConfig?.tools?.exclude) {
|
|
2356
|
+
for (const key of fileConfig.tools.exclude) {
|
|
2357
|
+
registry.unregister(key);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
let cachedData = null;
|
|
2361
|
+
let resolvedRoot = null;
|
|
2362
|
+
let componentIndex = null;
|
|
2363
|
+
let blockIndex = null;
|
|
2364
|
+
let tokenIndex = null;
|
|
2365
|
+
async function resolveProjectRoot() {
|
|
2366
|
+
if (resolvedRoot) return resolvedRoot;
|
|
2367
|
+
try {
|
|
2368
|
+
const result = await server.listRoots();
|
|
2369
|
+
if (result.roots?.length > 0) {
|
|
2370
|
+
const rootUri = result.roots[0].uri;
|
|
2371
|
+
resolvedRoot = fileURLToPath(rootUri);
|
|
2372
|
+
return resolvedRoot;
|
|
2373
|
+
}
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
resolvedRoot = config.projectRoot;
|
|
2377
|
+
return resolvedRoot;
|
|
2378
|
+
}
|
|
2379
|
+
async function loadData() {
|
|
2380
|
+
if (cachedData) return cachedData;
|
|
2381
|
+
const projectRoot = await resolveProjectRoot();
|
|
2382
|
+
cachedData = await adapter.load(projectRoot);
|
|
2383
|
+
const allFragments = Object.values(cachedData.components);
|
|
2384
|
+
const allBlocks = Object.values(cachedData.blocks ?? {});
|
|
2385
|
+
componentIndex = buildComponentIndex(allFragments);
|
|
2386
|
+
if (allBlocks.length > 0) {
|
|
2387
|
+
blockIndex = buildBlockIndex(allBlocks);
|
|
2388
|
+
}
|
|
2389
|
+
if (cachedData.tokens && cachedData.tokens.total > 0) {
|
|
2390
|
+
tokenIndex = buildTokenIndex(cachedData.tokens);
|
|
2391
|
+
}
|
|
2392
|
+
return cachedData;
|
|
2393
|
+
}
|
|
2394
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2395
|
+
return {
|
|
2396
|
+
tools: registry.listTools(
|
|
2397
|
+
{ hasViewer: !!config.viewerUrl, hasPlayground: !!(config.playgroundUrl ?? fileConfig?.playgroundUrl) },
|
|
2398
|
+
TOOLS
|
|
2399
|
+
)
|
|
2400
|
+
};
|
|
2401
|
+
});
|
|
2402
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2403
|
+
const { name, arguments: args } = request.params;
|
|
2404
|
+
const data = await loadData();
|
|
2405
|
+
const toolContext = {
|
|
2406
|
+
data,
|
|
2407
|
+
config: mergedConfig,
|
|
2408
|
+
indexes: { componentIndex, blockIndex, tokenIndex },
|
|
2409
|
+
resolvePackageName: (name2) => {
|
|
2410
|
+
if (name2) {
|
|
2411
|
+
const pkg = data.packageMap.get(name2);
|
|
2412
|
+
if (pkg) return pkg;
|
|
2413
|
+
}
|
|
2414
|
+
if (data.defaultPackageName) return data.defaultPackageName;
|
|
2415
|
+
const root = resolvedRoot ?? config.projectRoot;
|
|
2416
|
+
const packageJsonPath = join3(root, "package.json");
|
|
2417
|
+
if (existsSync3(packageJsonPath)) {
|
|
2418
|
+
try {
|
|
2419
|
+
const content = readFileSync4(packageJsonPath, "utf-8");
|
|
2420
|
+
const pkg = JSON.parse(content);
|
|
2421
|
+
if (pkg.name) {
|
|
2422
|
+
return pkg.name;
|
|
2423
|
+
}
|
|
2424
|
+
} catch {
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return "your-component-library";
|
|
2428
|
+
},
|
|
2429
|
+
toolNames: TOOL_NAMES
|
|
2430
|
+
};
|
|
2431
|
+
try {
|
|
2432
|
+
const toolKey = registry.resolveKey(name);
|
|
2433
|
+
const mCtx = {
|
|
2434
|
+
toolName: name,
|
|
2435
|
+
toolKey,
|
|
2436
|
+
args: args ?? {},
|
|
2437
|
+
ctx: toolContext
|
|
2438
|
+
};
|
|
2439
|
+
return await executeWithMiddleware(
|
|
2440
|
+
config.middleware ?? [],
|
|
2441
|
+
mCtx,
|
|
2442
|
+
() => registry.execute(name, args ?? {}, toolContext)
|
|
2443
|
+
);
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
return {
|
|
2446
|
+
content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }],
|
|
2447
|
+
isError: true
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
return server;
|
|
2452
|
+
}
|
|
2453
|
+
async function startMcpServer(config) {
|
|
2454
|
+
const server = createMcpServer(config);
|
|
2455
|
+
const transport = new StdioServerTransport();
|
|
2456
|
+
await server.connect(transport);
|
|
2457
|
+
}
|
|
2458
|
+
function createSandboxServer() {
|
|
2459
|
+
return createMcpServer({ projectRoot: process.cwd() });
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
export {
|
|
2463
|
+
SYNONYM_MAP,
|
|
2464
|
+
USE_CASE_TOKEN_CATEGORIES,
|
|
2465
|
+
findFragmentsJson,
|
|
2466
|
+
FragmentsJsonAdapter,
|
|
2467
|
+
MINIMUM_SCORE_THRESHOLD,
|
|
2468
|
+
BLOCK_BOOST_PER_OCCURRENCE,
|
|
2469
|
+
DEFAULT_ENDPOINTS,
|
|
2470
|
+
CORE_TOOLS,
|
|
2471
|
+
VIEWER_TOOLS,
|
|
2472
|
+
INFRA_TOOLS,
|
|
2473
|
+
BUILTIN_TOOLS,
|
|
2474
|
+
ToolRegistry,
|
|
2475
|
+
executeWithMiddleware,
|
|
2476
|
+
telemetryMiddleware,
|
|
2477
|
+
createMcpServer,
|
|
2478
|
+
startMcpServer,
|
|
2479
|
+
createSandboxServer
|
|
2480
|
+
};
|
|
2481
|
+
//# sourceMappingURL=chunk-WBOVO43F.js.map
|