@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/dist/mcp-bin.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
3
3
  import {
4
4
  generateContext
5
- } from "./chunk-G3M3MPQ6.js";
5
+ } from "./chunk-B2TQKOLW.js";
6
6
  import {
7
7
  BRAND,
8
8
  DEFAULTS
9
- } from "./chunk-AW7MWOUH.js";
9
+ } from "./chunk-ICAIQ57V.js";
10
10
 
11
11
  // src/mcp/server.ts
12
12
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -57,7 +57,7 @@ var _service = null;
57
57
  async function getService() {
58
58
  if (!_service) {
59
59
  try {
60
- _service = await import("./service-K52ORLCJ.js");
60
+ _service = await import("./service-2T26CBWE.js");
61
61
  } catch {
62
62
  throw new Error(
63
63
  "Visual tools require playwright. Install it with: npm install playwright"
@@ -69,7 +69,9 @@ async function getService() {
69
69
  var TOOL_NAMES = {
70
70
  discover: `${BRAND.nameLower}_discover`,
71
71
  inspect: `${BRAND.nameLower}_inspect`,
72
- recipe: `${BRAND.nameLower}_recipe`,
72
+ blocks: `${BRAND.nameLower}_blocks`,
73
+ tokens: `${BRAND.nameLower}_tokens`,
74
+ implement: `${BRAND.nameLower}_implement`,
73
75
  render: `${BRAND.nameLower}_render`,
74
76
  fix: `${BRAND.nameLower}_fix`
75
77
  };
@@ -279,26 +281,61 @@ var TOOLS = [
279
281
  }
280
282
  },
281
283
  {
282
- name: TOOL_NAMES.recipe,
283
- description: `Search and retrieve composition recipes \u2014 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.`,
284
+ name: TOOL_NAMES.blocks,
285
+ description: `Search and retrieve composition blocks \u2014 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.`,
284
286
  inputSchema: {
285
287
  type: "object",
286
288
  properties: {
287
289
  name: {
288
290
  type: "string",
289
- description: 'Exact recipe name to retrieve (e.g., "Login Form")'
291
+ description: 'Exact block name to retrieve (e.g., "Login Form")'
290
292
  },
291
293
  search: {
292
294
  type: "string",
293
- description: "Free-text search across recipe names, descriptions, tags, and components"
295
+ description: "Free-text search across block names, descriptions, tags, and components"
294
296
  },
295
297
  component: {
296
298
  type: "string",
297
- description: 'Filter recipes that use a specific component (e.g., "Button")'
299
+ description: 'Filter blocks that use a specific component (e.g., "Button")'
300
+ },
301
+ category: {
302
+ type: "string",
303
+ description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")'
304
+ }
305
+ }
306
+ }
307
+ },
308
+ {
309
+ name: TOOL_NAMES.tokens,
310
+ description: `List available CSS design tokens (custom properties) by category. Use this when you need to style custom elements or override defaults \u2014 no more guessing variable names. Filter by category or search by keyword.`,
311
+ inputSchema: {
312
+ type: "object",
313
+ properties: {
314
+ category: {
315
+ type: "string",
316
+ description: 'Filter by category (e.g., "colors", "spacing", "typography", "surfaces", "shadows", "radius", "borders", "text", "focus", "layout", "code", "component-sizing")'
317
+ },
318
+ search: {
319
+ type: "string",
320
+ description: 'Search token names (e.g., "accent", "hover", "padding")'
298
321
  }
299
322
  }
300
323
  }
301
324
  },
325
+ {
326
+ name: TOOL_NAMES.implement,
327
+ 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.`,
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ useCase: {
332
+ type: "string",
333
+ description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")'
334
+ }
335
+ },
336
+ required: ["useCase"]
337
+ }
338
+ },
302
339
  {
303
340
  name: TOOL_NAMES.render,
304
341
  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.`,
@@ -401,6 +438,9 @@ function createMcpServer(config) {
401
438
  }
402
439
  const content = await readFile(paths[0], "utf-8");
403
440
  segmentsData = JSON.parse(content);
441
+ if (!segmentsData.blocks && segmentsData.recipes) {
442
+ segmentsData.blocks = segmentsData.recipes;
443
+ }
404
444
  if (segmentsData.packageName) {
405
445
  for (const name of Object.keys(segmentsData.segments)) {
406
446
  segmentPackageMap.set(name, segmentsData.packageName);
@@ -414,13 +454,15 @@ function createMcpServer(config) {
414
454
  }
415
455
  }
416
456
  Object.assign(segmentsData.segments, extra.segments);
417
- if (extra.recipes) {
418
- segmentsData.recipes = { ...segmentsData.recipes, ...extra.recipes };
457
+ const extraBlocks = extra.blocks ?? extra.recipes;
458
+ if (extraBlocks) {
459
+ segmentsData.blocks = { ...segmentsData.blocks, ...extraBlocks };
419
460
  }
420
461
  }
421
462
  return segmentsData;
422
463
  }
423
464
  async function getPackageName(segmentName) {
465
+ await loadSegments();
424
466
  if (segmentName) {
425
467
  const segPkg = segmentPackageMap.get(segmentName);
426
468
  if (segPkg) return segPkg;
@@ -428,9 +470,8 @@ function createMcpServer(config) {
428
470
  if (defaultPackageName) {
429
471
  return defaultPackageName;
430
472
  }
431
- const data = await loadSegments();
432
- if (data.packageName) {
433
- defaultPackageName = data.packageName;
473
+ if (segmentsData?.packageName) {
474
+ defaultPackageName = segmentsData.packageName;
434
475
  return defaultPackageName;
435
476
  }
436
477
  const packageJsonPath = join(config.projectRoot, "package.json");
@@ -516,7 +557,7 @@ function createMcpServer(config) {
516
557
  const includeRelations = args2?.includeRelations ?? false;
517
558
  if (compact || args2?.format && !useCase && !componentForAlts && !category && !search && !status) {
518
559
  const segments2 = Object.values(data.segments);
519
- const recipes = Object.values(data.recipes ?? {});
560
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
520
561
  const { content: ctxContent, tokenEstimate } = generateContext(segments2, {
521
562
  format,
522
563
  compact,
@@ -524,7 +565,7 @@ function createMcpServer(config) {
524
565
  code: includeCode,
525
566
  relations: includeRelations
526
567
  }
527
- }, recipes);
568
+ }, allBlocks);
528
569
  return {
529
570
  content: [{
530
571
  type: "text",
@@ -650,6 +691,22 @@ function createMcpServer(config) {
650
691
  }
651
692
  }
652
693
  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;
694
+ const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
695
+ const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
696
+ const noMatch = suggestions.length === 0;
697
+ const weakMatch = !noMatch && suggestions.every((s) => s.confidence === "low");
698
+ let recommendation;
699
+ let nextStep;
700
+ if (noMatch) {
701
+ recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${TOOL_NAMES.tokens} to find CSS custom properties.` : "No matching components found. Try different keywords or browse all components with fragments_discover.";
702
+ nextStep = isStyleQuery ? `Use ${TOOL_NAMES.tokens}(search: "${searchTerms[0]}") to find design tokens.` : void 0;
703
+ } else if (weakMatch) {
704
+ recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ""}`;
705
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") to check if it fits, or try broader search terms.`;
706
+ } else {
707
+ recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
708
+ nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
709
+ }
653
710
  return {
654
711
  content: [{
655
712
  type: "text",
@@ -657,9 +714,11 @@ function createMcpServer(config) {
657
714
  useCase,
658
715
  context: context || void 0,
659
716
  suggestions: suggestions.map(({ score, ...rest }) => rest),
660
- recommendation: suggestions.length > 0 ? `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}` : "No matching components found. Try different keywords or browse with fragments_discover.",
717
+ noMatch,
718
+ weakMatch,
719
+ recommendation,
661
720
  compositionHint,
662
- nextStep: suggestions.length > 0 ? `Use fragments_inspect("${suggestions[0].component}") for full details.` : void 0
721
+ nextStep
663
722
  }, null, 2)
664
723
  }]
665
724
  };
@@ -848,47 +907,53 @@ function createMcpServer(config) {
848
907
  };
849
908
  }
850
909
  // ================================================================
851
- // RECIPEunchanged
910
+ // BLOCKScomposition patterns
852
911
  // ================================================================
853
- case TOOL_NAMES.recipe: {
912
+ case TOOL_NAMES.blocks: {
854
913
  const data = await loadSegments();
855
- const recipeName = args2?.name;
914
+ const blockName = args2?.name;
856
915
  const search = args2?.search?.toLowerCase() ?? void 0;
857
916
  const component = args2?.component?.toLowerCase() ?? void 0;
858
- const allRecipes = Object.values(data.recipes ?? {});
859
- if (allRecipes.length === 0) {
917
+ const category = args2?.category?.toLowerCase() ?? void 0;
918
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
919
+ if (allBlocks.length === 0) {
860
920
  return {
861
921
  content: [{
862
922
  type: "text",
863
923
  text: JSON.stringify({
864
924
  total: 0,
865
- recipes: [],
866
- hint: `No recipes found. Run \`${BRAND.cliCommand} build\` after adding .recipe.ts files.`
925
+ blocks: [],
926
+ hint: `No blocks found. Run \`${BRAND.cliCommand} build\` after adding .block.ts files.`
867
927
  }, null, 2)
868
928
  }]
869
929
  };
870
930
  }
871
- let filtered = allRecipes;
872
- if (recipeName) {
931
+ let filtered = allBlocks;
932
+ if (blockName) {
873
933
  filtered = filtered.filter(
874
- (r) => r.name.toLowerCase() === recipeName.toLowerCase()
934
+ (b) => b.name.toLowerCase() === blockName.toLowerCase()
875
935
  );
876
936
  }
877
937
  if (search) {
878
- filtered = filtered.filter((r) => {
938
+ filtered = filtered.filter((b) => {
879
939
  const haystack = [
880
- r.name,
881
- r.description,
882
- ...r.tags ?? [],
883
- ...r.components,
884
- r.category
940
+ b.name,
941
+ b.description,
942
+ ...b.tags ?? [],
943
+ ...b.components,
944
+ b.category
885
945
  ].join(" ").toLowerCase();
886
946
  return haystack.includes(search);
887
947
  });
888
948
  }
889
949
  if (component) {
890
950
  filtered = filtered.filter(
891
- (r) => r.components.some((c) => c.toLowerCase() === component)
951
+ (b) => b.components.some((c) => c.toLowerCase() === component)
952
+ );
953
+ }
954
+ if (category) {
955
+ filtered = filtered.filter(
956
+ (b) => b.category.toLowerCase() === category
892
957
  );
893
958
  }
894
959
  return {
@@ -896,7 +961,172 @@ function createMcpServer(config) {
896
961
  type: "text",
897
962
  text: JSON.stringify({
898
963
  total: filtered.length,
899
- recipes: filtered
964
+ blocks: filtered
965
+ }, null, 2)
966
+ }]
967
+ };
968
+ }
969
+ // ================================================================
970
+ // TOKENS — list CSS custom properties by category
971
+ // ================================================================
972
+ case TOOL_NAMES.tokens: {
973
+ const data = await loadSegments();
974
+ const category = args2?.category?.toLowerCase() ?? void 0;
975
+ const search = args2?.search?.toLowerCase() ?? void 0;
976
+ const tokenData = data.tokens;
977
+ if (!tokenData || tokenData.total === 0) {
978
+ return {
979
+ content: [{
980
+ type: "text",
981
+ text: JSON.stringify({
982
+ total: 0,
983
+ categories: {},
984
+ hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
985
+ }, null, 2)
986
+ }]
987
+ };
988
+ }
989
+ let filteredCategories = {};
990
+ let filteredTotal = 0;
991
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
992
+ if (category && cat !== category) continue;
993
+ let filtered = tokens;
994
+ if (search) {
995
+ filtered = tokens.filter(
996
+ (t) => t.name.toLowerCase().includes(search) || t.description && t.description.toLowerCase().includes(search)
997
+ );
998
+ }
999
+ if (filtered.length > 0) {
1000
+ filteredCategories[cat] = filtered;
1001
+ filteredTotal += filtered.length;
1002
+ }
1003
+ }
1004
+ let hint;
1005
+ if (filteredTotal === 0) {
1006
+ const availableCategories = Object.keys(tokenData.categories);
1007
+ hint = search ? `No tokens matching "${search}". Try: ${availableCategories.join(", ")}` : category ? `Category "${category}" not found. Available: ${availableCategories.join(", ")}` : void 0;
1008
+ } else if (!category && !search) {
1009
+ hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1010
+ }
1011
+ return {
1012
+ content: [{
1013
+ type: "text",
1014
+ text: JSON.stringify({
1015
+ prefix: tokenData.prefix,
1016
+ total: filteredTotal,
1017
+ totalAvailable: tokenData.total,
1018
+ categories: filteredCategories,
1019
+ ...hint && { hint },
1020
+ ...!category && !search && {
1021
+ availableCategories: Object.entries(tokenData.categories).map(
1022
+ ([cat, tokens]) => ({ category: cat, count: tokens.length })
1023
+ )
1024
+ }
1025
+ }, null, 2)
1026
+ }]
1027
+ };
1028
+ }
1029
+ // ================================================================
1030
+ // IMPLEMENT — one-shot discover + inspect + blocks + tokens
1031
+ // ================================================================
1032
+ case TOOL_NAMES.implement: {
1033
+ const data = await loadSegments();
1034
+ const useCase = args2?.useCase;
1035
+ if (!useCase) {
1036
+ throw new Error("useCase is required");
1037
+ }
1038
+ const useCaseLower = useCase.toLowerCase();
1039
+ const searchTerms = useCaseLower.split(/\s+/).filter(Boolean);
1040
+ const synonymMap = {
1041
+ "form": ["input", "field", "submit", "validation"],
1042
+ "input": ["form", "field", "text", "entry"],
1043
+ "button": ["action", "click", "submit", "trigger"],
1044
+ "alert": ["notification", "message", "warning", "error", "feedback"],
1045
+ "notification": ["alert", "message", "toast"],
1046
+ "card": ["container", "panel", "box", "content"],
1047
+ "toggle": ["switch", "checkbox", "boolean"],
1048
+ "badge": ["tag", "label", "status", "indicator"],
1049
+ "login": ["auth", "signin", "authentication", "form"],
1050
+ "chat": ["message", "conversation", "ai"],
1051
+ "table": ["data", "grid", "list", "rows"]
1052
+ };
1053
+ const expandedTerms = new Set(searchTerms);
1054
+ searchTerms.forEach((term) => {
1055
+ const synonyms = synonymMap[term];
1056
+ if (synonyms) synonyms.forEach((syn) => expandedTerms.add(syn));
1057
+ });
1058
+ const scored = Object.values(data.segments).map((s) => {
1059
+ let score = 0;
1060
+ const nameLower = s.meta.name.toLowerCase();
1061
+ if (searchTerms.some((t) => nameLower.includes(t))) score += 15;
1062
+ else if (Array.from(expandedTerms).some((t) => nameLower.includes(t))) score += 8;
1063
+ const desc = s.meta.description?.toLowerCase() ?? "";
1064
+ score += searchTerms.filter((t) => desc.includes(t)).length * 6;
1065
+ const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
1066
+ score += searchTerms.filter((t) => tags.some((tag) => tag.includes(t))).length * 4;
1067
+ const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
1068
+ score += searchTerms.filter((t) => whenUsed.includes(t)).length * 10;
1069
+ score += Array.from(expandedTerms).filter((t) => !searchTerms.includes(t) && whenUsed.includes(t)).length * 5;
1070
+ if (s.meta.category && searchTerms.some((t) => s.meta.category.toLowerCase().includes(t))) score += 8;
1071
+ if (s.meta.status === "stable") score += 5;
1072
+ if (s.meta.status === "deprecated") score -= 25;
1073
+ return { segment: s, score };
1074
+ });
1075
+ const topMatches = scored.filter((s) => s.score >= 8).sort((a, b) => b.score - a.score).slice(0, 3);
1076
+ const components = await Promise.all(
1077
+ topMatches.map(async ({ segment: s, score }) => {
1078
+ const pkgName = await getPackageName(s.meta.name);
1079
+ const examples = s.variants.slice(0, 2).map((v) => ({
1080
+ variant: v.name,
1081
+ code: v.code ?? `<${s.meta.name} />`
1082
+ }));
1083
+ const propsSummary = Object.entries(s.props ?? {}).slice(0, 10).map(
1084
+ ([name2, p]) => `${name2}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
1085
+ );
1086
+ return {
1087
+ name: s.meta.name,
1088
+ category: s.meta.category,
1089
+ description: s.meta.description,
1090
+ confidence: score >= 25 ? "high" : score >= 15 ? "medium" : "low",
1091
+ import: `import { ${s.meta.name} } from '${pkgName}';`,
1092
+ props: propsSummary,
1093
+ examples,
1094
+ guidelines: filterPlaceholders(s.usage?.when).slice(0, 3),
1095
+ accessibility: s.usage?.accessibility?.slice(0, 2) ?? []
1096
+ };
1097
+ })
1098
+ );
1099
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1100
+ const matchingBlocks = allBlocks.filter((b) => {
1101
+ const haystack = [b.name, b.description, ...b.tags ?? [], ...b.components, b.category].join(" ").toLowerCase();
1102
+ return searchTerms.some((t) => haystack.includes(t)) || topMatches.some(({ segment }) => b.components.some((c) => c.toLowerCase() === segment.meta.name.toLowerCase()));
1103
+ }).slice(0, 2).map((b) => ({ name: b.name, description: b.description, components: b.components, code: b.code }));
1104
+ const tokenData = data.tokens;
1105
+ let relevantTokens;
1106
+ if (tokenData) {
1107
+ const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "background", "hover", "theme"];
1108
+ const styleTerms = searchTerms.filter((t) => STYLE_KEYWORDS.includes(t));
1109
+ if (styleTerms.length > 0) {
1110
+ relevantTokens = {};
1111
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1112
+ const matching = tokens.filter((t) => styleTerms.some((st) => t.name.includes(st) || cat.includes(st)));
1113
+ if (matching.length > 0) {
1114
+ relevantTokens[cat] = matching.map((t) => t.name);
1115
+ }
1116
+ }
1117
+ if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
1118
+ }
1119
+ }
1120
+ return {
1121
+ content: [{
1122
+ type: "text",
1123
+ text: JSON.stringify({
1124
+ useCase,
1125
+ components,
1126
+ blocks: matchingBlocks.length > 0 ? matchingBlocks : void 0,
1127
+ tokens: relevantTokens,
1128
+ noMatch: components.length === 0,
1129
+ 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 ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ""}.`
900
1130
  }, null, 2)
901
1131
  }]
902
1132
  };