@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.
- package/README.md +515 -0
- package/bin/brandsystem-mcp.mjs +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/brand-dir.d.ts +56 -0
- package/dist/lib/brand-dir.d.ts.map +1 -0
- package/dist/lib/brand-dir.js +270 -0
- package/dist/lib/brand-dir.js.map +1 -0
- package/dist/lib/color-namer.d.ts +28 -0
- package/dist/lib/color-namer.d.ts.map +1 -0
- package/dist/lib/color-namer.js +155 -0
- package/dist/lib/color-namer.js.map +1 -0
- package/dist/lib/confidence.d.ts +19 -0
- package/dist/lib/confidence.d.ts.map +1 -0
- package/dist/lib/confidence.js +66 -0
- package/dist/lib/confidence.js.map +1 -0
- package/dist/lib/content-scorer.d.ts +38 -0
- package/dist/lib/content-scorer.d.ts.map +1 -0
- package/dist/lib/content-scorer.js +571 -0
- package/dist/lib/content-scorer.js.map +1 -0
- package/dist/lib/css-parser.d.ts +45 -0
- package/dist/lib/css-parser.d.ts.map +1 -0
- package/dist/lib/css-parser.js +330 -0
- package/dist/lib/css-parser.js.map +1 -0
- package/dist/lib/dtcg-compiler.d.ts +7 -0
- package/dist/lib/dtcg-compiler.d.ts.map +1 -0
- package/dist/lib/dtcg-compiler.js +89 -0
- package/dist/lib/dtcg-compiler.js.map +1 -0
- package/dist/lib/interaction-policy-compiler.d.ts +40 -0
- package/dist/lib/interaction-policy-compiler.d.ts.map +1 -0
- package/dist/lib/interaction-policy-compiler.js +60 -0
- package/dist/lib/interaction-policy-compiler.js.map +1 -0
- package/dist/lib/logo-extractor.d.ts +49 -0
- package/dist/lib/logo-extractor.d.ts.map +1 -0
- package/dist/lib/logo-extractor.js +384 -0
- package/dist/lib/logo-extractor.js.map +1 -0
- package/dist/lib/report-html.d.ts +20 -0
- package/dist/lib/report-html.d.ts.map +1 -0
- package/dist/lib/report-html.js +938 -0
- package/dist/lib/report-html.js.map +1 -0
- package/dist/lib/response.d.ts +20 -0
- package/dist/lib/response.d.ts.map +1 -0
- package/dist/lib/response.js +54 -0
- package/dist/lib/response.js.map +1 -0
- package/dist/lib/runtime-compiler.d.ts +60 -0
- package/dist/lib/runtime-compiler.d.ts.map +1 -0
- package/dist/lib/runtime-compiler.js +96 -0
- package/dist/lib/runtime-compiler.js.map +1 -0
- package/dist/lib/svg-resolver.d.ts +21 -0
- package/dist/lib/svg-resolver.d.ts.map +1 -0
- package/dist/lib/svg-resolver.js +115 -0
- package/dist/lib/svg-resolver.js.map +1 -0
- package/dist/lib/url-validator.d.ts +11 -0
- package/dist/lib/url-validator.d.ts.map +1 -0
- package/dist/lib/url-validator.js +93 -0
- package/dist/lib/url-validator.js.map +1 -0
- package/dist/lib/version.d.ts +2 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +19 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/vim-generator.d.ts +13 -0
- package/dist/lib/vim-generator.d.ts.map +1 -0
- package/dist/lib/vim-generator.js +718 -0
- package/dist/lib/vim-generator.js.map +1 -0
- package/dist/resources/brand-resources.d.ts +4 -0
- package/dist/resources/brand-resources.d.ts.map +1 -0
- package/dist/resources/brand-resources.js +34 -0
- package/dist/resources/brand-resources.js.map +1 -0
- package/dist/schemas/brand-config.d.ts +28 -0
- package/dist/schemas/brand-config.d.ts.map +1 -0
- package/dist/schemas/brand-config.js +11 -0
- package/dist/schemas/brand-config.js.map +1 -0
- package/dist/schemas/brand-runtime.d.ts +251 -0
- package/dist/schemas/brand-runtime.d.ts.map +1 -0
- package/dist/schemas/brand-runtime.js +54 -0
- package/dist/schemas/brand-runtime.js.map +1 -0
- package/dist/schemas/core-identity.d.ts +302 -0
- package/dist/schemas/core-identity.d.ts.map +1 -0
- package/dist/schemas/core-identity.js +51 -0
- package/dist/schemas/core-identity.js.map +1 -0
- package/dist/schemas/index.d.ts +11 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +11 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/interaction-policy.d.ts +150 -0
- package/dist/schemas/interaction-policy.d.ts.map +1 -0
- package/dist/schemas/interaction-policy.js +34 -0
- package/dist/schemas/interaction-policy.js.map +1 -0
- package/dist/schemas/messaging.d.ts +776 -0
- package/dist/schemas/messaging.d.ts.map +1 -0
- package/dist/schemas/messaging.js +68 -0
- package/dist/schemas/messaging.js.map +1 -0
- package/dist/schemas/needs-clarification.d.ts +62 -0
- package/dist/schemas/needs-clarification.d.ts.map +1 -0
- package/dist/schemas/needs-clarification.js +13 -0
- package/dist/schemas/needs-clarification.js.map +1 -0
- package/dist/schemas/strategy.d.ts +537 -0
- package/dist/schemas/strategy.d.ts.map +1 -0
- package/dist/schemas/strategy.js +71 -0
- package/dist/schemas/strategy.js.map +1 -0
- package/dist/schemas/tokens.d.ts +35 -0
- package/dist/schemas/tokens.d.ts.map +1 -0
- package/dist/schemas/tokens.js +15 -0
- package/dist/schemas/tokens.js.map +1 -0
- package/dist/schemas/visual-identity.d.ts +224 -0
- package/dist/schemas/visual-identity.d.ts.map +1 -0
- package/dist/schemas/visual-identity.js +42 -0
- package/dist/schemas/visual-identity.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +75 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/brand-audit-content.d.ts +3 -0
- package/dist/tools/brand-audit-content.d.ts.map +1 -0
- package/dist/tools/brand-audit-content.js +116 -0
- package/dist/tools/brand-audit-content.js.map +1 -0
- package/dist/tools/brand-audit-drift.d.ts +3 -0
- package/dist/tools/brand-audit-drift.d.ts.map +1 -0
- package/dist/tools/brand-audit-drift.js +301 -0
- package/dist/tools/brand-audit-drift.js.map +1 -0
- package/dist/tools/brand-audit.d.ts +3 -0
- package/dist/tools/brand-audit.d.ts.map +1 -0
- package/dist/tools/brand-audit.js +129 -0
- package/dist/tools/brand-audit.js.map +1 -0
- package/dist/tools/brand-build-journey.d.ts +3 -0
- package/dist/tools/brand-build-journey.d.ts.map +1 -0
- package/dist/tools/brand-build-journey.js +312 -0
- package/dist/tools/brand-build-journey.js.map +1 -0
- package/dist/tools/brand-build-matrix.d.ts +3 -0
- package/dist/tools/brand-build-matrix.d.ts.map +1 -0
- package/dist/tools/brand-build-matrix.js +525 -0
- package/dist/tools/brand-build-matrix.js.map +1 -0
- package/dist/tools/brand-build-personas.d.ts +3 -0
- package/dist/tools/brand-build-personas.d.ts.map +1 -0
- package/dist/tools/brand-build-personas.js +436 -0
- package/dist/tools/brand-build-personas.js.map +1 -0
- package/dist/tools/brand-build-themes.d.ts +3 -0
- package/dist/tools/brand-build-themes.d.ts.map +1 -0
- package/dist/tools/brand-build-themes.js +476 -0
- package/dist/tools/brand-build-themes.js.map +1 -0
- package/dist/tools/brand-check-compliance.d.ts +3 -0
- package/dist/tools/brand-check-compliance.d.ts.map +1 -0
- package/dist/tools/brand-check-compliance.js +243 -0
- package/dist/tools/brand-check-compliance.js.map +1 -0
- package/dist/tools/brand-clarify.d.ts +21 -0
- package/dist/tools/brand-clarify.d.ts.map +1 -0
- package/dist/tools/brand-clarify.js +497 -0
- package/dist/tools/brand-clarify.js.map +1 -0
- package/dist/tools/brand-compile-messaging.d.ts +3 -0
- package/dist/tools/brand-compile-messaging.d.ts.map +1 -0
- package/dist/tools/brand-compile-messaging.js +759 -0
- package/dist/tools/brand-compile-messaging.js.map +1 -0
- package/dist/tools/brand-compile.d.ts +3 -0
- package/dist/tools/brand-compile.d.ts.map +1 -0
- package/dist/tools/brand-compile.js +182 -0
- package/dist/tools/brand-compile.js.map +1 -0
- package/dist/tools/brand-deepen-identity.d.ts +3 -0
- package/dist/tools/brand-deepen-identity.d.ts.map +1 -0
- package/dist/tools/brand-deepen-identity.js +483 -0
- package/dist/tools/brand-deepen-identity.js.map +1 -0
- package/dist/tools/brand-export.d.ts +17 -0
- package/dist/tools/brand-export.d.ts.map +1 -0
- package/dist/tools/brand-export.js +730 -0
- package/dist/tools/brand-export.js.map +1 -0
- package/dist/tools/brand-extract-figma.d.ts +3 -0
- package/dist/tools/brand-extract-figma.d.ts.map +1 -0
- package/dist/tools/brand-extract-figma.js +174 -0
- package/dist/tools/brand-extract-figma.js.map +1 -0
- package/dist/tools/brand-extract-messaging.d.ts +3 -0
- package/dist/tools/brand-extract-messaging.d.ts.map +1 -0
- package/dist/tools/brand-extract-messaging.js +620 -0
- package/dist/tools/brand-extract-messaging.js.map +1 -0
- package/dist/tools/brand-extract-web.d.ts +3 -0
- package/dist/tools/brand-extract-web.d.ts.map +1 -0
- package/dist/tools/brand-extract-web.js +477 -0
- package/dist/tools/brand-extract-web.js.map +1 -0
- package/dist/tools/brand-feedback.d.ts +3 -0
- package/dist/tools/brand-feedback.d.ts.map +1 -0
- package/dist/tools/brand-feedback.js +366 -0
- package/dist/tools/brand-feedback.js.map +1 -0
- package/dist/tools/brand-ingest-assets.d.ts +3 -0
- package/dist/tools/brand-ingest-assets.d.ts.map +1 -0
- package/dist/tools/brand-ingest-assets.js +233 -0
- package/dist/tools/brand-ingest-assets.js.map +1 -0
- package/dist/tools/brand-init.d.ts +3 -0
- package/dist/tools/brand-init.d.ts.map +1 -0
- package/dist/tools/brand-init.js +66 -0
- package/dist/tools/brand-init.js.map +1 -0
- package/dist/tools/brand-preflight.d.ts +3 -0
- package/dist/tools/brand-preflight.d.ts.map +1 -0
- package/dist/tools/brand-preflight.js +608 -0
- package/dist/tools/brand-preflight.js.map +1 -0
- package/dist/tools/brand-report.d.ts +3 -0
- package/dist/tools/brand-report.d.ts.map +1 -0
- package/dist/tools/brand-report.js +154 -0
- package/dist/tools/brand-report.js.map +1 -0
- package/dist/tools/brand-runtime.d.ts +3 -0
- package/dist/tools/brand-runtime.d.ts.map +1 -0
- package/dist/tools/brand-runtime.js +37 -0
- package/dist/tools/brand-runtime.js.map +1 -0
- package/dist/tools/brand-set-logo.d.ts +3 -0
- package/dist/tools/brand-set-logo.d.ts.map +1 -0
- package/dist/tools/brand-set-logo.js +170 -0
- package/dist/tools/brand-set-logo.js.map +1 -0
- package/dist/tools/brand-start.d.ts +3 -0
- package/dist/tools/brand-start.d.ts.map +1 -0
- package/dist/tools/brand-start.js +686 -0
- package/dist/tools/brand-start.js.map +1 -0
- package/dist/tools/brand-status.d.ts +3 -0
- package/dist/tools/brand-status.d.ts.map +1 -0
- package/dist/tools/brand-status.js +175 -0
- package/dist/tools/brand-status.js.map +1 -0
- package/dist/tools/brand-write.d.ts +3 -0
- package/dist/tools/brand-write.d.ts.map +1 -0
- package/dist/tools/brand-write.js +442 -0
- package/dist/tools/brand-write.js.map +1 -0
- package/dist/types/index.d.ts +331 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +52 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BrandDir } from "../lib/brand-dir.js";
|
|
3
|
+
import { buildResponse, safeParseParams } from "../lib/response.js";
|
|
4
|
+
import { SCHEMA_VERSION } from "../schemas/index.js";
|
|
5
|
+
import { ERROR_CODES } from "../types/index.js";
|
|
6
|
+
const paramsShape = {
|
|
7
|
+
client_name: z.string().describe("Company or brand name"),
|
|
8
|
+
industry: z.string().optional().describe("Industry vertical (e.g. 'content marketing agency')"),
|
|
9
|
+
website_url: z.string().optional().describe("Primary website URL for web extraction"),
|
|
10
|
+
figma_file_key: z.string().optional().describe("Figma file key for design token extraction"),
|
|
11
|
+
};
|
|
12
|
+
const ParamsSchema = z.object(paramsShape);
|
|
13
|
+
async function handler(input) {
|
|
14
|
+
const brandDir = new BrandDir(process.cwd());
|
|
15
|
+
if (await brandDir.exists()) {
|
|
16
|
+
return buildResponse({
|
|
17
|
+
what_happened: ".brand/ directory already exists",
|
|
18
|
+
next_steps: [
|
|
19
|
+
"Run brand_status to see current state",
|
|
20
|
+
"Delete .brand/ manually if you want to start over",
|
|
21
|
+
],
|
|
22
|
+
data: { error: ERROR_CODES.ALREADY_EXISTS },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Shared init logic (scaffold + config + empty core identity)
|
|
26
|
+
await brandDir.initBrand({
|
|
27
|
+
schema_version: SCHEMA_VERSION,
|
|
28
|
+
session: 1,
|
|
29
|
+
client_name: input.client_name,
|
|
30
|
+
industry: input.industry,
|
|
31
|
+
website_url: input.website_url,
|
|
32
|
+
figma_file_key: input.figma_file_key,
|
|
33
|
+
created_at: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
const nextSteps = [];
|
|
36
|
+
if (input.website_url) {
|
|
37
|
+
nextSteps.push(`Run brand_extract_web with url "${input.website_url}" to pull colors, fonts, and logo`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
nextSteps.push("Run brand_extract_web with your website URL to pull colors, fonts, and logo");
|
|
41
|
+
}
|
|
42
|
+
if (input.figma_file_key) {
|
|
43
|
+
nextSteps.push(`Run brand_extract_figma with figma_file_key "${input.figma_file_key}" to extract design tokens`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
nextSteps.push("Run brand_extract_figma if you have a Figma file with brand tokens");
|
|
47
|
+
}
|
|
48
|
+
return buildResponse({
|
|
49
|
+
what_happened: `Created .brand/ directory for "${input.client_name}"`,
|
|
50
|
+
next_steps: nextSteps,
|
|
51
|
+
data: {
|
|
52
|
+
client_name: input.client_name,
|
|
53
|
+
brand_dir: ".brand/",
|
|
54
|
+
files_created: ["brand.config.yaml", "core-identity.yaml", "assets/logo/"],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function register(server) {
|
|
59
|
+
server.tool("brand_init", "Initialize a .brand/ directory with empty config scaffold. Low-level tool — prefer brand_start instead, which calls this automatically and also presents extraction options. Only use brand_init directly if you need to set up the directory without running extraction. Returns list of created files.", paramsShape, async (args) => {
|
|
60
|
+
const parsed = safeParseParams(ParamsSchema, args);
|
|
61
|
+
if (!parsed.success)
|
|
62
|
+
return parsed.response;
|
|
63
|
+
return handler(parsed.data);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=brand-init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand-init.js","sourceRoot":"","sources":["../../src/tools/brand-init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand-preflight.d.ts","sourceRoot":"","sources":["../../src/tools/brand-preflight.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAosBzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QAWzC"}
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BrandDir } from "../lib/brand-dir.js";
|
|
3
|
+
import { buildResponse, safeParseParams } from "../lib/response.js";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import * as cheerio from "cheerio";
|
|
7
|
+
import { ERROR_CODES } from "../types/index.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Color utilities
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/** Expand shorthand hex (#abc -> #aabbcc) and lowercase */
|
|
12
|
+
function normalizeHex(hex) {
|
|
13
|
+
let h = hex.toLowerCase().trim();
|
|
14
|
+
if (/^#[0-9a-f]{3}$/.test(h)) {
|
|
15
|
+
h = `#${h[1]}${h[1]}${h[2]}${h[2]}${h[3]}${h[3]}`;
|
|
16
|
+
}
|
|
17
|
+
// Strip alpha channel from 8-digit hex for comparison
|
|
18
|
+
if (/^#[0-9a-f]{8}$/.test(h)) {
|
|
19
|
+
h = h.slice(0, 7);
|
|
20
|
+
}
|
|
21
|
+
return h;
|
|
22
|
+
}
|
|
23
|
+
/** Convert rgb/rgba to hex. Returns null if not parseable. */
|
|
24
|
+
function rgbToHex(rgb) {
|
|
25
|
+
const match = rgb.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
26
|
+
if (!match)
|
|
27
|
+
return null;
|
|
28
|
+
const [, r, g, b] = match;
|
|
29
|
+
const toHex = (n) => parseInt(n, 10).toString(16).padStart(2, "0");
|
|
30
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
31
|
+
}
|
|
32
|
+
/** Extract all color values from CSS text, normalized to hex */
|
|
33
|
+
function extractColors(css) {
|
|
34
|
+
const colors = [];
|
|
35
|
+
// Hex colors
|
|
36
|
+
const hexRe = /#[0-9a-fA-F]{3,8}\b/g;
|
|
37
|
+
let m;
|
|
38
|
+
while ((m = hexRe.exec(css)) !== null) {
|
|
39
|
+
colors.push(normalizeHex(m[0]));
|
|
40
|
+
}
|
|
41
|
+
// rgb/rgba
|
|
42
|
+
const rgbRe = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
|
|
43
|
+
while ((m = rgbRe.exec(css)) !== null) {
|
|
44
|
+
const hex = rgbToHex(m[0]);
|
|
45
|
+
if (hex)
|
|
46
|
+
colors.push(normalizeHex(hex));
|
|
47
|
+
}
|
|
48
|
+
return [...new Set(colors)];
|
|
49
|
+
}
|
|
50
|
+
/** Extract all font-family values from CSS text */
|
|
51
|
+
function extractFontFamilies(css) {
|
|
52
|
+
const families = [];
|
|
53
|
+
const re = /font-family\s*:\s*([^;}"]+)/gi;
|
|
54
|
+
let m;
|
|
55
|
+
while ((m = re.exec(css)) !== null) {
|
|
56
|
+
const raw = m[1].trim();
|
|
57
|
+
// Split by comma and clean up
|
|
58
|
+
for (const part of raw.split(",")) {
|
|
59
|
+
const clean = part.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
60
|
+
if (clean)
|
|
61
|
+
families.push(clean);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...new Set(families)];
|
|
65
|
+
}
|
|
66
|
+
const SYSTEM_FONT_FAMILIES = new Set([
|
|
67
|
+
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
|
68
|
+
"ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded",
|
|
69
|
+
"-apple-system", "blinkmacsystemfont", "segoe ui", "roboto",
|
|
70
|
+
"helvetica neue", "arial", "noto sans", "liberation sans",
|
|
71
|
+
"helvetica", "times new roman", "times", "georgia", "courier new",
|
|
72
|
+
"courier", "verdana", "tahoma", "trebuchet ms", "lucida grande",
|
|
73
|
+
"lucida sans unicode", "lucida console", "monaco", "menlo", "consolas",
|
|
74
|
+
]);
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// CSS extraction from HTML
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
function extractAllCss($) {
|
|
79
|
+
const parts = [];
|
|
80
|
+
// <style> blocks
|
|
81
|
+
$("style").each((_i, el) => {
|
|
82
|
+
const text = $(el).text();
|
|
83
|
+
if (text)
|
|
84
|
+
parts.push(text);
|
|
85
|
+
});
|
|
86
|
+
// style attributes
|
|
87
|
+
$("[style]").each((_i, el) => {
|
|
88
|
+
const style = $(el).attr("style");
|
|
89
|
+
if (style)
|
|
90
|
+
parts.push(style);
|
|
91
|
+
});
|
|
92
|
+
return parts.join("\n");
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Rules compilation
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function compileRules(identity, antiPatterns) {
|
|
98
|
+
const rules = [];
|
|
99
|
+
// Color rules
|
|
100
|
+
rules.push({
|
|
101
|
+
id: "C-HEX",
|
|
102
|
+
description: "All hex colors in CSS must be in the brand palette",
|
|
103
|
+
severity: "soft",
|
|
104
|
+
source: "core-identity.yaml colors",
|
|
105
|
+
checkable: identity.colors.length > 0,
|
|
106
|
+
});
|
|
107
|
+
rules.push({
|
|
108
|
+
id: "C-PRIMARY",
|
|
109
|
+
description: "Primary brand color must appear somewhere in the content",
|
|
110
|
+
severity: "soft",
|
|
111
|
+
source: "core-identity.yaml colors[role=primary]",
|
|
112
|
+
checkable: identity.colors.some((c) => c.role === "primary"),
|
|
113
|
+
});
|
|
114
|
+
rules.push({
|
|
115
|
+
id: "C-PALETTE",
|
|
116
|
+
description: "No off-palette colors (all used colors should match brand palette)",
|
|
117
|
+
severity: "soft",
|
|
118
|
+
source: "core-identity.yaml colors",
|
|
119
|
+
checkable: identity.colors.length > 0,
|
|
120
|
+
});
|
|
121
|
+
// Typography rules
|
|
122
|
+
rules.push({
|
|
123
|
+
id: "T-FAMILY",
|
|
124
|
+
description: "All font-family declarations must reference known brand fonts",
|
|
125
|
+
severity: "soft",
|
|
126
|
+
source: "core-identity.yaml typography",
|
|
127
|
+
checkable: identity.typography.length > 0,
|
|
128
|
+
});
|
|
129
|
+
rules.push({
|
|
130
|
+
id: "T-SYSTEM",
|
|
131
|
+
description: "Brand fonts must be loaded (not only system fonts used as primary)",
|
|
132
|
+
severity: "soft",
|
|
133
|
+
source: "core-identity.yaml typography",
|
|
134
|
+
checkable: identity.typography.length > 0,
|
|
135
|
+
});
|
|
136
|
+
// Logo rules
|
|
137
|
+
rules.push({
|
|
138
|
+
id: "L-PRESENT",
|
|
139
|
+
description: "If brand name appears as text, logo SVG should also be present",
|
|
140
|
+
severity: "soft",
|
|
141
|
+
source: "core-identity.yaml logo + brand.config.yaml client_name",
|
|
142
|
+
checkable: true,
|
|
143
|
+
});
|
|
144
|
+
rules.push({
|
|
145
|
+
id: "L-APPROX",
|
|
146
|
+
description: "No logo approximation (brand name in styled span/div without actual SVG)",
|
|
147
|
+
severity: "soft",
|
|
148
|
+
source: "core-identity.yaml logo",
|
|
149
|
+
checkable: true,
|
|
150
|
+
});
|
|
151
|
+
// Anti-pattern rules from visual-identity.yaml
|
|
152
|
+
let apIndex = 1;
|
|
153
|
+
for (const ap of antiPatterns) {
|
|
154
|
+
const id = ap.preflight_id || `A-${apIndex}`;
|
|
155
|
+
apIndex++;
|
|
156
|
+
rules.push({
|
|
157
|
+
id,
|
|
158
|
+
description: ap.rule,
|
|
159
|
+
severity: ap.severity,
|
|
160
|
+
source: "visual-identity.yaml anti_patterns",
|
|
161
|
+
checkable: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return rules;
|
|
165
|
+
}
|
|
166
|
+
/** Map common anti-pattern phrases to CSS checks */
|
|
167
|
+
const ANTI_PATTERN_MATCHERS = [
|
|
168
|
+
{
|
|
169
|
+
keywords: ["drop shadow", "drop-shadow", "box shadow", "box-shadow"],
|
|
170
|
+
test: (css) => /box-shadow\s*:/i.test(css) ||
|
|
171
|
+
/text-shadow\s*:/i.test(css) ||
|
|
172
|
+
/filter\s*:.*drop-shadow/i.test(css),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
keywords: ["gradient"],
|
|
176
|
+
test: (css) => /(?:linear|radial|conic)-gradient/i.test(css),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
keywords: ["border radius", "border-radius", "rounded corner", "rounded corners", "pill shape"],
|
|
180
|
+
test: (css) => /border-radius\s*:/i.test(css),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
keywords: ["opacity"],
|
|
184
|
+
test: (css) => /\bopacity\s*:\s*(?!1\b|1\.0)/i.test(css),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
keywords: ["blur"],
|
|
188
|
+
test: (css) => /filter\s*:.*blur/i.test(css) || /backdrop-filter\s*:.*blur/i.test(css),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
keywords: ["animation", "animate", "transition"],
|
|
192
|
+
test: (css) => /animation\s*:/i.test(css) || /transition\s*:/i.test(css),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
keywords: ["outline"],
|
|
196
|
+
test: (css) => /\boutline\s*:/i.test(css),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
keywords: ["underline", "text-decoration"],
|
|
200
|
+
test: (css) => /text-decoration\s*:.*underline/i.test(css),
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
keywords: ["italic"],
|
|
204
|
+
test: (css) => /font-style\s*:\s*italic/i.test(css),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
keywords: ["uppercase", "text-transform"],
|
|
208
|
+
test: (css) => /text-transform\s*:\s*uppercase/i.test(css),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
keywords: ["centered body", "center body", "text-align: center", "centered text"],
|
|
212
|
+
test: (css) => /text-align\s*:\s*center/i.test(css),
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
function matchAntiPattern(rule, css) {
|
|
216
|
+
const lower = rule.toLowerCase();
|
|
217
|
+
for (const matcher of ANTI_PATTERN_MATCHERS) {
|
|
218
|
+
if (matcher.keywords.some((kw) => lower.includes(kw))) {
|
|
219
|
+
return matcher.test(css);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Check runners
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
function checkColors(css, identity) {
|
|
228
|
+
const checks = [];
|
|
229
|
+
const brandColors = identity.colors.map((c) => normalizeHex(c.value));
|
|
230
|
+
const usedColors = extractColors(css);
|
|
231
|
+
if (identity.colors.length === 0) {
|
|
232
|
+
checks.push({
|
|
233
|
+
id: "C-HEX",
|
|
234
|
+
status: "warn",
|
|
235
|
+
message: "No brand colors defined — cannot check hex compliance",
|
|
236
|
+
details: "Add colors to core-identity.yaml first",
|
|
237
|
+
});
|
|
238
|
+
checks.push({
|
|
239
|
+
id: "C-PRIMARY",
|
|
240
|
+
status: "warn",
|
|
241
|
+
message: "No brand colors defined",
|
|
242
|
+
});
|
|
243
|
+
checks.push({
|
|
244
|
+
id: "C-PALETTE",
|
|
245
|
+
status: "warn",
|
|
246
|
+
message: "No brand palette to check against",
|
|
247
|
+
});
|
|
248
|
+
return checks;
|
|
249
|
+
}
|
|
250
|
+
// C-HEX: Check unknown colors
|
|
251
|
+
const unknownColors = usedColors.filter((c) => !brandColors.includes(c));
|
|
252
|
+
// Filter out common non-brand colors (pure black, white)
|
|
253
|
+
const meaningfulUnknown = unknownColors.filter((c) => !["#000000", "#ffffff"].includes(c));
|
|
254
|
+
if (meaningfulUnknown.length === 0) {
|
|
255
|
+
checks.push({
|
|
256
|
+
id: "C-HEX",
|
|
257
|
+
status: "pass",
|
|
258
|
+
message: `All ${usedColors.length} colors are on-palette`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
checks.push({
|
|
263
|
+
id: "C-HEX",
|
|
264
|
+
status: "warn",
|
|
265
|
+
message: `${meaningfulUnknown.length} color(s) not in brand palette`,
|
|
266
|
+
details: meaningfulUnknown.slice(0, 10).join(", "),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// C-PRIMARY: Primary color used
|
|
270
|
+
const primaryColor = identity.colors.find((c) => c.role === "primary");
|
|
271
|
+
if (!primaryColor) {
|
|
272
|
+
checks.push({
|
|
273
|
+
id: "C-PRIMARY",
|
|
274
|
+
status: "warn",
|
|
275
|
+
message: "No primary color role assigned in brand identity",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const pHex = normalizeHex(primaryColor.value);
|
|
280
|
+
if (usedColors.includes(pHex)) {
|
|
281
|
+
checks.push({
|
|
282
|
+
id: "C-PRIMARY",
|
|
283
|
+
status: "pass",
|
|
284
|
+
message: `Primary color ${pHex} is used`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
checks.push({
|
|
289
|
+
id: "C-PRIMARY",
|
|
290
|
+
status: "warn",
|
|
291
|
+
message: `Primary color ${pHex} not found in content`,
|
|
292
|
+
details: `Brand primary is ${primaryColor.name} (${pHex})`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// C-PALETTE: Off-palette summary
|
|
297
|
+
if (meaningfulUnknown.length === 0) {
|
|
298
|
+
checks.push({
|
|
299
|
+
id: "C-PALETTE",
|
|
300
|
+
status: "pass",
|
|
301
|
+
message: "No off-palette colors detected",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
checks.push({
|
|
306
|
+
id: "C-PALETTE",
|
|
307
|
+
status: "warn",
|
|
308
|
+
message: `${meaningfulUnknown.length} off-palette color(s) found`,
|
|
309
|
+
details: `Brand palette: ${brandColors.join(", ")} | Off-palette: ${meaningfulUnknown.slice(0, 5).join(", ")}${meaningfulUnknown.length > 5 ? ` (+${meaningfulUnknown.length - 5} more)` : ""}`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return checks;
|
|
313
|
+
}
|
|
314
|
+
function checkTypography(css, identity) {
|
|
315
|
+
const checks = [];
|
|
316
|
+
const brandFonts = identity.typography.map((t) => t.family.toLowerCase());
|
|
317
|
+
const usedFamilies = extractFontFamilies(css).map((f) => f.toLowerCase());
|
|
318
|
+
if (identity.typography.length === 0) {
|
|
319
|
+
checks.push({
|
|
320
|
+
id: "T-FAMILY",
|
|
321
|
+
status: "warn",
|
|
322
|
+
message: "No brand fonts defined — cannot check typography compliance",
|
|
323
|
+
});
|
|
324
|
+
checks.push({
|
|
325
|
+
id: "T-SYSTEM",
|
|
326
|
+
status: "warn",
|
|
327
|
+
message: "No brand fonts defined",
|
|
328
|
+
});
|
|
329
|
+
return checks;
|
|
330
|
+
}
|
|
331
|
+
if (usedFamilies.length === 0) {
|
|
332
|
+
checks.push({
|
|
333
|
+
id: "T-FAMILY",
|
|
334
|
+
status: "warn",
|
|
335
|
+
message: "No font-family declarations found in content",
|
|
336
|
+
});
|
|
337
|
+
checks.push({
|
|
338
|
+
id: "T-SYSTEM",
|
|
339
|
+
status: "pass",
|
|
340
|
+
message: "No font declarations to check",
|
|
341
|
+
});
|
|
342
|
+
return checks;
|
|
343
|
+
}
|
|
344
|
+
// T-FAMILY: Check that declared fonts are brand fonts
|
|
345
|
+
const nonBrand = usedFamilies.filter((f) => !brandFonts.includes(f) && !SYSTEM_FONT_FAMILIES.has(f));
|
|
346
|
+
if (nonBrand.length === 0) {
|
|
347
|
+
checks.push({
|
|
348
|
+
id: "T-FAMILY",
|
|
349
|
+
status: "pass",
|
|
350
|
+
message: "All font-family declarations use brand fonts or system fallbacks",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
checks.push({
|
|
355
|
+
id: "T-FAMILY",
|
|
356
|
+
status: "warn",
|
|
357
|
+
message: `${nonBrand.length} non-brand font(s) detected`,
|
|
358
|
+
details: nonBrand.join(", "),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// T-SYSTEM: Check if ONLY system fonts are used (no brand fonts loaded)
|
|
362
|
+
const hasBrandFont = usedFamilies.some((f) => brandFonts.includes(f));
|
|
363
|
+
if (hasBrandFont) {
|
|
364
|
+
checks.push({
|
|
365
|
+
id: "T-SYSTEM",
|
|
366
|
+
status: "pass",
|
|
367
|
+
message: "Brand font(s) are loaded",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
checks.push({
|
|
372
|
+
id: "T-SYSTEM",
|
|
373
|
+
status: "warn",
|
|
374
|
+
message: "Only system fonts detected — brand fonts may not be loaded",
|
|
375
|
+
details: `Expected: ${identity.typography.map((t) => t.family).join(", ")} | Found: ${usedFamilies.join(", ")}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return checks;
|
|
379
|
+
}
|
|
380
|
+
function checkLogo($, identity, clientName) {
|
|
381
|
+
const checks = [];
|
|
382
|
+
const hasSvg = $("svg").length > 0;
|
|
383
|
+
const hasImgLogo = $("img[src*='logo'], img[alt*='logo']").length > 0;
|
|
384
|
+
const hasLogoElement = hasSvg || hasImgLogo;
|
|
385
|
+
// Check if brand name appears as text
|
|
386
|
+
const bodyText = $("body").text() || $.text();
|
|
387
|
+
const nameInText = clientName
|
|
388
|
+
? bodyText.toLowerCase().includes(clientName.toLowerCase())
|
|
389
|
+
: false;
|
|
390
|
+
// L-PRESENT
|
|
391
|
+
if (!nameInText) {
|
|
392
|
+
checks.push({
|
|
393
|
+
id: "L-PRESENT",
|
|
394
|
+
status: "pass",
|
|
395
|
+
message: "Brand name not found as text (no logo check needed)",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else if (hasLogoElement) {
|
|
399
|
+
checks.push({
|
|
400
|
+
id: "L-PRESENT",
|
|
401
|
+
status: "pass",
|
|
402
|
+
message: "Brand name and logo element both present",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
checks.push({
|
|
407
|
+
id: "L-PRESENT",
|
|
408
|
+
status: "warn",
|
|
409
|
+
message: "Brand name appears as text but no logo SVG/image found",
|
|
410
|
+
details: `"${clientName}" found in text but no <svg> or logo <img> detected`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// L-APPROX: Check for logo approximation patterns
|
|
414
|
+
let approxFound = false;
|
|
415
|
+
if (clientName) {
|
|
416
|
+
const nameLower = clientName.toLowerCase();
|
|
417
|
+
$("span, div, a, h1, h2, h3").each((_i, el) => {
|
|
418
|
+
const text = $(el).text().trim().toLowerCase();
|
|
419
|
+
const style = $(el).attr("style") || "";
|
|
420
|
+
// If element text IS the brand name (not just contains it in a paragraph)
|
|
421
|
+
// and has heavy styling — likely a logo approximation
|
|
422
|
+
if (text === nameLower &&
|
|
423
|
+
(style.includes("font-weight") ||
|
|
424
|
+
style.includes("font-size") ||
|
|
425
|
+
style.includes("letter-spacing") ||
|
|
426
|
+
style.includes("text-transform"))) {
|
|
427
|
+
// Only flag if there's no SVG sibling or child
|
|
428
|
+
const parent = $(el).parent();
|
|
429
|
+
if (parent.find("svg").length === 0 && !hasSvg) {
|
|
430
|
+
approxFound = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (approxFound) {
|
|
436
|
+
checks.push({
|
|
437
|
+
id: "L-APPROX",
|
|
438
|
+
status: "warn",
|
|
439
|
+
message: "Possible logo approximation detected — brand name styled as text without SVG",
|
|
440
|
+
details: "Use actual logo SVG instead of styling brand name with CSS",
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
checks.push({
|
|
445
|
+
id: "L-APPROX",
|
|
446
|
+
status: "pass",
|
|
447
|
+
message: "No logo approximation patterns detected",
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return checks;
|
|
451
|
+
}
|
|
452
|
+
function checkAntiPatterns(css, antiPatterns) {
|
|
453
|
+
const checks = [];
|
|
454
|
+
let idx = 1;
|
|
455
|
+
for (const ap of antiPatterns) {
|
|
456
|
+
const id = ap.preflight_id || `A-${idx}`;
|
|
457
|
+
idx++;
|
|
458
|
+
const matched = matchAntiPattern(ap.rule, css);
|
|
459
|
+
if (matched) {
|
|
460
|
+
checks.push({
|
|
461
|
+
id,
|
|
462
|
+
status: ap.severity === "hard" ? "fail" : "warn",
|
|
463
|
+
message: ap.rule,
|
|
464
|
+
details: `Anti-pattern matched (severity: ${ap.severity})`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
checks.push({
|
|
469
|
+
id,
|
|
470
|
+
status: "pass",
|
|
471
|
+
message: ap.rule,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return checks;
|
|
476
|
+
}
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Main handlers
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
async function handleRules(brandDir) {
|
|
481
|
+
const identity = await brandDir.readCoreIdentity();
|
|
482
|
+
let antiPatterns = [];
|
|
483
|
+
if (await brandDir.hasVisualIdentity()) {
|
|
484
|
+
const vi = await brandDir.readVisualIdentity();
|
|
485
|
+
antiPatterns = vi.anti_patterns || [];
|
|
486
|
+
}
|
|
487
|
+
const rules = compileRules(identity, antiPatterns);
|
|
488
|
+
return buildResponse({
|
|
489
|
+
what_happened: `Compiled ${rules.length} preflight rules from brand system`,
|
|
490
|
+
next_steps: [
|
|
491
|
+
"Run brand_preflight with mode 'check' and HTML content to run compliance checks",
|
|
492
|
+
rules.some((r) => !r.checkable)
|
|
493
|
+
? "Some rules are not checkable — add more data to core-identity.yaml"
|
|
494
|
+
: "All rules are checkable",
|
|
495
|
+
],
|
|
496
|
+
data: {
|
|
497
|
+
rule_count: rules.length,
|
|
498
|
+
rules: rules,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function handleCheck(brandDir, html) {
|
|
503
|
+
const identity = await brandDir.readCoreIdentity();
|
|
504
|
+
let antiPatterns = [];
|
|
505
|
+
if (await brandDir.hasVisualIdentity()) {
|
|
506
|
+
const vi = await brandDir.readVisualIdentity();
|
|
507
|
+
antiPatterns = vi.anti_patterns || [];
|
|
508
|
+
}
|
|
509
|
+
let clientName = "";
|
|
510
|
+
try {
|
|
511
|
+
const config = await brandDir.readConfig();
|
|
512
|
+
clientName = config.client_name || "";
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Non-critical — logo checks just won't match by name
|
|
516
|
+
}
|
|
517
|
+
// Parse HTML
|
|
518
|
+
const $ = cheerio.load(html);
|
|
519
|
+
const css = extractAllCss($);
|
|
520
|
+
// Run all checks
|
|
521
|
+
const checks = [
|
|
522
|
+
...checkColors(css, identity),
|
|
523
|
+
...checkTypography(css, identity),
|
|
524
|
+
...checkLogo($, identity, clientName),
|
|
525
|
+
...checkAntiPatterns(css, antiPatterns),
|
|
526
|
+
];
|
|
527
|
+
// Compute summary
|
|
528
|
+
const pass = checks.filter((c) => c.status === "pass").length;
|
|
529
|
+
const warn = checks.filter((c) => c.status === "warn").length;
|
|
530
|
+
const fail = checks.filter((c) => c.status === "fail").length;
|
|
531
|
+
const overall = fail > 0 ? "FAIL" : warn > 0 ? "WARN" : "PASS";
|
|
532
|
+
const nextSteps = [];
|
|
533
|
+
if (fail > 0)
|
|
534
|
+
nextSteps.push("Fix failing checks (hard anti-patterns) before shipping");
|
|
535
|
+
if (warn > 0)
|
|
536
|
+
nextSteps.push("Review warnings — some may be intentional deviations");
|
|
537
|
+
if (fail === 0 && warn === 0)
|
|
538
|
+
nextSteps.push("All checks pass — content is brand-compliant");
|
|
539
|
+
return buildResponse({
|
|
540
|
+
what_happened: `Preflight ${overall}: ${pass} pass, ${warn} warn, ${fail} fail`,
|
|
541
|
+
next_steps: nextSteps,
|
|
542
|
+
data: {
|
|
543
|
+
overall,
|
|
544
|
+
summary: { pass, warn, fail },
|
|
545
|
+
checks: checks,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// Tool registration
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
const paramsShape = {
|
|
553
|
+
html: z
|
|
554
|
+
.string()
|
|
555
|
+
.describe("HTML to validate: either a full HTML string (with <style> blocks) or a file path ending in .html (e.g. 'output.html')"),
|
|
556
|
+
mode: z
|
|
557
|
+
.enum(["check", "rules"])
|
|
558
|
+
.default("check")
|
|
559
|
+
.describe("'check' (default): validates HTML against all brand rules. 'rules': lists all active rules without running checks."),
|
|
560
|
+
};
|
|
561
|
+
const ParamsSchema = z.object(paramsShape);
|
|
562
|
+
async function handler(input) {
|
|
563
|
+
const brandDir = new BrandDir(process.cwd());
|
|
564
|
+
if (!(await brandDir.exists())) {
|
|
565
|
+
return buildResponse({
|
|
566
|
+
what_happened: "No .brand/ directory found",
|
|
567
|
+
next_steps: ["Run brand_start to create a brand system first"],
|
|
568
|
+
data: { error: ERROR_CODES.NOT_INITIALIZED },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
const mode = input.mode || "check";
|
|
572
|
+
if (mode === "rules") {
|
|
573
|
+
return handleRules(brandDir);
|
|
574
|
+
}
|
|
575
|
+
// Resolve HTML: if it looks like a file path, read it
|
|
576
|
+
let html = input.html;
|
|
577
|
+
if (!html.includes("<") &&
|
|
578
|
+
(html.endsWith(".html") || html.endsWith(".htm") || html.startsWith("/"))) {
|
|
579
|
+
const resolvedPath = resolve(process.cwd(), html);
|
|
580
|
+
if (!resolvedPath.startsWith(resolve(process.cwd()))) {
|
|
581
|
+
return buildResponse({
|
|
582
|
+
what_happened: "File path must be within the current working directory",
|
|
583
|
+
next_steps: ["Provide an HTML string or a file path within your project"],
|
|
584
|
+
data: { error: ERROR_CODES.PATH_OUTSIDE_CWD },
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
html = await readFile(resolvedPath, "utf-8");
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return buildResponse({
|
|
592
|
+
what_happened: `Could not read file: ${input.html}`,
|
|
593
|
+
next_steps: ["Provide valid HTML content or a readable file path"],
|
|
594
|
+
data: { error: ERROR_CODES.FILE_NOT_FOUND, path: input.html },
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return handleCheck(brandDir, html);
|
|
599
|
+
}
|
|
600
|
+
export function register(server) {
|
|
601
|
+
server.tool("brand_preflight", "Check HTML/CSS against brand rules — catches off-brand colors, wrong fonts, missing logo, and anti-pattern violations (drop shadows, gradients, etc.). Pass an HTML string or file path. Mode 'check' (default) runs all compliance checks and returns pass/warn/fail per rule. Mode 'rules' lists all active preflight rules without checking content. Use after generating any visual content to validate brand compliance. Returns overall status and per-check details. NOT for scoring content copy — use brand_audit_content. NOT for brand directory validation — use brand_audit.", paramsShape, async (args) => {
|
|
602
|
+
const parsed = safeParseParams(ParamsSchema, args);
|
|
603
|
+
if (!parsed.success)
|
|
604
|
+
return parsed.response;
|
|
605
|
+
return handler(parsed.data);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
//# sourceMappingURL=brand-preflight.js.map
|