@fragments-sdk/cli 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +12 -12
  3. package/dist/{chunk-NOTYONHY.js → chunk-2DJH4F4P.js} +2 -2
  4. package/dist/{chunk-5CKYLCJH.js → chunk-2H2JAA3U.js} +35 -7
  5. package/dist/chunk-2H2JAA3U.js.map +1 -0
  6. package/dist/{chunk-G3M3MPQ6.js → chunk-B2TQKOLW.js} +157 -30
  7. package/dist/chunk-B2TQKOLW.js.map +1 -0
  8. package/dist/{chunk-AW7MWOUH.js → chunk-ICAIQ57V.js} +9 -5
  9. package/dist/chunk-ICAIQ57V.js.map +1 -0
  10. package/dist/{chunk-5ZYEOHYK.js → chunk-IOJE35DZ.js} +2 -2
  11. package/dist/{chunk-ZFKGX3QK.js → chunk-UXRGD3DM.js} +47 -14
  12. package/dist/chunk-UXRGD3DM.js.map +1 -0
  13. package/dist/{chunk-J4SI5RIH.js → chunk-XNWDI6UT.js} +4 -4
  14. package/dist/{core-LNXDLXDP.js → core-NJVKKLJ4.js} +11 -3
  15. package/dist/{generate-OIXXHOWR.js → generate-OVGMDKCJ.js} +4 -4
  16. package/dist/index.d.ts +30 -4
  17. package/dist/index.js +6 -6
  18. package/dist/{init-EVPXIDW4.js → init-EOA7TTOR.js} +4 -4
  19. package/dist/mcp-bin.js +266 -36
  20. package/dist/mcp-bin.js.map +1 -1
  21. package/dist/scan-YN4LUDKY.js +12 -0
  22. package/dist/{service-K52ORLCJ.js → service-2T26CBWE.js} +4 -4
  23. package/dist/{static-viewer-JNQIHA4B.js → static-viewer-CLJJRYHK.js} +4 -4
  24. package/dist/{test-USARUEFW.js → test-ECPEXFDN.js} +3 -3
  25. package/dist/{tokens-C6YHBOQE.js → tokens-FHA2DO22.js} +5 -5
  26. package/dist/{viewer-H7TVFT4E.js → viewer-XDPD52L7.js} +13 -13
  27. package/package.json +1 -1
  28. package/src/build.ts +53 -13
  29. package/src/core/constants.ts +4 -1
  30. package/src/core/context.ts +28 -28
  31. package/src/core/defineSegment.ts +21 -11
  32. package/src/core/discovery.ts +52 -4
  33. package/src/core/index.ts +14 -4
  34. package/src/core/loader.ts +3 -0
  35. package/src/core/node.ts +3 -1
  36. package/src/core/parser.ts +1 -1
  37. package/src/core/schema.ts +7 -2
  38. package/src/core/token-parser.ts +211 -0
  39. package/src/core/types.ts +46 -6
  40. package/src/mcp/server.ts +321 -39
  41. package/dist/chunk-5CKYLCJH.js.map +0 -1
  42. package/dist/chunk-AW7MWOUH.js.map +0 -1
  43. package/dist/chunk-G3M3MPQ6.js.map +0 -1
  44. package/dist/chunk-ZFKGX3QK.js.map +0 -1
  45. package/dist/scan-YVYD64GD.js +0 -12
  46. /package/dist/{chunk-NOTYONHY.js.map → chunk-2DJH4F4P.js.map} +0 -0
  47. /package/dist/{chunk-5ZYEOHYK.js.map → chunk-IOJE35DZ.js.map} +0 -0
  48. /package/dist/{chunk-J4SI5RIH.js.map → chunk-XNWDI6UT.js.map} +0 -0
  49. /package/dist/{core-LNXDLXDP.js.map → core-NJVKKLJ4.js.map} +0 -0
  50. /package/dist/{generate-OIXXHOWR.js.map → generate-OVGMDKCJ.js.map} +0 -0
  51. /package/dist/{init-EVPXIDW4.js.map → init-EOA7TTOR.js.map} +0 -0
  52. /package/dist/{scan-YVYD64GD.js.map → scan-YN4LUDKY.js.map} +0 -0
  53. /package/dist/{service-K52ORLCJ.js.map → service-2T26CBWE.js.map} +0 -0
  54. /package/dist/{static-viewer-JNQIHA4B.js.map → static-viewer-CLJJRYHK.js.map} +0 -0
  55. /package/dist/{test-USARUEFW.js.map → test-ECPEXFDN.js.map} +0 -0
  56. /package/dist/{tokens-C6YHBOQE.js.map → tokens-FHA2DO22.js.map} +0 -0
  57. /package/dist/{viewer-H7TVFT4E.js.map → viewer-XDPD52L7.js.map} +0 -0
package/src/mcp/server.ts CHANGED
@@ -41,7 +41,9 @@ import { projectFields } from './utils.js';
41
41
  const TOOL_NAMES = {
42
42
  discover: `${BRAND.nameLower}_discover`,
43
43
  inspect: `${BRAND.nameLower}_inspect`,
44
- recipe: `${BRAND.nameLower}_recipe`,
44
+ blocks: `${BRAND.nameLower}_blocks`,
45
+ tokens: `${BRAND.nameLower}_tokens`,
46
+ implement: `${BRAND.nameLower}_implement`,
45
47
  render: `${BRAND.nameLower}_render`,
46
48
  fix: `${BRAND.nameLower}_fix`,
47
49
  } as const;
@@ -320,26 +322,61 @@ const TOOLS: Tool[] = [
320
322
  },
321
323
  },
322
324
  {
323
- name: TOOL_NAMES.recipe,
324
- description: `Search and retrieve composition recipes — named patterns showing how design system components wire together for common use cases (e.g., "Login Form", "Settings Page"). Returns the recipe with its code pattern.`,
325
+ name: TOOL_NAMES.blocks,
326
+ description: `Search and retrieve composition blocks — named patterns showing how design system components wire together for common use cases (e.g., "Login Form", "Settings Page"). Returns the block with its code pattern.`,
325
327
  inputSchema: {
326
328
  type: 'object' as const,
327
329
  properties: {
328
330
  name: {
329
331
  type: 'string',
330
- description: 'Exact recipe name to retrieve (e.g., "Login Form")',
332
+ description: 'Exact block name to retrieve (e.g., "Login Form")',
331
333
  },
332
334
  search: {
333
335
  type: 'string',
334
- description: 'Free-text search across recipe names, descriptions, tags, and components',
336
+ description: 'Free-text search across block names, descriptions, tags, and components',
335
337
  },
336
338
  component: {
337
339
  type: 'string',
338
- description: 'Filter recipes that use a specific component (e.g., "Button")',
340
+ description: 'Filter blocks that use a specific component (e.g., "Button")',
341
+ },
342
+ category: {
343
+ type: 'string',
344
+ description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")',
339
345
  },
340
346
  },
341
347
  },
342
348
  },
349
+ {
350
+ name: TOOL_NAMES.tokens,
351
+ description: `List available CSS design tokens (custom properties) by category. Use this when you need to style custom elements or override defaults — no more guessing variable names. Filter by category or search by keyword.`,
352
+ inputSchema: {
353
+ type: 'object' as const,
354
+ properties: {
355
+ category: {
356
+ type: 'string',
357
+ description: 'Filter by category (e.g., "colors", "spacing", "typography", "surfaces", "shadows", "radius", "borders", "text", "focus", "layout", "code", "component-sizing")',
358
+ },
359
+ search: {
360
+ type: 'string',
361
+ description: 'Search token names (e.g., "accent", "hover", "padding")',
362
+ },
363
+ },
364
+ },
365
+ },
366
+ {
367
+ name: TOOL_NAMES.implement,
368
+ description: `One-shot implementation helper. Describe what you want to build and get everything needed in a single call: best-matching component(s) with full props and code examples, relevant composition blocks, and applicable CSS tokens. Saves multiple round-trips.`,
369
+ inputSchema: {
370
+ type: 'object' as const,
371
+ properties: {
372
+ useCase: {
373
+ type: 'string',
374
+ description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")',
375
+ },
376
+ },
377
+ required: ['useCase'],
378
+ },
379
+ },
343
380
  {
344
381
  name: TOOL_NAMES.render,
345
382
  description: `Render a component and return a screenshot. Optionally compare against a stored baseline ('baseline: true') or against a Figma design ('figmaUrl'). Use this to verify your implementation looks correct.`,
@@ -457,6 +494,11 @@ export function createMcpServer(config: McpServerConfig): Server {
457
494
  const content = await readFile(paths[0], 'utf-8');
458
495
  segmentsData = JSON.parse(content) as CompiledSegmentsFile;
459
496
 
497
+ // Normalize legacy "recipes" key to "blocks"
498
+ if (!segmentsData.blocks && segmentsData.recipes) {
499
+ segmentsData.blocks = segmentsData.recipes;
500
+ }
501
+
460
502
  // Track per-segment package names from each source file
461
503
  if (segmentsData.packageName) {
462
504
  for (const name of Object.keys(segmentsData.segments)) {
@@ -473,8 +515,10 @@ export function createMcpServer(config: McpServerConfig): Server {
473
515
  }
474
516
  }
475
517
  Object.assign(segmentsData.segments, extra.segments);
476
- if (extra.recipes) {
477
- segmentsData.recipes = { ...segmentsData.recipes, ...extra.recipes };
518
+ // Support both "blocks" (new) and "recipes" (legacy) keys
519
+ const extraBlocks = extra.blocks ?? extra.recipes;
520
+ if (extraBlocks) {
521
+ segmentsData.blocks = { ...segmentsData.blocks, ...extraBlocks };
478
522
  }
479
523
  }
480
524
 
@@ -487,6 +531,9 @@ export function createMcpServer(config: McpServerConfig): Server {
487
531
  * falls back to the first fragments.json packageName, then project package.json.
488
532
  */
489
533
  async function getPackageName(segmentName?: string): Promise<string> {
534
+ // Ensure segments are loaded (populates segmentPackageMap)
535
+ await loadSegments();
536
+
490
537
  // Check per-segment map first (handles multi-library merges correctly)
491
538
  if (segmentName) {
492
539
  const segPkg = segmentPackageMap.get(segmentName);
@@ -497,10 +544,9 @@ export function createMcpServer(config: McpServerConfig): Server {
497
544
  return defaultPackageName;
498
545
  }
499
546
 
500
- // Prefer packageName from compiled fragments.json
501
- const data = await loadSegments();
502
- if (data.packageName) {
503
- defaultPackageName = data.packageName;
547
+ // Prefer packageName from first fragments.json
548
+ if (segmentsData?.packageName) {
549
+ defaultPackageName = segmentsData.packageName;
504
550
  return defaultPackageName;
505
551
  }
506
552
 
@@ -615,7 +661,7 @@ export function createMcpServer(config: McpServerConfig): Server {
615
661
  // --- Context mode: compact or format specified with no specific query ---
616
662
  if (compact || (args?.format && !useCase && !componentForAlts && !category && !search && !status)) {
617
663
  const segments = Object.values(data.segments);
618
- const recipes = Object.values(data.recipes ?? {});
664
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
619
665
 
620
666
  const { content: ctxContent, tokenEstimate } = generateContext(segments, {
621
667
  format,
@@ -624,7 +670,7 @@ export function createMcpServer(config: McpServerConfig): Server {
624
670
  code: includeCode,
625
671
  relations: includeRelations,
626
672
  },
627
- }, recipes);
673
+ }, allBlocks);
628
674
 
629
675
  return {
630
676
  content: [{
@@ -778,6 +824,31 @@ export function createMcpServer(config: McpServerConfig): Server {
778
824
  ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map(s => s.component).join(' and ')}.`
779
825
  : undefined;
780
826
 
827
+ // Detect if query is about styling/tokens rather than components
828
+ const STYLE_KEYWORDS = ['color', 'spacing', 'padding', 'margin', 'font', 'border', 'radius', 'shadow', 'variable', 'token', 'css', 'theme', 'dark mode', 'background', 'hover'];
829
+ const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
830
+
831
+ // Determine no-match quality
832
+ const noMatch = suggestions.length === 0;
833
+ const weakMatch = !noMatch && suggestions.every((s) => s.confidence === 'low');
834
+
835
+ let recommendation: string;
836
+ let nextStep: string | undefined;
837
+ if (noMatch) {
838
+ recommendation = isStyleQuery
839
+ ? `No matching components found. Your query seems styling-related — try ${TOOL_NAMES.tokens} to find CSS custom properties.`
840
+ : 'No matching components found. Try different keywords or browse all components with fragments_discover.';
841
+ nextStep = isStyleQuery
842
+ ? `Use ${TOOL_NAMES.tokens}(search: "${searchTerms[0]}") to find design tokens.`
843
+ : undefined;
844
+ } else if (weakMatch) {
845
+ recommendation = `Weak matches only — ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ''}`;
846
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
847
+ } else {
848
+ recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
849
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
850
+ }
851
+
781
852
  return {
782
853
  content: [{
783
854
  type: 'text' as const,
@@ -785,13 +856,11 @@ export function createMcpServer(config: McpServerConfig): Server {
785
856
  useCase,
786
857
  context: context || undefined,
787
858
  suggestions: suggestions.map(({ score, ...rest }) => rest),
788
- recommendation: suggestions.length > 0
789
- ? `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`
790
- : 'No matching components found. Try different keywords or browse with fragments_discover.',
859
+ noMatch,
860
+ weakMatch,
861
+ recommendation,
791
862
  compositionHint,
792
- nextStep: suggestions.length > 0
793
- ? `Use fragments_inspect("${suggestions[0].component}") for full details.`
794
- : undefined,
863
+ nextStep,
795
864
  }, null, 2),
796
865
  }],
797
866
  };
@@ -1031,53 +1100,60 @@ export function createMcpServer(config: McpServerConfig): Server {
1031
1100
  }
1032
1101
 
1033
1102
  // ================================================================
1034
- // RECIPEunchanged
1103
+ // BLOCKScomposition patterns
1035
1104
  // ================================================================
1036
- case TOOL_NAMES.recipe: {
1105
+ case TOOL_NAMES.blocks: {
1037
1106
  const data = await loadSegments();
1038
- const recipeName = args?.name as string | undefined;
1107
+ const blockName = args?.name as string | undefined;
1039
1108
  const search = (args?.search as string)?.toLowerCase() ?? undefined;
1040
1109
  const component = (args?.component as string)?.toLowerCase() ?? undefined;
1110
+ const category = (args?.category as string)?.toLowerCase() ?? undefined;
1041
1111
 
1042
- const allRecipes = Object.values(data.recipes ?? {});
1112
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1043
1113
 
1044
- if (allRecipes.length === 0) {
1114
+ if (allBlocks.length === 0) {
1045
1115
  return {
1046
1116
  content: [{
1047
1117
  type: 'text' as const,
1048
1118
  text: JSON.stringify({
1049
1119
  total: 0,
1050
- recipes: [],
1051
- hint: `No recipes found. Run \`${BRAND.cliCommand} build\` after adding .recipe.ts files.`,
1120
+ blocks: [],
1121
+ hint: `No blocks found. Run \`${BRAND.cliCommand} build\` after adding .block.ts files.`,
1052
1122
  }, null, 2),
1053
1123
  }],
1054
1124
  };
1055
1125
  }
1056
1126
 
1057
- let filtered = allRecipes;
1127
+ let filtered = allBlocks;
1058
1128
 
1059
- if (recipeName) {
1129
+ if (blockName) {
1060
1130
  filtered = filtered.filter(
1061
- r => r.name.toLowerCase() === recipeName.toLowerCase()
1131
+ b => b.name.toLowerCase() === blockName.toLowerCase()
1062
1132
  );
1063
1133
  }
1064
1134
 
1065
1135
  if (search) {
1066
- filtered = filtered.filter(r => {
1136
+ filtered = filtered.filter(b => {
1067
1137
  const haystack = [
1068
- r.name,
1069
- r.description,
1070
- ...(r.tags ?? []),
1071
- ...r.components,
1072
- r.category,
1138
+ b.name,
1139
+ b.description,
1140
+ ...(b.tags ?? []),
1141
+ ...b.components,
1142
+ b.category,
1073
1143
  ].join(' ').toLowerCase();
1074
1144
  return haystack.includes(search);
1075
1145
  });
1076
1146
  }
1077
1147
 
1078
1148
  if (component) {
1079
- filtered = filtered.filter(r =>
1080
- r.components.some(c => c.toLowerCase() === component)
1149
+ filtered = filtered.filter(b =>
1150
+ b.components.some(c => c.toLowerCase() === component)
1151
+ );
1152
+ }
1153
+
1154
+ if (category) {
1155
+ filtered = filtered.filter(b =>
1156
+ b.category.toLowerCase() === category
1081
1157
  );
1082
1158
  }
1083
1159
 
@@ -1086,7 +1162,213 @@ export function createMcpServer(config: McpServerConfig): Server {
1086
1162
  type: 'text' as const,
1087
1163
  text: JSON.stringify({
1088
1164
  total: filtered.length,
1089
- recipes: filtered,
1165
+ blocks: filtered,
1166
+ }, null, 2),
1167
+ }],
1168
+ };
1169
+ }
1170
+
1171
+ // ================================================================
1172
+ // TOKENS — list CSS custom properties by category
1173
+ // ================================================================
1174
+ case TOOL_NAMES.tokens: {
1175
+ const data = await loadSegments();
1176
+ const category = (args?.category as string)?.toLowerCase() ?? undefined;
1177
+ const search = (args?.search as string)?.toLowerCase() ?? undefined;
1178
+
1179
+ const tokenData = data.tokens;
1180
+
1181
+ if (!tokenData || tokenData.total === 0) {
1182
+ return {
1183
+ content: [{
1184
+ type: 'text' as const,
1185
+ text: JSON.stringify({
1186
+ total: 0,
1187
+ categories: {},
1188
+ hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`,
1189
+ }, null, 2),
1190
+ }],
1191
+ };
1192
+ }
1193
+
1194
+ // Filter by category and/or search
1195
+ let filteredCategories: Record<string, Array<{ name: string; description?: string }>> = {};
1196
+ let filteredTotal = 0;
1197
+
1198
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1199
+ // Filter by category
1200
+ if (category && cat !== category) continue;
1201
+
1202
+ // Filter by search term within this category
1203
+ let filtered = tokens;
1204
+ if (search) {
1205
+ filtered = tokens.filter(
1206
+ (t) => t.name.toLowerCase().includes(search) ||
1207
+ (t.description && t.description.toLowerCase().includes(search))
1208
+ );
1209
+ }
1210
+
1211
+ if (filtered.length > 0) {
1212
+ filteredCategories[cat] = filtered;
1213
+ filteredTotal += filtered.length;
1214
+ }
1215
+ }
1216
+
1217
+ // Build usage hint based on context
1218
+ let hint: string | undefined;
1219
+ if (filteredTotal === 0) {
1220
+ const availableCategories = Object.keys(tokenData.categories);
1221
+ hint = search
1222
+ ? `No tokens matching "${search}". Try: ${availableCategories.join(', ')}`
1223
+ : category
1224
+ ? `Category "${category}" not found. Available: ${availableCategories.join(', ')}`
1225
+ : undefined;
1226
+ } else if (!category && !search) {
1227
+ hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1228
+ }
1229
+
1230
+ return {
1231
+ content: [{
1232
+ type: 'text' as const,
1233
+ text: JSON.stringify({
1234
+ prefix: tokenData.prefix,
1235
+ total: filteredTotal,
1236
+ totalAvailable: tokenData.total,
1237
+ categories: filteredCategories,
1238
+ ...(hint && { hint }),
1239
+ ...((!category && !search) && {
1240
+ availableCategories: Object.entries(tokenData.categories).map(
1241
+ ([cat, tokens]) => ({ category: cat, count: tokens.length })
1242
+ ),
1243
+ }),
1244
+ }, null, 2),
1245
+ }],
1246
+ };
1247
+ }
1248
+
1249
+ // ================================================================
1250
+ // IMPLEMENT — one-shot discover + inspect + blocks + tokens
1251
+ // ================================================================
1252
+ case TOOL_NAMES.implement: {
1253
+ const data = await loadSegments();
1254
+ const useCase = args?.useCase as string;
1255
+ if (!useCase) {
1256
+ throw new Error('useCase is required');
1257
+ }
1258
+
1259
+ const useCaseLower = useCase.toLowerCase();
1260
+ const searchTerms = useCaseLower.split(/\s+/).filter(Boolean);
1261
+
1262
+ // 1. Score all segments (same logic as discover suggest)
1263
+ const synonymMap: Record<string, string[]> = {
1264
+ 'form': ['input', 'field', 'submit', 'validation'],
1265
+ 'input': ['form', 'field', 'text', 'entry'],
1266
+ 'button': ['action', 'click', 'submit', 'trigger'],
1267
+ 'alert': ['notification', 'message', 'warning', 'error', 'feedback'],
1268
+ 'notification': ['alert', 'message', 'toast'],
1269
+ 'card': ['container', 'panel', 'box', 'content'],
1270
+ 'toggle': ['switch', 'checkbox', 'boolean'],
1271
+ 'badge': ['tag', 'label', 'status', 'indicator'],
1272
+ 'login': ['auth', 'signin', 'authentication', 'form'],
1273
+ 'chat': ['message', 'conversation', 'ai'],
1274
+ 'table': ['data', 'grid', 'list', 'rows'],
1275
+ };
1276
+
1277
+ const expandedTerms = new Set(searchTerms);
1278
+ searchTerms.forEach((term) => {
1279
+ const synonyms = synonymMap[term];
1280
+ if (synonyms) synonyms.forEach((syn) => expandedTerms.add(syn));
1281
+ });
1282
+
1283
+ const scored = Object.values(data.segments).map((s) => {
1284
+ let score = 0;
1285
+ const nameLower = s.meta.name.toLowerCase();
1286
+ if (searchTerms.some((t) => nameLower.includes(t))) score += 15;
1287
+ else if (Array.from(expandedTerms).some((t) => nameLower.includes(t))) score += 8;
1288
+ const desc = s.meta.description?.toLowerCase() ?? '';
1289
+ score += searchTerms.filter((t) => desc.includes(t)).length * 6;
1290
+ const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
1291
+ score += searchTerms.filter((t) => tags.some((tag) => tag.includes(t))).length * 4;
1292
+ const whenUsed = s.usage?.when?.join(' ').toLowerCase() ?? '';
1293
+ score += searchTerms.filter((t) => whenUsed.includes(t)).length * 10;
1294
+ score += Array.from(expandedTerms).filter((t) => !searchTerms.includes(t) && whenUsed.includes(t)).length * 5;
1295
+ if (s.meta.category && searchTerms.some((t) => s.meta.category!.toLowerCase().includes(t))) score += 8;
1296
+ if (s.meta.status === 'stable') score += 5;
1297
+ if (s.meta.status === 'deprecated') score -= 25;
1298
+ return { segment: s, score };
1299
+ });
1300
+
1301
+ const topMatches = scored
1302
+ .filter((s) => s.score >= 8)
1303
+ .sort((a, b) => b.score - a.score)
1304
+ .slice(0, 3);
1305
+
1306
+ // 2. Build component details for top matches
1307
+ const components = await Promise.all(
1308
+ topMatches.map(async ({ segment: s, score }) => {
1309
+ const pkgName = await getPackageName(s.meta.name);
1310
+ const examples = s.variants.slice(0, 2).map((v) => ({
1311
+ variant: v.name,
1312
+ code: v.code ?? `<${s.meta.name} />`,
1313
+ }));
1314
+ const propsSummary = Object.entries(s.props ?? {}).slice(0, 10).map(
1315
+ ([name, p]) => `${name}${p.required ? ' (required)' : ''}: ${p.type}${p.values ? ` = ${p.values.join('|')}` : ''}`
1316
+ );
1317
+ return {
1318
+ name: s.meta.name,
1319
+ category: s.meta.category,
1320
+ description: s.meta.description,
1321
+ confidence: score >= 25 ? 'high' : score >= 15 ? 'medium' : 'low',
1322
+ import: `import { ${s.meta.name} } from '${pkgName}';`,
1323
+ props: propsSummary,
1324
+ examples,
1325
+ guidelines: filterPlaceholders(s.usage?.when).slice(0, 3),
1326
+ accessibility: s.usage?.accessibility?.slice(0, 2) ?? [],
1327
+ };
1328
+ })
1329
+ );
1330
+
1331
+ // 3. Find relevant blocks
1332
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1333
+ const matchingBlocks = allBlocks
1334
+ .filter((b) => {
1335
+ const haystack = [b.name, b.description, ...(b.tags ?? []), ...b.components, b.category].join(' ').toLowerCase();
1336
+ return searchTerms.some((t) => haystack.includes(t)) ||
1337
+ topMatches.some(({ segment }) => b.components.some((c) => c.toLowerCase() === segment.meta.name.toLowerCase()));
1338
+ })
1339
+ .slice(0, 2)
1340
+ .map((b) => ({ name: b.name, description: b.description, components: b.components, code: b.code }));
1341
+
1342
+ // 4. Find relevant tokens
1343
+ const tokenData = data.tokens;
1344
+ let relevantTokens: Record<string, string[]> | undefined;
1345
+ if (tokenData) {
1346
+ const STYLE_KEYWORDS = ['color', 'spacing', 'padding', 'margin', 'font', 'border', 'radius', 'shadow', 'background', 'hover', 'theme'];
1347
+ const styleTerms = searchTerms.filter((t) => STYLE_KEYWORDS.includes(t));
1348
+ if (styleTerms.length > 0) {
1349
+ relevantTokens = {};
1350
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1351
+ const matching = tokens.filter((t) => styleTerms.some((st) => t.name.includes(st) || cat.includes(st)));
1352
+ if (matching.length > 0) {
1353
+ relevantTokens[cat] = matching.map((t) => t.name);
1354
+ }
1355
+ }
1356
+ if (Object.keys(relevantTokens).length === 0) relevantTokens = undefined;
1357
+ }
1358
+ }
1359
+
1360
+ return {
1361
+ content: [{
1362
+ type: 'text' as const,
1363
+ text: JSON.stringify({
1364
+ useCase,
1365
+ components,
1366
+ blocks: matchingBlocks.length > 0 ? matchingBlocks : undefined,
1367
+ tokens: relevantTokens,
1368
+ noMatch: components.length === 0,
1369
+ summary: components.length > 0
1370
+ ? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ''}`
1371
+ : `No components match "${useCase}". Try ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ''}.`,
1090
1372
  }, null, 2),
1091
1373
  }],
1092
1374
  };