@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.
- package/README.md +1 -1
- package/dist/bin.js +12 -12
- package/dist/{chunk-NOTYONHY.js → chunk-2DJH4F4P.js} +2 -2
- package/dist/{chunk-5CKYLCJH.js → chunk-2H2JAA3U.js} +35 -7
- package/dist/chunk-2H2JAA3U.js.map +1 -0
- package/dist/{chunk-G3M3MPQ6.js → chunk-B2TQKOLW.js} +157 -30
- package/dist/chunk-B2TQKOLW.js.map +1 -0
- package/dist/{chunk-AW7MWOUH.js → chunk-ICAIQ57V.js} +9 -5
- package/dist/chunk-ICAIQ57V.js.map +1 -0
- package/dist/{chunk-5ZYEOHYK.js → chunk-IOJE35DZ.js} +2 -2
- package/dist/{chunk-ZFKGX3QK.js → chunk-UXRGD3DM.js} +47 -14
- package/dist/chunk-UXRGD3DM.js.map +1 -0
- package/dist/{chunk-J4SI5RIH.js → chunk-XNWDI6UT.js} +4 -4
- package/dist/{core-LNXDLXDP.js → core-NJVKKLJ4.js} +11 -3
- package/dist/{generate-OIXXHOWR.js → generate-OVGMDKCJ.js} +4 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +6 -6
- package/dist/{init-EVPXIDW4.js → init-EOA7TTOR.js} +4 -4
- package/dist/mcp-bin.js +266 -36
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-YN4LUDKY.js +12 -0
- package/dist/{service-K52ORLCJ.js → service-2T26CBWE.js} +4 -4
- package/dist/{static-viewer-JNQIHA4B.js → static-viewer-CLJJRYHK.js} +4 -4
- package/dist/{test-USARUEFW.js → test-ECPEXFDN.js} +3 -3
- package/dist/{tokens-C6YHBOQE.js → tokens-FHA2DO22.js} +5 -5
- package/dist/{viewer-H7TVFT4E.js → viewer-XDPD52L7.js} +13 -13
- package/package.json +1 -1
- package/src/build.ts +53 -13
- package/src/core/constants.ts +4 -1
- package/src/core/context.ts +28 -28
- package/src/core/defineSegment.ts +21 -11
- package/src/core/discovery.ts +52 -4
- package/src/core/index.ts +14 -4
- package/src/core/loader.ts +3 -0
- package/src/core/node.ts +3 -1
- package/src/core/parser.ts +1 -1
- package/src/core/schema.ts +7 -2
- package/src/core/token-parser.ts +211 -0
- package/src/core/types.ts +46 -6
- package/src/mcp/server.ts +321 -39
- package/dist/chunk-5CKYLCJH.js.map +0 -1
- package/dist/chunk-AW7MWOUH.js.map +0 -1
- package/dist/chunk-G3M3MPQ6.js.map +0 -1
- package/dist/chunk-ZFKGX3QK.js.map +0 -1
- package/dist/scan-YVYD64GD.js +0 -12
- /package/dist/{chunk-NOTYONHY.js.map → chunk-2DJH4F4P.js.map} +0 -0
- /package/dist/{chunk-5ZYEOHYK.js.map → chunk-IOJE35DZ.js.map} +0 -0
- /package/dist/{chunk-J4SI5RIH.js.map → chunk-XNWDI6UT.js.map} +0 -0
- /package/dist/{core-LNXDLXDP.js.map → core-NJVKKLJ4.js.map} +0 -0
- /package/dist/{generate-OIXXHOWR.js.map → generate-OVGMDKCJ.js.map} +0 -0
- /package/dist/{init-EVPXIDW4.js.map → init-EOA7TTOR.js.map} +0 -0
- /package/dist/{scan-YVYD64GD.js.map → scan-YN4LUDKY.js.map} +0 -0
- /package/dist/{service-K52ORLCJ.js.map → service-2T26CBWE.js.map} +0 -0
- /package/dist/{static-viewer-JNQIHA4B.js.map → static-viewer-CLJJRYHK.js.map} +0 -0
- /package/dist/{test-USARUEFW.js.map → test-ECPEXFDN.js.map} +0 -0
- /package/dist/{tokens-C6YHBOQE.js.map → tokens-FHA2DO22.js.map} +0 -0
- /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
|
-
|
|
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.
|
|
324
|
-
description: `Search and retrieve composition
|
|
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
|
|
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
|
|
336
|
+
description: 'Free-text search across block names, descriptions, tags, and components',
|
|
335
337
|
},
|
|
336
338
|
component: {
|
|
337
339
|
type: 'string',
|
|
338
|
-
description: 'Filter
|
|
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
|
-
|
|
477
|
-
|
|
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
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
859
|
+
noMatch,
|
|
860
|
+
weakMatch,
|
|
861
|
+
recommendation,
|
|
791
862
|
compositionHint,
|
|
792
|
-
nextStep
|
|
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
|
-
//
|
|
1103
|
+
// BLOCKS — composition patterns
|
|
1035
1104
|
// ================================================================
|
|
1036
|
-
case TOOL_NAMES.
|
|
1105
|
+
case TOOL_NAMES.blocks: {
|
|
1037
1106
|
const data = await loadSegments();
|
|
1038
|
-
const
|
|
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
|
|
1112
|
+
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
1043
1113
|
|
|
1044
|
-
if (
|
|
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
|
-
|
|
1051
|
-
hint: `No
|
|
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 =
|
|
1127
|
+
let filtered = allBlocks;
|
|
1058
1128
|
|
|
1059
|
-
if (
|
|
1129
|
+
if (blockName) {
|
|
1060
1130
|
filtered = filtered.filter(
|
|
1061
|
-
|
|
1131
|
+
b => b.name.toLowerCase() === blockName.toLowerCase()
|
|
1062
1132
|
);
|
|
1063
1133
|
}
|
|
1064
1134
|
|
|
1065
1135
|
if (search) {
|
|
1066
|
-
filtered = filtered.filter(
|
|
1136
|
+
filtered = filtered.filter(b => {
|
|
1067
1137
|
const haystack = [
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
...(
|
|
1071
|
-
...
|
|
1072
|
-
|
|
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(
|
|
1080
|
-
|
|
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
|
-
|
|
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
|
};
|