@brandsystem/mcp 0.3.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.
Files changed (223) hide show
  1. package/README.md +515 -0
  2. package/bin/brandsystem-mcp.mjs +2 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +20 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/brand-dir.d.ts +56 -0
  8. package/dist/lib/brand-dir.d.ts.map +1 -0
  9. package/dist/lib/brand-dir.js +270 -0
  10. package/dist/lib/brand-dir.js.map +1 -0
  11. package/dist/lib/color-namer.d.ts +28 -0
  12. package/dist/lib/color-namer.d.ts.map +1 -0
  13. package/dist/lib/color-namer.js +155 -0
  14. package/dist/lib/color-namer.js.map +1 -0
  15. package/dist/lib/confidence.d.ts +19 -0
  16. package/dist/lib/confidence.d.ts.map +1 -0
  17. package/dist/lib/confidence.js +66 -0
  18. package/dist/lib/confidence.js.map +1 -0
  19. package/dist/lib/content-scorer.d.ts +38 -0
  20. package/dist/lib/content-scorer.d.ts.map +1 -0
  21. package/dist/lib/content-scorer.js +571 -0
  22. package/dist/lib/content-scorer.js.map +1 -0
  23. package/dist/lib/css-parser.d.ts +45 -0
  24. package/dist/lib/css-parser.d.ts.map +1 -0
  25. package/dist/lib/css-parser.js +330 -0
  26. package/dist/lib/css-parser.js.map +1 -0
  27. package/dist/lib/dtcg-compiler.d.ts +7 -0
  28. package/dist/lib/dtcg-compiler.d.ts.map +1 -0
  29. package/dist/lib/dtcg-compiler.js +89 -0
  30. package/dist/lib/dtcg-compiler.js.map +1 -0
  31. package/dist/lib/interaction-policy-compiler.d.ts +40 -0
  32. package/dist/lib/interaction-policy-compiler.d.ts.map +1 -0
  33. package/dist/lib/interaction-policy-compiler.js +60 -0
  34. package/dist/lib/interaction-policy-compiler.js.map +1 -0
  35. package/dist/lib/logo-extractor.d.ts +49 -0
  36. package/dist/lib/logo-extractor.d.ts.map +1 -0
  37. package/dist/lib/logo-extractor.js +384 -0
  38. package/dist/lib/logo-extractor.js.map +1 -0
  39. package/dist/lib/report-html.d.ts +20 -0
  40. package/dist/lib/report-html.d.ts.map +1 -0
  41. package/dist/lib/report-html.js +938 -0
  42. package/dist/lib/report-html.js.map +1 -0
  43. package/dist/lib/response.d.ts +20 -0
  44. package/dist/lib/response.d.ts.map +1 -0
  45. package/dist/lib/response.js +54 -0
  46. package/dist/lib/response.js.map +1 -0
  47. package/dist/lib/runtime-compiler.d.ts +60 -0
  48. package/dist/lib/runtime-compiler.d.ts.map +1 -0
  49. package/dist/lib/runtime-compiler.js +96 -0
  50. package/dist/lib/runtime-compiler.js.map +1 -0
  51. package/dist/lib/svg-resolver.d.ts +21 -0
  52. package/dist/lib/svg-resolver.d.ts.map +1 -0
  53. package/dist/lib/svg-resolver.js +115 -0
  54. package/dist/lib/svg-resolver.js.map +1 -0
  55. package/dist/lib/url-validator.d.ts +11 -0
  56. package/dist/lib/url-validator.d.ts.map +1 -0
  57. package/dist/lib/url-validator.js +93 -0
  58. package/dist/lib/url-validator.js.map +1 -0
  59. package/dist/lib/version.d.ts +2 -0
  60. package/dist/lib/version.d.ts.map +1 -0
  61. package/dist/lib/version.js +19 -0
  62. package/dist/lib/version.js.map +1 -0
  63. package/dist/lib/vim-generator.d.ts +13 -0
  64. package/dist/lib/vim-generator.d.ts.map +1 -0
  65. package/dist/lib/vim-generator.js +718 -0
  66. package/dist/lib/vim-generator.js.map +1 -0
  67. package/dist/resources/brand-resources.d.ts +4 -0
  68. package/dist/resources/brand-resources.d.ts.map +1 -0
  69. package/dist/resources/brand-resources.js +34 -0
  70. package/dist/resources/brand-resources.js.map +1 -0
  71. package/dist/schemas/brand-config.d.ts +28 -0
  72. package/dist/schemas/brand-config.d.ts.map +1 -0
  73. package/dist/schemas/brand-config.js +11 -0
  74. package/dist/schemas/brand-config.js.map +1 -0
  75. package/dist/schemas/brand-runtime.d.ts +251 -0
  76. package/dist/schemas/brand-runtime.d.ts.map +1 -0
  77. package/dist/schemas/brand-runtime.js +54 -0
  78. package/dist/schemas/brand-runtime.js.map +1 -0
  79. package/dist/schemas/core-identity.d.ts +302 -0
  80. package/dist/schemas/core-identity.d.ts.map +1 -0
  81. package/dist/schemas/core-identity.js +51 -0
  82. package/dist/schemas/core-identity.js.map +1 -0
  83. package/dist/schemas/index.d.ts +11 -0
  84. package/dist/schemas/index.d.ts.map +1 -0
  85. package/dist/schemas/index.js +11 -0
  86. package/dist/schemas/index.js.map +1 -0
  87. package/dist/schemas/interaction-policy.d.ts +150 -0
  88. package/dist/schemas/interaction-policy.d.ts.map +1 -0
  89. package/dist/schemas/interaction-policy.js +34 -0
  90. package/dist/schemas/interaction-policy.js.map +1 -0
  91. package/dist/schemas/messaging.d.ts +776 -0
  92. package/dist/schemas/messaging.d.ts.map +1 -0
  93. package/dist/schemas/messaging.js +68 -0
  94. package/dist/schemas/messaging.js.map +1 -0
  95. package/dist/schemas/needs-clarification.d.ts +62 -0
  96. package/dist/schemas/needs-clarification.d.ts.map +1 -0
  97. package/dist/schemas/needs-clarification.js +13 -0
  98. package/dist/schemas/needs-clarification.js.map +1 -0
  99. package/dist/schemas/strategy.d.ts +537 -0
  100. package/dist/schemas/strategy.d.ts.map +1 -0
  101. package/dist/schemas/strategy.js +71 -0
  102. package/dist/schemas/strategy.js.map +1 -0
  103. package/dist/schemas/tokens.d.ts +35 -0
  104. package/dist/schemas/tokens.d.ts.map +1 -0
  105. package/dist/schemas/tokens.js +15 -0
  106. package/dist/schemas/tokens.js.map +1 -0
  107. package/dist/schemas/visual-identity.d.ts +224 -0
  108. package/dist/schemas/visual-identity.d.ts.map +1 -0
  109. package/dist/schemas/visual-identity.js +42 -0
  110. package/dist/schemas/visual-identity.js.map +1 -0
  111. package/dist/server.d.ts +3 -0
  112. package/dist/server.d.ts.map +1 -0
  113. package/dist/server.js +75 -0
  114. package/dist/server.js.map +1 -0
  115. package/dist/tools/brand-audit-content.d.ts +3 -0
  116. package/dist/tools/brand-audit-content.d.ts.map +1 -0
  117. package/dist/tools/brand-audit-content.js +116 -0
  118. package/dist/tools/brand-audit-content.js.map +1 -0
  119. package/dist/tools/brand-audit-drift.d.ts +3 -0
  120. package/dist/tools/brand-audit-drift.d.ts.map +1 -0
  121. package/dist/tools/brand-audit-drift.js +301 -0
  122. package/dist/tools/brand-audit-drift.js.map +1 -0
  123. package/dist/tools/brand-audit.d.ts +3 -0
  124. package/dist/tools/brand-audit.d.ts.map +1 -0
  125. package/dist/tools/brand-audit.js +129 -0
  126. package/dist/tools/brand-audit.js.map +1 -0
  127. package/dist/tools/brand-build-journey.d.ts +3 -0
  128. package/dist/tools/brand-build-journey.d.ts.map +1 -0
  129. package/dist/tools/brand-build-journey.js +312 -0
  130. package/dist/tools/brand-build-journey.js.map +1 -0
  131. package/dist/tools/brand-build-matrix.d.ts +3 -0
  132. package/dist/tools/brand-build-matrix.d.ts.map +1 -0
  133. package/dist/tools/brand-build-matrix.js +525 -0
  134. package/dist/tools/brand-build-matrix.js.map +1 -0
  135. package/dist/tools/brand-build-personas.d.ts +3 -0
  136. package/dist/tools/brand-build-personas.d.ts.map +1 -0
  137. package/dist/tools/brand-build-personas.js +436 -0
  138. package/dist/tools/brand-build-personas.js.map +1 -0
  139. package/dist/tools/brand-build-themes.d.ts +3 -0
  140. package/dist/tools/brand-build-themes.d.ts.map +1 -0
  141. package/dist/tools/brand-build-themes.js +476 -0
  142. package/dist/tools/brand-build-themes.js.map +1 -0
  143. package/dist/tools/brand-check-compliance.d.ts +3 -0
  144. package/dist/tools/brand-check-compliance.d.ts.map +1 -0
  145. package/dist/tools/brand-check-compliance.js +243 -0
  146. package/dist/tools/brand-check-compliance.js.map +1 -0
  147. package/dist/tools/brand-clarify.d.ts +21 -0
  148. package/dist/tools/brand-clarify.d.ts.map +1 -0
  149. package/dist/tools/brand-clarify.js +497 -0
  150. package/dist/tools/brand-clarify.js.map +1 -0
  151. package/dist/tools/brand-compile-messaging.d.ts +3 -0
  152. package/dist/tools/brand-compile-messaging.d.ts.map +1 -0
  153. package/dist/tools/brand-compile-messaging.js +759 -0
  154. package/dist/tools/brand-compile-messaging.js.map +1 -0
  155. package/dist/tools/brand-compile.d.ts +3 -0
  156. package/dist/tools/brand-compile.d.ts.map +1 -0
  157. package/dist/tools/brand-compile.js +182 -0
  158. package/dist/tools/brand-compile.js.map +1 -0
  159. package/dist/tools/brand-deepen-identity.d.ts +3 -0
  160. package/dist/tools/brand-deepen-identity.d.ts.map +1 -0
  161. package/dist/tools/brand-deepen-identity.js +483 -0
  162. package/dist/tools/brand-deepen-identity.js.map +1 -0
  163. package/dist/tools/brand-export.d.ts +17 -0
  164. package/dist/tools/brand-export.d.ts.map +1 -0
  165. package/dist/tools/brand-export.js +730 -0
  166. package/dist/tools/brand-export.js.map +1 -0
  167. package/dist/tools/brand-extract-figma.d.ts +3 -0
  168. package/dist/tools/brand-extract-figma.d.ts.map +1 -0
  169. package/dist/tools/brand-extract-figma.js +174 -0
  170. package/dist/tools/brand-extract-figma.js.map +1 -0
  171. package/dist/tools/brand-extract-messaging.d.ts +3 -0
  172. package/dist/tools/brand-extract-messaging.d.ts.map +1 -0
  173. package/dist/tools/brand-extract-messaging.js +620 -0
  174. package/dist/tools/brand-extract-messaging.js.map +1 -0
  175. package/dist/tools/brand-extract-web.d.ts +3 -0
  176. package/dist/tools/brand-extract-web.d.ts.map +1 -0
  177. package/dist/tools/brand-extract-web.js +477 -0
  178. package/dist/tools/brand-extract-web.js.map +1 -0
  179. package/dist/tools/brand-feedback.d.ts +3 -0
  180. package/dist/tools/brand-feedback.d.ts.map +1 -0
  181. package/dist/tools/brand-feedback.js +366 -0
  182. package/dist/tools/brand-feedback.js.map +1 -0
  183. package/dist/tools/brand-ingest-assets.d.ts +3 -0
  184. package/dist/tools/brand-ingest-assets.d.ts.map +1 -0
  185. package/dist/tools/brand-ingest-assets.js +233 -0
  186. package/dist/tools/brand-ingest-assets.js.map +1 -0
  187. package/dist/tools/brand-init.d.ts +3 -0
  188. package/dist/tools/brand-init.d.ts.map +1 -0
  189. package/dist/tools/brand-init.js +66 -0
  190. package/dist/tools/brand-init.js.map +1 -0
  191. package/dist/tools/brand-preflight.d.ts +3 -0
  192. package/dist/tools/brand-preflight.d.ts.map +1 -0
  193. package/dist/tools/brand-preflight.js +608 -0
  194. package/dist/tools/brand-preflight.js.map +1 -0
  195. package/dist/tools/brand-report.d.ts +3 -0
  196. package/dist/tools/brand-report.d.ts.map +1 -0
  197. package/dist/tools/brand-report.js +154 -0
  198. package/dist/tools/brand-report.js.map +1 -0
  199. package/dist/tools/brand-runtime.d.ts +3 -0
  200. package/dist/tools/brand-runtime.d.ts.map +1 -0
  201. package/dist/tools/brand-runtime.js +37 -0
  202. package/dist/tools/brand-runtime.js.map +1 -0
  203. package/dist/tools/brand-set-logo.d.ts +3 -0
  204. package/dist/tools/brand-set-logo.d.ts.map +1 -0
  205. package/dist/tools/brand-set-logo.js +170 -0
  206. package/dist/tools/brand-set-logo.js.map +1 -0
  207. package/dist/tools/brand-start.d.ts +3 -0
  208. package/dist/tools/brand-start.d.ts.map +1 -0
  209. package/dist/tools/brand-start.js +686 -0
  210. package/dist/tools/brand-start.js.map +1 -0
  211. package/dist/tools/brand-status.d.ts +3 -0
  212. package/dist/tools/brand-status.d.ts.map +1 -0
  213. package/dist/tools/brand-status.js +175 -0
  214. package/dist/tools/brand-status.js.map +1 -0
  215. package/dist/tools/brand-write.d.ts +3 -0
  216. package/dist/tools/brand-write.d.ts.map +1 -0
  217. package/dist/tools/brand-write.js +442 -0
  218. package/dist/tools/brand-write.js.map +1 -0
  219. package/dist/types/index.d.ts +331 -0
  220. package/dist/types/index.d.ts.map +1 -0
  221. package/dist/types/index.js +52 -0
  222. package/dist/types/index.js.map +1 -0
  223. package/package.json +60 -0
@@ -0,0 +1,686 @@
1
+ import { z } from "zod";
2
+ import * as cheerio from "cheerio";
3
+ import { BrandDir } from "../lib/brand-dir.js";
4
+ import { buildResponse, safeParseParams } from "../lib/response.js";
5
+ import { SCHEMA_VERSION } from "../schemas/index.js";
6
+ import { extractFromCSS, inferColorConfidence, inferColorRole, promotePrimaryColor, getTopChromaticCandidates } from "../lib/css-parser.js";
7
+ import { extractLogos, fetchLogo, fetchClearbitLogo, probeCommonLogoPaths, fetchGoogleFavicon, fetchAndEncodeLogo } from "../lib/logo-extractor.js";
8
+ import { resolveSvg, resolveImage } from "../lib/svg-resolver.js";
9
+ import { mergeColor, mergeTypography, needsClarification } from "../lib/confidence.js";
10
+ import { getVersion } from "../lib/version.js";
11
+ import { generateColorName, isCssArtifactName } from "../lib/color-namer.js";
12
+ import { safeFetch } from "../lib/url-validator.js";
13
+ import { compileDTCG } from "../lib/dtcg-compiler.js";
14
+ import { generateReportHTML, generateBrandInstructions } from "../lib/report-html.js";
15
+ import { ERROR_CODES } from "../types/index.js";
16
+ const paramsShape = {
17
+ client_name: z.string().describe("Company or brand name (e.g. 'Acme Corp')"),
18
+ website_url: z.string().url().optional().describe("Company website URL to extract brand identity from (e.g. 'https://acme.com')"),
19
+ industry: z.string().optional().describe("Industry vertical for smarter extraction (e.g. 'fintech', 'healthcare', 'content marketing')"),
20
+ mode: z.enum(["interactive", "auto"]).default("interactive")
21
+ .describe("'auto' (recommended): runs full pipeline in one call when website_url is provided. 'interactive': presents source menu for user to choose extraction method."),
22
+ };
23
+ const ParamsSchema = z.object(paramsShape);
24
+ function buildSourceMenu(websiteUrl) {
25
+ return [
26
+ {
27
+ key: "A",
28
+ label: "Scan your website",
29
+ description: "Pull colors, fonts, and logo directly from your live site. Lowest friction — no files needed.",
30
+ tool_to_run: "brand_extract_web",
31
+ recommended: true,
32
+ ready: true,
33
+ ...(websiteUrl
34
+ ? { ready_reason: `URL "${websiteUrl}" provided — can start immediately` }
35
+ : { ready_reason: "Just needs a URL" }),
36
+ },
37
+ {
38
+ key: "B",
39
+ label: "Connect to Figma",
40
+ description: "Extract design tokens, colors, and typography from a Figma design file.",
41
+ tool_to_run: "brand_extract_figma",
42
+ recommended: false,
43
+ ready: false,
44
+ ready_reason: "Requires a Figma file key",
45
+ },
46
+ {
47
+ key: "C",
48
+ label: "Upload brand guidelines",
49
+ description: "Share a PDF or document with your brand guidelines and we'll extract the values.",
50
+ tool_to_run: "(manual — ask user for the file, then extract values into core-identity)",
51
+ recommended: false,
52
+ ready: false,
53
+ ready_reason: "User needs to provide a file",
54
+ },
55
+ {
56
+ key: "D",
57
+ label: "Upload an on-brand asset",
58
+ description: "Share a known-good file (social graphic, presentation, screenshot) to sample colors and fonts from.",
59
+ tool_to_run: "(manual — analyze the asset and extract brand values)",
60
+ recommended: false,
61
+ ready: false,
62
+ ready_reason: "User needs to provide a file",
63
+ },
64
+ {
65
+ key: "E",
66
+ label: "Start from scratch",
67
+ description: "Skip extraction entirely. Manually enter colors, fonts, and logo values.",
68
+ tool_to_run: "(manual entry — no extraction tool needed)",
69
+ recommended: false,
70
+ ready: true,
71
+ },
72
+ ];
73
+ }
74
+ async function handleExistingBrand(brandDir) {
75
+ const config = await brandDir.readConfig();
76
+ const identity = await brandDir.readCoreIdentity();
77
+ const hasColors = identity.colors.length > 0;
78
+ const hasTypography = identity.typography.length > 0;
79
+ const hasLogo = identity.logo.length > 0;
80
+ const hasPrimary = identity.colors.some((c) => c.role === "primary");
81
+ const gaps = [];
82
+ if (!hasColors)
83
+ gaps.push("colors");
84
+ if (!hasTypography)
85
+ gaps.push("typography");
86
+ if (!hasLogo)
87
+ gaps.push("logo");
88
+ if (hasColors && !hasPrimary)
89
+ gaps.push("primary color role");
90
+ const nextSteps = [];
91
+ if (gaps.length > 0) {
92
+ nextSteps.push(`Missing: ${gaps.join(", ")}. Run brand_extract_web or brand_extract_figma to fill gaps`);
93
+ }
94
+ if (hasColors && hasTypography) {
95
+ nextSteps.push("Run brand_compile to generate tokens.json");
96
+ }
97
+ nextSteps.push("Run brand_status for full details");
98
+ nextSteps.push("Run brand_report to generate a portable brand identity report");
99
+ return buildResponse({
100
+ what_happened: `Brand system already exists for "${config.client_name}" (session ${config.session})`,
101
+ next_steps: nextSteps,
102
+ data: {
103
+ existing: true,
104
+ client_name: config.client_name,
105
+ summary: {
106
+ colors: identity.colors.length,
107
+ typography: identity.typography.length,
108
+ logos: identity.logo.length,
109
+ has_primary: hasPrimary,
110
+ gaps: gaps.length > 0 ? gaps : "none",
111
+ },
112
+ conversation_guide: {
113
+ instruction: gaps.length > 0
114
+ ? `The brand system has gaps (${gaps.join(", ")}). Present the summary, then suggest extraction tools to fill what's missing.`
115
+ : "The brand system has core identity populated. Suggest compiling tokens or generating a report.",
116
+ },
117
+ },
118
+ });
119
+ }
120
+ async function handleAutoMode(input, brandDir) {
121
+ const websiteUrl = input.website_url;
122
+ // SSRF guard: only allow http/https protocols
123
+ if (!websiteUrl.startsWith("http://") && !websiteUrl.startsWith("https://")) {
124
+ // fall back to interactive mode
125
+ const sourceMenu = buildSourceMenu(input.website_url);
126
+ return buildResponse({
127
+ what_happened: `Auto mode: website_url has unsupported protocol. Falling back to interactive mode.`,
128
+ next_steps: ["Provide a URL starting with http:// or https://"],
129
+ data: { source_menu: sourceMenu, fallback: "interactive" },
130
+ });
131
+ }
132
+ const url = websiteUrl;
133
+ // --- Step 1: Web extraction (same logic as brand_extract_web) ---
134
+ let html;
135
+ try {
136
+ const response = await safeFetch(url, {
137
+ signal: AbortSignal.timeout(15000),
138
+ headers: { "User-Agent": `brandsystem-mcp/${getVersion()}` },
139
+ });
140
+ if (!response.ok) {
141
+ return buildResponse({
142
+ what_happened: `Auto mode: failed to fetch ${url} (HTTP ${response.status}). Falling back to interactive mode.`,
143
+ next_steps: [
144
+ "Check the URL is correct and publicly accessible",
145
+ "Try brand_extract_web manually with a different URL, or use brand_extract_figma",
146
+ ],
147
+ data: { error: ERROR_CODES.AUTO_FETCH_FAILED, status: response.status, fallback: "interactive" },
148
+ });
149
+ }
150
+ html = await response.text();
151
+ }
152
+ catch (err) {
153
+ return buildResponse({
154
+ what_happened: `Auto mode: failed to fetch ${url}. Falling back to interactive mode.`,
155
+ next_steps: [
156
+ "Check the URL is correct and publicly accessible",
157
+ "Try brand_extract_web manually with a different URL, or use brand_extract_figma",
158
+ ],
159
+ data: { error: ERROR_CODES.AUTO_FETCH_FAILED, details: String(err), fallback: "interactive" },
160
+ });
161
+ }
162
+ const $ = cheerio.load(html);
163
+ // Extract CSS from <style> blocks and external stylesheets
164
+ let allCSS = "";
165
+ $("style").each((_, el) => {
166
+ allCSS += $(el).text() + "\n";
167
+ });
168
+ const stylesheetUrls = [];
169
+ $('link[rel="stylesheet"]').each((_, el) => {
170
+ const href = $(el).attr("href");
171
+ if (href) {
172
+ try {
173
+ const resolved = new URL(href, url).href;
174
+ if (resolved.startsWith("http://") || resolved.startsWith("https://")) {
175
+ stylesheetUrls.push(resolved);
176
+ }
177
+ }
178
+ catch {
179
+ // Invalid URL — skip
180
+ }
181
+ }
182
+ });
183
+ for (const sheetUrl of stylesheetUrls.slice(0, 5)) {
184
+ try {
185
+ const resp = await safeFetch(sheetUrl, {
186
+ signal: AbortSignal.timeout(5000),
187
+ headers: { "User-Agent": `brandsystem-mcp/${getVersion()}` },
188
+ });
189
+ allCSS += (await resp.text()) + "\n";
190
+ }
191
+ catch {
192
+ // Skip failed stylesheets
193
+ }
194
+ }
195
+ const { colors: extractedColors, fonts: extractedFonts } = extractFromCSS(allCSS);
196
+ // Get top chromatic candidates BEFORE promotion (for confirmation flow)
197
+ const chromaticCandidates = getTopChromaticCandidates(extractedColors);
198
+ // Promote the most frequent chromatic color to primary if none was explicitly named
199
+ const promotedColors = promotePrimaryColor(extractedColors);
200
+ // Track what the auto-promoted primary was (if any)
201
+ const autoPromoted = promotedColors.find((c) => c._promoted_role === "primary");
202
+ const suggestedPrimary = autoPromoted?.value ?? null;
203
+ const identity = await brandDir.readCoreIdentity();
204
+ let colors = [...identity.colors];
205
+ for (const ec of promotedColors.slice(0, 20)) {
206
+ const role = inferColorRole(ec);
207
+ const rawName = ec.property.startsWith("--")
208
+ ? ec.property.replace(/^--/, "").replace(/[-_]/g, " ")
209
+ : `${ec.property} ${ec.value}`;
210
+ const name = isCssArtifactName(rawName, ec.value)
211
+ ? generateColorName(ec.value, role)
212
+ : rawName;
213
+ const entry = {
214
+ name,
215
+ value: ec.value,
216
+ role,
217
+ source: "web",
218
+ confidence: inferColorConfidence(ec),
219
+ css_property: ec.property,
220
+ };
221
+ colors = mergeColor(colors, entry);
222
+ }
223
+ let typography = [...identity.typography];
224
+ for (const ef of extractedFonts.slice(0, 5)) {
225
+ const entry = {
226
+ name: ef.family,
227
+ family: ef.family,
228
+ source: "web",
229
+ confidence: ef.frequency >= 5 ? "high" : ef.frequency >= 2 ? "medium" : "low",
230
+ };
231
+ typography = mergeTypography(typography, entry);
232
+ }
233
+ // --- Logo extraction ---
234
+ const logos = [...identity.logo];
235
+ let logoFound = false;
236
+ const logoCandidates = extractLogos(html, url);
237
+ for (const candidate of logoCandidates.slice(0, 5)) {
238
+ if (candidate.inline_svg) {
239
+ const { inline_svg, data_uri } = resolveSvg(candidate.inline_svg);
240
+ const filename = `logo-${candidate.type}.svg`;
241
+ await brandDir.writeAsset(`logo/${filename}`, inline_svg);
242
+ logos.push({
243
+ type: "wordmark",
244
+ source: "web",
245
+ confidence: candidate.confidence,
246
+ variants: [{
247
+ name: "default",
248
+ file: `logo/${filename}`,
249
+ inline_svg,
250
+ data_uri,
251
+ }],
252
+ });
253
+ logoFound = true;
254
+ break;
255
+ }
256
+ const fetched = await fetchLogo(candidate.url);
257
+ if (!fetched)
258
+ continue;
259
+ const isSvg = fetched.contentType.includes("svg") || fetched.content.toString("utf-8").trim().startsWith("<");
260
+ if (isSvg) {
261
+ const svgContent = fetched.content.toString("utf-8");
262
+ const { inline_svg, data_uri } = resolveSvg(svgContent);
263
+ const filename = `logo-${candidate.type}.svg`;
264
+ await brandDir.writeAsset(`logo/${filename}`, inline_svg);
265
+ logos.push({
266
+ type: "wordmark",
267
+ source: "web",
268
+ confidence: candidate.confidence,
269
+ variants: [{
270
+ name: "default",
271
+ file: `logo/${filename}`,
272
+ inline_svg,
273
+ data_uri,
274
+ }],
275
+ });
276
+ logoFound = true;
277
+ break;
278
+ }
279
+ else {
280
+ const { data_uri } = resolveImage(fetched.content, fetched.contentType);
281
+ const ext = fetched.contentType.includes("png") ? "png" : "jpg";
282
+ const filename = `logo-${candidate.type}.${ext}`;
283
+ await brandDir.writeAsset(`logo/${filename}`, fetched.content);
284
+ logos.push({
285
+ type: "wordmark",
286
+ source: "web",
287
+ confidence: candidate.confidence,
288
+ variants: [{ name: "default", file: `logo/${filename}`, data_uri }],
289
+ });
290
+ logoFound = true;
291
+ break;
292
+ }
293
+ }
294
+ // ── Fallback 1: Clearbit Logo API ──
295
+ if (!logoFound) {
296
+ const clearbitLogo = await fetchClearbitLogo(websiteUrl);
297
+ if (clearbitLogo && clearbitLogo.data_uri) {
298
+ logos.push({
299
+ type: "wordmark",
300
+ source: "web",
301
+ confidence: "medium",
302
+ variants: [{ name: "default", data_uri: clearbitLogo.data_uri }],
303
+ });
304
+ logoFound = true;
305
+ }
306
+ }
307
+ // ── Fallback 2: Common logo paths ──
308
+ if (!logoFound) {
309
+ const probedLogo = await probeCommonLogoPaths(websiteUrl);
310
+ if (probedLogo) {
311
+ const fetched = await fetchLogo(probedLogo.url);
312
+ if (fetched) {
313
+ const isSvg = fetched.contentType.includes("svg") || fetched.content.toString("utf-8").trim().startsWith("<");
314
+ if (isSvg) {
315
+ const svgContent = fetched.content.toString("utf-8");
316
+ const { inline_svg, data_uri } = resolveSvg(svgContent);
317
+ logos.push({ type: "wordmark", source: "web", confidence: "medium", variants: [{ name: "default", inline_svg, data_uri }] });
318
+ }
319
+ else {
320
+ const { data_uri } = resolveImage(fetched.content, fetched.contentType);
321
+ logos.push({ type: "wordmark", source: "web", confidence: "low", variants: [{ name: "default", data_uri }] });
322
+ }
323
+ logoFound = true;
324
+ }
325
+ }
326
+ }
327
+ // ── Fallback 3: Fetch + encode apple-touch-icon or OG image ──
328
+ if (!logoFound) {
329
+ const fallbackCandidates = logoCandidates.filter(c => c.type === "apple-touch-icon" || c.type === "og-image");
330
+ for (const candidate of fallbackCandidates) {
331
+ const encoded = await fetchAndEncodeLogo(candidate.url);
332
+ if (encoded && encoded.data_uri) {
333
+ logos.push({ type: candidate.type === "apple-touch-icon" ? "logomark" : "wordmark", source: "web", confidence: "low", variants: [{ name: "default", data_uri: encoded.data_uri }] });
334
+ logoFound = true;
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ // ── Fallback 4: Google favicon (GUARANTEED) ──
340
+ if (!logoFound) {
341
+ const googleFav = await fetchGoogleFavicon(websiteUrl);
342
+ if (googleFav && googleFav.data_uri) {
343
+ logos.push({ type: "logomark", source: "web", confidence: "low", variants: [{ name: "default", data_uri: googleFav.data_uri }] });
344
+ logoFound = true;
345
+ }
346
+ }
347
+ // Write updated core identity
348
+ const updated = {
349
+ schema_version: identity.schema_version,
350
+ colors,
351
+ typography,
352
+ logo: logos,
353
+ spacing: identity.spacing,
354
+ };
355
+ await brandDir.writeCoreIdentity(updated);
356
+ // --- Extraction quality scoring (same logic as brand_extract_web) ---
357
+ let qualityPoints = 0;
358
+ const qualityReasons = [];
359
+ const hasInlineSvgLogo = logos.some((l) => l.variants.some((v) => v.inline_svg));
360
+ if (hasInlineSvgLogo) {
361
+ qualityPoints += 3;
362
+ qualityReasons.push("Logo found with inline SVG");
363
+ }
364
+ else if (logoFound) {
365
+ qualityReasons.push("Logo found but not as inline SVG");
366
+ }
367
+ if (colors.length >= 4) {
368
+ qualityPoints += 2;
369
+ qualityReasons.push(`${colors.length} colors extracted`);
370
+ }
371
+ else if (colors.length >= 2) {
372
+ qualityPoints += 1;
373
+ qualityReasons.push(`Only ${colors.length} colors extracted`);
374
+ }
375
+ else {
376
+ qualityReasons.push("Fewer than 2 colors extracted");
377
+ }
378
+ if (typography.length >= 3) {
379
+ qualityPoints += 2;
380
+ qualityReasons.push(`${typography.length} fonts extracted`);
381
+ }
382
+ else if (typography.length >= 1) {
383
+ qualityPoints += 1;
384
+ qualityReasons.push(`Only ${typography.length} font(s) extracted`);
385
+ }
386
+ else {
387
+ qualityReasons.push("No fonts extracted");
388
+ }
389
+ if (suggestedPrimary) {
390
+ qualityPoints += 1;
391
+ qualityReasons.push("Primary color candidate identified");
392
+ }
393
+ const hasSurfaceRole = colors.some((c) => c.role === "surface");
394
+ const hasTextRole = colors.some((c) => c.role === "text");
395
+ if (hasSurfaceRole && hasTextRole) {
396
+ qualityPoints += 1;
397
+ qualityReasons.push("Both surface and text color roles detected");
398
+ }
399
+ let qualityScore;
400
+ let qualityRecommendation;
401
+ if (qualityPoints >= 8) {
402
+ qualityScore = "HIGH";
403
+ qualityRecommendation = "Strong extraction. Ready to confirm and compile.";
404
+ }
405
+ else if (qualityPoints >= 5) {
406
+ qualityScore = "MEDIUM";
407
+ qualityRecommendation = "Decent extraction but some gaps. Consider Figma extraction for higher accuracy.";
408
+ }
409
+ else {
410
+ qualityScore = "LOW";
411
+ qualityRecommendation = "Limited extraction. Try a different page URL, connect to Figma, or add your brand assets manually.";
412
+ }
413
+ const extractionQuality = {
414
+ score: qualityScore,
415
+ points: qualityPoints,
416
+ reasons: qualityReasons,
417
+ recommendation: qualityRecommendation,
418
+ };
419
+ // --- Step 2: Compile (same logic as brand_compile) ---
420
+ const config = await brandDir.readConfig();
421
+ const freshIdentity = await brandDir.readCoreIdentity();
422
+ const tokens = compileDTCG(freshIdentity, config.client_name);
423
+ await brandDir.writeTokens(tokens);
424
+ const clarifications = [];
425
+ let itemId = 0;
426
+ if (!freshIdentity.colors.some((c) => c.role === "primary")) {
427
+ clarifications.push({
428
+ id: `clarify-${++itemId}`,
429
+ field: "colors.primary",
430
+ question: "No primary brand color identified. Which color is your primary brand color?",
431
+ source: "compilation",
432
+ priority: "high",
433
+ });
434
+ }
435
+ for (const color of freshIdentity.colors) {
436
+ if (needsClarification(color.confidence)) {
437
+ clarifications.push({
438
+ id: `clarify-${++itemId}`,
439
+ field: `colors.${color.role}`,
440
+ question: `Color ${color.value} (${color.name}) has low confidence. Is this correct and what role does it play?`,
441
+ source: color.source,
442
+ priority: "medium",
443
+ });
444
+ }
445
+ }
446
+ if (freshIdentity.typography.length === 0) {
447
+ clarifications.push({
448
+ id: `clarify-${++itemId}`,
449
+ field: "typography",
450
+ question: "No fonts detected. What font family does your brand use?",
451
+ source: "compilation",
452
+ priority: "high",
453
+ });
454
+ }
455
+ for (const typo of freshIdentity.typography) {
456
+ if (needsClarification(typo.confidence)) {
457
+ clarifications.push({
458
+ id: `clarify-${++itemId}`,
459
+ field: `typography.${typo.family}`,
460
+ question: `Font "${typo.family}" has low confidence. Is this your brand font?`,
461
+ source: typo.source,
462
+ priority: "medium",
463
+ });
464
+ }
465
+ }
466
+ if (freshIdentity.logo.length === 0) {
467
+ clarifications.push({
468
+ id: `clarify-${++itemId}`,
469
+ field: "logo",
470
+ question: "No logo detected. Provide your logo as SVG for best results.",
471
+ source: "compilation",
472
+ priority: "high",
473
+ });
474
+ }
475
+ const unknownColors = freshIdentity.colors.filter((c) => c.role === "unknown");
476
+ if (unknownColors.length > 0) {
477
+ clarifications.push({
478
+ id: `clarify-${++itemId}`,
479
+ field: "colors.roles",
480
+ question: `${unknownColors.length} color(s) have no assigned role: ${unknownColors.map((c) => c.value).join(", ")}. What role does each play?`,
481
+ source: "compilation",
482
+ priority: "medium",
483
+ });
484
+ }
485
+ await brandDir.writeClarifications({ schema_version: SCHEMA_VERSION, items: clarifications });
486
+ const brandTokens = tokens.brand;
487
+ const colorTokenCount = Object.keys(brandTokens.color || {}).length;
488
+ const typoTokenCount = Object.keys(brandTokens.typography || {}).length;
489
+ const tokenCount = colorTokenCount + typoTokenCount;
490
+ // --- Step 3: Generate report (same logic as brand_report) ---
491
+ let pass = 0, warn = 0, fail = 0;
492
+ if (freshIdentity.colors.length > 0)
493
+ pass++;
494
+ else
495
+ warn++;
496
+ if (freshIdentity.colors.some((c) => c.role === "primary"))
497
+ pass++;
498
+ else
499
+ warn++;
500
+ if (freshIdentity.typography.length > 0)
501
+ pass++;
502
+ else
503
+ warn++;
504
+ if (freshIdentity.logo.length > 0)
505
+ pass++;
506
+ else
507
+ warn++;
508
+ if (tokenCount > 0)
509
+ pass++;
510
+ else
511
+ warn++;
512
+ if (freshIdentity.colors.every((c) => /^#[0-9a-fA-F]{3,8}$/.test(c.value)))
513
+ pass++;
514
+ else
515
+ fail++;
516
+ const lowConf = [...freshIdentity.colors, ...freshIdentity.typography].filter((e) => e.confidence === "low").length;
517
+ if (lowConf === 0)
518
+ pass++;
519
+ else
520
+ warn++;
521
+ const reportHtml = generateReportHTML({
522
+ config,
523
+ identity: freshIdentity,
524
+ clarifications,
525
+ tokenCount,
526
+ auditSummary: { pass, warn, fail },
527
+ });
528
+ await brandDir.writeMarkdown("brand-report.html", reportHtml);
529
+ const brandInstructions = generateBrandInstructions(config, freshIdentity);
530
+ // --- Build the combined auto-mode response ---
531
+ const filesWritten = [
532
+ "brand.config.yaml",
533
+ "core-identity.yaml",
534
+ "tokens.json",
535
+ "needs-clarification.yaml",
536
+ "brand-report.html",
537
+ ];
538
+ const hasPrimary = freshIdentity.colors.some((c) => c.role === "primary");
539
+ return buildResponse({
540
+ what_happened: `Auto mode: created .brand/ for "${input.client_name}", extracted from ${url}, compiled tokens, and generated report`,
541
+ next_steps: [
542
+ "Show the user their brand summary and confirm key decisions before proceeding",
543
+ ],
544
+ data: {
545
+ mode: "auto",
546
+ client_name: input.client_name,
547
+ brand_dir: ".brand/",
548
+ files_written: filesWritten,
549
+ extraction_quality: extractionQuality,
550
+ extraction_summary: {
551
+ colors: colors.length,
552
+ typography: typography.length,
553
+ logos: logos.length,
554
+ tokens: tokenCount,
555
+ stylesheets_parsed: stylesheetUrls.slice(0, 5).length + 1,
556
+ },
557
+ all_colors: colors.map((c) => ({
558
+ name: c.name,
559
+ hex: c.value,
560
+ role: c.role,
561
+ confidence: c.confidence,
562
+ })),
563
+ fonts: typography.map((t) => ({ family: t.family, confidence: t.confidence })),
564
+ confirmation_needed: {
565
+ logo: {
566
+ found: logoFound,
567
+ preview_available: logos.length > 0 && !!(logos[logos.length - 1]?.variants[0]?.inline_svg || logos[logos.length - 1]?.variants[0]?.data_uri),
568
+ },
569
+ colors: {
570
+ chromatic_candidates: chromaticCandidates,
571
+ suggested_primary: suggestedPrimary,
572
+ all_extracted: colors.map((c) => ({
573
+ hex: c.value,
574
+ name: c.name,
575
+ role: c.role,
576
+ })),
577
+ instruction: "Show ALL extracted colors. Ask: 1) Which is primary? 2) Any that should NOT be in the brand? 3) Roles for unknowns.",
578
+ },
579
+ fonts: typography.map((t) => t.family),
580
+ },
581
+ clarifications: {
582
+ total: clarifications.length,
583
+ high_priority: clarifications.filter((c) => c.priority === "high").length,
584
+ },
585
+ report_file: ".brand/brand-report.html",
586
+ report_size: `${Math.round(reportHtml.length / 1024)}KB`,
587
+ brand_instructions: brandInstructions,
588
+ conversation_guide: {
589
+ instruction: [
590
+ "The entire Session 1 pipeline ran automatically. Present the results:",
591
+ `1. Show extraction quality (${qualityScore}) and mention: ${qualityRecommendation}`,
592
+ `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."}`,
593
+ `3. Show ALL ${colors.length} extracted colors (hex + name + role). Ask three things:`,
594
+ ` a) 'Which is your PRIMARY brand color?' (highlight candidates: ${chromaticCandidates.join(", ") || "none"})`,
595
+ ` b) 'Are any of these NOT part of your brand? (retired colors, third-party colors?)'`,
596
+ ` c) 'What roles should the remaining colors play? (secondary, accent, etc.)'`,
597
+ `4. List the fonts (${typography.map((t) => t.family).join(", ") || "none found"}) — ask 'Are these correct?'`,
598
+ "5. After confirmation, suggest Session 2: 'Your core identity is set. Want to go deeper into your visual identity?'",
599
+ ...(qualityScore === "LOW" ? ["If extraction quality is LOW, suggest: Figma extraction, different URL, or manual input via brand_set_logo."] : []),
600
+ ].join("\n"),
601
+ },
602
+ },
603
+ });
604
+ }
605
+ async function handler(input) {
606
+ const brandDir = new BrandDir(process.cwd());
607
+ // If .brand/ already exists, return status + actionable next steps
608
+ if (await brandDir.exists()) {
609
+ return handleExistingBrand(brandDir);
610
+ }
611
+ // Initialize the .brand/ directory (shared logic with brand_init)
612
+ await brandDir.initBrand({
613
+ schema_version: SCHEMA_VERSION,
614
+ session: 1,
615
+ client_name: input.client_name,
616
+ industry: input.industry,
617
+ website_url: input.website_url,
618
+ created_at: new Date().toISOString(),
619
+ });
620
+ // Auto mode: run entire Session 1 pipeline if website_url is provided
621
+ if (input.mode === "auto" && input.website_url) {
622
+ return handleAutoMode(input, brandDir);
623
+ }
624
+ const sourceMenu = buildSourceMenu(input.website_url);
625
+ const recommended = "A";
626
+ const nextSteps = [
627
+ "Present the source menu below and ask the user how they'd like to populate their brand identity",
628
+ ];
629
+ if (input.website_url) {
630
+ nextSteps.push(`Option A can start immediately — run brand_extract_web with url "${input.website_url}"`);
631
+ }
632
+ return buildResponse({
633
+ what_happened: `Created .brand/ directory for "${input.client_name}"`,
634
+ next_steps: nextSteps,
635
+ data: {
636
+ client_name: input.client_name,
637
+ brand_dir: ".brand/",
638
+ files_created: ["brand.config.yaml", "core-identity.yaml", "assets/logo/"],
639
+ source_menu: sourceMenu,
640
+ recommended,
641
+ conversation_guide: {
642
+ instruction: [
643
+ `Welcome the user and confirm the brand system was created for "${input.client_name}".`,
644
+ "",
645
+ "BEFORE presenting the source menu, ask these quick context questions (skip any already answered via params):",
646
+ "",
647
+ `${input.website_url ? "✓ Website URL already provided." : "1. \"What's your primary website URL?\" — needed for extraction"}`,
648
+ `${input.industry ? "✓ Industry already provided." : "2. \"What industry are you in, and who's your primary audience?\" — helps infer color/tone decisions"}`,
649
+ "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.'",
650
+ "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.",
651
+ "",
652
+ "Once you have context (or the user wants to skip ahead), present the source menu:",
653
+ "",
654
+ "Present the source menu as a numbered list with clear descriptions.",
655
+ `Highlight option A as the recommended starting point${input.website_url ? " — and note it can start immediately since a URL was provided" : ""}.`,
656
+ "Ask: 'Which would you like to start with?'",
657
+ "",
658
+ "Based on their choice:",
659
+ " A → Run brand_extract_web (with the website_url if provided), then immediately run brand_compile and brand_report to show results fast",
660
+ " B → Ask for their Figma file key, then run brand_extract_figma in plan mode",
661
+ " C → Ask them to share/upload their brand guidelines document, then extract values into core-identity manually",
662
+ " D → Ask them to share/upload an on-brand asset, then analyze it and extract brand values",
663
+ " E → Begin manual entry by asking for primary brand color, then font, then proceed through core identity fields",
664
+ "",
665
+ "AFTER extraction completes:",
666
+ " 1. Run brand_compile to generate tokens",
667
+ " 2. Run brand_report to generate the HTML report",
668
+ " 3. Show the report as an artifact (in Chat) or write to .brand/ (in Code)",
669
+ " 4. Ask: 'Does this look right? If anything's off, I can help fix it.'",
670
+ ].join("\n"),
671
+ conditionals: {
672
+ 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.",
673
+ },
674
+ },
675
+ },
676
+ });
677
+ }
678
+ export function register(server) {
679
+ 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, 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.", paramsShape, async (args) => {
680
+ const parsed = safeParseParams(ParamsSchema, args);
681
+ if (!parsed.success)
682
+ return parsed.response;
683
+ return handler(parsed.data);
684
+ });
685
+ }
686
+ //# sourceMappingURL=brand-start.js.map