vibetachyon 1.7.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 +368 -1
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -31,12 +31,44 @@ const supabase = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey, supab
|
|
|
31
31
|
const LOCAL_CACHE_DIR = path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', '.vibetachyon', 'cache');
|
|
32
32
|
const CALL_LIMIT_PER_SESSION = 150; // Sanity Trap to prevent agent infinite loops
|
|
33
33
|
let sessionCallCount = 0;
|
|
34
|
+
// --- Session Stats Tracker ---
|
|
35
|
+
const SESSION_START = Date.now();
|
|
36
|
+
const sessionStats = {
|
|
37
|
+
componentsLoaded: 0,
|
|
38
|
+
tsErrorsFixed: 0,
|
|
39
|
+
depsInstalled: 0,
|
|
40
|
+
missionsStarted: 0,
|
|
41
|
+
missionsCompleted: 0,
|
|
42
|
+
sectionsBuilt: 0,
|
|
43
|
+
};
|
|
34
44
|
async function checkSanity() {
|
|
35
45
|
sessionCallCount++;
|
|
36
46
|
if (sessionCallCount > CALL_LIMIT_PER_SESSION) {
|
|
37
47
|
throw new Error(`[VIBETACHYON SANITY TRAP] Execution blocked. Agent exceeded ${CALL_LIMIT_PER_SESSION} tool calls in a single session. This prevents infinite loops or excessive API usage. Please restart the CLI if this was intentional.`);
|
|
38
48
|
}
|
|
39
49
|
}
|
|
50
|
+
// --- Changelog (per version) ---
|
|
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
|
+
],
|
|
57
|
+
'1.8.0': [
|
|
58
|
+
'vibe_session_recap — resumo completo da sessão',
|
|
59
|
+
'Update check automático no startup com changelog',
|
|
60
|
+
],
|
|
61
|
+
'1.7.0': [
|
|
62
|
+
'vibe_launchpad — MISSION BRIEFING automático antes de qualquer código',
|
|
63
|
+
'Mandatory workflow reforçado nas descrições de tools',
|
|
64
|
+
'Detecção automática de stack, cores e brief existente',
|
|
65
|
+
],
|
|
66
|
+
'1.6.0': [
|
|
67
|
+
'Animmaster Engine com 42 componentes premium',
|
|
68
|
+
'Missions, Tasks, Squads e Personas',
|
|
69
|
+
'Visual QA, Self-heal TypeScript, Ephemeral Previews',
|
|
70
|
+
],
|
|
71
|
+
};
|
|
40
72
|
async function getLocalCache(key) {
|
|
41
73
|
const cachePath = path_1.default.join(LOCAL_CACHE_DIR, `${key}.json`);
|
|
42
74
|
if (await fs_extra_1.default.pathExists(cachePath)) {
|
|
@@ -169,12 +201,38 @@ async function validateTokenAtStartup() {
|
|
|
169
201
|
// Network error — allow graceful degradation (offline mode)
|
|
170
202
|
console.error('[VibeTachyon] Aviso: Não foi possível verificar o token (modo offline). Continuando...');
|
|
171
203
|
}
|
|
204
|
+
// --- Update Check ---
|
|
205
|
+
try {
|
|
206
|
+
const CURRENT_VERSION = '1.9.0'; // keep in sync with package.json
|
|
207
|
+
const npmRes = await fetch('https://registry.npmjs.org/vibetachyon/latest', {
|
|
208
|
+
signal: AbortSignal.timeout(4000)
|
|
209
|
+
});
|
|
210
|
+
const npmData = await npmRes.json();
|
|
211
|
+
const latest = npmData.version;
|
|
212
|
+
if (latest && latest !== CURRENT_VERSION) {
|
|
213
|
+
const sep = '═'.repeat(46);
|
|
214
|
+
const changes = CHANGELOG[latest] || ['Melhorias e correções'];
|
|
215
|
+
const lines = [
|
|
216
|
+
`╔${sep}╗`,
|
|
217
|
+
`║ 🆕 VibeTachyon ${latest} disponível!${' '.repeat(Math.max(0, 46 - 18 - latest.length - 1))}║`,
|
|
218
|
+
`╠${sep}╣`,
|
|
219
|
+
...changes.map(c => `║ ✦ ${c.slice(0, 42).padEnd(42)} ║`),
|
|
220
|
+
`╠${sep}╣`,
|
|
221
|
+
`║ Execute: npm i -g vibetachyon@latest${' '.repeat(8)}║`,
|
|
222
|
+
`╚${sep}╝`,
|
|
223
|
+
];
|
|
224
|
+
console.error(lines.join('\n'));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Silently ignore update check failures
|
|
229
|
+
}
|
|
172
230
|
}
|
|
173
231
|
async function startMcpServer() {
|
|
174
232
|
await validateTokenAtStartup();
|
|
175
233
|
const server = new mcp_js_1.McpServer({
|
|
176
234
|
name: "VibeTachyon MCP — Animmaster Engine",
|
|
177
|
-
version: "1.
|
|
235
|
+
version: "1.9.0"
|
|
178
236
|
});
|
|
179
237
|
/**
|
|
180
238
|
* VIBETACHYON FRONTEND PERSONA — Senior Frontend Designer (Animmaster Engine)
|
|
@@ -548,6 +606,7 @@ async function startMcpServer() {
|
|
|
548
606
|
name: zod_1.z.string().describe("Animmaster component name (e.g. 'parallax', 'cursor', 'marquee', 'splittype', 'preloader', 'ripple')")
|
|
549
607
|
}, async ({ name }) => {
|
|
550
608
|
await checkSanity();
|
|
609
|
+
sessionStats.componentsLoaded++;
|
|
551
610
|
try {
|
|
552
611
|
const { data, error } = await supabase
|
|
553
612
|
.from('animmaster_components')
|
|
@@ -1133,6 +1192,221 @@ COMPONENT SELECTION FILTER (apply mentally before using any component):
|
|
|
1133
1192
|
}
|
|
1134
1193
|
});
|
|
1135
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
|
+
});
|
|
1136
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.", {
|
|
1137
1411
|
code: zod_1.z.string().describe("The TSX/JSX code snippet to audit and fix."),
|
|
1138
1412
|
cssFilePath: zod_1.z.string().describe("Absolute path to the user's globals.css.")
|
|
@@ -2068,6 +2342,7 @@ export default function VibePreviewPage() {
|
|
|
2068
2342
|
projectDir: zod_1.z.string().optional().describe("Absolute path to the project root")
|
|
2069
2343
|
}, async ({ title, goal, tasks = [], projectDir }) => {
|
|
2070
2344
|
await checkSanity();
|
|
2345
|
+
sessionStats.missionsStarted++;
|
|
2071
2346
|
const cwd = projectDir || process.cwd();
|
|
2072
2347
|
await ensureVtfDir(cwd, 'missions', 'tasks');
|
|
2073
2348
|
const missionId = `mission_${shortId()}`;
|
|
@@ -2164,6 +2439,7 @@ export default function VibePreviewPage() {
|
|
|
2164
2439
|
projectDir: zod_1.z.string().optional().describe("Absolute path to the project root")
|
|
2165
2440
|
}, async ({ missionId, summary, decisions = [], lessons = [], nextSteps = [], projectDir }) => {
|
|
2166
2441
|
await checkSanity();
|
|
2442
|
+
sessionStats.missionsCompleted++;
|
|
2167
2443
|
const cwd = projectDir || process.cwd();
|
|
2168
2444
|
await ensureVtfDir(cwd, 'missions', 'debriefs');
|
|
2169
2445
|
const missionsDir = vtfPath(cwd, 'missions');
|
|
@@ -4386,6 +4662,7 @@ ${consequences.length > 0 ? consequences.map(c => `- ${c}`).join('\n') : '- (non
|
|
|
4386
4662
|
section.completedAt = new Date().toISOString();
|
|
4387
4663
|
await fs_extra_1.default.writeJson(statePath, state, { spaces: 2 });
|
|
4388
4664
|
}
|
|
4665
|
+
sessionStats.sectionsBuilt++;
|
|
4389
4666
|
const done = state.sections?.filter((s) => s.status === 'done') || [];
|
|
4390
4667
|
const planned = state.sections?.filter((s) => s.status === 'planned') || [];
|
|
4391
4668
|
const next = planned[0];
|
|
@@ -4402,6 +4679,96 @@ ${consequences.length > 0 ? consequences.map(c => `- ${c}`).join('\n') : '- (non
|
|
|
4402
4679
|
}]
|
|
4403
4680
|
};
|
|
4404
4681
|
});
|
|
4682
|
+
// ── Tool: Session Recap ───────────────────────────────────────────────────
|
|
4683
|
+
server.tool("vibe_session_recap", "Show a complete summary of everything accomplished in this session — duration, components loaded, errors fixed, sections built, missions completed. Call this at the end of any work session to review progress.", {
|
|
4684
|
+
projectDir: zod_1.z.string().optional().describe("Absolute path to the project root. If omitted, uses current working directory.")
|
|
4685
|
+
}, async ({ projectDir }) => {
|
|
4686
|
+
await checkSanity();
|
|
4687
|
+
const cwd = projectDir || process.cwd();
|
|
4688
|
+
const root = await findProjectRoot(cwd);
|
|
4689
|
+
// Duration
|
|
4690
|
+
const elapsedMs = Date.now() - SESSION_START;
|
|
4691
|
+
const minutes = Math.floor(elapsedMs / 60000);
|
|
4692
|
+
const seconds = Math.floor((elapsedMs % 60000) / 1000);
|
|
4693
|
+
const duration = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
4694
|
+
// Count git-changed files
|
|
4695
|
+
let filesChanged = 0;
|
|
4696
|
+
try {
|
|
4697
|
+
const { stdout } = await execPromise('git status --porcelain 2>/dev/null | wc -l', { cwd: root });
|
|
4698
|
+
filesChanged = parseInt(stdout.trim()) || 0;
|
|
4699
|
+
}
|
|
4700
|
+
catch { /* not a git repo */ }
|
|
4701
|
+
// Count tasks completed in .vibetachyon
|
|
4702
|
+
let tasksCompleted = 0;
|
|
4703
|
+
let missionsActive = 0;
|
|
4704
|
+
const tasksDir = path_1.default.join(root, '.vibetachyon', 'tasks');
|
|
4705
|
+
const missionsDir = path_1.default.join(root, '.vibetachyon', 'missions');
|
|
4706
|
+
try {
|
|
4707
|
+
if (await fs_extra_1.default.pathExists(tasksDir)) {
|
|
4708
|
+
const taskFiles = await fs_extra_1.default.readdir(tasksDir);
|
|
4709
|
+
for (const f of taskFiles) {
|
|
4710
|
+
if (!f.endsWith('.json'))
|
|
4711
|
+
continue;
|
|
4712
|
+
const t = await fs_extra_1.default.readJson(path_1.default.join(tasksDir, f));
|
|
4713
|
+
if (t.status === 'done')
|
|
4714
|
+
tasksCompleted++;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
if (await fs_extra_1.default.pathExists(missionsDir)) {
|
|
4718
|
+
const missionFiles = await fs_extra_1.default.readdir(missionsDir);
|
|
4719
|
+
for (const f of missionFiles) {
|
|
4720
|
+
if (!f.endsWith('.json'))
|
|
4721
|
+
continue;
|
|
4722
|
+
const m = await fs_extra_1.default.readJson(path_1.default.join(missionsDir, f));
|
|
4723
|
+
if (m.status === 'active')
|
|
4724
|
+
missionsActive++;
|
|
4725
|
+
if (m.status === 'completed')
|
|
4726
|
+
sessionStats.missionsCompleted++;
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
catch { /* ignore */ }
|
|
4731
|
+
const sep = '═'.repeat(48);
|
|
4732
|
+
const row = (label, value) => {
|
|
4733
|
+
const line = ` ${label}: ${value}`;
|
|
4734
|
+
return `║${line.padEnd(49)}║`;
|
|
4735
|
+
};
|
|
4736
|
+
// Score — simple quality heuristic
|
|
4737
|
+
const score = Math.min(100, 20 + // base
|
|
4738
|
+
(sessionStats.componentsLoaded > 0 ? 20 : 0) +
|
|
4739
|
+
(sessionStats.sectionsBuilt > 0 ? 20 : 0) +
|
|
4740
|
+
(tasksCompleted > 0 ? 20 : 0) +
|
|
4741
|
+
(sessionStats.tsErrorsFixed > 0 ? 10 : 0) +
|
|
4742
|
+
(filesChanged > 0 ? 10 : 0));
|
|
4743
|
+
const grade = score >= 90 ? 'S' : score >= 75 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D';
|
|
4744
|
+
const gradeEmoji = grade === 'S' ? '🏆' : grade === 'A' ? '🥇' : grade === 'B' ? '🥈' : '🥉';
|
|
4745
|
+
const lines = [
|
|
4746
|
+
`╔${sep}╗`,
|
|
4747
|
+
`║ ${gradeEmoji} VIBETACHYON SESSION RECAP${' '.repeat(20)}║`,
|
|
4748
|
+
`╠${sep}╣`,
|
|
4749
|
+
row('⏱ Duração', duration),
|
|
4750
|
+
row('📄 Arquivos alterados', filesChanged),
|
|
4751
|
+
row('🧩 Componentes carregados', sessionStats.componentsLoaded),
|
|
4752
|
+
row('🏗️ Seções construídas', sessionStats.sectionsBuilt),
|
|
4753
|
+
row('✅ Tasks concluídas', tasksCompleted),
|
|
4754
|
+
row('🎯 Missões ativas', missionsActive),
|
|
4755
|
+
row('🔧 Erros TS corrigidos', sessionStats.tsErrorsFixed),
|
|
4756
|
+
row('📦 Deps instaladas', sessionStats.depsInstalled),
|
|
4757
|
+
row('🔁 Tools chamados', sessionCallCount),
|
|
4758
|
+
`╠${sep}╣`,
|
|
4759
|
+
`║ NOTA DA SESSÃO: ${grade} (${score}/100)${' '.repeat(Math.max(0, 48 - 20 - grade.length - String(score).length))}║`,
|
|
4760
|
+
`╚${sep}╝`,
|
|
4761
|
+
];
|
|
4762
|
+
if (score < 60) {
|
|
4763
|
+
lines.push('');
|
|
4764
|
+
lines.push('💡 Dica: use vibe_launchpad antes de começar e vibe_section_done após cada seção.');
|
|
4765
|
+
}
|
|
4766
|
+
else if (score >= 90) {
|
|
4767
|
+
lines.push('');
|
|
4768
|
+
lines.push('🔥 Sessão excepcional. Considera fazer commit com vibe_git_smart_commit.');
|
|
4769
|
+
}
|
|
4770
|
+
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
4771
|
+
});
|
|
4405
4772
|
// Connect via stdio
|
|
4406
4773
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
4407
4774
|
await server.connect(transport);
|