vibetachyon 1.8.0 → 1.9.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/dist/mcp-server.js +222 -3
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -49,10 +49,14 @@ async function checkSanity() {
|
|
|
49
49
|
}
|
|
50
50
|
// --- Changelog (per version) ---
|
|
51
51
|
const CHANGELOG = {
|
|
52
|
+
'1.9.0': [
|
|
53
|
+
'vibe_clone_design — clone design system de qualquer URL',
|
|
54
|
+
'Detecta cores, tipografia, radius e sombras automaticamente',
|
|
55
|
+
'Gera globals.css pronto para usar no projeto',
|
|
56
|
+
],
|
|
52
57
|
'1.8.0': [
|
|
53
58
|
'vibe_session_recap — resumo completo da sessão',
|
|
54
59
|
'Update check automático no startup com changelog',
|
|
55
|
-
'vibe_clone_design — clone de design system por URL',
|
|
56
60
|
],
|
|
57
61
|
'1.7.0': [
|
|
58
62
|
'vibe_launchpad — MISSION BRIEFING automático antes de qualquer código',
|
|
@@ -199,7 +203,7 @@ async function validateTokenAtStartup() {
|
|
|
199
203
|
}
|
|
200
204
|
// --- Update Check ---
|
|
201
205
|
try {
|
|
202
|
-
const CURRENT_VERSION = '1.
|
|
206
|
+
const CURRENT_VERSION = '1.9.0'; // keep in sync with package.json
|
|
203
207
|
const npmRes = await fetch('https://registry.npmjs.org/vibetachyon/latest', {
|
|
204
208
|
signal: AbortSignal.timeout(4000)
|
|
205
209
|
});
|
|
@@ -228,7 +232,7 @@ async function startMcpServer() {
|
|
|
228
232
|
await validateTokenAtStartup();
|
|
229
233
|
const server = new mcp_js_1.McpServer({
|
|
230
234
|
name: "VibeTachyon MCP — Animmaster Engine",
|
|
231
|
-
version: "1.
|
|
235
|
+
version: "1.9.0"
|
|
232
236
|
});
|
|
233
237
|
/**
|
|
234
238
|
* VIBETACHYON FRONTEND PERSONA — Senior Frontend Designer (Animmaster Engine)
|
|
@@ -1188,6 +1192,221 @@ COMPONENT SELECTION FILTER (apply mentally before using any component):
|
|
|
1188
1192
|
}
|
|
1189
1193
|
});
|
|
1190
1194
|
// Tool: Enforce Vibe Style (Consistency Guard)
|
|
1195
|
+
// ── Tool: Clone Design ───────────────────────────────────────────────────
|
|
1196
|
+
server.tool("vibe_clone_design", "🎨 Give a URL (e.g. linear.app, stripe.com, vercel.com) and get its complete design system cloned: colors, typography, border radius, spacing, shadows — all mapped to standard CSS variables and saved as globals.css. Use when the user says 'I want my site to look like X'.", {
|
|
1197
|
+
url: zod_1.z.string().describe("Full URL of the site to clone the design system from (e.g. 'https://linear.app')"),
|
|
1198
|
+
outputPath: zod_1.z.string().optional().describe("Absolute path where globals.css should be saved. If omitted, returns the CSS as text only."),
|
|
1199
|
+
mode: zod_1.z.enum(['full', 'colors-only', 'typography-only']).optional().describe("What to extract. Default: 'full'")
|
|
1200
|
+
}, async ({ url, outputPath, mode = 'full' }) => {
|
|
1201
|
+
await checkSanity();
|
|
1202
|
+
const lines = [];
|
|
1203
|
+
const sep = '═'.repeat(50);
|
|
1204
|
+
lines.push(`╔${sep}╗`);
|
|
1205
|
+
lines.push(`║ 🎨 DESIGN CLONE: ${new URL(url).hostname.padEnd(31)}║`);
|
|
1206
|
+
lines.push(`╠${sep}╣`);
|
|
1207
|
+
try {
|
|
1208
|
+
// Fetch HTML
|
|
1209
|
+
const htmlRes = await fetch(url, {
|
|
1210
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; VibeTachyon/1.0)' },
|
|
1211
|
+
signal: AbortSignal.timeout(10000)
|
|
1212
|
+
});
|
|
1213
|
+
const html = await htmlRes.text();
|
|
1214
|
+
const baseUrl = new URL(url).origin;
|
|
1215
|
+
// Collect CSS sources: <style> tags + <link rel="stylesheet">
|
|
1216
|
+
let allCss = '';
|
|
1217
|
+
// Inline styles
|
|
1218
|
+
const styleMatches = html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
|
|
1219
|
+
for (const m of styleMatches)
|
|
1220
|
+
allCss += m[1] + '\n';
|
|
1221
|
+
// External stylesheets (fetch first 3 to stay fast)
|
|
1222
|
+
const linkMatches = [...html.matchAll(/<link[^>]+rel=["']stylesheet["'][^>]*href=["']([^"']+)["']/gi)];
|
|
1223
|
+
const cssUrls = linkMatches.map(m => {
|
|
1224
|
+
const href = m[1];
|
|
1225
|
+
if (href.startsWith('http'))
|
|
1226
|
+
return href;
|
|
1227
|
+
if (href.startsWith('//'))
|
|
1228
|
+
return 'https:' + href;
|
|
1229
|
+
return baseUrl + (href.startsWith('/') ? href : '/' + href);
|
|
1230
|
+
}).slice(0, 4);
|
|
1231
|
+
await Promise.allSettled(cssUrls.map(async (cssUrl) => {
|
|
1232
|
+
try {
|
|
1233
|
+
const r = await fetch(cssUrl, { signal: AbortSignal.timeout(5000) });
|
|
1234
|
+
allCss += await r.text() + '\n';
|
|
1235
|
+
}
|
|
1236
|
+
catch { /* skip failed CSS */ }
|
|
1237
|
+
}));
|
|
1238
|
+
// ── Extract design tokens ──────────────────────────────────
|
|
1239
|
+
// 1. CSS Custom Properties from :root
|
|
1240
|
+
const rootVars = {};
|
|
1241
|
+
const rootBlocks = allCss.matchAll(/:root\s*\{([^}]+)\}/g);
|
|
1242
|
+
for (const block of rootBlocks) {
|
|
1243
|
+
const varMatches = block[1].matchAll(/--([\w-]+)\s*:\s*([^;]+);/g);
|
|
1244
|
+
for (const v of varMatches)
|
|
1245
|
+
rootVars[v[1]] = v[2].trim();
|
|
1246
|
+
}
|
|
1247
|
+
// 2. Font families
|
|
1248
|
+
const fontMatches = allCss.matchAll(/font-family\s*:\s*([^;}{]+)/g);
|
|
1249
|
+
const fonts = [];
|
|
1250
|
+
for (const m of fontMatches) {
|
|
1251
|
+
const f = m[1].trim().split(',')[0].replace(/['"]/g, '').trim();
|
|
1252
|
+
if (f && !fonts.includes(f) && f !== 'inherit' && f !== 'sans-serif')
|
|
1253
|
+
fonts.push(f);
|
|
1254
|
+
}
|
|
1255
|
+
// Also check Google Fonts links
|
|
1256
|
+
const gFontMatch = html.match(/fonts\.googleapis\.com[^"']*family=([^&"']+)/);
|
|
1257
|
+
if (gFontMatch) {
|
|
1258
|
+
const gFont = decodeURIComponent(gFontMatch[1]).split(':')[0].replace(/\+/g, ' ');
|
|
1259
|
+
if (!fonts.includes(gFont))
|
|
1260
|
+
fonts.unshift(gFont);
|
|
1261
|
+
}
|
|
1262
|
+
// 3. Border radius (from buttons/cards)
|
|
1263
|
+
const radiusMatches = allCss.matchAll(/border-radius\s*:\s*([\d.]+(?:px|rem|em))/g);
|
|
1264
|
+
const radii = [];
|
|
1265
|
+
for (const m of radiusMatches) {
|
|
1266
|
+
const val = parseFloat(m[1]);
|
|
1267
|
+
if (val > 0 && val <= 32)
|
|
1268
|
+
radii.push(val);
|
|
1269
|
+
}
|
|
1270
|
+
const medianRadius = radii.length > 0
|
|
1271
|
+
? radii.sort((a, b) => a - b)[Math.floor(radii.length / 2)]
|
|
1272
|
+
: 6;
|
|
1273
|
+
const radiusUnit = allCss.includes('rem') ? 'rem' : 'px';
|
|
1274
|
+
const radiusVal = radiusUnit === 'rem' ? `${(medianRadius / 16).toFixed(3)}rem` : `${medianRadius}px`;
|
|
1275
|
+
// 4. Colors — find dominant non-white/black hex colors
|
|
1276
|
+
const hexColors = [...new Set([...allCss.matchAll(/#([0-9a-fA-F]{6})\b/g)].map(m => '#' + m[1].toLowerCase()))];
|
|
1277
|
+
const isNeutral = (hex) => {
|
|
1278
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
1279
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
1280
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
1281
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
1282
|
+
return (max - min) < 30; // low saturation = neutral
|
|
1283
|
+
};
|
|
1284
|
+
const brandColors = hexColors.filter(h => !isNeutral(h)).slice(0, 5);
|
|
1285
|
+
const darkColors = hexColors.filter(h => {
|
|
1286
|
+
const r = parseInt(h.slice(1, 3), 16);
|
|
1287
|
+
return r < 30 && isNeutral(h);
|
|
1288
|
+
});
|
|
1289
|
+
const lightColors = hexColors.filter(h => {
|
|
1290
|
+
const r = parseInt(h.slice(1, 3), 16);
|
|
1291
|
+
return r > 225 && isNeutral(h);
|
|
1292
|
+
});
|
|
1293
|
+
// 5. Detect dark/light theme from background
|
|
1294
|
+
const bgColorMatch = allCss.match(/body\s*\{[^}]*background(?:-color)?\s*:\s*(#[0-9a-fA-F]{6}|[a-z]+)/);
|
|
1295
|
+
const bgHex = bgColorMatch?.[1] || (darkColors[0] ?? '#ffffff');
|
|
1296
|
+
const isDark = bgHex.startsWith('#') ? parseInt(bgHex.slice(1, 3), 16) < 100 : bgHex === 'black';
|
|
1297
|
+
// 6. Detect shadow style
|
|
1298
|
+
const hasShadow = allCss.includes('box-shadow');
|
|
1299
|
+
const shadowVal = hasShadow
|
|
1300
|
+
? (isDark ? '0 1px 3px rgba(0,0,0,0.4)' : '0 1px 3px rgba(0,0,0,0.1)')
|
|
1301
|
+
: 'none';
|
|
1302
|
+
// ── Map to standard tokens ─────────────────────────────────
|
|
1303
|
+
const primary = brandColors[0] || (isDark ? '#6366f1' : '#3b82f6');
|
|
1304
|
+
const primaryFg = '#ffffff';
|
|
1305
|
+
const background = isDark ? (darkColors[0] || '#0a0a0a') : (lightColors[0] || '#ffffff');
|
|
1306
|
+
const foreground = isDark ? '#f4f4f5' : '#09090b';
|
|
1307
|
+
const muted = isDark ? '#27272a' : '#f4f4f5';
|
|
1308
|
+
const mutedFg = isDark ? '#a1a1aa' : '#71717a';
|
|
1309
|
+
const border = isDark ? '#3f3f46' : '#e4e4e7';
|
|
1310
|
+
const card = isDark ? (darkColors[1] || '#18181b') : '#ffffff';
|
|
1311
|
+
const primaryFont = fonts[0] || (isDark ? 'Inter' : 'Inter');
|
|
1312
|
+
const monoFont = fonts.find(f => f.toLowerCase().includes('mono') || f.toLowerCase().includes('code')) || 'JetBrains Mono';
|
|
1313
|
+
// ── Detect visual style for Animmaster brief ──────────────
|
|
1314
|
+
const hasGradients = allCss.includes('linear-gradient') || allCss.includes('radial-gradient');
|
|
1315
|
+
const hasBlur = allCss.includes('backdrop-filter') || allCss.includes('blur(');
|
|
1316
|
+
const visualTone = isDark && hasBlur ? 'glassmorphism' :
|
|
1317
|
+
isDark && hasGradients ? 'dark-gradient' :
|
|
1318
|
+
isDark ? 'dark-minimal' :
|
|
1319
|
+
hasGradients ? 'light-gradient' : 'light-minimal';
|
|
1320
|
+
// ── Use existing root vars if abundant ────────────────────
|
|
1321
|
+
const hasRichVars = Object.keys(rootVars).length > 8;
|
|
1322
|
+
// ── Generate globals.css ───────────────────────────────────
|
|
1323
|
+
const cssOutput = [
|
|
1324
|
+
`/* ─────────────────────────────────────────────────────`,
|
|
1325
|
+
` * Design System cloned from: ${new URL(url).hostname}`,
|
|
1326
|
+
` * Generated by VibeTachyon vibe_clone_design`,
|
|
1327
|
+
` * Visual tone: ${visualTone}`,
|
|
1328
|
+
` * ──────────────────────────────────────────────────── */`,
|
|
1329
|
+
'',
|
|
1330
|
+
hasRichVars
|
|
1331
|
+
? `/* Original CSS variables preserved from source */\n:root {\n${Object.entries(rootVars).slice(0, 30).map(([k, v]) => ` --${k}: ${v};`).join('\n')}\n}`
|
|
1332
|
+
: '',
|
|
1333
|
+
'',
|
|
1334
|
+
`/* VibeTachyon Standard Tokens */`,
|
|
1335
|
+
`:root {`,
|
|
1336
|
+
` /* Colors */`,
|
|
1337
|
+
` --background: ${background};`,
|
|
1338
|
+
` --foreground: ${foreground};`,
|
|
1339
|
+
` --card: ${card};`,
|
|
1340
|
+
` --card-fg: ${foreground};`,
|
|
1341
|
+
` --primary: ${primary};`,
|
|
1342
|
+
` --primary-fg: ${primaryFg};`,
|
|
1343
|
+
` --muted: ${muted};`,
|
|
1344
|
+
` --muted-fg: ${mutedFg};`,
|
|
1345
|
+
` --border: ${border};`,
|
|
1346
|
+
` --input: ${border};`,
|
|
1347
|
+
` --ring: ${primary};`,
|
|
1348
|
+
...(brandColors.slice(1).map((c, i) => ` --accent-${i + 1}: ${c};`)),
|
|
1349
|
+
'',
|
|
1350
|
+
` /* Typography */`,
|
|
1351
|
+
` --font-sans: '${primaryFont}', system-ui, sans-serif;`,
|
|
1352
|
+
` --font-mono: '${monoFont}', monospace;`,
|
|
1353
|
+
'',
|
|
1354
|
+
` /* Shape */`,
|
|
1355
|
+
` --radius: ${radiusVal};`,
|
|
1356
|
+
` --radius-sm: calc(var(--radius) * 0.5);`,
|
|
1357
|
+
` --radius-lg: calc(var(--radius) * 1.5);`,
|
|
1358
|
+
'',
|
|
1359
|
+
` /* Shadows */`,
|
|
1360
|
+
` --shadow-sm: ${shadowVal};`,
|
|
1361
|
+
` --shadow: ${shadowVal.replace('3px', '8px').replace('0.1', '0.12').replace('0.4', '0.5')};`,
|
|
1362
|
+
`}`,
|
|
1363
|
+
'',
|
|
1364
|
+
isDark ? [
|
|
1365
|
+
`.dark {`,
|
|
1366
|
+
` color-scheme: dark;`,
|
|
1367
|
+
`}`,
|
|
1368
|
+
].join('\n') : '',
|
|
1369
|
+
].filter(Boolean).join('\n');
|
|
1370
|
+
// Save to file if requested
|
|
1371
|
+
if (outputPath) {
|
|
1372
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
|
|
1373
|
+
await fs_extra_1.default.writeFile(outputPath, cssOutput, 'utf-8');
|
|
1374
|
+
}
|
|
1375
|
+
// Build briefing output
|
|
1376
|
+
lines.push(` Theme : ${isDark ? '🌑 Dark' : '☀️ Light'} (${visualTone})`);
|
|
1377
|
+
lines.push(` Primary : ${primary}`);
|
|
1378
|
+
lines.push(` BG : ${background}`);
|
|
1379
|
+
lines.push(` FG : ${foreground}`);
|
|
1380
|
+
lines.push(` Border : ${border}`);
|
|
1381
|
+
lines.push(` Radius : ${radiusVal}`);
|
|
1382
|
+
lines.push(` Font : ${primaryFont}`);
|
|
1383
|
+
if (fonts[1])
|
|
1384
|
+
lines.push(` Font 2 : ${fonts[1]}`);
|
|
1385
|
+
if (brandColors.length > 1)
|
|
1386
|
+
lines.push(` Accents : ${brandColors.slice(1, 4).join(', ')}`);
|
|
1387
|
+
lines.push(` Vars src : ${hasRichVars ? `✅ ${Object.keys(rootVars).length} CSS vars found` : '⚙️ Generated from computed styles'}`);
|
|
1388
|
+
lines.push(`╠${sep}╣`);
|
|
1389
|
+
lines.push(` Animmaster tone: ${visualTone}`);
|
|
1390
|
+
lines.push(` Use in brief : visualTone: "${visualTone}"`);
|
|
1391
|
+
lines.push(`╠${sep}╣`);
|
|
1392
|
+
if (outputPath) {
|
|
1393
|
+
lines.push(` ✅ globals.css saved to:`);
|
|
1394
|
+
lines.push(` ${outputPath}`);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
lines.push(` 💡 Pass outputPath to save globals.css automatically`);
|
|
1398
|
+
}
|
|
1399
|
+
lines.push(`╚${sep}╝`);
|
|
1400
|
+
const fullOutput = lines.join('\n') + '\n\n' + '```css\n' + cssOutput + '\n```';
|
|
1401
|
+
return { content: [{ type: "text", text: fullOutput }] };
|
|
1402
|
+
}
|
|
1403
|
+
catch (err) {
|
|
1404
|
+
lines.push(` ❌ Erro: ${err.message}`);
|
|
1405
|
+
lines.push(` Verifique se a URL está acessível e tente novamente.`);
|
|
1406
|
+
lines.push(`╚${sep}╝`);
|
|
1407
|
+
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1191
1410
|
server.tool("vibe_enforce_style", "Passive audit tool that scan a code snippet and automatically patches 'off-theme' Tailwind classes (like bg-blue-500) with project-specific design tokens (--primary, --background) extracted from the CSS.", {
|
|
1192
1411
|
code: zod_1.z.string().describe("The TSX/JSX code snippet to audit and fix."),
|
|
1193
1412
|
cssFilePath: zod_1.z.string().describe("Absolute path to the user's globals.css.")
|