reicon-mcp 1.0.0 → 1.0.1
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/cli/index.js +1 -1
- package/core/codegen.js +12 -2
- package/core/search.js +132 -9
- package/core/types.d.ts +1 -1
- package/package.json +1 -1
- package/server/index.js +60 -16
- package/server/tools/apply-icon.d.ts +15 -1
- package/server/tools/apply-icon.js +26 -2
- package/server/tools/view-icon.d.ts +20 -3
- package/server/tools/view-icon.js +18 -4
package/cli/index.js
CHANGED
|
@@ -94,7 +94,7 @@ export async function run(argv) {
|
|
|
94
94
|
case 'apply': {
|
|
95
95
|
const name = positional[1];
|
|
96
96
|
if (!name) {
|
|
97
|
-
console.error('Usage: reicon-mcp apply <name> --framework react|vue|svelte|html|svg');
|
|
97
|
+
console.error('Usage: reicon-mcp apply <name> --framework react|react-native|vue|svelte|html|svg [--weight Outline|Filled] [--size 24] [--color #hex] [--file path] [--marker "{/* ICON */}"]');
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
100
|
const framework = flags.framework || 'react';
|
package/core/codegen.js
CHANGED
|
@@ -10,7 +10,7 @@ function sizeProp(size, framework) {
|
|
|
10
10
|
return ` :size="${size}"`;
|
|
11
11
|
if (framework === 'html')
|
|
12
12
|
return ` size="${size}"`;
|
|
13
|
-
if (framework === 'svelte' || framework === 'react')
|
|
13
|
+
if (framework === 'svelte' || framework === 'react' || framework === 'react-native')
|
|
14
14
|
return ` size={${size}}`;
|
|
15
15
|
return ` width="${size}" height="${size}"`;
|
|
16
16
|
}
|
|
@@ -19,7 +19,7 @@ function colorProp(color, framework) {
|
|
|
19
19
|
return ` color="${color}"`;
|
|
20
20
|
if (framework === 'html')
|
|
21
21
|
return ` color="${color}"`;
|
|
22
|
-
if (framework === 'svelte' || framework === 'react') {
|
|
22
|
+
if (framework === 'svelte' || framework === 'react' || framework === 'react-native') {
|
|
23
23
|
const val = color === 'currentColor' ? 'currentColor' : `"${color}"`;
|
|
24
24
|
return ` color={${val}}`;
|
|
25
25
|
}
|
|
@@ -47,6 +47,16 @@ export function generateCode(icon, input) {
|
|
|
47
47
|
const usageSnippet = `<${component}${props} />`;
|
|
48
48
|
return { importStatement, usageSnippet };
|
|
49
49
|
}
|
|
50
|
+
case 'react-native': {
|
|
51
|
+
const importStatement = `import { ${component} } from 'reicon-react-native';`;
|
|
52
|
+
const props = [
|
|
53
|
+
sizeProp(size, 'react-native'),
|
|
54
|
+
colorAttr('react-native'),
|
|
55
|
+
weightProp(weight, 'react-native'),
|
|
56
|
+
].join('');
|
|
57
|
+
const usageSnippet = `<${component}${props} />`;
|
|
58
|
+
return { importStatement, usageSnippet };
|
|
59
|
+
}
|
|
50
60
|
case 'vue': {
|
|
51
61
|
const importStatement = `import { ${component} } from 'reicon-vue';`;
|
|
52
62
|
const props = [
|
package/core/search.js
CHANGED
|
@@ -1,17 +1,99 @@
|
|
|
1
|
+
// Only flag clearly conversational strings — short "add home icon" should be fine
|
|
1
2
|
const SENTENCE_MARKERS = /\?|!|\.{2,}/;
|
|
2
|
-
const SENTENCE_WORDS = /\b(please|could|would|want|need|show|find|give|get
|
|
3
|
+
const SENTENCE_WORDS = /\b(please|could|would|want|need|show|find|give|get me|looking for|i am|i'm|can you|help me|what is)\b/i;
|
|
3
4
|
const SYNONYMS = {
|
|
4
5
|
close: ['x'],
|
|
6
|
+
dismiss: ['x', 'close'],
|
|
7
|
+
cancel: ['x', 'close'],
|
|
5
8
|
cart: ['cart', 'cart-large', 'shopping-cart2'],
|
|
6
9
|
'shopping cart': ['cart', 'cart3', 'shopping-cart2', 'cart-large'],
|
|
7
10
|
delete: ['trash', 'trash-bin-trash'],
|
|
8
11
|
remove: ['trash', 'x'],
|
|
9
|
-
|
|
12
|
+
bin: ['trash', 'trash-bin-trash'],
|
|
13
|
+
settings: ['settings', 'settings-minimalistic', 'cog'],
|
|
14
|
+
gear: ['settings', 'settings-minimalistic'],
|
|
15
|
+
cog: ['settings'],
|
|
10
16
|
arrow: ['arrow-right', 'arrow-down', 'arrow-up', 'arrow-left'],
|
|
11
17
|
search: ['search', 'minimalistic-magnifer'],
|
|
18
|
+
magnifier: ['search', 'minimalistic-magnifer'],
|
|
12
19
|
heart: ['heart', 'heart-angle'],
|
|
20
|
+
love: ['heart', 'heart-angle'],
|
|
21
|
+
favorite: ['heart', 'star'],
|
|
13
22
|
trash: ['trash', 'trash-bin-trash'],
|
|
14
23
|
user: ['user', 'user-circle'],
|
|
24
|
+
person: ['user', 'user-circle'],
|
|
25
|
+
profile: ['user', 'user-circle'],
|
|
26
|
+
avatar: ['user', 'user-circle'],
|
|
27
|
+
home: ['home', 'home-2'],
|
|
28
|
+
house: ['home', 'home-2'],
|
|
29
|
+
menu: ['hamburger-menu', 'menu'],
|
|
30
|
+
hamburger: ['hamburger-menu'],
|
|
31
|
+
check: ['check', 'check-circle', 'check-read'],
|
|
32
|
+
tick: ['check', 'check-circle'],
|
|
33
|
+
notification: ['bell', 'bell-bing'],
|
|
34
|
+
bell: ['bell', 'bell-bing'],
|
|
35
|
+
alert: ['bell', 'shield-warning'],
|
|
36
|
+
warning: ['shield-warning'],
|
|
37
|
+
info: ['info-circle'],
|
|
38
|
+
information: ['info-circle'],
|
|
39
|
+
edit: ['pen', 'pen-2', 'edit'],
|
|
40
|
+
pencil: ['pen', 'pen-2'],
|
|
41
|
+
pen: ['pen', 'pen-2'],
|
|
42
|
+
send: ['send-2', 'send'],
|
|
43
|
+
share: ['share', 'share-2'],
|
|
44
|
+
download: ['download', 'download-minimalistic'],
|
|
45
|
+
upload: ['upload', 'upload-minimalistic'],
|
|
46
|
+
link: ['link', 'link-2'],
|
|
47
|
+
copy: ['copy', 'clipboard'],
|
|
48
|
+
clipboard: ['clipboard', 'copy'],
|
|
49
|
+
globe: ['globe', 'map'],
|
|
50
|
+
world: ['globe'],
|
|
51
|
+
lock: ['lock', 'lock-key-minimalistic'],
|
|
52
|
+
unlock: ['lock-opened', 'lock-opened-2'],
|
|
53
|
+
star: ['star', 'stars'],
|
|
54
|
+
eye: ['eye', 'eye-closed'],
|
|
55
|
+
visibility: ['eye', 'eye-closed'],
|
|
56
|
+
phone: ['phone', 'phone-calling-rounded'],
|
|
57
|
+
call: ['phone', 'phone-calling-rounded'],
|
|
58
|
+
calendar: ['calendar', 'calendar-date'],
|
|
59
|
+
date: ['calendar'],
|
|
60
|
+
time: ['clock', 'stopwatch'],
|
|
61
|
+
clock: ['clock'],
|
|
62
|
+
camera: ['camera'],
|
|
63
|
+
photo: ['camera', 'gallery'],
|
|
64
|
+
image: ['gallery', 'camera'],
|
|
65
|
+
message: ['chat-round', 'letter'],
|
|
66
|
+
chat: ['chat-round', 'chat-square'],
|
|
67
|
+
email: ['letter', 'letter-opened'],
|
|
68
|
+
mail: ['letter', 'letter-opened'],
|
|
69
|
+
add: ['add', 'add-circle'],
|
|
70
|
+
plus: ['add', 'add-circle'],
|
|
71
|
+
minus: ['subtract'],
|
|
72
|
+
subtract: ['subtract'],
|
|
73
|
+
refresh: ['refresh', 'restart'],
|
|
74
|
+
reload: ['refresh', 'restart'],
|
|
75
|
+
filter: ['filter'],
|
|
76
|
+
sort: ['sort', 'alt-arrow-down'],
|
|
77
|
+
grid: ['widget', 'four-circles'],
|
|
78
|
+
list: ['list'],
|
|
79
|
+
tag: ['tag'],
|
|
80
|
+
folder: ['folder', 'folder-open'],
|
|
81
|
+
file: ['file', 'file-text'],
|
|
82
|
+
document: ['file-text', 'document'],
|
|
83
|
+
chart: ['chart', 'pie-chart', 'chart-2'],
|
|
84
|
+
graph: ['chart', 'chart-2'],
|
|
85
|
+
play: ['play'],
|
|
86
|
+
pause: ['pause'],
|
|
87
|
+
stop: ['stop-circle'],
|
|
88
|
+
volume: ['volume'],
|
|
89
|
+
music: ['music-note', 'music-notes'],
|
|
90
|
+
map: ['map', 'map-point'],
|
|
91
|
+
location: ['map-point', 'target'],
|
|
92
|
+
pin: ['map-point'],
|
|
93
|
+
power: ['power'],
|
|
94
|
+
wifi: ['wifi'],
|
|
95
|
+
bluetooth: ['bluetooth'],
|
|
96
|
+
battery: ['battery'],
|
|
15
97
|
};
|
|
16
98
|
function normalize(text) {
|
|
17
99
|
return text.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
@@ -26,9 +108,11 @@ export function isSentenceQuery(query) {
|
|
|
26
108
|
if (SENTENCE_MARKERS.test(trimmed))
|
|
27
109
|
return true;
|
|
28
110
|
const words = tokenize(trimmed);
|
|
29
|
-
|
|
111
|
+
// Only flag > 7 words as a sentence (was 6, which was too aggressive)
|
|
112
|
+
if (words.length > 7)
|
|
30
113
|
return true;
|
|
31
|
-
if
|
|
114
|
+
// Only flag if sentence words appear AND query is 5+ words
|
|
115
|
+
if (words.length >= 5 && SENTENCE_WORDS.test(trimmed))
|
|
32
116
|
return true;
|
|
33
117
|
if (/^(how|what|where|which|why|when)\b/i.test(trimmed))
|
|
34
118
|
return true;
|
|
@@ -44,37 +128,70 @@ function synonymBoost(query, name) {
|
|
|
44
128
|
return 0;
|
|
45
129
|
return 3000 - idx * 100;
|
|
46
130
|
}
|
|
131
|
+
// Fuzzy partial match — how many chars of `token` appear sequentially in `str`
|
|
132
|
+
function fuzzyScore(str, token) {
|
|
133
|
+
let ti = 0;
|
|
134
|
+
let hits = 0;
|
|
135
|
+
for (const c of str) {
|
|
136
|
+
if (ti < token.length && c === token[ti]) {
|
|
137
|
+
ti++;
|
|
138
|
+
hits++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return ti === token.length ? Math.round((hits / str.length) * 100) : 0;
|
|
142
|
+
}
|
|
47
143
|
function scoreIcon(icon, query, tokens, weight) {
|
|
48
144
|
const q = normalize(query);
|
|
49
145
|
const name = icon.name;
|
|
50
146
|
const nameNorm = name.replace(/-/g, ' ');
|
|
147
|
+
const catNorm = icon.category.replace(/-/g, ' ').toLowerCase();
|
|
51
148
|
const tagsNorm = icon.tags.map((t) => normalize(t));
|
|
52
149
|
let score = 0;
|
|
150
|
+
// Synonym boost
|
|
53
151
|
score += synonymBoost(query, name);
|
|
152
|
+
// Exact name match
|
|
54
153
|
if (name === q || name === q.replace(/ /g, '-')) {
|
|
55
154
|
score += 10000;
|
|
56
155
|
}
|
|
156
|
+
// Single token exact match
|
|
57
157
|
if (tokens.length === 1 && name === tokens[0]) {
|
|
58
158
|
score += 5000;
|
|
59
159
|
}
|
|
160
|
+
// All tokens appear in name
|
|
60
161
|
if (tokens.every((t) => name.includes(t) || nameNorm.includes(t))) {
|
|
61
162
|
score += 2000;
|
|
62
163
|
}
|
|
164
|
+
// Full tag exact match
|
|
63
165
|
if (tagsNorm.some((t) => t === q)) {
|
|
64
166
|
score += 1500;
|
|
65
167
|
}
|
|
66
168
|
for (const token of tokens) {
|
|
169
|
+
// Exact tag match
|
|
67
170
|
if (tagsNorm.some((t) => t === token))
|
|
68
171
|
score += 800;
|
|
172
|
+
// Name includes token
|
|
69
173
|
if (name.includes(token))
|
|
70
174
|
score += 600;
|
|
175
|
+
// Name starts with token (prefix boost)
|
|
176
|
+
if (name.startsWith(token))
|
|
177
|
+
score += 400;
|
|
178
|
+
// Tag includes token
|
|
71
179
|
if (tagsNorm.some((t) => t.includes(token)))
|
|
72
180
|
score += 300;
|
|
181
|
+
// Category match
|
|
182
|
+
if (catNorm.includes(token))
|
|
183
|
+
score += 150;
|
|
184
|
+
// Fuzzy fallback — only if no direct match
|
|
185
|
+
const directMatch = name.includes(token) || tagsNorm.some((t) => t.includes(token));
|
|
186
|
+
if (!directMatch) {
|
|
187
|
+
const fuzz = fuzzyScore(name, token);
|
|
188
|
+
if (fuzz > 70)
|
|
189
|
+
score += Math.round(fuzz * 0.8);
|
|
190
|
+
}
|
|
73
191
|
}
|
|
74
|
-
|
|
75
|
-
score += 400;
|
|
76
|
-
}
|
|
192
|
+
// Prefer shorter names (more specific)
|
|
77
193
|
score += Math.max(0, 50 - name.length);
|
|
194
|
+
// Weight filter
|
|
78
195
|
if (weight) {
|
|
79
196
|
if (icon.weights[weight])
|
|
80
197
|
score += 10;
|
|
@@ -89,7 +206,8 @@ export function searchIcons(index, query, options = {}) {
|
|
|
89
206
|
error: 'Query looks like a full sentence. Use concise keywords instead, such as "cart", "user", or "settings".',
|
|
90
207
|
};
|
|
91
208
|
}
|
|
92
|
-
|
|
209
|
+
// Default limit raised from 5 → 8 for better agent context
|
|
210
|
+
const limit = options.limit ?? 8;
|
|
93
211
|
const tokens = tokenize(query);
|
|
94
212
|
const weights = options.weight ? [options.weight] : ['Outline', 'Filled'];
|
|
95
213
|
const results = [];
|
|
@@ -121,9 +239,14 @@ export function searchIcons(index, query, options = {}) {
|
|
|
121
239
|
if (deduped.length >= limit)
|
|
122
240
|
break;
|
|
123
241
|
}
|
|
242
|
+
if (deduped.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
error: `No icons found for "${query}". Try a shorter or different keyword.`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
124
247
|
return {
|
|
125
248
|
results: deduped,
|
|
126
|
-
instruction: 'Pick exactly one result from the list above
|
|
249
|
+
instruction: 'Pick exactly one result from the list above. Prefer the highest score. Then call view_icon to confirm the SVG, or apply_icon to generate ready-to-use code. Do not ask the user to choose.',
|
|
127
250
|
};
|
|
128
251
|
}
|
|
129
252
|
export function findIcon(index, name) {
|
package/core/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type IconWeight = 'Outline' | 'Filled';
|
|
2
|
-
export type Framework = 'react' | 'vue' | 'svelte' | 'html' | 'svg';
|
|
2
|
+
export type Framework = 'react' | 'react-native' | 'vue' | 'svelte' | 'html' | 'svg';
|
|
3
3
|
export interface IconWeightData {
|
|
4
4
|
code: string;
|
|
5
5
|
viewBox: string;
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -7,41 +7,85 @@ import { handleSearchIcons } from './tools/search-icons.js';
|
|
|
7
7
|
import { handleViewIcon } from './tools/view-icon.js';
|
|
8
8
|
const server = new McpServer({
|
|
9
9
|
name: 'reicon-mcp',
|
|
10
|
-
version: '1.0.
|
|
10
|
+
version: '1.0.1',
|
|
11
11
|
});
|
|
12
|
-
server.tool('search_icons',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
server.tool('search_icons', [
|
|
13
|
+
'Search 2,700+ Reicon icons by keyword.',
|
|
14
|
+
'Returns ranked matches. Use short, specific terms — not full sentences.',
|
|
15
|
+
'Good queries: "cart", "user circle", "arrow down", "settings".',
|
|
16
|
+
'Bad queries: "please find me a heart icon" or "I need a shopping cart".',
|
|
17
|
+
'After getting results, pick the highest-scoring match and call apply_icon directly.',
|
|
18
|
+
'Only call view_icon first if you need to inspect the raw SVG before generating code.',
|
|
19
|
+
].join(' '), {
|
|
20
|
+
query: z.string().describe('Concise keyword(s) — e.g. "cart", "bell", "user circle". Not full sentences.'),
|
|
21
|
+
weight: z
|
|
22
|
+
.enum(['Outline', 'Filled'])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Filter by weight. Omit to search both.'),
|
|
25
|
+
limit: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(20)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Max results to return. Defaults to 8.'),
|
|
16
32
|
}, async (args) => {
|
|
17
33
|
const result = handleSearchIcons(args);
|
|
18
34
|
return {
|
|
19
35
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
20
36
|
};
|
|
21
37
|
});
|
|
22
|
-
server.tool('view_icon',
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
server.tool('view_icon', [
|
|
39
|
+
'Fetch raw SVG markup and metadata for a specific Reicon icon.',
|
|
40
|
+
'Use this to inspect the SVG before applying, or when you only need the raw markup.',
|
|
41
|
+
'For generating import/usage code, prefer apply_icon instead.',
|
|
42
|
+
].join(' '), {
|
|
43
|
+
name: z.string().describe('Icon kebab-case name, e.g. "heart", "arrow-right", "user-circle"'),
|
|
44
|
+
weight: z.enum(['Outline', 'Filled']).describe('Which weight variant to fetch'),
|
|
25
45
|
}, async (args) => {
|
|
26
46
|
const result = handleViewIcon(args);
|
|
27
47
|
return {
|
|
28
48
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
29
49
|
};
|
|
30
50
|
});
|
|
31
|
-
server.tool('apply_icon',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
server.tool('apply_icon', [
|
|
52
|
+
'Generate ready-to-paste import and usage code for a Reicon icon.',
|
|
53
|
+
'Returns importStatement and usageSnippet for the chosen framework.',
|
|
54
|
+
'Supported frameworks: react, react-native, vue, svelte, html (CDN custom element), svg (raw markup).',
|
|
55
|
+
'Insert importStatement at the top of the file and usageSnippet where the icon should appear.',
|
|
56
|
+
'Always call search_icons first to confirm the icon name and weight exist.',
|
|
57
|
+
].join(' '), {
|
|
58
|
+
name: z.string().describe('Icon kebab-case name, e.g. "heart"'),
|
|
59
|
+
weight: z.enum(['Outline', 'Filled']).describe('Icon weight variant'),
|
|
60
|
+
framework: z
|
|
61
|
+
.enum(['react', 'react-native', 'vue', 'svelte', 'html', 'svg'])
|
|
62
|
+
.describe('Target framework or output format'),
|
|
63
|
+
size: z
|
|
64
|
+
.number()
|
|
65
|
+
.int()
|
|
66
|
+
.min(8)
|
|
67
|
+
.max(256)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Icon size in pixels. Defaults to 24.'),
|
|
70
|
+
color: z
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('CSS color value, e.g. "#ef4444" or "currentColor". Defaults to currentColor.'),
|
|
74
|
+
componentName: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('Override the component name. Defaults to the PascalCase icon name.'),
|
|
38
78
|
}, async (args) => {
|
|
39
79
|
const result = handleApplyIcon(args);
|
|
40
80
|
return {
|
|
41
81
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
42
82
|
};
|
|
43
83
|
});
|
|
44
|
-
server.tool('list_categories',
|
|
84
|
+
server.tool('list_categories', [
|
|
85
|
+
'List all icon categories available in the Reicon dataset.',
|
|
86
|
+
'Use this to explore what types of icons exist before searching.',
|
|
87
|
+
'Each category can be used as a search keyword with search_icons.',
|
|
88
|
+
].join(' '), {}, async () => {
|
|
45
89
|
const result = handleListCategories();
|
|
46
90
|
return {
|
|
47
91
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { ApplyIconInput } from '../../core/types.js';
|
|
2
|
-
export declare function handleApplyIcon(args: ApplyIconInput):
|
|
2
|
+
export declare function handleApplyIcon(args: ApplyIconInput): {
|
|
3
3
|
error: string;
|
|
4
|
+
} | {
|
|
5
|
+
error: string;
|
|
6
|
+
suggestion: string;
|
|
7
|
+
} | {
|
|
8
|
+
meta: {
|
|
9
|
+
name: string;
|
|
10
|
+
weight: import("../../core/types.js").IconWeight;
|
|
11
|
+
framework: import("../../core/types.js").Framework;
|
|
12
|
+
size: number;
|
|
13
|
+
};
|
|
14
|
+
importStatement: string;
|
|
15
|
+
usageSnippet: string;
|
|
16
|
+
error?: undefined;
|
|
17
|
+
suggestion?: undefined;
|
|
4
18
|
};
|
|
@@ -5,7 +5,31 @@ export function handleApplyIcon(args) {
|
|
|
5
5
|
const index = loadIndex();
|
|
6
6
|
const icon = findIcon(index, args.name);
|
|
7
7
|
if (!icon) {
|
|
8
|
-
|
|
8
|
+
// Soft-match: try normalized kebab-case
|
|
9
|
+
const lower = args.name.toLowerCase().replace(/\s+/g, '-');
|
|
10
|
+
const match = index.icons.find((i) => i.name.toLowerCase() === lower ||
|
|
11
|
+
i.pascal.toLowerCase() === lower.replace(/-/g, ''));
|
|
12
|
+
if (match) {
|
|
13
|
+
return {
|
|
14
|
+
error: `Icon "${args.name}" not found. Did you mean "${match.name}"? Call apply_icon again with name="${match.name}".`,
|
|
15
|
+
suggestion: match.name,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
error: `Icon "${args.name}" not found. Use search_icons to find the correct name.`,
|
|
20
|
+
};
|
|
9
21
|
}
|
|
10
|
-
|
|
22
|
+
const result = generateCode(icon, args);
|
|
23
|
+
if ('error' in result)
|
|
24
|
+
return result;
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
// Echo back key info so the agent can confirm what was applied
|
|
28
|
+
meta: {
|
|
29
|
+
name: icon.name,
|
|
30
|
+
weight: args.weight,
|
|
31
|
+
framework: args.framework,
|
|
32
|
+
size: args.size ?? 24,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
11
35
|
}
|
|
@@ -5,8 +5,25 @@ export declare function handleViewIcon(args: {
|
|
|
5
5
|
}): {
|
|
6
6
|
error: string;
|
|
7
7
|
} | {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
error: string;
|
|
9
|
+
suggestion: string;
|
|
10
|
+
name?: undefined;
|
|
11
|
+
weight?: undefined;
|
|
12
|
+
availableWeights?: undefined;
|
|
13
|
+
category?: undefined;
|
|
14
|
+
tags?: undefined;
|
|
15
|
+
viewBox?: undefined;
|
|
16
|
+
svg?: undefined;
|
|
17
|
+
hint?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
name: string;
|
|
20
|
+
weight: IconWeight;
|
|
21
|
+
availableWeights: IconWeight[];
|
|
11
22
|
category: string;
|
|
23
|
+
tags: string[];
|
|
24
|
+
viewBox: string;
|
|
25
|
+
svg: string;
|
|
26
|
+
hint: string;
|
|
27
|
+
error?: undefined;
|
|
28
|
+
suggestion?: undefined;
|
|
12
29
|
};
|
|
@@ -5,17 +5,31 @@ export function handleViewIcon(args) {
|
|
|
5
5
|
const index = loadIndex();
|
|
6
6
|
const icon = findIcon(index, args.name);
|
|
7
7
|
if (!icon) {
|
|
8
|
-
|
|
8
|
+
// Try a soft match in case casing or dashes differ
|
|
9
|
+
const lower = args.name.toLowerCase().replace(/\s+/g, '-');
|
|
10
|
+
const match = index.icons.find((i) => i.name.toLowerCase() === lower || i.pascal.toLowerCase() === lower.replace(/-/g, ''));
|
|
11
|
+
if (match) {
|
|
12
|
+
return {
|
|
13
|
+
error: `Icon "${args.name}" not found. Did you mean "${match.name}"?`,
|
|
14
|
+
suggestion: match.name,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { error: `Icon "${args.name}" not found. Use search_icons to find the correct name.` };
|
|
9
18
|
}
|
|
10
19
|
const svg = buildSvgMarkup(icon, args.weight);
|
|
11
20
|
if (typeof svg !== 'string') {
|
|
12
21
|
return svg;
|
|
13
22
|
}
|
|
14
23
|
const weightData = icon.weights[args.weight];
|
|
24
|
+
const availableWeights = Object.keys(icon.weights);
|
|
15
25
|
return {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
name: icon.name,
|
|
27
|
+
weight: args.weight,
|
|
28
|
+
availableWeights,
|
|
19
29
|
category: icon.category,
|
|
30
|
+
tags: icon.tags,
|
|
31
|
+
viewBox: weightData.viewBox,
|
|
32
|
+
svg,
|
|
33
|
+
hint: 'Call apply_icon with this name and weight to generate framework-specific code.',
|
|
20
34
|
};
|
|
21
35
|
}
|