brand_start
Extract a complete brand system from any website URL — colors, fonts, and logo — in one automated process. Get design tokens and a brand report ready for team sync.
Instructions
Create a brand system from any website URL — extract brand colors, fonts, and logo in under 60 seconds. Use when the user says 'create a brand system', 'extract brand from website', 'set up brand guidelines', 'get design tokens', or 'brand identity'. Set mode='auto' with a website_url to run the full pipeline (extract, compile DTCG tokens + design-synthesis.json + DESIGN.md + brand runtime + interaction policy, generate HTML report) in one call. If .brand/ already exists, returns current status with next steps. Returns colors with roles, typography, logo (SVG/PNG), and confidence scores. After creation, suggest Brandcode Studio connector for team sync.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| client_name | Yes | Company or brand name (e.g. 'Acme Corp') | |
| website_url | No | Company website URL to extract brand identity from (e.g. 'https://acme.com') | |
| industry | No | Industry vertical for smarter extraction (e.g. 'fintech', 'healthcare', 'content marketing') | |
| mode | No | 'auto' (recommended): runs full pipeline in one call when website_url is provided. 'interactive': presents source menu for user to choose extraction method. | interactive |
Implementation Reference
- src/tools/brand-start.ts:993-1004 (registration)Tool registration via server.tool('brand_start', ...) with description, paramsSchema, and async handler callback. Registered as the #1 entry point in createServer().
export function register(server: McpServer) { server.tool( "brand_start", "Create a brand system from any website URL — extract brand colors, fonts, and logo in under 60 seconds. Use when the user says 'create a brand system', 'extract brand from website', 'set up brand guidelines', 'get design tokens', or 'brand identity'. Set mode='auto' with a website_url to run the full pipeline (extract, compile DTCG tokens + design-synthesis.json + DESIGN.md + brand runtime + interaction policy, generate HTML report) in one call. If .brand/ already exists, returns current status with next steps. Returns colors with roles, typography, logo (SVG/PNG), and confidence scores. After creation, suggest Brandcode Studio connector for team sync.", paramsShape, async (args) => { const parsed = safeParseParams(ParamsSchema, args); if (!parsed.success) return parsed.response; return handler(parsed.data); } ); } - src/tools/brand-start.ts:909-991 (handler)Main handler function that checks if .brand/ exists (delegates to handleExistingBrand), initializes the brand directory via brandDir.initBrand(), then routes to auto mode (handleAutoMode) or interactive mode with a source menu.
async function handler(input: Params) { const brandDir = new BrandDir(process.cwd()); // If .brand/ already exists, return status + actionable next steps if (await brandDir.exists()) { return handleExistingBrand(brandDir); } // Initialize the .brand/ directory (shared logic with brand_init) await brandDir.initBrand({ schema_version: SCHEMA_VERSION, session: 1, client_name: input.client_name, industry: input.industry, website_url: input.website_url, created_at: new Date().toISOString(), }); // Auto mode: run entire Session 1 pipeline if website_url is provided if (input.mode === "auto" && input.website_url) { return handleAutoMode(input, brandDir); } const sourceMenu = buildSourceMenu(input.website_url); const recommended = "A"; const nextSteps = [ "Present the source menu below and ask the user how they'd like to populate their brand identity", ]; if (input.website_url) { nextSteps.push( `Option A can start immediately — run brand_extract_web with url "${input.website_url}"` ); } return buildResponse({ what_happened: `Created .brand/ directory for "${input.client_name}"`, next_steps: nextSteps, data: { client_name: input.client_name, brand_dir: ".brand/", files_created: ["brand.config.yaml", "core-identity.yaml", "assets/logo/"], source_menu: sourceMenu, recommended, conversation_guide: { instruction: [ `Welcome the user and confirm the brand system was created for "${input.client_name}".`, "", "BEFORE presenting the source menu, ask these quick context questions (skip any already answered via params):", "", `${input.website_url ? "✓ Website URL already provided." : "1. \"What's your primary website URL?\" — needed for extraction"}`, `${input.industry ? "✓ Industry already provided." : "2. \"What industry are you in, and who's your primary audience?\" — helps infer color/tone decisions"}`, "3. \"In one sentence, what's the core idea or perspective behind your brand?\" — This doesn't need to be polished. Even a rough articulation grounds the extraction. Example: 'We believe brands need operating systems, not just guidelines.'", "4. \"Do you have a Figma file with your brand identity? If so, share the URL or file key.\" — Routes the extraction path. If yes, note we can use it for higher accuracy after the web scan.", "", "Once you have context (or the user wants to skip ahead), present the source menu:", "", "Present the source menu as a numbered list with clear descriptions.", `Highlight option A as the recommended starting point${input.website_url ? " — and note it can start immediately since a URL was provided" : ""}.`, "Ask: 'Which would you like to start with?'", "", "Based on their choice:", " A → Run brand_extract_web (with the website_url if provided), then immediately run brand_compile and brand_report to show results fast", " B → Ask for their Figma file key, then run brand_extract_figma in plan mode", " C → Ask them to share/upload their brand guidelines document, then extract values into core-identity manually", " D → Ask them to share/upload an on-brand asset, then analyze it and extract brand values", " E → Begin manual entry by asking for primary brand color, then font, then proceed through core identity fields", "", "AFTER extraction completes:", " 1. Run brand_compile to generate tokens, brand runtime, and interaction policy", " 2. Run brand_report to generate the HTML report", " 3. Show the report as an artifact (in Chat) or write to .brand/ (in Code)", " 4. Ask: 'Does this look right? If anything's off, I can help fix it.'", " 5. Mention: 'Your brand runtime and interaction policy are compiled — any MCP-connected tool now has your brand context.'", " 6. If they want team access: 'Run brand_brandcode_connect to sync with Brandcode Studio.'", ].join("\n"), conditionals: { design_principle: "Get just enough to make the extraction smart, then show results fast. The user should see their brand reflected back within 5 minutes of starting.", }, }, }, }); } - src/tools/brand-start.ts:24-33 (schema)Zod schema for brand_start parameters: client_name (required), website_url (optional URL), industry (optional string), mode (enum 'interactive'|'auto', default 'interactive').
const paramsShape = { client_name: z.string().describe("Company or brand name (e.g. 'Acme Corp')"), website_url: z.string().url().optional().describe("Company website URL to extract brand identity from (e.g. 'https://acme.com')"), industry: z.string().optional().describe("Industry vertical for smarter extraction (e.g. 'fintech', 'healthcare', 'content marketing')"), mode: z.enum(["interactive", "auto"]).default("interactive") .describe("'auto' (recommended): runs full pipeline in one call when website_url is provided. 'interactive': presents source menu for user to choose extraction method."), }; const ParamsSchema = z.object(paramsShape); type Params = z.infer<typeof ParamsSchema>; - src/tools/brand-start.ts:144-907 (handler)handleAutoMode: Full pipeline when mode='auto' — fetches HTML/CSS from website_url, extracts colors/fonts/logos, uses visual extraction (Playwright) if available, compiles DTCG tokens, runtime, and interaction policy, generates HTML report.
async function handleAutoMode(input: Params, brandDir: BrandDir): Promise<ReturnType<typeof buildResponse>> { const websiteUrl = input.website_url!; // SSRF guard: only allow http/https protocols if (!websiteUrl.startsWith("http://") && !websiteUrl.startsWith("https://")) { // fall back to interactive mode const sourceMenu = buildSourceMenu(input.website_url); return buildResponse({ what_happened: `Auto mode: website_url has unsupported protocol. Falling back to interactive mode.`, next_steps: ["Provide a URL starting with http:// or https://"], data: { source_menu: sourceMenu, fallback: "interactive" }, }); } const url = websiteUrl; // --- Step 0: Check if visual extraction (Playwright) is available --- // When Chrome/Playwright is available, use it as the PRIMARY extraction path. // This produces dramatically better results than static CSS parsing because it // reads computed styles from the rendered page. Static CSS is the fallback. const hasVisualExtraction = isVisualExtractionAvailable(); // --- Step 1: Fetch HTML --- // Priority: Firecrawl (if API key set) → static fetch (always available) // Firecrawl handles JS rendering, anti-bot, and proxy management. let html: string = ""; let fetchSource: "firecrawl" | "static" = "static"; if (isFirecrawlAvailable()) { const fcResult = await scrapeWithFirecrawl(url); if (fcResult.success) { html = fcResult.html; fetchSource = "firecrawl"; } } if (!html) { try { const response = await safeFetch(url, { signal: AbortSignal.timeout(15000), headers: { "User-Agent": `brandsystem-mcp/${getVersion()}` }, }); if (!response.ok) { return buildResponse({ what_happened: `Auto mode: failed to fetch ${url} (HTTP ${response.status}). Falling back to interactive mode.`, next_steps: [ "Check the URL is correct and publicly accessible", "Try brand_extract_web manually with a different URL, or use brand_extract_figma", ], data: { error: ERROR_CODES.AUTO_FETCH_FAILED, status: response.status, fallback: "interactive" }, }); } html = await readResponseWithLimit(response, MAX_HTML_BYTES); fetchSource = "static"; } catch (err) { return buildResponse({ what_happened: `Auto mode: failed to fetch ${url}. Falling back to interactive mode.`, next_steps: [ "Check the URL is correct and publicly accessible", "Try brand_extract_web manually with a different URL, or use brand_extract_figma", ], data: { error: ERROR_CODES.AUTO_FETCH_FAILED, details: String(err), fallback: "interactive" }, }); } } const $ = cheerio.load(html); // Extract CSS from <style> blocks and external stylesheets let allCSS = ""; $("style").each((_, el) => { allCSS += $(el).text() + "\n"; }); const stylesheetUrls: string[] = []; $('link[rel="stylesheet"]').each((_, el) => { const href = $(el).attr("href"); if (href) { try { const resolved = new URL(href, url).href; if (resolved.startsWith("http://") || resolved.startsWith("https://")) { stylesheetUrls.push(resolved); } } catch { // Invalid URL — skip } } }); for (const sheetUrl of stylesheetUrls.slice(0, 5)) { try { const resp = await safeFetch(sheetUrl, { signal: AbortSignal.timeout(5000), headers: { "User-Agent": `brandsystem-mcp/${getVersion()}` }, }); const cssText = await readResponseWithLimit(resp, MAX_CSS_BYTES); allCSS += cssText + "\n"; } catch { // Skip failed stylesheets } } // Extract inline styles from key semantic elements (catches page builders) const inlineStyleSelectors = [ "body", "header", "nav", "footer", "[class*=hero]", "[class*=banner]", "h1", "h2", "h3", "a", "button", "[class*=btn]", "[class*=cta]", "section", "[class*=elementor-section]", "[class*=sqs-block]", ]; const inlineCSS: string[] = []; for (const sel of inlineStyleSelectors) { $(sel).each((i, el) => { if (i >= 10) return false; const style = $(el).attr("style"); if (style) { inlineCSS.push(`${sel} { ${style} }`); } }); } if (inlineCSS.length > 0) { allCSS += "\n/* inline styles */\n" + inlineCSS.join("\n") + "\n"; } const { colors: extractedColors, fonts: extractedFonts } = extractFromCSS(allCSS); // Get top chromatic candidates BEFORE promotion (for confirmation flow) const chromaticCandidates = getTopChromaticCandidates(extractedColors); // Promote the most frequent chromatic color to primary if none was explicitly named const promotedColors = promotePrimaryColor(extractedColors); // Track what the auto-promoted primary was (if any) const autoPromoted = promotedColors.find( (c) => (c as unknown as { _promoted_role?: string })._promoted_role === "primary" ); const suggestedPrimary = autoPromoted?.value ?? null; const identity = await brandDir.readCoreIdentity(); let colors = [...identity.colors]; for (const ec of promotedColors.slice(0, 20)) { const role = inferColorRole(ec as Parameters<typeof inferColorRole>[0]); const rawName = ec.property.startsWith("--") ? ec.property.replace(/^--/, "").replace(/[-_]/g, " ") : `${ec.property} ${ec.value}`; const name = isCssArtifactName(rawName, ec.value) ? generateColorName(ec.value, role) : rawName; const entry: ColorEntry = { name, value: ec.value, role, source: "web", confidence: inferColorConfidence(ec), css_property: ec.property, }; colors = mergeColor(colors, entry); } const cleanedFonts = extractedFonts.filter(f => !f.family.startsWith("var(")); let typography = [...identity.typography]; for (const ef of cleanedFonts.slice(0, 8)) { const entry: TypographyEntry = { name: ef.family, family: ef.family, source: "web", confidence: ef.frequency >= 5 ? "high" : ef.frequency >= 2 ? "medium" : "low", }; typography = mergeTypography(typography, entry); } // --- Logo extraction --- const logos: LogoSpec[] = [...identity.logo]; let logoFound = false; const logoCandidates = extractLogos(html, url); for (const candidate of logoCandidates.slice(0, 5)) { if (candidate.inline_svg) { const { inline_svg, data_uri } = resolveSvg(candidate.inline_svg); const filename = `logo-${candidate.type}.svg`; await brandDir.writeAsset(`logo/${filename}`, inline_svg); logos.push({ type: "wordmark", source: "web", confidence: candidate.confidence, variants: [{ name: "default", file: `logo/${filename}`, inline_svg, data_uri, }], }); logoFound = true; break; } const fetched = await fetchLogo(candidate.url); if (!fetched) continue; const isSvg = fetched.contentType.includes("svg") || fetched.content.toString("utf-8").trim().startsWith("<"); if (isSvg) { const svgContent = fetched.content.toString("utf-8"); const { inline_svg, data_uri } = resolveSvg(svgContent); const filename = `logo-${candidate.type}.svg`; await brandDir.writeAsset(`logo/${filename}`, inline_svg); logos.push({ type: "wordmark", source: "web", confidence: candidate.confidence, variants: [{ name: "default", file: `logo/${filename}`, inline_svg, data_uri, }], }); logoFound = true; break; } else { const { data_uri } = resolveImage(fetched.content, fetched.contentType); const ext = fetched.contentType.includes("png") ? "png" : "jpg"; const filename = `logo-${candidate.type}.${ext}`; await brandDir.writeAsset(`logo/${filename}`, fetched.content); logos.push({ type: "wordmark", source: "web", confidence: candidate.confidence, variants: [{ name: "default", file: `logo/${filename}`, data_uri }], }); logoFound = true; break; } } // ── Fallback 1: Clearbit Logo API ── if (!logoFound) { const clearbitLogo = await fetchClearbitLogo(websiteUrl); if (clearbitLogo && clearbitLogo.data_uri) { logos.push({ type: "wordmark", source: "web", confidence: "medium", variants: [{ name: "default", data_uri: clearbitLogo.data_uri }], }); logoFound = true; } } // ── Fallback 2: Common logo paths ── if (!logoFound) { const probedLogo = await probeCommonLogoPaths(websiteUrl); if (probedLogo) { const fetched = await fetchLogo(probedLogo.url); if (fetched) { const isSvg = fetched.contentType.includes("svg") || fetched.content.toString("utf-8").trim().startsWith("<"); if (isSvg) { const svgContent = fetched.content.toString("utf-8"); const { inline_svg, data_uri } = resolveSvg(svgContent); logos.push({ type: "wordmark", source: "web", confidence: "medium", variants: [{ name: "default", inline_svg, data_uri }] }); } else { const { data_uri } = resolveImage(fetched.content, fetched.contentType); logos.push({ type: "wordmark", source: "web", confidence: "low", variants: [{ name: "default", data_uri }] }); } logoFound = true; } } } // ── Fallback 3: Fetch + encode apple-touch-icon or OG image ── if (!logoFound) { const fallbackCandidates = logoCandidates.filter(c => c.type === "apple-touch-icon" || c.type === "og-image"); for (const candidate of fallbackCandidates) { const encoded = await fetchAndEncodeLogo(candidate.url); if (encoded && encoded.data_uri) { logos.push({ type: candidate.type === "apple-touch-icon" ? "logomark" : "wordmark", source: "web", confidence: "low", variants: [{ name: "default", data_uri: encoded.data_uri }] }); logoFound = true; break; } } } // ── Fallback 4: Google favicon (GUARANTEED) ── if (!logoFound) { const googleFav = await fetchGoogleFavicon(websiteUrl); if (googleFav && googleFav.data_uri) { logos.push({ type: "logomark", source: "web", confidence: "low", variants: [{ name: "default", data_uri: googleFav.data_uri }] }); logoFound = true; } } // Write updated core identity const updated: CoreIdentity = { schema_version: identity.schema_version, colors, typography, logo: logos, spacing: identity.spacing, }; await brandDir.writeCoreIdentity(updated); // --- Extraction quality scoring (same logic as brand_extract_web) --- let qualityPoints = 0; const qualityReasons: string[] = []; const hasInlineSvgLogo = logos.some((l) => l.variants.some((v) => v.inline_svg)); if (hasInlineSvgLogo) { qualityPoints += 3; qualityReasons.push("Logo found with inline SVG"); } else if (logoFound) { qualityReasons.push("Logo found but not as inline SVG"); } if (colors.length >= 4) { qualityPoints += 2; qualityReasons.push(`${colors.length} colors extracted`); } else if (colors.length >= 2) { qualityPoints += 1; qualityReasons.push(`Only ${colors.length} colors extracted`); } else { qualityReasons.push("Fewer than 2 colors extracted"); } if (typography.length >= 3) { qualityPoints += 2; qualityReasons.push(`${typography.length} fonts extracted`); } else if (typography.length >= 1) { qualityPoints += 1; qualityReasons.push(`Only ${typography.length} font(s) extracted`); } else { qualityReasons.push("No fonts extracted"); } if (suggestedPrimary) { qualityPoints += 1; qualityReasons.push("Primary color candidate identified"); } const hasSurfaceRole = colors.some((c) => c.role === "surface"); const hasTextRole = colors.some((c) => c.role === "text"); if (hasSurfaceRole && hasTextRole) { qualityPoints += 1; qualityReasons.push("Both surface and text color roles detected"); } let qualityScore: "HIGH" | "MEDIUM" | "LOW"; let qualityRecommendation: string; if (qualityPoints >= 8) { qualityScore = "HIGH"; qualityRecommendation = "Strong extraction. Ready to confirm and compile."; } else if (qualityPoints >= 5) { qualityScore = "MEDIUM"; qualityRecommendation = "Decent extraction but some gaps. Consider Figma extraction for higher accuracy."; } else { qualityScore = "LOW"; qualityRecommendation = "Limited extraction. Try a different page URL, connect to Figma, or add your brand assets manually."; } let extractionQuality = { score: qualityScore, points: qualityPoints, reasons: qualityReasons, recommendation: qualityRecommendation, }; // --- Step 1b: Visual fallback when CSS parsing yields poor results --- let visualScreenshot: Buffer | null = null; let visualUsed = false; let siteExtractionUsed = false; let siteExtractionSummary: { pages: number; colors_added: number; fonts_added: number; screenshots_saved: number } | null = null; // Visual extraction: run in parallel with static CSS when Chrome is available. // Static CSS results are already computed above (fast, 1-2s). // Visual extraction runs concurrently (slower, 15-45s) and merges richer data. // If visual extraction times out or fails, the static CSS results stand alone. if (hasVisualExtraction) { // Race visual extraction against a 30-second timeout // (don't let slow Playwright block the whole pipeline) const siteResultPromise = Promise.race([ extractSite(url, { pageLimit: 4, viewports: ["desktop", "mobile"], }), new Promise<null>((resolve) => setTimeout(() => resolve(null), 30000)), ]); const siteResult = await siteResultPromise; if (siteResult && siteResult.success) { const persisted = await persistSiteExtraction(brandDir, siteResult, { merge: true }); siteExtractionUsed = true; visualUsed = true; siteExtractionSummary = { pages: siteResult.selectedPages.length, colors_added: persisted.colors_added, fonts_added: persisted.fonts_added, screenshots_saved: persisted.screenshots_saved, }; visualScreenshot = siteResult.selectedPages[0]?.viewports.find((viewport) => viewport.viewport === "desktop")?.screenshot ?? null; const mergedIdentity = await brandDir.readCoreIdentity(); colors = [...mergedIdentity.colors]; typography = [...mergedIdentity.typography]; // Rescore quality with site evidence included qualityPoints = 0; qualityReasons.length = 0; const hasInlineSvgLogo2 = logos.some((l) => l.variants.some((v) => v.inline_svg)); if (hasInlineSvgLogo2) { qualityPoints += 3; qualityReasons.push("Logo found with inline SVG"); } else if (logoFound) { qualityReasons.push("Logo found but not as inline SVG"); } if (colors.length >= 4) { qualityPoints += 2; qualityReasons.push(`${colors.length} colors (CSS + site extraction)`); } else if (colors.length >= 2) { qualityPoints += 1; qualityReasons.push(`${colors.length} colors (CSS + site extraction)`); } if (typography.length >= 3) { qualityPoints += 2; qualityReasons.push(`${typography.length} fonts (CSS + site extraction)`); } else if (typography.length >= 1) { qualityPoints += 1; qualityReasons.push(`${typography.length} font(s) (CSS + site extraction)`); } if (colors.some((c) => c.role === "primary")) { qualityPoints += 1; qualityReasons.push("Primary color identified via multi-page visual context"); } if (colors.some((c) => c.role === "surface") && colors.some((c) => c.role === "text")) { qualityPoints += 1; qualityReasons.push("Surface + text roles from computed styles"); } qualityReasons.push(`Deep site extraction sampled ${siteResult.selectedPages.length} pages and added ${persisted.colors_added} colors, ${persisted.fonts_added} fonts`); if (qualityPoints >= 8) { qualityScore = "HIGH"; qualityRecommendation = "Strong extraction (CSS + site evidence). Ready to confirm and compile."; } else if (qualityPoints >= 5) { qualityScore = "MEDIUM"; qualityRecommendation = "Deep site extraction improved results. Some gaps remain."; } else { qualityScore = "LOW"; qualityRecommendation = "Even with deep site extraction, results are limited. Try Figma or manual entry."; } extractionQuality = { score: qualityScore, points: qualityPoints, reasons: qualityReasons, recommendation: qualityRecommendation }; } else if (siteResult === null) { // Site extraction timed out (30s cap). Static CSS results stand. qualityReasons.push("Visual extraction timed out (30s). Results are from static CSS only. For richer extraction, try brand_extract_site or brand_extract_visual separately."); extractionQuality = { ...extractionQuality, reasons: qualityReasons }; } else { // Site extraction returned but wasn't successful. Try single-page visual fallback. const visualResult = await extractVisual(url); if (visualResult.success) { visualUsed = true; visualScreenshot = visualResult.screenshot; // Infer roles from visual context (button → primary, body → surface, etc.) const roleCandidates = inferRolesFromVisual(visualResult.computedElements, visualResult.cssCustomProperties); // Merge visual colors into the identity let visualColorsAdded = 0; for (const vc of roleCandidates) { const entry: ColorEntry = { name: generateColorName(vc.hex, vc.role), value: vc.hex, role: vc.role as ColorEntry["role"], source: "web", confidence: vc.confidence, css_property: `computed:${vc.source_context}`, }; const before = colors.length; colors = mergeColor(colors, entry); if (colors.length > before) visualColorsAdded++; } // Merge visual fonts for (const font of visualResult.uniqueFonts) { const entry: TypographyEntry = { name: font, family: font, source: "web", confidence: "medium", }; typography = mergeTypography(typography, entry); } // Rewrite identity with merged data const mergedIdentity: CoreIdentity = { schema_version: identity.schema_version, colors, typography, logo: logos, spacing: identity.spacing, }; await brandDir.writeCoreIdentity(mergedIdentity); // Rescore quality with visual data included qualityPoints = 0; qualityReasons.length = 0; const hasInlineSvgLogo2 = logos.some((l) => l.variants.some((v) => v.inline_svg)); if (hasInlineSvgLogo2) { qualityPoints += 3; qualityReasons.push("Logo found with inline SVG"); } else if (logoFound) { qualityReasons.push("Logo found but not as inline SVG"); } if (colors.length >= 4) { qualityPoints += 2; qualityReasons.push(`${colors.length} colors (CSS + visual)`); } else if (colors.length >= 2) { qualityPoints += 1; qualityReasons.push(`${colors.length} colors (CSS + visual)`); } if (typography.length >= 3) { qualityPoints += 2; qualityReasons.push(`${typography.length} fonts (CSS + visual)`); } else if (typography.length >= 1) { qualityPoints += 1; qualityReasons.push(`${typography.length} font(s) (CSS + visual)`); } if (colors.some((c) => c.role === "primary")) { qualityPoints += 1; qualityReasons.push("Primary color identified via visual context"); } if (colors.some((c) => c.role === "surface") && colors.some((c) => c.role === "text")) { qualityPoints += 1; qualityReasons.push("Surface + text roles from computed styles"); } qualityReasons.push(`Visual extraction added ${visualColorsAdded} colors, ${visualResult.uniqueFonts.length} fonts from rendered page`); if (qualityPoints >= 8) { qualityScore = "HIGH"; qualityRecommendation = "Strong extraction (CSS + rendered page). Ready to confirm and compile."; } else if (qualityPoints >= 5) { qualityScore = "MEDIUM"; qualityRecommendation = "Good extraction from rendered page. Some gaps may need manual confirmation."; } else { qualityScore = "LOW"; qualityRecommendation = "Even with rendered-page extraction, results are limited. Try Figma or provide brand assets manually."; } extractionQuality = { score: qualityScore, points: qualityPoints, reasons: qualityReasons, recommendation: qualityRecommendation }; } } } // If Chrome was not available, add a note about what was missed if (!hasVisualExtraction && qualityScore !== "HIGH") { qualityReasons.push("Chrome/Playwright not available — extraction used static CSS only. For better results (computed styles, JS-rendered content, multi-page crawl), install Chrome or run in an environment with browser support."); extractionQuality = { ...extractionQuality, reasons: qualityReasons }; } // --- Step 2: Compile (same logic as brand_compile) --- const config = await brandDir.readConfig(); const freshIdentity = await brandDir.readCoreIdentity(); const designArtifacts = await generateAndPersistDesignArtifacts(brandDir, { overwrite: true }); const tokens = compileDTCG(freshIdentity, config.client_name, designArtifacts.synthesis); await brandDir.writeTokens(tokens); const clarifications: ClarificationItem[] = []; let itemId = 0; if (!freshIdentity.colors.some((c) => c.role === "primary")) { clarifications.push({ id: `clarify-${++itemId}`, field: "colors.primary", question: "No primary brand color identified. Which color is your primary brand color?", source: "compilation", priority: "high", }); } for (const color of freshIdentity.colors) { if (needsClarification(color.confidence)) { clarifications.push({ id: `clarify-${++itemId}`, field: `colors.${color.role}`, question: `Color ${color.value} (${color.name}) has low confidence. Is this correct and what role does it play?`, source: color.source, priority: "medium", }); } } if (freshIdentity.typography.length === 0) { clarifications.push({ id: `clarify-${++itemId}`, field: "typography", question: "No fonts detected. What font family does your brand use?", source: "compilation", priority: "high", }); } for (const typo of freshIdentity.typography) { if (needsClarification(typo.confidence)) { clarifications.push({ id: `clarify-${++itemId}`, field: `typography.${typo.family}`, question: `Font "${typo.family}" has low confidence. Is this your brand font?`, source: typo.source, priority: "medium", }); } } if (freshIdentity.logo.length === 0) { clarifications.push({ id: `clarify-${++itemId}`, field: "logo", question: "No logo detected. Provide your logo as SVG for best results.", source: "compilation", priority: "high", }); } const unknownColors = freshIdentity.colors.filter((c) => c.role === "unknown"); if (unknownColors.length > 0) { clarifications.push({ id: `clarify-${++itemId}`, field: "colors.roles", question: `${unknownColors.length} color(s) have no assigned role: ${unknownColors.map((c) => c.value).join(", ")}. What role does each play?`, source: "compilation", priority: "medium", }); } await brandDir.writeClarifications({ schema_version: SCHEMA_VERSION, items: clarifications }); const brandTokens = tokens.brand as Record<string, unknown>; const colorTokenCount = Object.keys((brandTokens.color as Record<string, unknown>) || {}).length; const typoTokenCount = Object.keys((brandTokens.typography as Record<string, unknown>) || {}).length; const tokenCount = colorTokenCount + typoTokenCount; // --- Step 2b: Compile runtime + interaction policy (same logic as brand_compile) --- const runtime = compileRuntime(config, freshIdentity, null, null, null); await brandDir.writeRuntime(runtime); const policy = compileInteractionPolicy(config.schema_version, null, null, null); await brandDir.writePolicy(policy); // --- Step 3: Generate report (same logic as brand_report) --- let pass = 0, warn = 0, fail = 0; if (freshIdentity.colors.length > 0) pass++; else warn++; if (freshIdentity.colors.some((c) => c.role === "primary")) pass++; else warn++; if (freshIdentity.typography.length > 0) pass++; else warn++; if (freshIdentity.logo.length > 0) pass++; else warn++; if (tokenCount > 0) pass++; else warn++; if (freshIdentity.colors.every((c) => /^#[0-9a-fA-F]{3,8}$/.test(c.value))) pass++; else fail++; const lowConf = [...freshIdentity.colors, ...freshIdentity.typography].filter( (e) => e.confidence === "low" ).length; if (lowConf === 0) pass++; else warn++; const reportHtml = generateReportHTML({ config, identity: freshIdentity, clarifications, tokenCount, auditSummary: { pass, warn, fail }, }); await brandDir.writeMarkdown("brand-report.html", reportHtml); const brandInstructions = generateBrandInstructions(config, freshIdentity); // --- Build the combined auto-mode response --- const filesWritten = [ "brand.config.yaml", "core-identity.yaml", ...designArtifacts.files_written, "tokens.json", "brand-runtime.json", "interaction-policy.json", "needs-clarification.yaml", "brand-report.html", ...(siteExtractionUsed ? ["extraction-evidence.json"] : []), ]; const hasPrimary = freshIdentity.colors.some((c) => c.role === "primary"); const textResponse = buildResponse({ what_happened: `Auto mode: created .brand/ for "${input.client_name}", extracted from ${url}${siteExtractionUsed ? " (CSS + deep site extraction)" : visualUsed ? " (CSS + visual)" : ""}, compiled tokens + runtime + policy, generated design synthesis, and generated report`, next_steps: [ "Show the user their brand summary and confirm key decisions before proceeding", "Mention that DESIGN.md and design-synthesis.json were generated as portable brand system artifacts", ...(visualUsed ? ["A screenshot of the website is included — use it to validate the extracted colors and assess brand personality"] : []), ], data: { mode: "auto", client_name: input.client_name, brand_dir: ".brand/", files_written: filesWritten, ...(siteExtractionUsed ? { evidence_file: ".brand/extraction-evidence.json" } : {}), design_synthesis_file: ".brand/design-synthesis.json", design_markdown_file: ".brand/DESIGN.md", design_synthesis_source: designArtifacts.source_used, visual_extraction_used: visualUsed, site_extraction_used: siteExtractionUsed, extraction_quality: extractionQuality, extraction_summary: { colors: colors.length, typography: typography.length, logos: logos.length, tokens: tokenCount, stylesheets_parsed: stylesheetUrls.slice(0, 5).length + 1, ...(siteExtractionSummary ? { site_pages_sampled: siteExtractionSummary.pages, site_screenshots_saved: siteExtractionSummary.screenshots_saved, } : {}), }, all_colors: colors.map((c) => ({ name: c.name, hex: c.value, role: c.role, confidence: c.confidence, })), fonts: typography.map((t) => ({ family: t.family, confidence: t.confidence })), confirmation_needed: { logo: { found: logoFound, preview_available: logos.length > 0 && !!(logos[logos.length - 1]?.variants[0]?.inline_svg || logos[logos.length - 1]?.variants[0]?.data_uri), }, colors: { chromatic_candidates: chromaticCandidates, suggested_primary: suggestedPrimary, all_extracted: colors.map((c) => ({ hex: c.value, name: c.name, role: c.role, })), instruction: "Show ALL extracted colors. Ask: 1) Which is primary? 2) Any that should NOT be in the brand? 3) Roles for unknowns.", }, fonts: typography.map((t) => t.family), }, clarifications: { total: clarifications.length, high_priority: clarifications.filter((c) => c.priority === "high").length, }, report_file: ".brand/brand-report.html", report_size: `${Math.round(reportHtml.length / 1024)}KB`, brand_instructions: brandInstructions, ...(visualUsed ? { visual_analysis_prompt: [ `A screenshot of the website is attached${siteExtractionUsed ? " from the deeper multi-page extraction pass" : ""}. Use it to:`, "1. Validate the extracted primary color — does it match the dominant CTA/brand color you see?", "2. Assess the brand personality — professional/playful, premium/accessible, minimal/rich?", "3. Note any colors visible in the screenshot that the extraction may have missed", "4. Describe the typography character — geometric, humanist, serif? Weight usage?", "5. Note the spatial feel — dense/spacious, sharp/rounded corners?", ].join("\n"), } : {}), conversation_guide: { instruction: [ "The entire Session 1 pipeline ran automatically. Present the results:", `1. Show extraction quality (${qualityScore}) and mention: ${qualityRecommendation}`, ...(siteExtractionUsed ? [" NOTE: Deep site extraction was used because the cheap CSS pass was weak. The screenshot is attached, and extraction-evidence.json was saved with multi-page evidence."] : visualUsed ? [" NOTE: Visual extraction was used as a fallback because CSS parsing yielded limited results. The screenshot is attached — describe what you see in the brand."] : []), `2. ${logoFound ? "Show the logo if possible — ask 'Is this your logo?'" : "No logo was found. Suggest: Figma extraction, direct logo URL via brand_set_logo, or manual upload."}`, `3. Show ALL ${colors.length} extracted colors (hex + name + role). Ask three things:`, ` a) 'Which is your PRIMARY brand color?' (highlight candidates: ${chromaticCandidates.join(", ") || "none"})`, ` b) 'Are any of these NOT part of your brand? (retired colors, third-party colors?)'`, ` c) 'What roles should the remaining colors play? (secondary, accent, etc.)'`, `4. List the fonts (${typography.map((t) => t.family).join(", ") || "none found"}) — ask 'Are these correct?'`, "5. After confirmation: 'Your brand runtime is compiled. Any agent you give brand-runtime.json to will produce on-brand content with the right colors, fonts, and logo.'", "6. Mention: 'I also generated .brand/DESIGN.md and .brand/design-synthesis.json. DESIGN.md is the portable design brief for agents; design-synthesis.json is the structured layer behind the richer tokens.'", "7. Explain what Session 2 adds (don't just say 'go deeper'): 'Session 2 captures your visual rules — composition guidelines, anti-patterns, illustration direction. It makes the runtime dramatically more useful because agents won't just use the right colors, they'll use them the right way. Your sub-agents could reject off-brand image prompts automatically.'", "8. Ask: 'Want to add visual rules now, or is the core identity enough for what you need?'", "9. If they want team sharing: 'Run brand_brandcode_connect to save this on Brandcode Studio and sync across your team.'", ...(qualityScore === "LOW" ? ["If extraction quality is LOW, suggest: Figma extraction, different URL, or manual input via brand_set_logo."] : []), ].join("\n"), }, }, }); // If visual extraction produced a screenshot, return it as a multi-content MCP response if (visualScreenshot) { const textContent = textResponse.content[0]; return { content: [ { type: "image", data: visualScreenshot.toString("base64"), mimeType: "image/png", } as unknown as { type: "text"; text: string }, textContent, ], }; } return textResponse; } - src/lib/brand-dir.ts:61-75 (helper)initBrand helper method on BrandDir class. Scaffolds .brand/ directory and writes initial config + empty core identity. Shared by brand_start and brand_init.
/** * Scaffold + write initial config and empty core identity in one call. * Shared by brand_start and brand_init to avoid duplicated init logic. */ async initBrand(config: BrandConfigData): Promise<void> { await this.scaffold(); await this.writeConfig(config); await this.writeCoreIdentity({ schema_version: SCHEMA_VERSION, colors: [], typography: [], logo: [], spacing: null, }); }