@fragments-sdk/cli 0.4.3 → 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 +308 -48
  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 +378 -56
  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")',
345
+ },
346
+ },
347
+ },
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")',
339
362
  },
340
363
  },
341
364
  },
342
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.`,
@@ -430,8 +467,10 @@ export function createMcpServer(config: McpServerConfig): Server {
430
467
 
431
468
  // Lazy-loaded resources
432
469
  let segmentsData: CompiledSegmentsFile | null = null;
433
- let packageName: string | null = null;
434
-
470
+ // Per-segment package name map (segment name → package name from its source fragments.json)
471
+ const segmentPackageMap = new Map<string, string>();
472
+ let defaultPackageName: string | null = null;
473
+
435
474
  let browserPool: any = null;
436
475
  let storageManager: any = null;
437
476
  let diffEngine: any = null;
@@ -455,11 +494,31 @@ export function createMcpServer(config: McpServerConfig): Server {
455
494
  const content = await readFile(paths[0], 'utf-8');
456
495
  segmentsData = JSON.parse(content) as CompiledSegmentsFile;
457
496
 
497
+ // Normalize legacy "recipes" key to "blocks"
498
+ if (!segmentsData.blocks && segmentsData.recipes) {
499
+ segmentsData.blocks = segmentsData.recipes;
500
+ }
501
+
502
+ // Track per-segment package names from each source file
503
+ if (segmentsData.packageName) {
504
+ for (const name of Object.keys(segmentsData.segments)) {
505
+ segmentPackageMap.set(name, segmentsData.packageName);
506
+ }
507
+ }
508
+
458
509
  for (let i = 1; i < paths.length; i++) {
459
510
  const extra = JSON.parse(await readFile(paths[i], 'utf-8')) as CompiledSegmentsFile;
511
+ // Track package name for each segment from this file
512
+ if (extra.packageName) {
513
+ for (const name of Object.keys(extra.segments)) {
514
+ segmentPackageMap.set(name, extra.packageName);
515
+ }
516
+ }
460
517
  Object.assign(segmentsData.segments, extra.segments);
461
- if (extra.recipes) {
462
- 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 };
463
522
  }
464
523
  }
465
524
 
@@ -467,20 +526,28 @@ export function createMcpServer(config: McpServerConfig): Server {
467
526
  }
468
527
 
469
528
  /**
470
- * Get the package name for import statements.
471
- * Prefers packageName from fragments.json (set at build time),
472
- * falls back to the project's package.json name.
529
+ * Get the package name for import statements for a specific component.
530
+ * Uses per-segment tracking when multiple fragments.json files are merged,
531
+ * falls back to the first fragments.json packageName, then project package.json.
473
532
  */
474
- async function getPackageName(): Promise<string> {
475
- if (packageName) {
476
- return packageName;
533
+ async function getPackageName(segmentName?: string): Promise<string> {
534
+ // Ensure segments are loaded (populates segmentPackageMap)
535
+ await loadSegments();
536
+
537
+ // Check per-segment map first (handles multi-library merges correctly)
538
+ if (segmentName) {
539
+ const segPkg = segmentPackageMap.get(segmentName);
540
+ if (segPkg) return segPkg;
477
541
  }
478
542
 
479
- // Prefer packageName from compiled fragments.json
480
- const data = await loadSegments();
481
- if (data.packageName) {
482
- packageName = data.packageName;
483
- return packageName;
543
+ if (defaultPackageName) {
544
+ return defaultPackageName;
545
+ }
546
+
547
+ // Prefer packageName from first fragments.json
548
+ if (segmentsData?.packageName) {
549
+ defaultPackageName = segmentsData.packageName;
550
+ return defaultPackageName;
484
551
  }
485
552
 
486
553
  // Fallback to project package.json
@@ -490,8 +557,8 @@ export function createMcpServer(config: McpServerConfig): Server {
490
557
  const content = await readFile(packageJsonPath, 'utf-8');
491
558
  const pkg = JSON.parse(content) as { name?: string };
492
559
  if (pkg.name) {
493
- packageName = pkg.name;
494
- return packageName;
560
+ defaultPackageName = pkg.name;
561
+ return defaultPackageName;
495
562
  }
496
563
  } catch {
497
564
  // Fall through to default
@@ -499,8 +566,8 @@ export function createMcpServer(config: McpServerConfig): Server {
499
566
  }
500
567
 
501
568
  // Default fallback
502
- packageName = 'your-component-library';
503
- return packageName;
569
+ defaultPackageName = 'your-component-library';
570
+ return defaultPackageName;
504
571
  }
505
572
 
506
573
  /**
@@ -594,7 +661,7 @@ export function createMcpServer(config: McpServerConfig): Server {
594
661
  // --- Context mode: compact or format specified with no specific query ---
595
662
  if (compact || (args?.format && !useCase && !componentForAlts && !category && !search && !status)) {
596
663
  const segments = Object.values(data.segments);
597
- const recipes = Object.values(data.recipes ?? {});
664
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
598
665
 
599
666
  const { content: ctxContent, tokenEstimate } = generateContext(segments, {
600
667
  format,
@@ -603,7 +670,7 @@ export function createMcpServer(config: McpServerConfig): Server {
603
670
  code: includeCode,
604
671
  relations: includeRelations,
605
672
  },
606
- }, recipes);
673
+ }, allBlocks);
607
674
 
608
675
  return {
609
676
  content: [{
@@ -757,6 +824,31 @@ export function createMcpServer(config: McpServerConfig): Server {
757
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 ')}.`
758
825
  : undefined;
759
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
+
760
852
  return {
761
853
  content: [{
762
854
  type: 'text' as const,
@@ -764,13 +856,11 @@ export function createMcpServer(config: McpServerConfig): Server {
764
856
  useCase,
765
857
  context: context || undefined,
766
858
  suggestions: suggestions.map(({ score, ...rest }) => rest),
767
- recommendation: suggestions.length > 0
768
- ? `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`
769
- : 'No matching components found. Try different keywords or browse with fragments_discover.',
859
+ noMatch,
860
+ weakMatch,
861
+ recommendation,
770
862
  compositionHint,
771
- nextStep: suggestions.length > 0
772
- ? `Use fragments_inspect("${suggestions[0].component}") for full details.`
773
- : undefined,
863
+ nextStep,
774
864
  }, null, 2),
775
865
  }],
776
866
  };
@@ -893,16 +983,35 @@ export function createMcpServer(config: McpServerConfig): Server {
893
983
  }
894
984
 
895
985
  // Build the full inspect result combining get + guidelines + example
896
- const pkgName = await getPackageName();
986
+ const pkgName = await getPackageName(segment.meta.name);
897
987
 
898
- // Filter variants for examples
988
+ // Filter variants for examples — fuzzy match: exact → prefix → contains
899
989
  let variants = segment.variants;
900
990
  if (variantName) {
901
- const filtered = variants.filter(
902
- (v) => v.name.toLowerCase() === variantName.toLowerCase()
991
+ const query = variantName.toLowerCase();
992
+ // 1. Exact match
993
+ let filtered = variants.filter(
994
+ (v) => v.name.toLowerCase() === query
903
995
  );
996
+ // 2. Prefix match (e.g. "Dots" matches "Dots (Default)")
997
+ if (filtered.length === 0) {
998
+ filtered = variants.filter(
999
+ (v) => v.name.toLowerCase().startsWith(query)
1000
+ );
1001
+ }
1002
+ // 3. Contains match (e.g. "elapsed" matches "With Elapsed Time")
1003
+ if (filtered.length === 0) {
1004
+ filtered = variants.filter(
1005
+ (v) => v.name.toLowerCase().includes(query)
1006
+ );
1007
+ }
904
1008
  if (filtered.length > 0) {
905
1009
  variants = filtered;
1010
+ } else {
1011
+ throw new Error(
1012
+ `Variant "${variantName}" not found for ${componentName}. ` +
1013
+ `Available: ${segment.variants.map((v) => v.name).join(', ')}`
1014
+ );
906
1015
  }
907
1016
  }
908
1017
  if (maxExamples && maxExamples > 0) {
@@ -991,53 +1100,60 @@ export function createMcpServer(config: McpServerConfig): Server {
991
1100
  }
992
1101
 
993
1102
  // ================================================================
994
- // RECIPEunchanged
1103
+ // BLOCKScomposition patterns
995
1104
  // ================================================================
996
- case TOOL_NAMES.recipe: {
1105
+ case TOOL_NAMES.blocks: {
997
1106
  const data = await loadSegments();
998
- const recipeName = args?.name as string | undefined;
1107
+ const blockName = args?.name as string | undefined;
999
1108
  const search = (args?.search as string)?.toLowerCase() ?? undefined;
1000
1109
  const component = (args?.component as string)?.toLowerCase() ?? undefined;
1110
+ const category = (args?.category as string)?.toLowerCase() ?? undefined;
1001
1111
 
1002
- const allRecipes = Object.values(data.recipes ?? {});
1112
+ const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1003
1113
 
1004
- if (allRecipes.length === 0) {
1114
+ if (allBlocks.length === 0) {
1005
1115
  return {
1006
1116
  content: [{
1007
1117
  type: 'text' as const,
1008
1118
  text: JSON.stringify({
1009
1119
  total: 0,
1010
- recipes: [],
1011
- 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.`,
1012
1122
  }, null, 2),
1013
1123
  }],
1014
1124
  };
1015
1125
  }
1016
1126
 
1017
- let filtered = allRecipes;
1127
+ let filtered = allBlocks;
1018
1128
 
1019
- if (recipeName) {
1129
+ if (blockName) {
1020
1130
  filtered = filtered.filter(
1021
- r => r.name.toLowerCase() === recipeName.toLowerCase()
1131
+ b => b.name.toLowerCase() === blockName.toLowerCase()
1022
1132
  );
1023
1133
  }
1024
1134
 
1025
1135
  if (search) {
1026
- filtered = filtered.filter(r => {
1136
+ filtered = filtered.filter(b => {
1027
1137
  const haystack = [
1028
- r.name,
1029
- r.description,
1030
- ...(r.tags ?? []),
1031
- ...r.components,
1032
- r.category,
1138
+ b.name,
1139
+ b.description,
1140
+ ...(b.tags ?? []),
1141
+ ...b.components,
1142
+ b.category,
1033
1143
  ].join(' ').toLowerCase();
1034
1144
  return haystack.includes(search);
1035
1145
  });
1036
1146
  }
1037
1147
 
1038
1148
  if (component) {
1039
- filtered = filtered.filter(r =>
1040
- 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
1041
1157
  );
1042
1158
  }
1043
1159
 
@@ -1046,7 +1162,213 @@ export function createMcpServer(config: McpServerConfig): Server {
1046
1162
  type: 'text' as const,
1047
1163
  text: JSON.stringify({
1048
1164
  total: filtered.length,
1049
- 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` : ''}.`,
1050
1372
  }, null, 2),
1051
1373
  }],
1052
1374
  };