seedflip-mcp 0.1.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 +79 -0
- package/dist/exporters.d.ts +11 -0
- package/dist/exporters.js +238 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +122 -0
- package/dist/search.d.ts +45 -0
- package/dist/search.js +187 -0
- package/dist/seeds-data.json +3562 -0
- package/package.json +44 -0
package/dist/search.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeedFlip MCP — Search Engine
|
|
3
|
+
*
|
|
4
|
+
* Scores seeds against a natural language query.
|
|
5
|
+
* Handles brand references ("Stripe"), vibes ("dark minimal"),
|
|
6
|
+
* and style descriptors ("brutalist", "warm editorial").
|
|
7
|
+
*/
|
|
8
|
+
// ── Brand reference map ──────────────────────────────────────────
|
|
9
|
+
// Maps common queries to the brand names that appear in aiPromptRules.
|
|
10
|
+
// An agent saying "make it look like Stripe" triggers a search for "Stripe"
|
|
11
|
+
// in the seed's AI prompts.
|
|
12
|
+
const BRAND_ALIASES = {
|
|
13
|
+
stripe: ['stripe', 'fintech'],
|
|
14
|
+
vercel: ['vercel'],
|
|
15
|
+
linear: ['linear purple', 'linear\'s'],
|
|
16
|
+
github: ['github'],
|
|
17
|
+
notion: ['notion'],
|
|
18
|
+
supabase: ['supabase'],
|
|
19
|
+
spotify: ['spotify'],
|
|
20
|
+
framer: ['framer'],
|
|
21
|
+
resend: ['resend'],
|
|
22
|
+
superhuman: ['superhuman'],
|
|
23
|
+
raycast: ['raycast'],
|
|
24
|
+
arc: ['arc browser'],
|
|
25
|
+
railway: ['railway'],
|
|
26
|
+
tailwind: ['tailwind css', 'tailwind'],
|
|
27
|
+
atlassian: ['atlassian', 'jira', 'confluence'],
|
|
28
|
+
phantom: ['phantom wallet'],
|
|
29
|
+
};
|
|
30
|
+
// Some brand names collide with CSS terms (e.g. "linear" in "linear-gradient").
|
|
31
|
+
// For these, we use phrase-level matching instead of single-word boundaries.
|
|
32
|
+
// The aliases above are already configured to avoid false positives.
|
|
33
|
+
// ── Style synonyms ───────────────────────────────────────────────
|
|
34
|
+
// Maps natural language to tag values that exist on seeds.
|
|
35
|
+
const STYLE_SYNONYMS = {
|
|
36
|
+
minimal: ['minimal', 'clean', 'precise'],
|
|
37
|
+
minimalist: ['minimal', 'clean', 'precise'],
|
|
38
|
+
clean: ['clean', 'minimal'],
|
|
39
|
+
bold: ['bold', 'brutalist'],
|
|
40
|
+
brutalist: ['brutalist', 'bold'],
|
|
41
|
+
warm: ['warm', 'elegant'],
|
|
42
|
+
elegant: ['elegant', 'warm'],
|
|
43
|
+
editorial: ['editorial'],
|
|
44
|
+
neon: ['neon', 'cyberpunk', 'vibrant'],
|
|
45
|
+
cyberpunk: ['cyberpunk', 'neon'],
|
|
46
|
+
retro: ['retro', 'vintage', 'nostalgic'],
|
|
47
|
+
playful: ['playful', 'vibrant', 'rounded'],
|
|
48
|
+
professional: ['professional', 'spacious', 'clean'],
|
|
49
|
+
luxury: ['luxury', 'elegant', 'premium'],
|
|
50
|
+
premium: ['premium', 'elegant', 'luxury'],
|
|
51
|
+
developer: ['developer', 'precise', 'mono'],
|
|
52
|
+
dev: ['developer', 'precise', 'mono'],
|
|
53
|
+
modern: ['modern', 'clean', 'cool'],
|
|
54
|
+
geometric: ['geometric', 'bold'],
|
|
55
|
+
organic: ['organic', 'natural', 'warm'],
|
|
56
|
+
nature: ['organic', 'natural', 'warm'],
|
|
57
|
+
industrial: ['industrial', 'bold'],
|
|
58
|
+
};
|
|
59
|
+
// ── Luminance helper ─────────────────────────────────────────────
|
|
60
|
+
function isDark(bg) {
|
|
61
|
+
const hex = bg.replace('#', '');
|
|
62
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
63
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
64
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
65
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5;
|
|
66
|
+
}
|
|
67
|
+
// ── Tokenizer ────────────────────────────────────────────────────
|
|
68
|
+
function tokenize(query) {
|
|
69
|
+
return query
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9\s#]/g, ' ')
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
export function searchSeeds(seeds, query) {
|
|
76
|
+
const tokens = tokenize(query);
|
|
77
|
+
if (tokens.length === 0) {
|
|
78
|
+
// No query — return a random seed
|
|
79
|
+
const idx = Math.floor(Math.random() * seeds.length);
|
|
80
|
+
return [{ seed: seeds[idx], score: 1, matchReasons: ['random'] }];
|
|
81
|
+
}
|
|
82
|
+
// Detect dark/light preference from query
|
|
83
|
+
const wantsDark = tokens.includes('dark');
|
|
84
|
+
const wantsLight = tokens.includes('light');
|
|
85
|
+
const scored = seeds.map((seed) => {
|
|
86
|
+
let score = 0;
|
|
87
|
+
const reasons = [];
|
|
88
|
+
const allPrompts = [
|
|
89
|
+
seed.aiPromptRules,
|
|
90
|
+
seed.aiPromptColors,
|
|
91
|
+
seed.aiPromptTypography,
|
|
92
|
+
seed.aiPromptShape,
|
|
93
|
+
seed.aiPromptDepth,
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ')
|
|
97
|
+
.toLowerCase();
|
|
98
|
+
const seedIsDark = isDark(seed.bg);
|
|
99
|
+
// 1. Exact name match (highest signal — agent asked for a specific seed)
|
|
100
|
+
const nameLower = seed.name.toLowerCase();
|
|
101
|
+
for (const token of tokens) {
|
|
102
|
+
if (nameLower === token || nameLower.includes(token)) {
|
|
103
|
+
score += 25;
|
|
104
|
+
reasons.push(`name: "${seed.name}"`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 2. Brand reference in AI prompts (word boundary match)
|
|
108
|
+
for (const token of tokens) {
|
|
109
|
+
const brandAliases = BRAND_ALIASES[token];
|
|
110
|
+
if (brandAliases) {
|
|
111
|
+
for (const alias of brandAliases) {
|
|
112
|
+
// Use word boundary regex to avoid "stripes" matching "stripe"
|
|
113
|
+
const pattern = new RegExp(`\\b${alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
114
|
+
if (pattern.test(allPrompts)) {
|
|
115
|
+
score += 20;
|
|
116
|
+
reasons.push(`brand: "${token}"`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 3. Tag matches
|
|
123
|
+
const seedTags = seed.tags.map((t) => t.toLowerCase());
|
|
124
|
+
for (const token of tokens) {
|
|
125
|
+
if (seedTags.includes(token)) {
|
|
126
|
+
score += 10;
|
|
127
|
+
reasons.push(`tag: "${token}"`);
|
|
128
|
+
}
|
|
129
|
+
// Also check style synonyms
|
|
130
|
+
const synonyms = STYLE_SYNONYMS[token];
|
|
131
|
+
if (synonyms) {
|
|
132
|
+
for (const syn of synonyms) {
|
|
133
|
+
if (seedTags.includes(syn)) {
|
|
134
|
+
score += 6;
|
|
135
|
+
reasons.push(`style: "${token}" → "${syn}"`);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 4. Dark/light preference
|
|
142
|
+
if (wantsDark && seedIsDark) {
|
|
143
|
+
score += 8;
|
|
144
|
+
reasons.push('dark mode');
|
|
145
|
+
}
|
|
146
|
+
else if (wantsLight && !seedIsDark) {
|
|
147
|
+
score += 8;
|
|
148
|
+
reasons.push('light mode');
|
|
149
|
+
}
|
|
150
|
+
else if (wantsDark && !seedIsDark) {
|
|
151
|
+
score -= 15; // Strong penalty for wrong mode
|
|
152
|
+
}
|
|
153
|
+
else if (wantsLight && seedIsDark) {
|
|
154
|
+
score -= 15;
|
|
155
|
+
}
|
|
156
|
+
// 5. Vibe text match
|
|
157
|
+
const vibeLower = seed.vibe.toLowerCase();
|
|
158
|
+
for (const token of tokens) {
|
|
159
|
+
if (vibeLower.includes(token)) {
|
|
160
|
+
score += 5;
|
|
161
|
+
reasons.push(`vibe: "${seed.vibe}"`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 6. Font match
|
|
165
|
+
const fontsLower = `${seed.headingFont} ${seed.bodyFont}`.toLowerCase();
|
|
166
|
+
for (const token of tokens) {
|
|
167
|
+
if (fontsLower.includes(token)) {
|
|
168
|
+
score += 5;
|
|
169
|
+
reasons.push(`font: "${token}"`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 7. General keyword in AI prompts (weak signal but catches everything)
|
|
173
|
+
for (const token of tokens) {
|
|
174
|
+
if (token.length >= 4 &&
|
|
175
|
+
!BRAND_ALIASES[token] &&
|
|
176
|
+
!STYLE_SYNONYMS[token] &&
|
|
177
|
+
token !== 'dark' &&
|
|
178
|
+
token !== 'light' &&
|
|
179
|
+
allPrompts.includes(token)) {
|
|
180
|
+
score += 3;
|
|
181
|
+
reasons.push(`keyword: "${token}"`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { seed, score, matchReasons: [...new Set(reasons)] };
|
|
185
|
+
});
|
|
186
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score);
|
|
187
|
+
}
|