better-design 0.31.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1232,6 +1232,7 @@ var init_tool_names = __esm({
1232
1232
  "get-design-config",
1233
1233
  "get-design-system-kit",
1234
1234
  "get-design-system-status",
1235
+ "get-inspiration",
1235
1236
  "get-jig-guide",
1236
1237
  "get-motion-guide",
1237
1238
  "get-react-native-guide",
@@ -1251,6 +1252,7 @@ var init_tool_names = __esm({
1251
1252
  "review-dashboard-structure",
1252
1253
  "review-illustration",
1253
1254
  "review-ui-code",
1255
+ "send-feedback",
1254
1256
  "set-design-config",
1255
1257
  "update-feature-map"
1256
1258
  ];
@@ -1269,6 +1271,7 @@ var init_tool_names = __esm({
1269
1271
  "get-design-config",
1270
1272
  "get-design-system-kit",
1271
1273
  "get-design-system-status",
1274
+ "get-inspiration",
1272
1275
  "get-jig-guide",
1273
1276
  "get-motion-guide",
1274
1277
  "get-react-native-guide",
@@ -1288,6 +1291,7 @@ var init_tool_names = __esm({
1288
1291
  "review-dashboard-structure",
1289
1292
  "review-illustration",
1290
1293
  "review-ui-code",
1294
+ "send-feedback",
1291
1295
  "set-design-config",
1292
1296
  "update-feature-map"
1293
1297
  ]);
@@ -1374,6 +1378,7 @@ Better Design provides design systems, design principles and UI review checks fo
1374
1378
 
1375
1379
  - find-design-system searches the catalog. preview-design-system shows one system. get-design-system-kit returns its install command or CSS kit.
1376
1380
  - create-design-system generates a custom system. get-design-system-status reports its progress.
1381
+ - get-inspiration returns real App Store screenshot sets from shipped apps as images.
1377
1382
  - get-ui-principle, get-ux-principle and the platform guides return design principles and best practices on a topic.
1378
1383
  - get-review-rules, check-comprehension, inspect-spacing and review-dashboard-structure check finished interfaces.
1379
1384
  - find-icon-library, find-icons and install-icons return icon component files.`;
@@ -1386,9 +1391,9 @@ Better Design provides design systems, design principles and UI review checks fo
1386
1391
  "get-shopify-guide": "Returns Better Design's best practices for Shopify Liquid and Online Store 2.0 themes: theme architecture, Liquid objects and filters, section schema, localization, accessibility, performance and Theme Check validation, with links to Shopify's documentation. Pass `topic`, `query`, or nothing for the index of topics.",
1387
1392
  "get-motion-guide": "Returns best practices and components for building a product demo or promo video in code with Remotion: the brief, brand and motion tokens, format profiles, captions, pacing, rendering and review. `scaffold: true` returns a complete Remotion starter project. `component` returns one video component's source and install command, and no arguments lists the components. The components come from snapcn (MIT) through the Better Design registry.",
1388
1393
  "get-swiftui-guide": "Returns best practices and Swift examples for SwiftUI interfaces: layout and view composition, animation and gestures, Metal shader effects and the platform APIs they use. Pass `topic`, `query`, or nothing for the index of topics.",
1389
- "get-review-rules": "Returns Better Design's review rules for UI code, each with a severity: accessibility (WCAG 2.1), visual design, animation, content, comprehension and AI features. Pass `category` for one group. Use it when the user wants UI code reviewed. `maxTokens` limits the length of the response.",
1394
+ "get-review-rules": "Returns Better Design's review rules for UI code, each with a severity: accessibility (WCAG 2.1), visual design, animation, content, comprehension and AI features. Pass `category` for one group. Use it when the user wants UI code reviewed. `maxTokens` limits the length of the response. The rules are read against source, so a clean self-review is not visual approval.",
1390
1395
  "get-widget-guide": "Returns a build brief for an interactive widget on an article or landing page, such as a calculator, estimator, converter, quiz or checklist. Pass what is known about the widget. The response lists any details still missing, and once the details are complete it returns the brief with rules for calculation accuracy, form behavior, search visibility, performance and privacy.",
1391
- "check-comprehension": "Checks interface copy for clarity with three tests: whether a person outside the team understands it, whether the screen can be explained in 15 seconds, and whether a screenshot alone says what the screen does. Pass the headline, body and action labels, or the markup. Returns each finding with a severity and a suggested fix. `knownTerms` lists words the audience already knows. Better Design runs the checks itself and calls no other API.",
1396
+ "check-comprehension": 'Checks interface copy for clarity with three tests: whether a person outside the team understands it, whether the screen can be explained in 15 seconds, and whether a screenshot alone says what the screen does. Pass the headline, body and action labels, or the markup. Returns each finding with a severity and a suggested fix. `knownTerms` lists words the audience already knows. `surface: "slide"` adds a density test for slides. It reads text only, not screenshots or visual hierarchy, so a pass is not visual approval. Better Design runs the checks itself and calls no other API.',
1392
1397
  "review-dashboard-structure": "Reviews the source of a dashboard or overview page against a fixed rubric: complete source, unchanged data, supported claims, hierarchy, scan order and use of the installed Sidebar, StatCard and Table components. Pass the page source, the data fixture, its interpretation and the component sources. Returns PASS, or FAIL with every finding as a tool error.",
1393
1398
  "finalize-dashboard-review": "Captures a deployed dashboard at a public HTTPS URL in Better Design's own browser at 390, 1440 and 1728 pixels wide, compares it with the submitted source, data and components, and returns a receipt tied to that deployment. The receipt reports hierarchy, component, copy and spacing faults and any hidden data fields.",
1394
1399
  "find-icon-library": `Searches icon libraries by style, mood or use case, such as "minimal clean professional", and returns the closest matches with a note on each library's style.`,
@@ -1396,7 +1401,8 @@ Better Design provides design systems, design principles and UI review checks fo
1396
1401
  "install-icons": "Fetches the named icons from the public Iconify API and returns one React component file per icon, plus an index.ts barrel, as text for the caller to save. It writes no files. Takes a `libraryId` and icon names from find-icons.",
1397
1402
  "detect-icon-library": "Reads the contents of the `.library.json` file that an earlier icon install left in a project and returns the icon library the project uses and the icons it has installed, or reports that none is recorded.",
1398
1403
  "find-design-system": "Searches Better Design's catalog of design systems by industry, personality or a description of the interface, and returns ranked matches with a preview link for each. Use it when the user wants a design system or a visual direction for an app, site, page, dashboard or presentation.",
1399
- "get-design-system-kit": "Returns the build kit for a catalog design system or a generated project. `target` picks the format: `react` returns one shadcn install command for every component and the tokens, `html` returns framework-free token and component CSS for inline canvases, `react-native` and `swiftui` return mobile kits, `shopify` returns Liquid theme files, and `dtcg` returns a W3C Design Tokens document. `component` narrows the React install to one component.",
1404
+ "get-design-system-kit": "Returns the build kit for a catalog design system or a generated project. `target` picks the format: `react` returns one shadcn install command for every component and the tokens, `html` returns framework-free token and component CSS for inline canvases, `react-native` and `swiftui` return mobile kits, `shopify` returns Liquid theme files, `dtcg` returns a W3C Design Tokens document, and `store-screenshots` returns slide files and a render script for App Store and Google Play screenshots. `component` narrows the React install to one component.",
1405
+ "get-inspiration": 'Returns real reference designs from Better Design\'s library as images. `type` picks the collection: today `app-store-screenshots`, the App Store screenshot sets of shipped apps, each with its title and category. `category`, such as "Productivity", narrows the sets, and `limit` sets how many come back, 1 to 6.',
1400
1406
  "create-design-system": "Starts generating a custom design system from a description of the product and its look, and returns the project id and project URL at once. Generation runs in the background and typically takes 1 to 3 minutes. get-design-system-status reports progress and returns the install command when it finishes. `baseDesignSystemId` starts from a catalog system, and `brandCss` builds from an existing brand token sheet, such as the output of extract-design-system. Each call uses one credit from the user's Better Design account. Anyone with the registry or preview URL can view the generated source and previews.",
1401
1407
  "get-design-system-status": "Reports the progress of a design system started with create-design-system. While generation runs it returns the current stage. When generation finishes it returns the live preview URL and the `npx shadcn add` install command.",
1402
1408
  "preview-design-system": "Returns a visual preview of a catalog design system: its palette, typography, buttons, card, input and badge, with a link to the full live showcase. Takes an id from find-design-system.",
@@ -1406,12 +1412,13 @@ Better Design provides design systems, design principles and UI review checks fo
1406
1412
  "extract-design-system": "Reads design tokens from CSS the caller passes in and returns the semantic palette, radius, fonts and shadow scale, with a token sheet that converts every color to `oklch()`. It accepts hex, rgb, hsl, named and oklch colors across `:root`, `.dark` and `@theme` blocks. `sources` merges several inputs, including a sample of computed styles from a live page. The server cannot read files, so the caller passes the text.",
1407
1413
  "extract-from-url": "Fetches a public web page, reads its stylesheets and renders it in a headless browser to sample the colors, fonts and radii of its body, headings, links, buttons, inputs and cards, then returns the brand as an oklch token sheet. It reads the page the way a browser does and calls no other API. A call takes about 10 to 45 seconds.",
1408
1414
  "review-illustration": "Renders an SVG illustration in a browser and returns its visible faults against Better Design's illustration principles, such as open shapes, floating parts, inconsistent light and contents outside their container. Pass the SVG and what it is meant to show. Pass the returned `reviewToken` back to review the same drawing again. SVG that carries scripts, event handlers or remote references is refused.",
1409
- "inspect-spacing": "Measures spacing in a rendered interface from DOM geometry and returns the spacing faults with a correction for each. The first call returns a read-only capture script to run in the page. The second call takes the captured states, such as viewport widths or hover and focus states, and returns the measurements and findings.",
1415
+ "inspect-spacing": "Measures spacing in a rendered interface from DOM geometry and returns the spacing faults with a correction for each. The first call returns a read-only capture script to run in the page. The second call takes the captured states, such as viewport widths or hover and focus states, and returns the measurements and findings. It measures geometry only, so a clean result is not visual approval.",
1410
1416
  "build-feature-map": "Drafts a map of a product's features and flows from its repository file list and returns it as JSON for .better-design/feature-map.json. Routes become screens, nested routes become flows, and components, API routes and hooks join the step they share a name with. Pass every path from `git ls-files` as `files`.",
1411
1417
  "read-feature-map": "Returns the flows in a feature map that match a flow name, a feature or a file path, with the screen, route, components and data files of every step. Pass the contents of .better-design/feature-map.json as `map`. With no filter it lists every flow.",
1412
1418
  "update-feature-map": "Applies one change to a feature map, such as adding, renaming or removing a feature, or setting, renaming or removing a flow, and returns the updated map.",
1413
1419
  "get-team-rules": "Returns the Markdown rules the user's Better Design team wrote for how their product is built and reviewed, or a note that the team has none yet.",
1414
1420
  "review-ui-code": "Reviews React component source and CSS and returns findings with line numbers and fixes: native elements that bypass the installed design system, undefined font variables, hardcoded colors and raw palette classes, generated-UI patterns, and WCAG faults such as missing alt text or accessible names. The same input always returns the same findings.",
1421
+ "send-feedback": "Sends Better Design a report of a bug, a missing feature or wrong output in its tools or API: the kind of problem, the tool or endpoint, a one-sentence summary, and optionally what was tried, expected and returned. A person reads every report. It is available only on connections that use a Better Design API key.",
1415
1422
  "create-figma-prototype": "Reads a frame from the Figma file that has the Better Design plugin open and returns it as one HTML page built from the design system's components, with a report of every layer that could not be mapped. Name a frame, or omit the name to use the current selection."
1416
1423
  };
1417
1424
  }
@@ -1891,6 +1898,7 @@ function buildKitOutputSchema(enableShopifyKit = true) {
1891
1898
  componentExampleHtml: z5.string().min(1).optional().describe("HTML target only. Starter markup that demonstrates the required .ds wrapper and ds-* component classes."),
1892
1899
  tokensJson: z5.string().min(1).optional().describe("DTCG target only. The design tokens as a W3C Design Tokens (DTCG) document. Write it to tokens.json; every token tool reads this format, including Style Dictionary, Tokens Studio, Terrazzo, and the Figma plugins that create variables."),
1893
1900
  nativeFiles: z5.array(z5.object({ path: z5.string().min(1), content: z5.string().min(1) })).optional().describe("React Native and SwiftUI targets only. Write each file verbatim at its path: this system's stored theme and material, and the primitives that read it."),
1901
+ screenshotFiles: z5.array(z5.object({ path: z5.string().min(1), content: z5.string().min(1) })).optional().describe("Store screenshots target only. Write each file verbatim at its path, then follow buildInstructions to capture screens, write the slides and render the PNG files."),
1894
1902
  unsupportedTokens: z5.array(z5.object({
1895
1903
  name: z5.string().min(1),
1896
1904
  value: z5.string().min(1),
@@ -4270,12 +4278,15 @@ var init_review_rules_description = __esm({
4270
4278
  "content",
4271
4279
  "comprehension",
4272
4280
  "ai-features",
4281
+ "coverage-gaps",
4273
4282
  "all"
4274
4283
  ];
4275
4284
  REVIEW_RULES_DESCRIPTION = `Retrieves accessibility (WCAG 2.1), visual design, animation, content, comprehension, and AI feature review rules for analyzing code.
4276
4285
 
4277
4286
  You MUST call this after writing UI code to self-review your output, then fix every critical and serious issue BEFORE presenting the result.
4278
4287
 
4288
+ It returns rules to check against. It approves nothing: a clean self-review against these rules is not visual approval, because the rules are read against source, not against what renders.
4289
+
4279
4290
  ${REDESIGN_SCOPE_CONTRACT}
4280
4291
 
4281
4292
  ${DESIGN_TRADEOFF_RULE}
@@ -4318,6 +4329,8 @@ ${DESIGN_TRADEOFF_RULE}
4318
4329
  - Critical: Output saved or sent without the user accepting it, silent failures, no manual path when AI is unavailable, user data read before consent, invented citations or unearned certainty
4319
4330
  - Serious: AI output with no visible marker, long runs with no sign or a Stop that deletes partial output, refusals that name no topic and offer no alternative, feedback controls with no destination
4320
4331
 
4332
+ Every rule carries an ID (\`rule/\u2026\`), a source and an exceptions line. Cite the ID in each finding. The \`coverage-gaps\` category lists the surfaces that have no rule yet.
4333
+
4321
4334
  Use maxTokens to limit rule content (recommended: 5000\u201310000). Required workflow guidance is always returned in full.`;
4322
4335
  }
4323
4336
  });
@@ -5156,6 +5169,22 @@ These rules were read against source. They cannot see layout. Three checks remai
5156
5169
  2. Call \`inspect-spacing\` on the rendered DOM at 1440, 1728 and 390 wide. The first call returns a capture script and measures nothing; the layout is measured only when a second call, with the captured states, returns status \`completed\`. Rules read code; only measurement catches a label sitting 9px off its own circle, an active state that is 2px wider than its siblings and shifts the row, or a sidebar that leaves page background beside it on a wide screen.
5157
5170
  3. For dashboards, deploy the exact source to a public HTTPS preview, then call \`finalize-dashboard-review\` with that URL, the exact complete and visible source, unchanged fixture, and installed component source. Better Design's own browser captures the preview at 390, 1440, and 1728, content-addresses the live deployment, derives the factual interpretation, and issues the only valid review receipt. No trusted browser capture means incomplete. An invalid deployment revision, invalid geometry, or hidden fixture field also blocks approval. Any later source or deployment edit voids the receipt. Then confirm every nav item, link, tab and button works.
5158
5171
 
5172
+ ## Presentations
5173
+
5174
+ A deck passes only on rendered evidence. For every slide:
5175
+
5176
+ 1. Call \`check-comprehension\` with \`surface: "slide"\`. Keep one idea per slide and fix every density finding.
5177
+ 2. Render the deck and review a screenshot of every slide: the title reads first, one element holds the eye, and nothing competes with it. Text checks and \`inspect-spacing\` cannot see this, so no score stands in for the screenshot.
5178
+
5179
+ ## Check the rules below in two passes
5180
+
5181
+ An agent that reviews its own screen in one pass tends to approve it. Split the review:
5182
+
5183
+ 1. Per rule: give each rule section below (each \`# \` heading) to its own subagent, in a clean context that holds only that section and the source. A host without subagents checks one section at a time and writes down its findings before it reads the next.
5184
+ 2. Skeptic pass: give every flag to one fresh subagent that tries to disprove it. It keeps a flag only when it can cite the file and line that break the rule.
5185
+
5186
+ Cite the rule ID with every finding: the \`rule/\u2026\` line under each rule heading. Read its Exceptions line before you flag. A surface listed under coverage gaps has no rule yet, so say that instead of inventing one.
5187
+
5159
5188
  Fix every critical and serious finding before you present.`;
5160
5189
  }
5161
5190
  });
@@ -5179,6 +5208,8 @@ function looksLikeCard(candidate) {
5179
5208
  }
5180
5209
  function redactSecrets(value) {
5181
5210
  let out = value.replace(/\u0000/g, "");
5211
+ for (const pattern of CREDENTIAL_VALUE_PATTERNS) out = out.replace(pattern, "[redacted]");
5212
+ out = out.replace(JWT_PATTERN, "$1[redacted]");
5182
5213
  for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, "[redacted]");
5183
5214
  out = out.replace(EMAIL_PATTERN, "[redacted-email]");
5184
5215
  out = out.replace(
@@ -5197,7 +5228,7 @@ function redactUrl(raw) {
5197
5228
  return "[redacted]";
5198
5229
  }
5199
5230
  }
5200
- var SECRET_PATTERNS, EMAIL_PATTERN, CARD_CANDIDATE_PATTERN, PHONE_PATTERNS;
5231
+ var SECRET_PATTERNS, CREDENTIAL_VALUE_PATTERNS, JWT_PATTERN, EMAIL_PATTERN, CARD_CANDIDATE_PATTERN, PHONE_PATTERNS;
5201
5232
  var init_redact = __esm({
5202
5233
  "../shared/src/redact.ts"() {
5203
5234
  "use strict";
@@ -5213,9 +5244,9 @@ var init_redact = __esm({
5213
5244
  /\bwhsec_[A-Za-z0-9]{16,}/g,
5214
5245
  /\bAIza[A-Za-z0-9_-]{30,}/g,
5215
5246
  /\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
5216
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g,
5217
- // JWT
5218
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
5247
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g
5248
+ ];
5249
+ CREDENTIAL_VALUE_PATTERNS = [
5219
5250
  // key=value forms for the names that carry credentials. A quoted value is
5220
5251
  // taken whole, so a passphrase with spaces and a short password both go:
5221
5252
  // the old form needed eight characters and no spaces, which let
@@ -5223,8 +5254,13 @@ var init_redact = __esm({
5223
5254
  // through into a corpus held for 180 days. [BD-697]
5224
5255
  /(?:\b|_)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret(?:[_-]?key)?|password|passwd|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9._~+/-]+)/gi,
5225
5256
  // Authorization headers separate the scheme from the token with a space.
5226
- /\b(?:bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi
5257
+ /\b(?:bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
5258
+ // A token in a query string, such as the claim link an agent gets when its
5259
+ // free budget runs out. That link hands the agent's keys to whoever opens
5260
+ // it. [BD-956]
5261
+ /[?&]token=[A-Za-z0-9_-]{16,}/gi
5227
5262
  ];
5263
+ JWT_PATTERN = /(?<![A-Za-z0-9_-])(?=[A-Za-z0-9_-]*\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-])([A-Za-z0-9_-]*?)(?<![A-Za-z0-9_])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g;
5228
5264
  EMAIL_PATTERN = /(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
5229
5265
  CARD_CANDIDATE_PATTERN = /\b(?:\d[ -]?){12,18}\d\b/g;
5230
5266
  PHONE_PATTERNS = [
@@ -5358,7 +5394,7 @@ var init_extract = __esm({
5358
5394
  });
5359
5395
 
5360
5396
  // ../shared/src/mcp/comprehension/lexicon.ts
5361
- var TECHNICAL_TERMS, PRODUCT_TERMS, KNOWN_ACRONYMS, VAGUE_ACTIONS, MAX_SENTENCE_WORDS, MAX_SCREEN_WORDS, MAX_ACTIONS, EXPLAIN_BUDGET_SECONDS;
5397
+ var TECHNICAL_TERMS, PRODUCT_TERMS, KNOWN_ACRONYMS, VAGUE_ACTIONS, MAX_SENTENCE_WORDS, MAX_SCREEN_WORDS, MAX_ACTIONS, EXPLAIN_BUDGET_SECONDS, MAX_SLIDE_WORDS, MAX_SLIDE_LINES, SLIDE_NAME;
5362
5398
  var init_lexicon = __esm({
5363
5399
  "../shared/src/mcp/comprehension/lexicon.ts"() {
5364
5400
  "use strict";
@@ -5467,6 +5503,9 @@ var init_lexicon = __esm({
5467
5503
  MAX_SCREEN_WORDS = 60;
5468
5504
  MAX_ACTIONS = 3;
5469
5505
  EXPLAIN_BUDGET_SECONDS = 15;
5506
+ MAX_SLIDE_WORDS = 40;
5507
+ MAX_SLIDE_LINES = 5;
5508
+ SLIDE_NAME = /\b(slides?|deck|presentation)\b/i;
5470
5509
  }
5471
5510
  });
5472
5511
 
@@ -5541,7 +5580,7 @@ function estimateExplainSeconds(words2, insiderTermCount, actionCount) {
5541
5580
  const choosing = Math.max(0, actionCount - 1) * 3;
5542
5581
  return Math.round(speaking + defining + choosing);
5543
5582
  }
5544
- function fifteenSecondFindings(words2, actions, seconds) {
5583
+ function fifteenSecondFindings(words2, actions, seconds, surface) {
5545
5584
  const findings = [];
5546
5585
  if (seconds > EXPLAIN_BUDGET_SECONDS) {
5547
5586
  findings.push({
@@ -5552,7 +5591,7 @@ function fifteenSecondFindings(words2, actions, seconds) {
5552
5591
  fix: "Cut to one idea and one next step. Move the rest behind a link or a later screen."
5553
5592
  });
5554
5593
  }
5555
- if (words2 > MAX_SCREEN_WORDS) {
5594
+ if (surface === "screen" && words2 > MAX_SCREEN_WORDS) {
5556
5595
  findings.push({
5557
5596
  test: "fifteen-second",
5558
5597
  severity: "serious",
@@ -5613,7 +5652,30 @@ function screenshotFindings(copy, words2) {
5613
5652
  }
5614
5653
  return findings;
5615
5654
  }
5655
+ function slideFindings(words2, body) {
5656
+ const findings = [];
5657
+ if (words2 > MAX_SLIDE_WORDS) {
5658
+ findings.push({
5659
+ test: "slide",
5660
+ severity: "serious",
5661
+ problem: `The slide shows ${words2} words, so the audience reads instead of listening.`,
5662
+ evidence: `${words2} words of visible copy`,
5663
+ fix: `Keep a slide under about ${MAX_SLIDE_WORDS} words: a title that states the point and a few short lines.`
5664
+ });
5665
+ }
5666
+ if (body.length > MAX_SLIDE_LINES) {
5667
+ findings.push({
5668
+ test: "slide",
5669
+ severity: "serious",
5670
+ problem: `The slide stacks ${body.length} lines of copy, which usually means more than one idea.`,
5671
+ evidence: body.join(" | "),
5672
+ fix: "Give each idea its own slide, with the point in the title."
5673
+ });
5674
+ }
5675
+ return findings;
5676
+ }
5616
5677
  function analyzeComprehension(input) {
5678
+ const surface = input.surface ?? (SLIDE_NAME.test(input.screen) ? "slide" : "screen");
5617
5679
  const copy = resolveCopy(input);
5618
5680
  const known = new Set((input.knownTerms ?? []).map((term) => term.trim().toLowerCase()));
5619
5681
  const visible = [copy.headline ?? "", ...copy.body, ...copy.actions].join(". ");
@@ -5622,13 +5684,16 @@ function analyzeComprehension(input) {
5622
5684
  const estimatedExplainSeconds = estimateExplainSeconds(words2, insiderTerms.length, copy.actions.length);
5623
5685
  const findings = [
5624
5686
  ...momTestFindings(visible, known, insiderTerms),
5625
- ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds),
5626
- ...screenshotFindings(copy, words2)
5687
+ ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds, surface),
5688
+ ...screenshotFindings(copy, words2),
5689
+ ...surface === "slide" ? slideFindings(words2, copy.body) : []
5627
5690
  ];
5691
+ const blocking = findings.some((finding3) => finding3.severity !== "minor");
5628
5692
  const longestSentence = sentences(visible).sort((a, b) => countWords(b) - countWords(a))[0] ?? "";
5629
5693
  return {
5630
5694
  screen: input.screen,
5631
- verdict: findings.some((finding3) => finding3.severity !== "minor") ? "revise" : "pass",
5695
+ surface,
5696
+ verdict: blocking ? "revise" : surface === "slide" ? "needs-screenshot" : "pass",
5632
5697
  findings,
5633
5698
  copy: { headline: copy.headline, body: copy.body, actions: copy.actions },
5634
5699
  stats: {
@@ -5672,7 +5737,8 @@ function formatStats(report) {
5672
5737
  "",
5673
5738
  `- Visible copy: ${words2} words across ${actionCount} action${actionCount === 1 ? "" : "s"}.`,
5674
5739
  `- Insider words found: ${insiderTerms.length > 0 ? insiderTerms.join(", ") : "none"}.`,
5675
- `- Rough explain time: about ${estimatedExplainSeconds} seconds. This is a heuristic budget from word count, insider words and choices, not a measurement of real users.`
5740
+ `- Rough explain time: about ${estimatedExplainSeconds} seconds. This is a heuristic budget from word count, insider words and choices, not a measurement of real users.`,
5741
+ "- It read text only: the copy passed in, or the text in the markup. It cannot see screenshots, text inside images, or visual hierarchy."
5676
5742
  ].join("\n");
5677
5743
  }
5678
5744
  function formatComprehensionReport(report) {
@@ -5680,8 +5746,11 @@ function formatComprehensionReport(report) {
5680
5746
  (totals, finding3) => ({ ...totals, [finding3.severity]: totals[finding3.severity] + 1 }),
5681
5747
  { critical: 0, serious: 0, minor: 0 }
5682
5748
  );
5683
- const verdict = report.verdict === "pass" ? "Pass. Nothing blocking. Read the copy once more out loud before you present it." : `Revise before presenting. ${counts.critical} critical, ${counts.serious} serious, ${counts.minor} minor.`;
5684
- const next = report.verdict === "pass" ? "Say in one sentence what a first-time user can do on this screen. If that sentence needs a second sentence to make sense, simplify the screen." : "Rewrite the copy so it survives all three tests, then call check-comprehension again on the new copy. Do not present the screen while a critical or serious finding stands.";
5749
+ const { line: verdict, next } = report.verdict === "revise" ? {
5750
+ line: `Revise before presenting. ${counts.critical} critical, ${counts.serious} serious, ${counts.minor} minor.`,
5751
+ next: "Rewrite the copy so it survives every test, then call check-comprehension again on the new copy. Do not present the screen while a critical or serious finding stands."
5752
+ } : VERDICTS[report.verdict];
5753
+ const slideSection = report.surface === "slide" ? ["", formatTestSection(report, "slide")] : [];
5685
5754
  return [
5686
5755
  `# Comprehension check: ${report.screen}`,
5687
5756
  "",
@@ -5692,6 +5761,7 @@ function formatComprehensionReport(report) {
5692
5761
  formatTestSection(report, "fifteen-second"),
5693
5762
  "",
5694
5763
  formatTestSection(report, "screenshot"),
5764
+ ...slideSection,
5695
5765
  "",
5696
5766
  formatStats(report),
5697
5767
  "",
@@ -5700,16 +5770,27 @@ function formatComprehensionReport(report) {
5700
5770
  next
5701
5771
  ].join("\n");
5702
5772
  }
5703
- var TEST_TITLES, SEVERITY_ORDER;
5773
+ var TEST_TITLES, SEVERITY_ORDER, VERDICTS;
5704
5774
  var init_format = __esm({
5705
5775
  "../shared/src/mcp/comprehension/format.ts"() {
5706
5776
  "use strict";
5707
5777
  TEST_TITLES = {
5708
5778
  mom: "Mom test: would someone outside the team understand this?",
5709
5779
  "fifteen-second": "15 second test: can you explain the screen in 15 seconds?",
5710
- screenshot: "Screenshot test: with zero context, is it clear what this does?"
5780
+ screenshot: "Screenshot test: with zero context, is it clear what this does?",
5781
+ slide: "Slide test: one idea, and few enough words to hear the speaker?"
5711
5782
  };
5712
5783
  SEVERITY_ORDER = { critical: 0, serious: 1, minor: 2 };
5784
+ VERDICTS = {
5785
+ pass: {
5786
+ line: "Pass. Nothing blocking. Read the copy once more out loud before you present it.",
5787
+ next: "Say in one sentence what a first-time user can do on this screen. If that sentence needs a second sentence to make sense, simplify the screen."
5788
+ },
5789
+ "needs-screenshot": {
5790
+ line: "The text passes. This is not approval: a slide passes only after you review its rendered screenshot.",
5791
+ next: "Render the slide at its real size and look at the screenshot. Confirm it carries one idea, the title reads first, and one element holds the eye. Split or restyle the slide if any of the three fails."
5792
+ }
5793
+ };
5713
5794
  }
5714
5795
  });
5715
5796
 
@@ -5734,6 +5815,8 @@ function registerComprehensionTools({ server, provider, track, userId }) {
5734
5815
 
5735
5816
  Review rules catch what is broken. This catches what is unclear: copy that makes sense to the people who built the product and to nobody else.
5736
5817
 
5818
+ It reads text only: the copy you pass, or the text it extracts from your markup. It does not see what renders: not screenshots, text inside images, or visual hierarchy. A pass is a text result, not visual approval.
5819
+
5737
5820
  Three tests, run on the screen's own words:
5738
5821
  1. Mom test. Would a person outside the team understand this? Flags insider vocabulary, unexplained acronyms and sentences too long to follow.
5739
5822
  2. 15 second test. If the screen takes longer than 15 seconds to explain, it is too complex. Flags word count, stacked concepts and competing actions.
@@ -5743,6 +5826,8 @@ Pass either the copy or the markup:
5743
5826
  - check-comprehension({ screen: "Lesson end card", headline: "Lesson complete", body: ["Claim the June badge by collecting more quest points from your daily, friend and weekend quests."], actions: ["Continue"] })
5744
5827
  - check-comprehension({ screen: "Billing page", code: "<section>\u2026the JSX you wrote\u2026</section>" })
5745
5828
 
5829
+ For a slide, pass surface: "slide" (a screen named slide, deck or presentation gets it by default) and call once per slide. A slide adds a density test, and it never passes on text: approve it only after you review its rendered screenshot.
5830
+
5746
5831
  Use knownTerms for words your audience genuinely knows, so the check stops flagging them. Fix every critical and serious finding, then call this again on the rewritten copy.`,
5747
5832
  inputSchema: z9.object({
5748
5833
  screen: z9.string().trim().min(1).max(120).describe("What this screen is, e.g. 'lesson end card', 'billing page'."),
@@ -5751,6 +5836,7 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
5751
5836
  actions: z9.array(z9.string().trim().min(1).max(120)).max(20).optional().describe("Visible button and link labels, e.g. ['Claim badge', 'Not now']."),
5752
5837
  code: z9.string().max(6e4).optional().describe("JSX, TSX or HTML for the screen. Copy is extracted from it when headline/body/actions are omitted."),
5753
5838
  knownTerms: z9.array(z9.string().trim().min(1).max(60)).max(50).optional().describe("Words this audience already knows, so they are not flagged as insider language."),
5839
+ surface: z9.enum(["screen", "slide"]).optional().describe('"slide" adds the slide density test and withholds a pass until a screenshot review. Defaults to "slide" when the screen name says slide, deck or presentation.'),
5754
5840
  context: CONTEXT_ARG
5755
5841
  }),
5756
5842
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -10867,6 +10953,184 @@ ${FAQ_CARD_ITEM}
10867
10953
  }
10868
10954
  });
10869
10955
 
10956
+ // ../shared/src/mcp/store-screenshots.ts
10957
+ function renderScript() {
10958
+ const sizes = JSON.stringify(STORE_SCREENSHOT_SIZES);
10959
+ const feature = JSON.stringify(PLAY_FEATURE_GRAPHIC);
10960
+ return `// Renders store screenshots from slides.json at the exact store sizes. [Better Design]
10961
+ // Run once per change: node store-screenshots/render.mjs
10962
+ import { chromium } from "playwright";
10963
+ import { mkdirSync, readFileSync } from "node:fs";
10964
+ import { dirname, join } from "node:path";
10965
+ import { fileURLToPath, pathToFileURL } from "node:url";
10966
+
10967
+ const here = dirname(fileURLToPath(import.meta.url));
10968
+ const SIZES = ${sizes};
10969
+ const FEATURE = ${feature};
10970
+ const RANGE = ${JSON.stringify(STORE_SLIDE_RANGE)};
10971
+ const deck = JSON.parse(readFileSync(join(here, "slides.json"), "utf8"));
10972
+
10973
+ if (deck.slides.length < RANGE.min || deck.slides.length > RANGE.max) {
10974
+ throw new Error(\`slides.json holds \${deck.slides.length} slides. Use \${RANGE.min} to \${RANGE.max}: Google Play shows at most \${RANGE.max}.\`);
10975
+ }
10976
+
10977
+ // App Store Connect rejects a PNG with an alpha channel, so check the header, not the look.
10978
+ function check(file, size) {
10979
+ const png = readFileSync(file);
10980
+ const width = png.readUInt32BE(16), height = png.readUInt32BE(20), colorType = png[25];
10981
+ if (width !== size.width || height !== size.height) throw new Error(\`\${file} is \${width} x \${height}, not \${size.width} x \${size.height}.\`);
10982
+ if (colorType !== 2) throw new Error(\`\${file} has a transparency layer. Give every slide an opaque background.\`);
10983
+ }
10984
+
10985
+ async function shoot(browser, size, data, file) {
10986
+ const page = await browser.newPage({ viewport: { width: size.width, height: size.height }, deviceScaleFactor: 1 });
10987
+ await page.addInitScript((slide) => { window.__SLIDE__ = slide; }, data);
10988
+ await page.goto(pathToFileURL(join(here, "slide.html")).href);
10989
+ await page.evaluate(async () => {
10990
+ await document.fonts.ready;
10991
+ await Promise.all([...document.images].map((img) => img.decode().catch(() => { throw new Error("Missing capture: " + img.src); })));
10992
+ });
10993
+ await page.screenshot({ path: file, type: "png" });
10994
+ await page.close();
10995
+ check(file, size);
10996
+ }
10997
+
10998
+ const browser = await chromium.launch();
10999
+ try {
11000
+ for (const size of SIZES) {
11001
+ mkdirSync(join(here, "out", size.id), { recursive: true });
11002
+ for (const [i, slide] of deck.slides.entries()) {
11003
+ const file = join(here, "out", size.id, String(i + 1).padStart(2, "0") + ".png");
11004
+ await shoot(browser, size, { kind: "slide", app: deck.app, slide, frame: size.frame }, file);
11005
+ console.log("wrote", file);
11006
+ }
11007
+ }
11008
+ mkdirSync(join(here, "out", FEATURE.id), { recursive: true });
11009
+ const feature = join(here, "out", FEATURE.id, "feature-graphic.png");
11010
+ await shoot(browser, FEATURE, { kind: "feature", app: deck.app, slide: deck.featureGraphic, frame: "none" }, feature);
11011
+ console.log("wrote", feature);
11012
+ } finally {
11013
+ await browser.close();
11014
+ }
11015
+ `;
11016
+ }
11017
+ function buildStoreScreenshotFiles(tokensCss) {
11018
+ return [
11019
+ { path: "store-screenshots/theme.css", content: tokensCss },
11020
+ { path: "store-screenshots/slides.json", content: `${JSON.stringify(EXAMPLE_SLIDES, null, 2)}
11021
+ ` },
11022
+ { path: "store-screenshots/slide.html", content: SLIDE_HTML },
11023
+ { path: "store-screenshots/render.mjs", content: renderScript() }
11024
+ ];
11025
+ }
11026
+ function fencedFile(file) {
11027
+ const longest = Math.max(0, ...[...file.content.matchAll(/`+/g)].map((run) => run[0].length));
11028
+ const fence3 = "`".repeat(Math.max(3, longest + 1));
11029
+ return `## ${file.path}
11030
+
11031
+ ${fence3}
11032
+ ${file.content.replace(/\n$/, "")}
11033
+ ${fence3}`;
11034
+ }
11035
+ function storeScreenshotText(name, files) {
11036
+ return [storeScreenshotInstructions(name), ...files.map(fencedFile)].join("\n\n");
11037
+ }
11038
+ function storeScreenshotInstructions(name) {
11039
+ const sizes = [...STORE_SCREENSHOT_SIZES, PLAY_FEATURE_GRAPHIC].map((size) => `- ${size.label}: ${size.width} x ${size.height}, in \`store-screenshots/out/${size.id}/\``).join("\n");
11040
+ return [
11041
+ `# Store screenshots in ${name}`,
11042
+ "",
11043
+ "1. Write each file in `screenshotFiles` verbatim at its path.",
11044
+ `2. Capture ${STORE_SLIDE_RANGE.min} to ${STORE_SLIDE_RANGE.max} app screens into \`store-screenshots/captures/\`. iOS Simulator: \`xcrun simctl io booted screenshot store-screenshots/captures/01.png\`. Android emulator: \`adb exec-out screencap -p > store-screenshots/captures/01.png\`.`,
11045
+ '3. Edit `slides.json`: one outcome per slide, a headline a person understands in one second, and no claims such as "Best", "#1" or "Free", which Google Play rejects. Check every headline with Better Design\'s `check-comprehension` tool before you render.',
11046
+ "4. Install Playwright once as a dev dependency with the project's package manager, then its browser: `npx playwright install chromium`.",
11047
+ "5. Google Play asks the feature graphic to avoid pure white, black and dark gray. When this system's primary color is one of those, set `--feature-background` and `--feature-foreground` to a brand color pair at the end of `theme.css`.",
11048
+ "6. Run `node store-screenshots/render.mjs`. It stops on any file with the wrong size or a transparency layer.",
11049
+ "",
11050
+ "It writes:",
11051
+ sizes,
11052
+ "",
11053
+ "Google Play screenshots carry no drawn phone, because Google asks for no device imagery. Show the user every PNG before they upload it."
11054
+ ].join("\n");
11055
+ }
11056
+ var STORE_SCREENSHOT_SIZES, PLAY_FEATURE_GRAPHIC, STORE_SLIDE_RANGE, EXAMPLE_SLIDES, SLIDE_HTML;
11057
+ var init_store_screenshots = __esm({
11058
+ "../shared/src/mcp/store-screenshots.ts"() {
11059
+ "use strict";
11060
+ STORE_SCREENSHOT_SIZES = [
11061
+ { id: "iphone-6-9", label: "iPhone 6.9 inch", width: 1320, height: 2868, frame: "phone" },
11062
+ { id: "play-phone", label: "Google Play phone", width: 1080, height: 1920, frame: "none" }
11063
+ ];
11064
+ PLAY_FEATURE_GRAPHIC = {
11065
+ id: "play-feature-graphic",
11066
+ label: "Google Play feature graphic",
11067
+ width: 1024,
11068
+ height: 500,
11069
+ frame: "none"
11070
+ };
11071
+ STORE_SLIDE_RANGE = { min: 3, max: 8 };
11072
+ EXAMPLE_SLIDES = {
11073
+ app: "Your app",
11074
+ slides: [
11075
+ { headline: "Say the first outcome in five words", subhead: "", capture: "captures/01.png" },
11076
+ { headline: "Show the second outcome next", subhead: "", capture: "captures/02.png" },
11077
+ { headline: "End on the reason to stay", subhead: "", capture: "captures/03.png" }
11078
+ ],
11079
+ featureGraphic: { headline: "Your app, in one line" }
11080
+ };
11081
+ SLIDE_HTML = `<!doctype html>
11082
+ <html lang="en">
11083
+ <head>
11084
+ <meta charset="utf-8">
11085
+ <link rel="stylesheet" href="theme.css">
11086
+ <style>
11087
+ html, body { margin: 0; width: 100vw; height: 100vh; overflow: hidden; }
11088
+ body { background: var(--background); color: var(--foreground); font-family: var(--font-sans, system-ui, sans-serif);
11089
+ display: flex; flex-direction: column; align-items: center; box-sizing: border-box; padding: 7vh 8vw 0; gap: 3vh; }
11090
+ h1 { margin: 0; font-size: 7.5vw; line-height: 1.12; font-weight: 700; text-align: center; text-wrap: balance; letter-spacing: -0.02em; }
11091
+ p { margin: 0; font-size: 4vw; line-height: 1.35; text-align: center; color: var(--muted-foreground); text-wrap: balance; }
11092
+ .shot { flex: 1; min-height: 0; width: 100%; display: flex; justify-content: center; align-items: flex-start; }
11093
+ .shot img { display: block; height: 100%; max-width: 100%; object-fit: cover; object-position: top; }
11094
+ .phone { height: 104%; aspect-ratio: 9 / 19.5; box-sizing: border-box; padding: 1.6vw; border-radius: 11vw 11vw 0 0;
11095
+ background: var(--foreground); overflow: hidden; }
11096
+ .phone img { width: 100%; height: 100%; border-radius: 9.4vw 9.4vw 0 0; }
11097
+ .bare img { border-radius: calc(var(--radius, 0.75rem) * 3) calc(var(--radius, 0.75rem) * 3) 0 0; }
11098
+ body.feature { background: var(--feature-background, var(--primary)); color: var(--feature-foreground, var(--primary-foreground)); justify-content: center; align-items: flex-start; padding: 0 10vw; gap: 4vh; }
11099
+ body.feature h1 { font-size: 7vw; text-align: left; }
11100
+ body.feature .app { font-size: 3.4vw; font-weight: 600; opacity: 0.85; }
11101
+ </style>
11102
+ </head>
11103
+ <body>
11104
+ <script>
11105
+ // The render script sets window.__SLIDE__ before this page loads.
11106
+ const { kind, app, slide, frame } = window.__SLIDE__;
11107
+ const add = (tag, className, text) => {
11108
+ const el = document.createElement(tag);
11109
+ if (className) el.className = className;
11110
+ if (text) el.textContent = text;
11111
+ return document.body.appendChild(el);
11112
+ };
11113
+ if (kind === "feature") {
11114
+ document.body.classList.add("feature");
11115
+ add("div", "app", app);
11116
+ add("h1", "", slide.headline);
11117
+ } else {
11118
+ add("h1", "", slide.headline);
11119
+ if (slide.subhead) add("p", "", slide.subhead);
11120
+ const shot = add("div", "shot " + (frame === "phone" ? "" : "bare"));
11121
+ const holder = frame === "phone" ? shot.appendChild(document.createElement("div")) : shot;
11122
+ if (frame === "phone") holder.className = "phone";
11123
+ const img = holder.appendChild(document.createElement("img"));
11124
+ img.src = slide.capture;
11125
+ img.alt = "";
11126
+ }
11127
+ </script>
11128
+ </body>
11129
+ </html>
11130
+ `;
11131
+ }
11132
+ });
11133
+
10870
11134
  // ../shared/src/registry-css.ts
10871
11135
  function parseCssVars(css) {
10872
11136
  const out = { theme: {}, light: {}, dark: {} };
@@ -13892,36 +14156,38 @@ ${fence3}` }],
13892
14156
  )
13893
14157
  }, icons);
13894
14158
  });
14159
+ const loadTokenSource = async ({ designSystemId, projectId }) => {
14160
+ if (projectId) {
14161
+ const result = await fetchProjectTokensCss(projectId, apiFetch, authContext);
14162
+ return {
14163
+ tokensCss: result?.tokensCss,
14164
+ name: result?.name ?? `Project ${projectId.slice(0, 8)}`,
14165
+ viewUrl: `${APP_BASE_URL}/projects/${projectId}`,
14166
+ icons: result?.iconLibrary
14167
+ };
14168
+ }
14169
+ if (!designSystemId) return null;
14170
+ const ds = await provider.getDesignSystemById(designSystemId, { includeComponents: false, includeHtml: false });
14171
+ return {
14172
+ tokensCss: await fetchCatalogTokensCss(designSystemId),
14173
+ name: ds?.metadata.title ?? designSystemId,
14174
+ viewUrl: `${APP_BASE_URL}/design-systems/${designSystemId}`,
14175
+ icons: ds ? await catalogKitIcons(ds.metadata, provider) : void 0
14176
+ };
14177
+ };
13895
14178
  const getDtcgKit = async ({
13896
14179
  designSystemId,
13897
14180
  projectId,
13898
14181
  context
13899
14182
  }) => tracked("get-design-system-kit", { target: "dtcg", designSystemId, projectId, context }, async () => {
13900
- let tokensCss;
13901
- let name;
13902
- let viewUrl;
13903
- let icons;
13904
- if (projectId) {
13905
- viewUrl = `${APP_BASE_URL}/projects/${projectId}`;
13906
- const result = await fetchProjectTokensCss(projectId, apiFetch, authContext);
13907
- tokensCss = result?.tokensCss;
13908
- icons = result?.iconLibrary;
13909
- name = result?.name ?? `Project ${projectId.slice(0, 8)}`;
13910
- } else if (designSystemId) {
13911
- viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
13912
- const ds = await provider.getDesignSystemById(designSystemId, {
13913
- includeComponents: false,
13914
- includeHtml: false
13915
- });
13916
- tokensCss = await fetchCatalogTokensCss(designSystemId);
13917
- name = ds?.metadata.title ?? designSystemId;
13918
- icons = ds ? await catalogKitIcons(ds.metadata, provider) : void 0;
13919
- } else {
14183
+ const source = await loadTokenSource({ designSystemId, projectId });
14184
+ if (!source) {
13920
14185
  return {
13921
14186
  content: [{ type: "text", text: "For target dtcg, pass either a catalog designSystemId (from find-design-system) or a projectId (from create-design-system)." }],
13922
14187
  structuredContent: { status: "error", name: "Missing design system", tokensCss: "" }
13923
14188
  };
13924
14189
  }
14190
+ const { tokensCss, name, viewUrl, icons } = source;
13925
14191
  if (!tokensCss) {
13926
14192
  return {
13927
14193
  content: [{ type: "text", text: `Could not load tokens for "${projectId ?? designSystemId}". A generated project may still be building. Check ${viewUrl}.` }],
@@ -13947,6 +14213,33 @@ ${fence3}` }],
13947
14213
  structuredContent: { status: "completed", name, viewUrl, tokensCss, tokensJson }
13948
14214
  }, icons);
13949
14215
  });
14216
+ const getStoreScreenshots = async ({
14217
+ designSystemId,
14218
+ projectId,
14219
+ context
14220
+ }) => tracked("get-design-system-kit", { target: "store-screenshots", designSystemId, projectId, context }, async () => {
14221
+ const source = await loadTokenSource({ designSystemId, projectId });
14222
+ if (!source?.tokensCss) {
14223
+ const reason = source ? `Could not load tokens for "${projectId ?? designSystemId}". A generated project may still be building. Check ${source.viewUrl}.` : "For target store-screenshots, pass either a catalog designSystemId (from find-design-system) or a projectId (from create-design-system).";
14224
+ return {
14225
+ content: [{ type: "text", text: reason }],
14226
+ structuredContent: { status: "error", name: source?.name ?? "Missing design system", tokensCss: "" }
14227
+ };
14228
+ }
14229
+ const files = buildStoreScreenshotFiles(source.tokensCss);
14230
+ const instructions = storeScreenshotInstructions(source.name);
14231
+ return {
14232
+ content: [{ type: "text", text: storeScreenshotText(source.name, files) }],
14233
+ structuredContent: {
14234
+ status: "completed",
14235
+ name: source.name,
14236
+ viewUrl: source.viewUrl,
14237
+ tokensCss: source.tokensCss,
14238
+ buildInstructions: instructions,
14239
+ screenshotFiles: files
14240
+ }
14241
+ };
14242
+ });
13950
14243
  const fetchProjectMobilePrimitives = async (projectId) => {
13951
14244
  try {
13952
14245
  const res = await apiFetch(`${APP_BASE_URL}/api/projects/${projectId}/mobile/all.json`, { headers: authHeaders(authContext) });
@@ -14162,9 +14455,10 @@ ${BUILD_TARGET_RULE}`,
14162
14455
  "html",
14163
14456
  ...enableShopifyKit ? ["shopify"] : [],
14164
14457
  "dtcg",
14458
+ "store-screenshots",
14165
14459
  ...enableMobileKits ? ["react-native", "swiftui"] : []
14166
14460
  ]
14167
- ).describe(`Required build target. Use "react" for any host with filesystem + terminal; use "html" only for inline chat canvases/artifacts.${enableShopifyKit ? ' Use "shopify" for Shopify stores and themes.' : ""} Use "dtcg" to hand the tokens to a design tool: it returns a W3C Design Tokens document for Style Dictionary, Tokens Studio, Terrazzo, or a Figma plugin that creates variables. ${enableMobileKits ? ` Use "react-native" for a React Native or Expo app and "swiftui" for an iOS app: each returns this system's stored mobile kit, its own material on native primitives, plus the handoff to the matching guide.` : ""}`),
14461
+ ).describe(`Required build target. Use "react" for any host with filesystem + terminal; use "html" only for inline chat canvases/artifacts.${enableShopifyKit ? ' Use "shopify" for Shopify stores and themes.' : ""} Use "dtcg" to hand the tokens to a design tool: it returns a W3C Design Tokens document for Style Dictionary, Tokens Studio, Terrazzo, or a Figma plugin that creates variables. Use "store-screenshots" for App Store and Google Play screenshots in this system: it returns slide files and a render script that writes PNG files at the store sizes. ${enableMobileKits ? ` Use "react-native" for a React Native or Expo app and "swiftui" for an iOS app: each returns this system's stored mobile kit, its own material on native primitives, plus the handoff to the matching guide.` : ""}`),
14168
14462
  designSystemId: z14.string().min(1).optional().describe(`Catalog design system ID from find-design-system. Required for target react; for every other target pass this or projectId.`),
14169
14463
  projectId: z14.string().min(1).optional().describe(`Generated project ID from create-design-system / get-design-system-status. Supported with every target but react.`),
14170
14464
  component: z14.union([z14.string(), z14.array(z14.string())]).optional().describe("React target only. Pass only when the user asks to inspect specific component source code. Omit to scaffold the entire design system."),
@@ -14180,7 +14474,7 @@ ${BUILD_TARGET_RULE}`,
14180
14474
  // interactive preview path. [BD-240]
14181
14475
  },
14182
14476
  async ({ target, designSystemId, projectId, component, library, context }) => {
14183
- if (target !== "react" && target !== "html" && target !== "shopify" && target !== "dtcg" && target !== "react-native" && target !== "swiftui") {
14477
+ if (target !== "react" && target !== "html" && target !== "shopify" && target !== "dtcg" && target !== "store-screenshots" && target !== "react-native" && target !== "swiftui") {
14184
14478
  return {
14185
14479
  content: [{ type: "text", text: `Unknown target "${target}".` }],
14186
14480
  structuredContent: { status: "error", name: "Unknown target", tokensCss: "" }
@@ -14192,6 +14486,9 @@ ${BUILD_TARGET_RULE}`,
14192
14486
  if (target === "dtcg") {
14193
14487
  return withRedesignContract(await getDtcgKit({ designSystemId, projectId, context }));
14194
14488
  }
14489
+ if (target === "store-screenshots") {
14490
+ return getStoreScreenshots({ designSystemId, projectId, context });
14491
+ }
14195
14492
  if (target === "react-native" || target === "swiftui") {
14196
14493
  if (!enableMobileKits) {
14197
14494
  return {
@@ -14687,6 +14984,7 @@ var init_design_system_tools = __esm({
14687
14984
  init_shopify_kit();
14688
14985
  init_redesign_contract();
14689
14986
  init_selected_icon_handoff();
14987
+ init_store_screenshots();
14690
14988
  init_shopify_primitives();
14691
14989
  init_preview_html();
14692
14990
  init_shared();
@@ -22772,7 +23070,7 @@ function columnToSchema(column, factory) {
22772
23070
  }
22773
23071
  return schema;
22774
23072
  }
22775
- function numberColumnToSchema(column, z35, coerce) {
23073
+ function numberColumnToSchema(column, z38, coerce) {
22776
23074
  let unsigned = column.getSQLType().includes("unsigned");
22777
23075
  let min;
22778
23076
  let max;
@@ -22840,20 +23138,20 @@ function numberColumnToSchema(column, z35, coerce) {
22840
23138
  min = Number.MIN_SAFE_INTEGER;
22841
23139
  max = Number.MAX_SAFE_INTEGER;
22842
23140
  }
22843
- let schema = coerce === true || coerce?.number ? z35.coerce.number() : z35.number();
23141
+ let schema = coerce === true || coerce?.number ? z38.coerce.number() : z38.number();
22844
23142
  schema = schema.min(min).max(max);
22845
23143
  return integer2 ? schema.int() : schema;
22846
23144
  }
22847
- function bigintColumnToSchema(column, z35, coerce) {
23145
+ function bigintColumnToSchema(column, z38, coerce) {
22848
23146
  const unsigned = column.getSQLType().includes("unsigned");
22849
23147
  const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
22850
23148
  const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
22851
- const schema = coerce === true || coerce?.bigint ? z35.coerce.bigint() : z35.bigint();
23149
+ const schema = coerce === true || coerce?.bigint ? z38.coerce.bigint() : z38.bigint();
22852
23150
  return schema.min(min).max(max);
22853
23151
  }
22854
- function stringColumnToSchema(column, z35, coerce) {
23152
+ function stringColumnToSchema(column, z38, coerce) {
22855
23153
  if (isColumnType(column, ["PgUUID"])) {
22856
- return z35.string().uuid();
23154
+ return z38.string().uuid();
22857
23155
  }
22858
23156
  let max;
22859
23157
  let regex;
@@ -22885,7 +23183,7 @@ function stringColumnToSchema(column, z35, coerce) {
22885
23183
  regex = /^[01]+$/;
22886
23184
  max = column.dimensions;
22887
23185
  }
22888
- let schema = coerce === true || coerce?.string ? z35.coerce.string() : z35.string();
23186
+ let schema = coerce === true || coerce?.string ? z38.coerce.string() : z38.string();
22889
23187
  schema = regex ? schema.regex(regex) : schema;
22890
23188
  return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
22891
23189
  }
@@ -22981,15 +23279,74 @@ var init_drizzle_zod = __esm({
22981
23279
  }
22982
23280
  });
22983
23281
 
22984
- // ../shared/src/schema.ts
23282
+ // ../shared/src/agent-feedback.ts
22985
23283
  import { z as z16 } from "zod";
22986
- var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, mcpIssueCategoryValues, mcpIssueStatusValues, mcpIssueImprovementStatusValues, mcpIssueImprovementVerdictValues, mcpIssueImprovementStatusSchema, mcpIssueImprovementVerdictSchema, mcpIssueImprovementStatusSqlValues, mcpIssueImprovementVerdictSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, signupAlertDeliveries, billingAlertDeliveries, TEAM_RULES_MAX_BYTES, teamRules, billingSubscription, githubInstallationClaim, githubInstallation, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, mcpToolCalls, mcpClientConnections, evalVerdicts, generationInputs, mcpIssues, mcpIssueImprovements, projectsRelations, projectMemoryRelations, mcpIssuesRelations, mcpIssueImprovementsRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertSignupAlertDeliverySchema, selectSignupAlertDeliverySchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema, insertMcpIssueImprovementSchema, selectMcpIssueImprovementSchema, pluginAuthNonces;
23284
+ var agentFeedbackKindValues, TEXT_MAX, SURFACE_MAX, SUMMARY_MAX, AGENT_FEEDBACK_CLIENT_MAX, FIELD_DESCRIPTIONS, optionalText, agentFeedbackInputSchema, AGENT_FEEDBACK_CONTRACT;
23285
+ var init_agent_feedback = __esm({
23286
+ "../shared/src/agent-feedback.ts"() {
23287
+ "use strict";
23288
+ agentFeedbackKindValues = [
23289
+ "bug",
23290
+ "missing-feature",
23291
+ "wrong-output",
23292
+ "docs-gap",
23293
+ "other"
23294
+ ];
23295
+ TEXT_MAX = 4e3;
23296
+ SURFACE_MAX = 200;
23297
+ SUMMARY_MAX = 500;
23298
+ AGENT_FEEDBACK_CLIENT_MAX = 100;
23299
+ FIELD_DESCRIPTIONS = {
23300
+ kind: "What kind of problem this is.",
23301
+ surface: "The endpoint path or MCP tool name you called.",
23302
+ summary: "One sentence: what is wrong or missing.",
23303
+ attempted: "What you tried to do, and the request you sent.",
23304
+ expected: "What you expected to get back.",
23305
+ actual: "What you got back instead.",
23306
+ client: "Your agent name and version."
23307
+ };
23308
+ optionalText = (description) => z16.string().trim().max(TEXT_MAX).optional().describe(description);
23309
+ agentFeedbackInputSchema = z16.object({
23310
+ kind: z16.enum(agentFeedbackKindValues).describe(FIELD_DESCRIPTIONS.kind),
23311
+ surface: z16.string().trim().min(1).max(SURFACE_MAX).describe(FIELD_DESCRIPTIONS.surface),
23312
+ summary: z16.string().trim().min(1).max(SUMMARY_MAX).describe(FIELD_DESCRIPTIONS.summary),
23313
+ attempted: optionalText(FIELD_DESCRIPTIONS.attempted),
23314
+ expected: optionalText(FIELD_DESCRIPTIONS.expected),
23315
+ actual: optionalText(FIELD_DESCRIPTIONS.actual),
23316
+ client: z16.string().trim().max(AGENT_FEEDBACK_CLIENT_MAX).optional().describe(FIELD_DESCRIPTIONS.client)
23317
+ });
23318
+ AGENT_FEEDBACK_CONTRACT = {
23319
+ method: "POST",
23320
+ path: "/api/v1/feedback",
23321
+ purpose: "Tell Better Design about a bug or a missing feature you hit while calling this API. A person reads every report.",
23322
+ fields: {
23323
+ kind: { required: true, description: FIELD_DESCRIPTIONS.kind, oneOf: agentFeedbackKindValues },
23324
+ surface: { required: true, description: FIELD_DESCRIPTIONS.surface, maxLength: SURFACE_MAX },
23325
+ summary: { required: true, description: FIELD_DESCRIPTIONS.summary, maxLength: SUMMARY_MAX },
23326
+ attempted: { required: false, description: FIELD_DESCRIPTIONS.attempted, maxLength: TEXT_MAX },
23327
+ expected: { required: false, description: FIELD_DESCRIPTIONS.expected, maxLength: TEXT_MAX },
23328
+ actual: { required: false, description: FIELD_DESCRIPTIONS.actual, maxLength: TEXT_MAX },
23329
+ client: { required: false, description: FIELD_DESCRIPTIONS.client, maxLength: AGENT_FEEDBACK_CLIENT_MAX }
23330
+ },
23331
+ notes: [
23332
+ "Send it with your Better Design API key: Authorization: Bearer <key>.",
23333
+ "Do not include secrets or personal data. Known secret shapes are removed before storage.",
23334
+ "Sending feedback never spends credit."
23335
+ ]
23336
+ };
23337
+ }
23338
+ });
23339
+
23340
+ // ../shared/src/schema.ts
23341
+ import { z as z17 } from "zod";
23342
+ var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, mcpIssueCategoryValues, mcpIssueStatusValues, mcpIssueImprovementStatusValues, mcpIssueImprovementVerdictValues, mcpIssueImprovementStatusSchema, mcpIssueImprovementVerdictSchema, mcpIssueImprovementStatusSqlValues, mcpIssueImprovementVerdictSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, signupAlertDeliveries, billingAlertDeliveries, billingEmailDeliveries, TEAM_RULES_MAX_BYTES, teamRules, agentFeedbackStatusValues, agentFeedback, billingSubscription, githubInstallationClaim, githubInstallation, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, mcpToolCalls, mcpClientConnections, evalVerdicts, generationInputs, mcpIssues, mcpIssueImprovements, projectsRelations, projectMemoryRelations, mcpIssuesRelations, mcpIssueImprovementsRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertSignupAlertDeliverySchema, selectSignupAlertDeliverySchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema, insertMcpIssueImprovementSchema, selectMcpIssueImprovementSchema, pluginAuthNonces;
22987
23343
  var init_schema2 = __esm({
22988
23344
  "../shared/src/schema.ts"() {
22989
23345
  "use strict";
22990
23346
  init_pg_core();
22991
23347
  init_drizzle_orm();
22992
23348
  init_drizzle_zod();
23349
+ init_agent_feedback();
22993
23350
  vector2 = customType({
22994
23351
  dataType() {
22995
23352
  return "vector(768)";
@@ -23017,7 +23374,7 @@ var init_schema2 = __esm({
23017
23374
  "skip",
23018
23375
  "legacy"
23019
23376
  ];
23020
- onboardingPathSchema = z16.enum(onboardingPathValues);
23377
+ onboardingPathSchema = z17.enum(onboardingPathValues);
23021
23378
  onboardingPathSqlValues = sql.raw(
23022
23379
  onboardingPathValues.map((path6) => `'${path6}'`).join(", ")
23023
23380
  );
@@ -23040,10 +23397,10 @@ var init_schema2 = __esm({
23040
23397
  "not_improved",
23041
23398
  "inconclusive"
23042
23399
  ];
23043
- mcpIssueImprovementStatusSchema = z16.enum(
23400
+ mcpIssueImprovementStatusSchema = z17.enum(
23044
23401
  mcpIssueImprovementStatusValues
23045
23402
  );
23046
- mcpIssueImprovementVerdictSchema = z16.enum(
23403
+ mcpIssueImprovementVerdictSchema = z17.enum(
23047
23404
  mcpIssueImprovementVerdictValues
23048
23405
  );
23049
23406
  mcpIssueImprovementStatusSqlValues = sql.raw(
@@ -23052,38 +23409,38 @@ var init_schema2 = __esm({
23052
23409
  mcpIssueImprovementVerdictSqlValues = sql.raw(
23053
23410
  mcpIssueImprovementVerdictValues.map((verdict) => `'${verdict}'`).join(", ")
23054
23411
  );
23055
- designConfigSchema = z16.object({
23056
- colors: z16.object({
23057
- primary: z16.string(),
23058
- secondary: z16.string(),
23059
- accent: z16.string(),
23060
- muted: z16.string(),
23061
- destructive: z16.string(),
23062
- background: z16.string(),
23063
- foreground: z16.string(),
23064
- card: z16.string(),
23065
- cardForeground: z16.string(),
23066
- popover: z16.string(),
23067
- popoverForeground: z16.string(),
23068
- border: z16.string(),
23069
- input: z16.string(),
23070
- ring: z16.string()
23412
+ designConfigSchema = z17.object({
23413
+ colors: z17.object({
23414
+ primary: z17.string(),
23415
+ secondary: z17.string(),
23416
+ accent: z17.string(),
23417
+ muted: z17.string(),
23418
+ destructive: z17.string(),
23419
+ background: z17.string(),
23420
+ foreground: z17.string(),
23421
+ card: z17.string(),
23422
+ cardForeground: z17.string(),
23423
+ popover: z17.string(),
23424
+ popoverForeground: z17.string(),
23425
+ border: z17.string(),
23426
+ input: z17.string(),
23427
+ ring: z17.string()
23071
23428
  }).partial(),
23072
- radius: z16.string(),
23073
- font: z16.object({
23074
- sans: z16.string(),
23075
- mono: z16.string()
23429
+ radius: z17.string(),
23430
+ font: z17.object({
23431
+ sans: z17.string(),
23432
+ mono: z17.string()
23076
23433
  }).partial(),
23077
- icons: z16.object({
23078
- libraryId: z16.string(),
23079
- variant: z16.string().nullable()
23434
+ icons: z17.object({
23435
+ libraryId: z17.string(),
23436
+ variant: z17.string().nullable()
23080
23437
  }).partial()
23081
23438
  }).partial();
23082
- projectMemoryDataSchema = z16.object({
23083
- lockedIn: z16.array(z16.string()),
23084
- rejected: z16.array(z16.string()),
23085
- tone: z16.string().nullable(),
23086
- notes: z16.string().nullable()
23439
+ projectMemoryDataSchema = z17.object({
23440
+ lockedIn: z17.array(z17.string()),
23441
+ rejected: z17.array(z17.string()),
23442
+ tone: z17.string().nullable(),
23443
+ notes: z17.string().nullable()
23087
23444
  });
23088
23445
  projects = pgTable("projects", {
23089
23446
  id: uuid("id").primaryKey().defaultRandom(),
@@ -23314,6 +23671,40 @@ var init_schema2 = __esm({
23314
23671
  )
23315
23672
  ]
23316
23673
  );
23674
+ billingEmailDeliveries = pgTable(
23675
+ "billing_email_deliveries",
23676
+ {
23677
+ id: text("id").primaryKey(),
23678
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
23679
+ toEmail: text("to_email").notNull(),
23680
+ /** The Stripe subscription the email is about, so the daily cap can count per subscription. */
23681
+ stripeSubscriptionId: text("stripe_subscription_id").notNull(),
23682
+ email: jsonb("email").$type().notNull(),
23683
+ occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
23684
+ status: text("status").$type().default("pending").notNull(),
23685
+ attemptCount: integer("attempt_count").default(0).notNull(),
23686
+ nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).defaultNow().notNull(),
23687
+ leaseUntil: timestamp("lease_until", { withTimezone: true }),
23688
+ leaseToken: uuid("lease_token"),
23689
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
23690
+ resendEmailId: text("resend_email_id"),
23691
+ lastError: text("last_error"),
23692
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
23693
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
23694
+ },
23695
+ (t) => [
23696
+ index("billing_email_deliveries_due_idx").on(t.status, t.nextAttemptAt, t.leaseUntil),
23697
+ index("billing_email_deliveries_subscription_idx").on(t.stripeSubscriptionId, t.createdAt),
23698
+ check(
23699
+ "billing_email_deliveries_status_valid",
23700
+ sql`${t.status} in ('pending', 'processing', 'delivered', 'failed')`
23701
+ ),
23702
+ check(
23703
+ "billing_email_deliveries_processing_lease_valid",
23704
+ sql`${t.status} <> 'processing' or (${t.leaseUntil} is not null and ${t.leaseToken} is not null)`
23705
+ )
23706
+ ]
23707
+ );
23317
23708
  TEAM_RULES_MAX_BYTES = 32 * 1024;
23318
23709
  teamRules = pgTable(
23319
23710
  "team_rules",
@@ -23329,6 +23720,43 @@ var init_schema2 = __esm({
23329
23720
  )
23330
23721
  ]
23331
23722
  );
23723
+ agentFeedbackStatusValues = [
23724
+ "open",
23725
+ "accepted",
23726
+ "rejected",
23727
+ "shipped"
23728
+ ];
23729
+ agentFeedback = pgTable(
23730
+ "agent_feedback",
23731
+ {
23732
+ id: uuid("id").primaryKey().defaultRandom(),
23733
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
23734
+ apiKeyId: text("api_key_id").references(() => apiKeys.id, { onDelete: "set null" }),
23735
+ kind: text("kind", { enum: agentFeedbackKindValues }).notNull(),
23736
+ // The endpoint path or MCP tool name the agent was calling.
23737
+ surface: text("surface").notNull(),
23738
+ summary: text("summary").notNull(),
23739
+ attempted: text("attempted"),
23740
+ expected: text("expected"),
23741
+ actual: text("actual"),
23742
+ // The agent's own name for itself, such as "claude-code/2.1".
23743
+ client: text("client"),
23744
+ // SHA-256 of kind, surface and the normalized summary, so identical
23745
+ // reports group in the admin queue without a model call.
23746
+ fingerprint: text("fingerprint").notNull(),
23747
+ status: text("status", { enum: agentFeedbackStatusValues }).notNull().default("open"),
23748
+ pullRequestUrl: text("pull_request_url"),
23749
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
23750
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
23751
+ },
23752
+ (t) => [
23753
+ index("agent_feedback_status_created_idx").on(t.status, t.createdAt),
23754
+ index("agent_feedback_fingerprint_idx").on(t.fingerprint),
23755
+ // Deleting a user or an API key looks up its reports through these.
23756
+ index("agent_feedback_user_id_idx").on(t.userId),
23757
+ index("agent_feedback_api_key_id_idx").on(t.apiKeyId)
23758
+ ]
23759
+ );
23332
23760
  billingSubscription = pgTable(
23333
23761
  "billing_subscription",
23334
23762
  {
@@ -24135,7 +24563,7 @@ var init_schema2 = __esm({
24135
24563
  });
24136
24564
 
24137
24565
  // ../shared/src/mcp/tools/design-config-tools.ts
24138
- import { z as z17 } from "zod";
24566
+ import { z as z18 } from "zod";
24139
24567
  function registerDesignConfigTools({
24140
24568
  server,
24141
24569
  tracked,
@@ -24186,7 +24614,7 @@ function registerDesignConfigTools({
24186
24614
  {
24187
24615
  title: "Get a Project's Tuned Design Config",
24188
24616
  description: `Read the user-tuned design config (exact color tokens, radius, fonts, icon library) of a generated project. Call this BEFORE building or restyling UI for the project, and again after the user tunes values on the project page, so you apply their exact numbers instead of guessing from adjectives.`,
24189
- inputSchema: z17.object({
24617
+ inputSchema: z18.object({
24190
24618
  projectId: PROJECT_ID_ARG,
24191
24619
  context: CONTEXT_ARG
24192
24620
  }),
@@ -24215,7 +24643,7 @@ function registerDesignConfigTools({
24215
24643
  {
24216
24644
  title: "Set a Project's Design Config",
24217
24645
  description: `Write exact design-token values (colors, radius, fonts, icon library) onto a generated project. Use this when the user states concrete values ("make primary oklch(0.62 0.11 45)", "radius 12px"). It merges over the current config and persists, so the change survives restarts and every later get-design-config returns it. Saved config is publicly readable by anyone with the fragment config URL. For vague direction ("warmer", "more playful"), iterate with the user toward concrete values first; do not invent numbers on their behalf.`,
24218
- inputSchema: z17.object({
24646
+ inputSchema: z18.object({
24219
24647
  projectId: PROJECT_ID_ARG,
24220
24648
  config: CONFIG_ARG,
24221
24649
  context: CONTEXT_ARG
@@ -24266,7 +24694,7 @@ var init_design_config_tools = __esm({
24266
24694
  "use strict";
24267
24695
  init_schema2();
24268
24696
  init_shared();
24269
- PROJECT_ID_ARG = z17.string().min(1).describe("The project id returned by create-design-system.");
24697
+ PROJECT_ID_ARG = z18.string().min(1).describe("The project id returned by create-design-system.");
24270
24698
  CONFIG_ARG = designConfigSchema.describe(
24271
24699
  "Partial design config to merge over the current one. Only pass the fields the user changed. Colors are oklch() strings (e.g. 'oklch(0.62 0.11 45)'); radius is a CSS length (e.g. '0.75rem'); fonts are family names."
24272
24700
  );
@@ -24274,16 +24702,16 @@ var init_design_config_tools = __esm({
24274
24702
  });
24275
24703
 
24276
24704
  // ../shared/src/mcp/tools/jig-tools.ts
24277
- import { z as z18 } from "zod";
24705
+ import { z as z19 } from "zod";
24278
24706
  function registerJigTools({ server, tracked }) {
24279
24707
  server.registerTool(
24280
24708
  "get-jig-guide",
24281
24709
  {
24282
24710
  title: "Get a Jig for Parameter Work",
24283
24711
  description: `Return the setup for a "jig", the tool you build to make the real thing precisely. Two kinds, both here. Use kind "tune-in-place" (default) when you are about to hard-code taste-based values in UI you are building: animation duration/easing/springs/stagger, particles, canvas physics, blur/glow/grain, gradient stops, shadow recipes, parallax speeds. It returns a control panel to wire to those parameters so the USER tunes by eye and you hard-code what they copy back. Use kind "standalone-tool" when the user wants a reusable custom tool instead: a generator, an image effect, a shader playground, an asset maker. That returns the Toolcraft starter kit (MIT), which ships a canvas and an export flow, with optional layers and a timeline. Tuning setups: React recommends DialKit (MIT, no re-render lag) with a dependency-free fallback; "vanilla" and "react-native" get dependency-free scaffolds.`,
24284
- inputSchema: z18.object({
24285
- kind: z18.enum(["tune-in-place", "standalone-tool"]).default("tune-in-place").describe("What the user needs. 'tune-in-place' (default): a control panel for parameters in the UI you are building. 'standalone-tool': a separate reusable design tool, scaffolded with Toolcraft."),
24286
- framework: z18.enum(["react", "vanilla", "react-native"]).optional().describe("Which tune-in-place setup to return. 'react' (default) for React/Next web projects; 'vanilla' for plain DOM or canvas work; 'react-native' for RN/Expo apps. Ignored when kind is 'standalone-tool'."),
24712
+ inputSchema: z19.object({
24713
+ kind: z19.enum(["tune-in-place", "standalone-tool"]).default("tune-in-place").describe("What the user needs. 'tune-in-place' (default): a control panel for parameters in the UI you are building. 'standalone-tool': a separate reusable design tool, scaffolded with Toolcraft."),
24714
+ framework: z19.enum(["react", "vanilla", "react-native"]).optional().describe("Which tune-in-place setup to return. 'react' (default) for React/Next web projects; 'vanilla' for plain DOM or canvas work; 'react-native' for RN/Expo apps. Ignored when kind is 'standalone-tool'."),
24287
24715
  context: CONTEXT_ARG
24288
24716
  }),
24289
24717
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -25115,7 +25543,7 @@ var init_brand_sources = __esm({
25115
25543
  });
25116
25544
 
25117
25545
  // ../shared/src/mcp/tools/extract-tools.ts
25118
- import { z as z19 } from "zod";
25546
+ import { z as z20 } from "zod";
25119
25547
  function authHeaders2(authContext) {
25120
25548
  if (!authContext) return {};
25121
25549
  const bearer = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
@@ -25152,15 +25580,15 @@ JSON.stringify(${buildRoleSampleScript()})
25152
25580
  - kind "tokens": a design-tokens JSON export ($value entries, W3C format or the Figma variables API shape with 0..1 rgb floats).
25153
25581
 
25154
25582
  Common vendor names (Shopify's --color-button family) are mapped into empty semantic slots automatically and the mapping is reported in the output.`,
25155
- inputSchema: z19.object({
25156
- css: z19.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
25157
- sources: z19.array(z19.object({
25158
- name: z19.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
25159
- kind: z19.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
25160
- content: z19.string().trim().min(1).max(2e5)
25583
+ inputSchema: z20.object({
25584
+ css: z20.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
25585
+ sources: z20.array(z20.object({
25586
+ name: z20.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
25587
+ kind: z20.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
25588
+ content: z20.string().trim().min(1).max(2e5)
25161
25589
  })).max(10).optional().describe("Any mix of brand sources, merged in order (later sources win). See the tool description for how to gather each kind."),
25162
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
25163
- maxTokens: z19.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25590
+ name: z20.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
25591
+ maxTokens: z20.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25164
25592
  }),
25165
25593
  annotations: READ_ONLY_TOOL_ANNOTATIONS
25166
25594
  },
@@ -25191,10 +25619,10 @@ Common vendor names (Shopify's --color-button family) are mapped into empty sema
25191
25619
  Use this when the user names their site ("match uclips.eu", "use our website's style") or hands you any public URL whose look should be reused. Server side, Better Design fetches the page's stylesheets AND renders the page in a headless browser to sample what role elements actually look like (body, heading, link, button, input, card), then merges both, rendered truth last. That reads the brand even when the site's variable names are unrecognizable.
25192
25620
 
25193
25621
  Slow by nature (a real browser loads the page): expect 10 to 45 seconds. For CSS the user already has locally, prefer extract-design-system, which is instant.`,
25194
- inputSchema: z19.object({
25195
- url: z19.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
25196
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
25197
- maxTokens: z19.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25622
+ inputSchema: z20.object({
25623
+ url: z20.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
25624
+ name: z20.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
25625
+ maxTokens: z20.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
25198
25626
  }),
25199
25627
  // Reads any public page the user names, so the set of hosts it reaches is
25200
25628
  // the open web, not Better Design's catalog. It stores nothing. [BD-316]
@@ -25244,7 +25672,7 @@ var init_extract_tools = __esm({
25244
25672
  });
25245
25673
 
25246
25674
  // ../shared/src/mcp/tools/illustration-tools.ts
25247
- import { z as z20 } from "zod";
25675
+ import { z as z21 } from "zod";
25248
25676
  function fail(message) {
25249
25677
  return {
25250
25678
  content: [{ type: "text", text: message }],
@@ -25290,11 +25718,11 @@ How to call it:
25290
25718
  4. Stop when \`faults\` is empty.
25291
25719
 
25292
25720
  Markup carrying script, event handlers, remote references, or styles that reach outside the drawing is refused rather than cleaned, so the drawing you fix is the drawing you sent.`,
25293
- inputSchema: z20.object({
25294
- svg: z20.string().min(1).describe("The complete svg element you drew."),
25295
- intent: z20.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
25296
- animated: z20.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
25297
- reviewToken: z20.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
25721
+ inputSchema: z21.object({
25722
+ svg: z21.string().min(1).describe("The complete svg element you drew."),
25723
+ intent: z21.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
25724
+ animated: z21.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
25725
+ reviewToken: z21.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
25298
25726
  }),
25299
25727
  outputSchema: REVIEW_OUTPUT_SCHEMA,
25300
25728
  // A review without a valid reviewToken passes the paywall, so it spends
@@ -25373,16 +25801,16 @@ var init_illustration_tools = __esm({
25373
25801
  init_shared();
25374
25802
  REVIEW_OUTPUT_SCHEMA = {
25375
25803
  /** Empty when nothing is visibly wrong. */
25376
- faults: z20.array(z20.string()),
25804
+ faults: z21.array(z21.string()),
25377
25805
  /** Group ids in paint order, the parts an animation can target. */
25378
- layers: z20.array(z20.string()),
25806
+ layers: z21.array(z21.string()),
25379
25807
  /** False when the markup was refused; faults says why. */
25380
- reviewed: z20.boolean(),
25808
+ reviewed: z21.boolean(),
25381
25809
  /** True when the drawing could not be rendered, so only structure was checked. */
25382
- unseen: z20.boolean(),
25810
+ unseen: z21.boolean(),
25383
25811
  /** Pass this back to review the same drawing again without spending another credit. */
25384
- reviewToken: z20.string(),
25385
- message: z20.string()
25812
+ reviewToken: z21.string(),
25813
+ message: z21.string()
25386
25814
  };
25387
25815
  }
25388
25816
  });
@@ -26744,7 +27172,7 @@ ul.evidence { padding-inline-start: 1rem; }
26744
27172
  });
26745
27173
 
26746
27174
  // ../shared/src/mcp/tools/inspection-tools.ts
26747
- import { z as z21 } from "zod";
27175
+ import { z as z22 } from "zod";
26748
27176
  function coversRecommendedWidth(states, width) {
26749
27177
  return states.some(
26750
27178
  (state) => Math.abs(state.viewport.width - width) <= WIDTH_TOLERANCE
@@ -26800,36 +27228,38 @@ function registerInspectionTools({ server, provider, tracked }) {
26800
27228
 
26801
27229
  This is the runtime counterpart to get-review-rules. It automatically loads the live visual-design rubric, but it does not guess pixel values from a screenshot. Exact claims come from DOM geometry.
26802
27230
 
27231
+ It measures geometry only. A score of 100 is not visual approval: it cannot judge hierarchy, density, or whether the design works, so look at a screenshot before you call a screen done.
27232
+
26803
27233
  Workflow:
26804
27234
  1. Call with no states to receive a read-only browser capture script in structuredContent.captureScript. Use detail: "full" if your client reads only text; it includes the script in text. That call measures nothing.
26805
27235
  2. Evaluate that script in each rendered state you need to review: at minimum 1440, 1728 and 390 wide. Also capture named hover, focus, selected, and open states when they change a control's surface or layout.
26806
27236
  3. Call again with the captured states and the project's spacing scale when known. The layout counts as measured only when this second call returns status \`completed\`; reading the snapshot yourself is not a measurement. The report and summary start with the highest-priority correction. Read structuredContent.findings or use detail: "full" for every returned finding, measurement, and fix. Structured evidence retains captured geometry and optional screenshots for report renderers.
26807
27237
 
26808
27238
  The capture reads visible layout geometry, computed spacing, corner radii, surface styles, the number of line boxes each text block rendered, and short accessible labels for interactive controls. It does not collect source code, cookies, form values, or network data. Add data-bd-inspect-label, data-bd-symmetry, data-bd-group, data-bd-compare-size, data-bd-align-group, and data-bd-align-edge attributes when the intended relationship cannot be inferred safely.`,
26809
- inputSchema: z21.object({
26810
- states: z21.array(spacingStateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
26811
- spacingScale: z21.array(finiteNumber.positive()).min(2).max(32).optional().describe("Project spacing tokens in CSS pixels. Defaults to 4, 8, 12, 16, 24, 32, 48, 64, 96, 128."),
27239
+ inputSchema: z22.object({
27240
+ states: z22.array(spacingStateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
27241
+ spacingScale: z22.array(finiteNumber.positive()).min(2).max(32).optional().describe("Project spacing tokens in CSS pixels. Defaults to 4, 8, 12, 16, 24, 32, 48, 64, 96, 128."),
26812
27242
  tolerance: finiteNumber.min(0).max(4).optional().describe("Pixel tolerance for grouping measurements. Defaults to 1px."),
26813
- detail: z21.enum(["summary", "full"]).default("summary").describe("Defaults to a short summary alongside the inline report. Use full for the capture script or all findings and measurements in text-only clients."),
27243
+ detail: z22.enum(["summary", "full"]).default("summary").describe("Defaults to a short summary alongside the inline report. Use full for the capture script or all findings and measurements in text-only clients."),
26814
27244
  context: CONTEXT_ARG
26815
27245
  }),
26816
- outputSchema: z21.object({
26817
- status: z21.enum(["needs-capture", "completed"]),
26818
- score: z21.number().optional(),
26819
- summary: z21.object({
26820
- critical: z21.number(),
26821
- serious: z21.number(),
26822
- moderate: z21.number(),
26823
- total: z21.number(),
26824
- truncated: z21.number().optional()
27246
+ outputSchema: z22.object({
27247
+ status: z22.enum(["needs-capture", "completed"]),
27248
+ score: z22.number().optional(),
27249
+ summary: z22.object({
27250
+ critical: z22.number(),
27251
+ serious: z22.number(),
27252
+ moderate: z22.number(),
27253
+ total: z22.number(),
27254
+ truncated: z22.number().optional()
26825
27255
  }).optional(),
26826
- findings: z21.array(findingSchema).optional(),
26827
- capturedWidths: z21.array(z21.number()).optional(),
26828
- missingWidths: z21.array(z21.number()).optional(),
26829
- states: z21.array(overlayStateSchema).optional(),
26830
- rubric: z21.array(z21.object({ id: z21.string(), title: z21.string() })).optional(),
26831
- rubricStatus: z21.enum(["loaded", "unavailable"]).optional(),
26832
- captureScript: z21.string().optional()
27256
+ findings: z22.array(findingSchema).optional(),
27257
+ capturedWidths: z22.array(z22.number()).optional(),
27258
+ missingWidths: z22.array(z22.number()).optional(),
27259
+ states: z22.array(overlayStateSchema).optional(),
27260
+ rubric: z22.array(z22.object({ id: z22.string(), title: z22.string() })).optional(),
27261
+ rubricStatus: z22.enum(["loaded", "unavailable"]).optional(),
27262
+ captureScript: z22.string().optional()
26833
27263
  }),
26834
27264
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
26835
27265
  _meta: {
@@ -26889,7 +27319,8 @@ ${SPACING_CAPTURE_SCRIPT}
26889
27319
  detail === "full" ? `Spacing score: ${result.score}/100.` : "",
26890
27320
  coverageLine,
26891
27321
  findingsText,
26892
- result.summary.truncated ? `${result.summary.truncated} additional findings exceeded the analysis limit. Inspect smaller sections before treating this review as complete.` : ""
27322
+ result.summary.truncated ? `${result.summary.truncated} additional findings exceeded the analysis limit. Inspect smaller sections before treating this review as complete.` : "",
27323
+ GEOMETRY_ONLY
26893
27324
  ].filter(Boolean).join("\n\n");
26894
27325
  return {
26895
27326
  content: [{ type: "text", text: text3 }],
@@ -26906,7 +27337,7 @@ ${SPACING_CAPTURE_SCRIPT}
26906
27337
  )
26907
27338
  );
26908
27339
  }
26909
- var finiteNumber, rectSchema, edgesSchema, cornerRadiusAxesSchema, cornerRadiusSchema, cornerRadiiSchema, styleSchema, elementSchema, screenshotSchema, spacingStateSchema, evidenceSchema, findingSchema, overlayStateSchema, RECOMMENDED_VIEWPORT_WIDTHS, WIDTH_TOLERANCE;
27340
+ var finiteNumber, rectSchema, edgesSchema, cornerRadiusAxesSchema, cornerRadiusSchema, cornerRadiiSchema, styleSchema, elementSchema, screenshotSchema, spacingStateSchema, evidenceSchema, findingSchema, overlayStateSchema, RECOMMENDED_VIEWPORT_WIDTHS, WIDTH_TOLERANCE, GEOMETRY_ONLY;
26910
27341
  var init_inspection_tools = __esm({
26911
27342
  "../shared/src/mcp/tools/inspection-tools.ts"() {
26912
27343
  "use strict";
@@ -26914,67 +27345,67 @@ var init_inspection_tools = __esm({
26914
27345
  init_finding_copy();
26915
27346
  init_spacing_inspection_html();
26916
27347
  init_shared();
26917
- finiteNumber = z21.number().finite();
26918
- rectSchema = z21.object({
27348
+ finiteNumber = z22.number().finite();
27349
+ rectSchema = z22.object({
26919
27350
  x: finiteNumber,
26920
27351
  y: finiteNumber,
26921
27352
  width: finiteNumber.nonnegative(),
26922
27353
  height: finiteNumber.nonnegative()
26923
27354
  });
26924
- edgesSchema = z21.object({
27355
+ edgesSchema = z22.object({
26925
27356
  top: finiteNumber,
26926
27357
  right: finiteNumber,
26927
27358
  bottom: finiteNumber,
26928
27359
  left: finiteNumber
26929
27360
  });
26930
- cornerRadiusAxesSchema = z21.object({
27361
+ cornerRadiusAxesSchema = z22.object({
26931
27362
  horizontal: finiteNumber.nonnegative(),
26932
27363
  vertical: finiteNumber.nonnegative()
26933
27364
  });
26934
- cornerRadiusSchema = z21.union([
27365
+ cornerRadiusSchema = z22.union([
26935
27366
  cornerRadiusAxesSchema,
26936
27367
  finiteNumber.nonnegative().transform((value) => ({
26937
27368
  horizontal: value,
26938
27369
  vertical: value
26939
27370
  }))
26940
27371
  ]);
26941
- cornerRadiiSchema = z21.object({
27372
+ cornerRadiiSchema = z22.object({
26942
27373
  topLeft: cornerRadiusSchema,
26943
27374
  topRight: cornerRadiusSchema,
26944
27375
  bottomRight: cornerRadiusSchema,
26945
27376
  bottomLeft: cornerRadiusSchema
26946
27377
  });
26947
- styleSchema = z21.object({
26948
- display: z21.string().max(60).optional(),
26949
- position: z21.string().max(60).optional(),
26950
- flexDirection: z21.string().max(60).optional(),
26951
- flexWrap: z21.string().max(60).optional(),
26952
- alignItems: z21.string().max(60).optional(),
26953
- justifyContent: z21.string().max(60).optional(),
27378
+ styleSchema = z22.object({
27379
+ display: z22.string().max(60).optional(),
27380
+ position: z22.string().max(60).optional(),
27381
+ flexDirection: z22.string().max(60).optional(),
27382
+ flexWrap: z22.string().max(60).optional(),
27383
+ alignItems: z22.string().max(60).optional(),
27384
+ justifyContent: z22.string().max(60).optional(),
26954
27385
  rowGap: finiteNumber.optional(),
26955
27386
  columnGap: finiteNumber.optional(),
26956
27387
  padding: edgesSchema.optional(),
26957
27388
  margin: edgesSchema.optional(),
26958
- overflowX: z21.string().max(60).optional(),
26959
- overflowY: z21.string().max(60).optional(),
27389
+ overflowX: z22.string().max(60).optional(),
27390
+ overflowY: z22.string().max(60).optional(),
26960
27391
  borderRadius: cornerRadiiSchema.optional(),
26961
- backgroundColor: z21.string().max(160).optional(),
26962
- backgroundImage: z21.string().max(1e3).optional(),
26963
- boxShadow: z21.string().max(1e3).optional()
27392
+ backgroundColor: z22.string().max(160).optional(),
27393
+ backgroundImage: z22.string().max(1e3).optional(),
27394
+ boxShadow: z22.string().max(1e3).optional()
26964
27395
  });
26965
- elementSchema = z21.object({
26966
- id: z21.string().min(1).max(300),
26967
- selector: z21.string().max(500).optional(),
26968
- tag: z21.string().max(80).optional(),
26969
- role: z21.string().max(80).optional(),
26970
- label: z21.string().max(160).optional(),
26971
- parentId: z21.string().max(300).optional(),
26972
- groupId: z21.string().max(300).optional(),
26973
- alignmentGroup: z21.string().max(300).optional(),
26974
- alignmentEdge: z21.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
26975
- expectedSymmetry: z21.enum(["horizontal", "vertical", "both"]).optional(),
26976
- compareSize: z21.enum(["width", "height", "both"]).optional(),
26977
- interactive: z21.boolean().optional(),
27396
+ elementSchema = z22.object({
27397
+ id: z22.string().min(1).max(300),
27398
+ selector: z22.string().max(500).optional(),
27399
+ tag: z22.string().max(80).optional(),
27400
+ role: z22.string().max(80).optional(),
27401
+ label: z22.string().max(160).optional(),
27402
+ parentId: z22.string().max(300).optional(),
27403
+ groupId: z22.string().max(300).optional(),
27404
+ alignmentGroup: z22.string().max(300).optional(),
27405
+ alignmentEdge: z22.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
27406
+ expectedSymmetry: z22.enum(["horizontal", "vertical", "both"]).optional(),
27407
+ compareSize: z22.enum(["width", "height", "both"]).optional(),
27408
+ interactive: z22.boolean().optional(),
26978
27409
  rect: rectSchema,
26979
27410
  style: styleSchema.optional(),
26980
27411
  lineCount: finiteNumber.int().positive().optional(),
@@ -26983,44 +27414,45 @@ var init_inspection_tools = __esm({
26983
27414
  scrollWidth: finiteNumber.nonnegative().optional(),
26984
27415
  scrollHeight: finiteNumber.nonnegative().optional()
26985
27416
  });
26986
- screenshotSchema = z21.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
26987
- spacingStateSchema = z21.object({
26988
- name: z21.string().min(1).max(100),
26989
- viewport: z21.object({
27417
+ screenshotSchema = z22.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
27418
+ spacingStateSchema = z22.object({
27419
+ name: z22.string().min(1).max(100),
27420
+ viewport: z22.object({
26990
27421
  width: finiteNumber.positive().max(2e4),
26991
27422
  height: finiteNumber.positive().max(2e4),
26992
27423
  devicePixelRatio: finiteNumber.positive().max(10).optional()
26993
27424
  }),
26994
- elements: z21.array(elementSchema).max(1500),
27425
+ elements: z22.array(elementSchema).max(1500),
26995
27426
  screenshotDataUrl: screenshotSchema.optional().describe("Optional browser screenshot as a data URL. It remains in the structured evidence for report renderers and is not persisted.")
26996
27427
  });
26997
- evidenceSchema = z21.object({
26998
- label: z21.string(),
26999
- value: z21.union([z21.number(), z21.string()]),
27000
- unit: z21.enum(["px", "count"]).optional()
27428
+ evidenceSchema = z22.object({
27429
+ label: z22.string(),
27430
+ value: z22.union([z22.number(), z22.string()]),
27431
+ unit: z22.enum(["px", "count"]).optional()
27001
27432
  });
27002
- findingSchema = z21.object({
27003
- id: z21.string(),
27004
- ruleId: z21.string(),
27005
- state: z21.string(),
27006
- severity: z21.enum(["critical", "serious", "moderate"]),
27007
- message: z21.string(),
27008
- fix: z21.string(),
27009
- elementIds: z21.array(z21.string()),
27010
- evidence: z21.array(evidenceSchema)
27433
+ findingSchema = z22.object({
27434
+ id: z22.string(),
27435
+ ruleId: z22.string(),
27436
+ state: z22.string(),
27437
+ severity: z22.enum(["critical", "serious", "moderate"]),
27438
+ message: z22.string(),
27439
+ fix: z22.string(),
27440
+ elementIds: z22.array(z22.string()),
27441
+ evidence: z22.array(evidenceSchema)
27011
27442
  });
27012
- overlayStateSchema = z21.object({
27013
- name: z21.string(),
27014
- viewport: z21.object({
27015
- width: z21.number(),
27016
- height: z21.number(),
27017
- devicePixelRatio: z21.number().optional()
27443
+ overlayStateSchema = z22.object({
27444
+ name: z22.string(),
27445
+ viewport: z22.object({
27446
+ width: z22.number(),
27447
+ height: z22.number(),
27448
+ devicePixelRatio: z22.number().optional()
27018
27449
  }),
27019
- elements: z21.array(z21.object({ id: z21.string(), label: z21.string().optional(), rect: rectSchema })),
27020
- screenshotDataUrl: z21.string().optional()
27450
+ elements: z22.array(z22.object({ id: z22.string(), label: z22.string().optional(), rect: rectSchema })),
27451
+ screenshotDataUrl: z22.string().optional()
27021
27452
  });
27022
27453
  RECOMMENDED_VIEWPORT_WIDTHS = [390, 1440, 1728];
27023
27454
  WIDTH_TOLERANCE = 40;
27455
+ GEOMETRY_ONLY = "This measured geometry only. A clean result or a score of 100 is not visual approval: it cannot judge hierarchy, density or whether the design works.";
27024
27456
  }
27025
27457
  });
27026
27458
 
@@ -27059,7 +27491,7 @@ var init_inspection_resource = __esm({
27059
27491
  });
27060
27492
 
27061
27493
  // ../shared/src/mcp/tools/dashboard-review-tools.ts
27062
- import { z as z22 } from "zod";
27494
+ import { z as z23 } from "zod";
27063
27495
  function finding(id, severity, message, fix) {
27064
27496
  return { id, severity, message, fix };
27065
27497
  }
@@ -27566,11 +27998,11 @@ function registerDashboardReviewTools({ server, track }) {
27566
27998
  Pass the complete page module source, the unchanged fixture, the factual interpretation, and the installed Sidebar, StatCard, and Table source files. Never summarize, reconstruct, or submit only the overview component. The tool derives the overview contract from the fixture. It fails incomplete source, rewritten copy, unsupported claims, weak hierarchy, missing or under-composed installed components, raw aside/table markup, the wrong scan order, action color on every rate, and stepper rails on short flows.
27567
27999
 
27568
28000
  FAIL is blocking and returns an MCP tool error. Fix every finding and call the tool again until PASS. Then run check-comprehension and inspect-spacing, and call finalize-dashboard-review. Only its VALID source-bound receipt permits presentation.`,
27569
- inputSchema: z22.object({
27570
- screen: z22.string().trim().min(1).max(120).describe("What this dashboard is, e.g. 'funnel report overview'."),
27571
- source: z22.string().min(1).max(12e4).describe("Complete TSX or JSX source for the dashboard page."),
28001
+ inputSchema: z23.object({
28002
+ screen: z23.string().trim().min(1).max(120).describe("What this dashboard is, e.g. 'funnel report overview'."),
28003
+ source: z23.string().min(1).max(12e4).describe("Complete TSX or JSX source for the dashboard page."),
27572
28004
  fixture: dashboardFixtureSchema.describe("The request's unchanged fixture object. The first step is the overview; remaining steps are dependent task screens."),
27573
- interpretation: z22.string().trim().min(1).max(2e3).describe("Exact supplied or factually derived takeaway that must appear after records."),
28005
+ interpretation: z23.string().trim().min(1).max(2e3).describe("Exact supplied or factually derived takeaway that must appear after records."),
27574
28006
  componentSources: dashboardComponentSourcesSchema.optional().describe("Installed Sidebar, StatCard, and Table source files for compound-composition review."),
27575
28007
  context: CONTEXT_ARG
27576
28008
  }),
@@ -27618,34 +28050,34 @@ var init_dashboard_review_tools = __esm({
27618
28050
  "use strict";
27619
28051
  init_review_capture();
27620
28052
  init_shared();
27621
- dashboardFixtureSchema = z22.object({
27622
- stats: z22.array(z22.object({
27623
- label: z22.string().trim().min(1).max(300),
27624
- value: z22.string().trim().min(1).max(300)
28053
+ dashboardFixtureSchema = z23.object({
28054
+ stats: z23.array(z23.object({
28055
+ label: z23.string().trim().min(1).max(300),
28056
+ value: z23.string().trim().min(1).max(300)
27625
28057
  })).max(100).optional(),
27626
- records: z22.array(z22.object({
27627
- title: z22.string().trim().min(1).max(300),
27628
- detail: z22.string().trim().min(1).max(1e3),
27629
- meta: z22.string().trim().min(1).max(300),
27630
- status: z22.string().trim().min(1).max(300)
28058
+ records: z23.array(z23.object({
28059
+ title: z23.string().trim().min(1).max(300),
28060
+ detail: z23.string().trim().min(1).max(1e3),
28061
+ meta: z23.string().trim().min(1).max(300),
28062
+ status: z23.string().trim().min(1).max(300)
27631
28063
  })).max(100).optional(),
27632
- steps: z22.array(z22.object({
27633
- title: z22.string().trim().min(1).max(300),
27634
- description: z22.string().trim().min(1).max(1e3),
27635
- action: z22.string().trim().min(1).max(200)
28064
+ steps: z23.array(z23.object({
28065
+ title: z23.string().trim().min(1).max(300),
28066
+ description: z23.string().trim().min(1).max(1e3),
28067
+ action: z23.string().trim().min(1).max(200)
27636
28068
  })).min(2).max(50)
27637
28069
  });
27638
- dashboardComponentSourcesSchema = z22.object({
27639
- sidebar: z22.string().min(1).max(12e4),
27640
- statCard: z22.string().min(1).max(12e4),
27641
- table: z22.string().min(1).max(12e4)
28070
+ dashboardComponentSourcesSchema = z23.object({
28071
+ sidebar: z23.string().min(1).max(12e4),
28072
+ statCard: z23.string().min(1).max(12e4),
28073
+ table: z23.string().min(1).max(12e4)
27642
28074
  });
27643
28075
  }
27644
28076
  });
27645
28077
 
27646
28078
  // ../shared/src/mcp/tools/final-ui-review-tools.ts
27647
28079
  import { createHash as createHash2 } from "crypto";
27648
- import { z as z23 } from "zod";
28080
+ import { z as z24 } from "zod";
27649
28081
  function digest(value) {
27650
28082
  return createHash2("sha256").update(value).digest("hex");
27651
28083
  }
@@ -27656,7 +28088,9 @@ function comprehensionFindings(input) {
27656
28088
  const report = analyzeComprehension({
27657
28089
  screen: input.screen,
27658
28090
  code: input.visibleSource,
27659
- knownTerms: input.knownTerms
28091
+ knownTerms: input.knownTerms,
28092
+ // A dashboard is a screen even when its name mentions slides. [BD-593]
28093
+ surface: "screen"
27660
28094
  });
27661
28095
  return report.findings.flatMap(
27662
28096
  (item, index2) => item.severity === "minor" ? [] : [
@@ -28057,15 +28491,15 @@ function registerFinalUiReviewTools({
28057
28491
  description: `Issue a source-bound receipt for a completed dashboard.
28058
28492
 
28059
28493
  Submit the public HTTPS preview plus exact source, unchanged data, and installed components. Better Design's own browser captures the deployed page at 390, 1440, and 1728 pixels and content-addresses the live deployment. No trusted browser capture means incomplete. The review blocks weak hierarchy, under-composed components, hidden fixture fields, unclear copy, spacing defects, unsupported interpretations, or source edits.`,
28060
- inputSchema: z23.object({
28061
- screen: z23.string().trim().min(1).max(120),
28062
- previewUrl: z23.string().url().max(2048).refine((value) => new URL(value).protocol === "https:", {
28494
+ inputSchema: z24.object({
28495
+ screen: z24.string().trim().min(1).max(120),
28496
+ previewUrl: z24.string().url().max(2048).refine((value) => new URL(value).protocol === "https:", {
28063
28497
  message: "Use a public HTTPS preview URL."
28064
28498
  }).describe("Public HTTPS URL for the exact deployed dashboard route."),
28065
- source: z23.string().min(1).max(24e4).describe(
28499
+ source: z24.string().min(1).max(24e4).describe(
28066
28500
  "Complete TSX or JSX page source, including its default export."
28067
28501
  ),
28068
- visibleSource: z23.string().min(1).max(12e4).describe(
28502
+ visibleSource: z24.string().min(1).max(12e4).describe(
28069
28503
  "Exact visible screen component copied unchanged from source."
28070
28504
  ),
28071
28505
  fixture: dashboardFixtureSchema.describe(
@@ -28074,7 +28508,7 @@ Submit the public HTTPS preview plus exact source, unchanged data, and installed
28074
28508
  componentSources: dashboardComponentSourcesSchema.describe(
28075
28509
  "Exact installed Sidebar, StatCard, and Table component source files."
28076
28510
  ),
28077
- knownTerms: z23.array(z23.string().trim().min(1).max(60)).max(50).optional(),
28511
+ knownTerms: z24.array(z24.string().trim().min(1).max(60)).max(50).optional(),
28078
28512
  context: CONTEXT_ARG
28079
28513
  }),
28080
28514
  annotations: READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS
@@ -28145,7 +28579,7 @@ var init_final_ui_review_tools = __esm({
28145
28579
  });
28146
28580
 
28147
28581
  // ../shared/src/mcp/tools/widget-tools.ts
28148
- import { z as z24 } from "zod";
28582
+ import { z as z25 } from "zod";
28149
28583
  function hasIntake(args) {
28150
28584
  return Boolean(args.widgetType || INTAKE_FIELDS.some(({ key }) => args[key]));
28151
28585
  }
@@ -28268,16 +28702,16 @@ var init_widget_tools = __esm({
28268
28702
  WIDGET_GUIDE_TOPIC = "better-widgets";
28269
28703
  WIDGET_GUIDE_TOOL = "get-widget-guide";
28270
28704
  MINIMUM_CONTRACT_HEADING = "## Minimum build contract";
28271
- WIDGET_GUIDE_INPUT_SCHEMA = z24.object({
28272
- widgetType: z24.enum(BETTER_WIDGET_TYPES).optional().describe("Widget kind: calculator, estimator, converter, generator, quiz, or checklist."),
28273
- userGoal: z24.string().trim().min(1).max(500).optional().describe("Who will use the widget and the decision or task it helps them complete."),
28274
- inputs: z24.string().trim().min(1).max(1e3).optional().describe("Required inputs, including units, allowed ranges, and defaults."),
28275
- interaction: z24.string().trim().min(1).max(750).optional().describe("How readers tune inputs and see feedback, including presets, custom controls, direct manipulation, and exact-value entry."),
28276
- transformation: z24.string().trim().min(1).max(1500).optional().describe("Formula, scoring rule, conversion, or transformation that produces the result."),
28277
- result: z24.string().trim().min(1).max(750).optional().describe("What the result shows, explains, and lets the user do next."),
28278
- hostFramework: z24.string().trim().min(1).max(300).optional().describe("The host framework and the page or component that will contain the widget."),
28279
- visualContext: z24.string().trim().min(1).max(750).optional().describe("Existing design tokens, components, or brand reference the widget must match."),
28280
- maxTokens: z24.number().min(2e3).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
28705
+ WIDGET_GUIDE_INPUT_SCHEMA = z25.object({
28706
+ widgetType: z25.enum(BETTER_WIDGET_TYPES).optional().describe("Widget kind: calculator, estimator, converter, generator, quiz, or checklist."),
28707
+ userGoal: z25.string().trim().min(1).max(500).optional().describe("Who will use the widget and the decision or task it helps them complete."),
28708
+ inputs: z25.string().trim().min(1).max(1e3).optional().describe("Required inputs, including units, allowed ranges, and defaults."),
28709
+ interaction: z25.string().trim().min(1).max(750).optional().describe("How readers tune inputs and see feedback, including presets, custom controls, direct manipulation, and exact-value entry."),
28710
+ transformation: z25.string().trim().min(1).max(1500).optional().describe("Formula, scoring rule, conversion, or transformation that produces the result."),
28711
+ result: z25.string().trim().min(1).max(750).optional().describe("What the result shows, explains, and lets the user do next."),
28712
+ hostFramework: z25.string().trim().min(1).max(300).optional().describe("The host framework and the page or component that will contain the widget."),
28713
+ visualContext: z25.string().trim().min(1).max(750).optional().describe("Existing design tokens, components, or brand reference the widget must match."),
28714
+ maxTokens: z25.number().min(2e3).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
28281
28715
  context: CONTEXT_ARG
28282
28716
  });
28283
28717
  INTAKE_FIELDS = [
@@ -28331,7 +28765,7 @@ The completed response combines a scoped build brief with Better Design's stable
28331
28765
  });
28332
28766
 
28333
28767
  // ../shared/src/mcp/feature-map/schema.ts
28334
- import { z as z25 } from "zod";
28768
+ import { z as z26 } from "zod";
28335
28769
  function oneLine(text3) {
28336
28770
  return text3.replace(/\s+/g, " ").trim();
28337
28771
  }
@@ -28366,38 +28800,38 @@ var init_schema3 = __esm({
28366
28800
  "use strict";
28367
28801
  FEATURE_MAP_PATH = ".better-design/feature-map.json";
28368
28802
  FEATURE_MAP_LIMITS = { features: 200, flows: 100, steps: 50, paths: 200, name: 120 };
28369
- NAME = z25.string().trim().min(1).max(FEATURE_MAP_LIMITS.name);
28370
- PATHS = z25.array(z25.string().trim().min(1).max(500)).max(FEATURE_MAP_LIMITS.paths).default([]);
28371
- featureMapStepSchema = z25.object({
28803
+ NAME = z26.string().trim().min(1).max(FEATURE_MAP_LIMITS.name);
28804
+ PATHS = z26.array(z26.string().trim().min(1).max(500)).max(FEATURE_MAP_LIMITS.paths).default([]);
28805
+ featureMapStepSchema = z26.object({
28372
28806
  name: NAME,
28373
28807
  /** URL path such as `/settings/billing`. Absent for a dialog or an email. */
28374
- route: z25.string().trim().max(300).optional(),
28808
+ route: z26.string().trim().max(300).optional(),
28375
28809
  /** The file that renders the step, such as the route's page file. */
28376
- screen: z25.string().trim().max(500).optional(),
28810
+ screen: z26.string().trim().max(500).optional(),
28377
28811
  /** What the user does here, such as "confirms the cancellation". */
28378
- action: z25.string().trim().max(300).optional(),
28812
+ action: z26.string().trim().max(300).optional(),
28379
28813
  components: PATHS,
28380
28814
  /** API routes, server actions, hooks and queries the step reads or writes. */
28381
28815
  data: PATHS
28382
28816
  });
28383
- featureMapFlowSchema = z25.object({
28817
+ featureMapFlowSchema = z26.object({
28384
28818
  id: NAME,
28385
28819
  name: NAME,
28386
- description: z25.string().trim().max(500).optional(),
28387
- steps: z25.array(featureMapStepSchema).min(1).max(FEATURE_MAP_LIMITS.steps)
28820
+ description: z26.string().trim().max(500).optional(),
28821
+ steps: z26.array(featureMapStepSchema).min(1).max(FEATURE_MAP_LIMITS.steps)
28388
28822
  });
28389
- featureMapFeatureSchema = z25.object({
28823
+ featureMapFeatureSchema = z26.object({
28390
28824
  id: NAME,
28391
28825
  name: NAME,
28392
- description: z25.string().trim().max(500).optional(),
28393
- flows: z25.array(featureMapFlowSchema).max(FEATURE_MAP_LIMITS.flows),
28826
+ description: z26.string().trim().max(500).optional(),
28827
+ flows: z26.array(featureMapFlowSchema).max(FEATURE_MAP_LIMITS.flows),
28394
28828
  /** Components that belong to the feature but to no single step. */
28395
28829
  components: PATHS
28396
28830
  });
28397
- featureMapSchema = z25.object({
28398
- version: z25.literal(1),
28399
- product: z25.string().trim().max(120).optional(),
28400
- features: z25.array(featureMapFeatureSchema).max(FEATURE_MAP_LIMITS.features)
28831
+ featureMapSchema = z26.object({
28832
+ version: z26.literal(1),
28833
+ product: z26.string().trim().max(120).optional(),
28834
+ features: z26.array(featureMapFeatureSchema).max(FEATURE_MAP_LIMITS.features)
28401
28835
  });
28402
28836
  }
28403
28837
  });
@@ -28675,7 +29109,7 @@ var init_read = __esm({
28675
29109
  });
28676
29110
 
28677
29111
  // ../shared/src/mcp/feature-map/update.ts
28678
- import { z as z26 } from "zod";
29112
+ import { z as z27 } from "zod";
28679
29113
  function find(items, ref) {
28680
29114
  const r = ref.trim();
28681
29115
  const index2 = items.findIndex((i) => i.id === r || i.name.toLowerCase() === r.toLowerCase());
@@ -28738,21 +29172,21 @@ var init_update2 = __esm({
28738
29172
  "../shared/src/mcp/feature-map/update.ts"() {
28739
29173
  "use strict";
28740
29174
  init_schema3();
28741
- NAME2 = z26.string().trim().min(1).max(120);
28742
- REF = z26.string().trim().min(1).max(120).describe("The id or exact name of an existing feature or flow.");
29175
+ NAME2 = z27.string().trim().min(1).max(120);
29176
+ REF = z27.string().trim().min(1).max(120).describe("The id or exact name of an existing feature or flow.");
28743
29177
  FLOW_INPUT = featureMapFlowSchema.omit({ id: true });
28744
- featureMapChangeSchema = z26.discriminatedUnion("op", [
28745
- z26.object({
28746
- op: z26.literal("add-feature"),
29178
+ featureMapChangeSchema = z27.discriminatedUnion("op", [
29179
+ z27.object({
29180
+ op: z27.literal("add-feature"),
28747
29181
  name: NAME2,
28748
- description: z26.string().trim().max(500).optional(),
28749
- flows: z26.array(FLOW_INPUT).max(100).optional()
29182
+ description: z27.string().trim().max(500).optional(),
29183
+ flows: z27.array(FLOW_INPUT).max(100).optional()
28750
29184
  }),
28751
- z26.object({ op: z26.literal("rename-feature"), feature: REF, name: NAME2 }),
28752
- z26.object({ op: z26.literal("remove-feature"), feature: REF }),
28753
- z26.object({ op: z26.literal("set-flow"), feature: REF, flow: FLOW_INPUT }),
28754
- z26.object({ op: z26.literal("rename-flow"), feature: REF, flow: REF, name: NAME2 }),
28755
- z26.object({ op: z26.literal("remove-flow"), feature: REF, flow: REF })
29185
+ z27.object({ op: z27.literal("rename-feature"), feature: REF, name: NAME2 }),
29186
+ z27.object({ op: z27.literal("remove-feature"), feature: REF }),
29187
+ z27.object({ op: z27.literal("set-flow"), feature: REF, flow: FLOW_INPUT }),
29188
+ z27.object({ op: z27.literal("rename-flow"), feature: REF, flow: REF, name: NAME2 }),
29189
+ z27.object({ op: z27.literal("remove-flow"), feature: REF, flow: REF })
28756
29190
  ]);
28757
29191
  idTaken = (items, id, except2) => items.some((item, i) => item.id === id && i !== except2);
28758
29192
  toFlow = (f) => ({ id: slugify(f.name), ...f });
@@ -28771,7 +29205,7 @@ var init_feature_map = __esm({
28771
29205
  });
28772
29206
 
28773
29207
  // ../shared/src/mcp/tools/feature-map-tools.ts
28774
- import { z as z27 } from "zod";
29208
+ import { z as z28 } from "zod";
28775
29209
  function registerFeatureMapTools({ server, tracked }) {
28776
29210
  server.registerTool(
28777
29211
  "build-feature-map",
@@ -28782,9 +29216,9 @@ function registerFeatureMapTools({ server, tracked }) {
28782
29216
  Run \`git ls-files\` in the repository and pass every path as \`files\`. Routes become screens (Next.js app and pages routers, Remix and React Router flat routes, SvelteKit, Nuxt), each chain of nested routes becomes a flow (/settings/billing/cancel is Settings \u2192 Billing \u2192 Cancel), and components, API routes, hooks and server actions join the step whose name they share.
28783
29217
 
28784
29218
  The result is a first draft. Write it to ${FEATURE_MAP_PATH}, show the user the flows, and ask which steps are missing: a dialog, an email or a background job has no route, so add those with update-feature-map (op "set-flow").`,
28785
- inputSchema: z27.object({
28786
- files: z27.array(z27.string().max(500)).min(1).max(5e4).describe("Every file path in the repository, relative to its root, as printed by `git ls-files`."),
28787
- product: z27.string().trim().max(120).optional().describe("The product's name, stored at the top of the map."),
29219
+ inputSchema: z28.object({
29220
+ files: z28.array(z28.string().max(500)).min(1).max(5e4).describe("Every file path in the repository, relative to its root, as printed by `git ls-files`."),
29221
+ product: z28.string().trim().max(120).optional().describe("The product's name, stored at the top of the map."),
28788
29222
  context: CONTEXT_ARG
28789
29223
  }),
28790
29224
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -28814,11 +29248,11 @@ ${serializeFeatureMap(map).trimEnd()}
28814
29248
  description: `Find the flows a change touches, with the screen, route, components and data files of every step. ${WHEN_TO_USE}
28815
29249
 
28816
29250
  Pass \`flow\` ("cancel account"), \`feature\` ("billing") or \`file\` (a path you are about to edit) to narrow it. With no query it lists every flow.`,
28817
- inputSchema: z27.object({
29251
+ inputSchema: z28.object({
28818
29252
  map: MAP_ARG,
28819
- feature: z27.string().trim().max(120).optional().describe("A feature id or part of its name."),
28820
- flow: z27.string().trim().max(120).optional().describe("A flow id or part of its name."),
28821
- file: z27.string().trim().max(500).optional().describe("A file path or route; returns every flow that uses it."),
29253
+ feature: z28.string().trim().max(120).optional().describe("A feature id or part of its name."),
29254
+ flow: z28.string().trim().max(120).optional().describe("A flow id or part of its name."),
29255
+ file: z28.string().trim().max(500).optional().describe("A file path or route; returns every flow that uses it."),
28822
29256
  context: CONTEXT_ARG
28823
29257
  }),
28824
29258
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -28844,7 +29278,7 @@ Changes, one per call:
28844
29278
  - { op: "remove-flow", feature, flow }
28845
29279
 
28846
29280
  \`feature\` and \`flow\` take an id or an exact name. Write the returned file to ${FEATURE_MAP_PATH}.`,
28847
- inputSchema: z27.object({
29281
+ inputSchema: z28.object({
28848
29282
  map: MAP_ARG,
28849
29283
  change: featureMapChangeSchema,
28850
29284
  context: CONTEXT_ARG
@@ -28866,7 +29300,7 @@ var init_feature_map_tools = __esm({
28866
29300
  "use strict";
28867
29301
  init_feature_map();
28868
29302
  init_shared();
28869
- MAP_ARG = z27.string().min(1).max(2e6).describe(`The exact contents of ${FEATURE_MAP_PATH}.`);
29303
+ MAP_ARG = z28.string().min(1).max(2e6).describe(`The exact contents of ${FEATURE_MAP_PATH}.`);
28870
29304
  text2 = (body, isError = false) => ({
28871
29305
  content: [{ type: "text", text: body }],
28872
29306
  isError
@@ -28887,8 +29321,185 @@ var init_feature_map_tools = __esm({
28887
29321
  }
28888
29322
  });
28889
29323
 
29324
+ // ../shared/src/mcp/inspiration.ts
29325
+ import { z as z29 } from "zod";
29326
+ function describeSet(set) {
29327
+ const heading = `## ${set.title}${set.category ? ` (${set.category})` : ""}`;
29328
+ if (!set.notes) return heading;
29329
+ const written = /* @__PURE__ */ new Date(`${set.notes.writtenAt}T00:00:00Z`);
29330
+ const date2 = Number.isNaN(written.getTime()) ? set.notes.writtenAt : NOTE_DATE.format(written);
29331
+ return [
29332
+ heading,
29333
+ `Headlines: ${set.notes.headline}`,
29334
+ `Slide order: ${set.notes.order}`,
29335
+ `Framing: ${set.notes.framing}`,
29336
+ `Color: ${set.notes.color}`,
29337
+ `Better Design's notes, written by a vision model from these screenshots on ${date2}. The listing may have changed since.`
29338
+ ].join("\n");
29339
+ }
29340
+ var INSPIRATION_TYPES, MAX_INSPIRATION_LIMIT, noteSchema, inspirationNotesFileSchema, indexFileSchema, indexEntrySchema, inspirationIndexSchema, NOTE_DATE;
29341
+ var init_inspiration = __esm({
29342
+ "../shared/src/mcp/inspiration.ts"() {
29343
+ "use strict";
29344
+ INSPIRATION_TYPES = ["app-store-screenshots"];
29345
+ MAX_INSPIRATION_LIMIT = 6;
29346
+ noteSchema = z29.object({
29347
+ slug: z29.string(),
29348
+ /** The day the note was written, as YYYY-MM-DD. A listing can change after it. */
29349
+ writtenAt: z29.string(),
29350
+ headline: z29.string(),
29351
+ order: z29.string(),
29352
+ framing: z29.string(),
29353
+ color: z29.string()
29354
+ });
29355
+ inspirationNotesFileSchema = z29.object({
29356
+ model: z29.string(),
29357
+ notes: z29.array(noteSchema)
29358
+ });
29359
+ indexFileSchema = z29.object({
29360
+ key: z29.string(),
29361
+ type: z29.string(),
29362
+ width: z29.number(),
29363
+ height: z29.number()
29364
+ });
29365
+ indexEntrySchema = z29.object({
29366
+ title: z29.string(),
29367
+ slug: z29.string(),
29368
+ category: z29.string().nullish(),
29369
+ files: z29.array(indexFileSchema)
29370
+ });
29371
+ inspirationIndexSchema = z29.object({
29372
+ entries: z29.array(indexEntrySchema)
29373
+ });
29374
+ NOTE_DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
29375
+ }
29376
+ });
29377
+
29378
+ // ../shared/src/mcp/tools/inspiration-tools.ts
29379
+ import { z as z30 } from "zod";
29380
+ function describeSets(result) {
29381
+ const lines = result.sets.map((set, i) => {
29382
+ const category = set.category ? `, ${set.category}` : "";
29383
+ return `${i + 1}. ${set.title}${category}: ${set.images.length} screenshot${set.images.length === 1 ? "" : "s"} below`;
29384
+ });
29385
+ return [
29386
+ `# App Store screenshot sets`,
29387
+ "",
29388
+ ...lines,
29389
+ "",
29390
+ "Each image follows the set it belongs to. Read each set for its headline style, slide order, device framing and color, then propose two or three directions for the user's own screenshots and let the user pick one."
29391
+ ].join("\n");
29392
+ }
29393
+ function noMatch(result, category) {
29394
+ const asked = category ? `No App Store screenshot sets match the category "${category}".` : "The library holds no App Store screenshot sets yet.";
29395
+ return `${asked} Categories in the library: ${result.categories.join(", ")}.`;
29396
+ }
29397
+ function registerInspirationTools({ server, tracked }, readInspiration2) {
29398
+ server.registerTool(
29399
+ "get-inspiration",
29400
+ {
29401
+ title: "Get Inspiration",
29402
+ description: `Send real reference designs from Better Design's library as images, so you can study how shipped products solved the same design problem before you design the user's own.
29403
+
29404
+ Use it before designing App Store or Google Play screenshots: pass \`type: "app-store-screenshots"\`, and a \`category\` such as "Health & Fitness" when the user's app has one. Each set arrives as its title and category, followed by its first screenshots.
29405
+
29406
+ Then propose two or three directions drawn from what the sets do (headline style, slide order, device framing, color), and let the user choose before you build anything.`,
29407
+ inputSchema: z30.object({
29408
+ type: z30.enum(INSPIRATION_TYPES).describe('The kind of reference to send. Today: "app-store-screenshots".'),
29409
+ category: z30.string().trim().min(1).max(60).optional().describe('Optional App Store category, for example "Productivity" or "Health & Fitness". Ignores case.'),
29410
+ limit: z30.number().int().min(1).max(MAX_INSPIRATION_LIMIT).optional().describe(`How many sets to send, 1 to ${MAX_INSPIRATION_LIMIT}. Defaults to 3.`),
29411
+ context: CONTEXT_ARG
29412
+ }),
29413
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
29414
+ },
29415
+ async ({ type, category, limit, context }) => tracked("get-inspiration", { type, category: category ?? null, limit: limit ?? null, context }, async () => {
29416
+ if (!readInspiration2) return { content: [{ type: "text", text: NOT_AVAILABLE }] };
29417
+ const result = await readInspiration2({ type, category, limit });
29418
+ if (result.sets.length === 0) {
29419
+ return { content: [{ type: "text", text: noMatch(result, category) }] };
29420
+ }
29421
+ return {
29422
+ content: [
29423
+ { type: "text", text: describeSets(result) },
29424
+ ...result.sets.flatMap((set) => [
29425
+ { type: "text", text: describeSet(set) },
29426
+ ...set.images.map((image) => ({
29427
+ type: "image",
29428
+ data: image.data,
29429
+ mimeType: image.mimeType
29430
+ }))
29431
+ ])
29432
+ ]
29433
+ };
29434
+ })
29435
+ );
29436
+ }
29437
+ var NOT_AVAILABLE;
29438
+ var init_inspiration_tools = __esm({
29439
+ "../shared/src/mcp/tools/inspiration-tools.ts"() {
29440
+ "use strict";
29441
+ init_shared();
29442
+ init_inspiration();
29443
+ NOT_AVAILABLE = "The inspiration library is not available from this server. Try again with the hosted Better Design MCP server or the latest `better-design` npm package.";
29444
+ }
29445
+ });
29446
+
29447
+ // ../shared/src/mcp/tools/feedback-tools.ts
29448
+ function createFeedbackSender(apiFetch, apiKey) {
29449
+ return async (report) => {
29450
+ const res = await apiFetch(`${APP_BASE_URL}/api/v1/feedback`, {
29451
+ method: "POST",
29452
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
29453
+ body: JSON.stringify(report),
29454
+ signal: AbortSignal.timeout(15e3)
29455
+ });
29456
+ const body = await res.json().catch(() => ({}));
29457
+ if (!res.ok || !body.id) throw new Error(body.message ?? `HTTP ${res.status}`);
29458
+ return { id: body.id, status: body.status ?? "open", message: body.message ?? "" };
29459
+ };
29460
+ }
29461
+ function registerFeedbackTools({ server, tracked }, sendFeedback) {
29462
+ if (!sendFeedback) return;
29463
+ server.registerTool(
29464
+ "send-feedback",
29465
+ {
29466
+ title: "Send Feedback",
29467
+ description: `Tell Better Design about a bug, a missing feature or wrong output you hit while using its tools or API. A person reads every report and approves any fix.
29468
+
29469
+ Send one report per problem, once you have finished what the user asked. Say what you tried, what you expected and what came back. Leave out secrets, personal data and the user's private code. Sending a report never spends credit.`,
29470
+ inputSchema: reportSchema,
29471
+ // Each call stores one new report and publishes nothing. See
29472
+ // `docs/mcp-tool-annotations.md`.
29473
+ annotations: {
29474
+ readOnlyHint: false,
29475
+ destructiveHint: false,
29476
+ idempotentHint: false,
29477
+ openWorldHint: false
29478
+ }
29479
+ },
29480
+ // Usage telemetry records the kind only. The report itself goes to the queue.
29481
+ async (report) => tracked("send-feedback", { kind: report.kind }, async () => {
29482
+ const info = server.server.getClientVersion();
29483
+ const client = info ? `${info.name}/${info.version}`.slice(0, AGENT_FEEDBACK_CLIENT_MAX) : void 0;
29484
+ const receipt = await sendFeedback({ ...report, client });
29485
+ return {
29486
+ content: [{ type: "text", text: `Report ${receipt.id} sent. ${receipt.message}` }]
29487
+ };
29488
+ })
29489
+ );
29490
+ }
29491
+ var reportSchema;
29492
+ var init_feedback_tools = __esm({
29493
+ "../shared/src/mcp/tools/feedback-tools.ts"() {
29494
+ "use strict";
29495
+ init_agent_feedback();
29496
+ init_shared();
29497
+ reportSchema = agentFeedbackInputSchema.omit({ client: true });
29498
+ }
29499
+ });
29500
+
28890
29501
  // ../shared/src/mcp/tools.ts
28891
- function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2) {
29502
+ function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2, readInspiration2, sendFeedback) {
28892
29503
  const { track, tracked } = createTracking(server, onToolUsage);
28893
29504
  const deps = {
28894
29505
  server,
@@ -28914,6 +29525,8 @@ function registerAllTools(server, provider, onToolUsage, authContext, options =
28914
29525
  registerInspectionTools(deps);
28915
29526
  registerFeatureMapTools(deps);
28916
29527
  registerTeamRulesTools(deps);
29528
+ registerInspirationTools(deps, readInspiration2);
29529
+ registerFeedbackTools(deps, sendFeedback);
28917
29530
  registerPreviewResource(deps);
28918
29531
  registerInspectionResource(deps);
28919
29532
  }
@@ -28937,6 +29550,8 @@ var init_tools = __esm({
28937
29550
  init_widget_tools();
28938
29551
  init_feature_map_tools();
28939
29552
  init_team_rules_tools();
29553
+ init_inspiration_tools();
29554
+ init_feedback_tools();
28940
29555
  init_shared();
28941
29556
  }
28942
29557
  });
@@ -28946,7 +29561,7 @@ var MCP_IMPLEMENTATION_VERSION;
28946
29561
  var init_version2 = __esm({
28947
29562
  "../shared/src/mcp/version.ts"() {
28948
29563
  "use strict";
28949
- MCP_IMPLEMENTATION_VERSION = "0.31.1";
29564
+ MCP_IMPLEMENTATION_VERSION = "0.34.0";
28950
29565
  }
28951
29566
  });
28952
29567
 
@@ -28979,6 +29594,7 @@ __export(mcp_exports, {
28979
29594
  analyzeDashboardStructure: () => analyzeDashboardStructure,
28980
29595
  buildShopifyPrimitiveSet: () => buildShopifyPrimitiveSet,
28981
29596
  canonicalMcpToolName: () => canonicalMcpToolName,
29597
+ createFeedbackSender: () => createFeedbackSender,
28982
29598
  createMcpServer: () => createMcpServer,
28983
29599
  declareAdvertisedSchemaDialect: () => declareAdvertisedSchemaDialect,
28984
29600
  deriveDashboardInterpretation: () => deriveDashboardInterpretation,
@@ -29011,7 +29627,9 @@ function createMcpServer(options) {
29011
29627
  apiFetch = globalThis.fetch,
29012
29628
  userId,
29013
29629
  captureDashboardBrowser: captureDashboardBrowser2,
29014
- directoryListing = false
29630
+ directoryListing = false,
29631
+ readInspiration: readInspiration2,
29632
+ sendFeedback
29015
29633
  } = options;
29016
29634
  const server = new McpServer(
29017
29635
  { name, version: version2 },
@@ -29038,7 +29656,9 @@ function createMcpServer(options) {
29038
29656
  },
29039
29657
  apiFetch,
29040
29658
  userId,
29041
- captureDashboardBrowser2
29659
+ captureDashboardBrowser2,
29660
+ readInspiration2,
29661
+ sendFeedback
29042
29662
  );
29043
29663
  return server;
29044
29664
  }
@@ -29055,6 +29675,7 @@ var init_mcp = __esm({
29055
29675
  init_advertised_schema();
29056
29676
  init_tool_names();
29057
29677
  init_principle_tools();
29678
+ init_feedback_tools();
29058
29679
  init_instructions();
29059
29680
  init_session_guidance();
29060
29681
  init_shared();
@@ -29078,10 +29699,12 @@ __export(api_client_exports, {
29078
29699
  getSessionGuidance: () => getSessionGuidance,
29079
29700
  getTeamRules: () => getTeamRules,
29080
29701
  listPrinciples: () => listPrinciples,
29702
+ readInspiration: () => readInspiration,
29081
29703
  searchDesignSystems: () => searchDesignSystems,
29082
29704
  searchIconLibraries: () => searchIconLibraries,
29083
29705
  searchIcons: () => searchIcons,
29084
29706
  searchPrinciples: () => searchPrinciples,
29707
+ sendAgentFeedback: () => sendAgentFeedback,
29085
29708
  trackMcpInstalled: () => trackMcpInstalled,
29086
29709
  trackToolUsage: () => trackToolUsage
29087
29710
  });
@@ -29115,6 +29738,15 @@ function captureDashboardBrowser(input) {
29115
29738
  timeoutMs: 9e4
29116
29739
  });
29117
29740
  }
29741
+ function readInspiration(input) {
29742
+ const params = new URLSearchParams({ type: input.type });
29743
+ if (input.category) params.set("category", input.category);
29744
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
29745
+ return request(`/v1/inspiration?${params}`, { timeoutMs: 3e4 });
29746
+ }
29747
+ function sendAgentFeedback(report) {
29748
+ return request("/v1/feedback", { method: "POST", body: report });
29749
+ }
29118
29750
  async function searchDesignSystems(query, limit = 5) {
29119
29751
  return request(
29120
29752
  `/v1/design-systems/search?q=${encodeURIComponent(query)}&limit=${limit}`
@@ -31091,7 +31723,7 @@ var init_engine = __esm({
31091
31723
  });
31092
31724
 
31093
31725
  // ../shared/src/lint/config.ts
31094
- import { z as z28 } from "zod";
31726
+ import { z as z31 } from "zod";
31095
31727
  function mergeReviewConfig(override) {
31096
31728
  if (!override) return DEFAULT_REVIEW_CONFIG;
31097
31729
  return {
@@ -31149,47 +31781,47 @@ var init_config = __esm({
31149
31781
  semanticTokens: { enabled: true }
31150
31782
  }
31151
31783
  };
31152
- reviewFlowStepSchema = z28.discriminatedUnion("action", [
31153
- z28.object({ action: z28.literal("click"), selector: z28.string() }),
31154
- z28.object({ action: z28.literal("type"), selector: z28.string(), value: z28.string() }),
31155
- z28.object({ action: z28.literal("press"), key: z28.string() }),
31156
- z28.object({ action: z28.literal("wait"), milliseconds: z28.number().finite().nonnegative() })
31784
+ reviewFlowStepSchema = z31.discriminatedUnion("action", [
31785
+ z31.object({ action: z31.literal("click"), selector: z31.string() }),
31786
+ z31.object({ action: z31.literal("type"), selector: z31.string(), value: z31.string() }),
31787
+ z31.object({ action: z31.literal("press"), key: z31.string() }),
31788
+ z31.object({ action: z31.literal("wait"), milliseconds: z31.number().finite().nonnegative() })
31157
31789
  ]);
31158
- visualReviewConfigSchema = z28.object({
31159
- enabled: z28.boolean().optional(),
31160
- model: z28.object({
31161
- enabled: z28.boolean().optional(),
31162
- maxScreenshots: z28.number().int().nonnegative().optional()
31790
+ visualReviewConfigSchema = z31.object({
31791
+ enabled: z31.boolean().optional(),
31792
+ model: z31.object({
31793
+ enabled: z31.boolean().optional(),
31794
+ maxScreenshots: z31.number().int().nonnegative().optional()
31163
31795
  }).optional(),
31164
- routes: z28.array(z28.string()).optional(),
31165
- viewports: z28.array(z28.enum(["desktop", "mobile", "reflow"])).optional(),
31166
- flows: z28.array(z28.object({
31167
- name: z28.string(),
31168
- persona: z28.string(),
31169
- path: z28.string().optional(),
31170
- steps: z28.array(reviewFlowStepSchema)
31796
+ routes: z31.array(z31.string()).optional(),
31797
+ viewports: z31.array(z31.enum(["desktop", "mobile", "reflow"])).optional(),
31798
+ flows: z31.array(z31.object({
31799
+ name: z31.string(),
31800
+ persona: z31.string(),
31801
+ path: z31.string().optional(),
31802
+ steps: z31.array(reviewFlowStepSchema)
31171
31803
  })).optional(),
31172
- componentGallery: z28.object({
31173
- enabled: z28.boolean().optional(),
31174
- componentSelector: z28.string().optional(),
31175
- canvasSelector: z28.string().optional(),
31176
- labelSelector: z28.string().optional(),
31177
- maxComponents: z28.number().int().nonnegative().optional()
31804
+ componentGallery: z31.object({
31805
+ enabled: z31.boolean().optional(),
31806
+ componentSelector: z31.string().optional(),
31807
+ canvasSelector: z31.string().optional(),
31808
+ labelSelector: z31.string().optional(),
31809
+ maxComponents: z31.number().int().nonnegative().optional()
31178
31810
  }).optional()
31179
31811
  });
31180
- reviewConfigOverrideSchema = z28.object({
31181
- designSystem: z28.string().optional(),
31812
+ reviewConfigOverrideSchema = z31.object({
31813
+ designSystem: z31.string().optional(),
31182
31814
  visual: visualReviewConfigSchema.optional(),
31183
- rules: z28.object({
31184
- elements: z28.record(z28.string(), z28.object({ use: z28.string(), from: z28.string() })).optional(),
31185
- tokens: z28.record(z28.string(), z28.string()).optional(),
31186
- wcag: z28.object({
31187
- enabled: z28.boolean().optional(),
31188
- severity: z28.enum(["all", "critical+serious"]).optional()
31815
+ rules: z31.object({
31816
+ elements: z31.record(z31.string(), z31.object({ use: z31.string(), from: z31.string() })).optional(),
31817
+ tokens: z31.record(z31.string(), z31.string()).optional(),
31818
+ wcag: z31.object({
31819
+ enabled: z31.boolean().optional(),
31820
+ severity: z31.enum(["all", "critical+serious"]).optional()
31189
31821
  }).optional(),
31190
- slop: z28.object({ enabled: z28.boolean().optional() }).optional(),
31191
- copy: z28.object({ enabled: z28.boolean().optional() }).optional(),
31192
- semanticTokens: z28.object({ enabled: z28.boolean().optional() }).optional()
31822
+ slop: z31.object({ enabled: z31.boolean().optional() }).optional(),
31823
+ copy: z31.object({ enabled: z31.boolean().optional() }).optional(),
31824
+ semanticTokens: z31.object({ enabled: z31.boolean().optional() }).optional()
31193
31825
  }).optional()
31194
31826
  });
31195
31827
  }
@@ -31565,7 +32197,7 @@ __export(review_tool_exports, {
31565
32197
  isReviewableSourcePath: () => isReviewableSourcePath,
31566
32198
  registerReviewTool: () => registerReviewTool
31567
32199
  });
31568
- import { z as z29 } from "zod";
32200
+ import { z as z32 } from "zod";
31569
32201
  function isReviewableSourcePath(path6) {
31570
32202
  return /\.(tsx|jsx|ts|js|css)$/i.test(path6);
31571
32203
  }
@@ -31589,11 +32221,11 @@ get-review-rules gives you the judgment rules to apply yourself. This tool is th
31589
32221
  This source reviewer cannot judge rendered hierarchy or semantic color roles. After it passes, inspect the rendered dashboard and confirm title \u2192 metrics \u2192 records \u2192 interpretation, distinct action and status colors, and that a stepper begins only after a task starts.
31590
32222
 
31591
32223
  Fix every critical and serious finding, then call it again on the fixed source. Findings feed the same loop as a human code review: read, fix, re-check.`,
31592
- inputSchema: z29.object({
31593
- files: z29.array(
31594
- z29.object({
31595
- path: z29.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
31596
- content: z29.string().max(MAX_FILE_CHARS).describe("The file's full source.")
32224
+ inputSchema: z32.object({
32225
+ files: z32.array(
32226
+ z32.object({
32227
+ path: z32.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
32228
+ content: z32.string().max(MAX_FILE_CHARS).describe("The file's full source.")
31597
32229
  })
31598
32230
  ).min(1).max(MAX_FILES).describe("The .tsx, .jsx, .ts, .js, and .css files to review, as written."),
31599
32231
  context: CONTEXT_ARG
@@ -31654,7 +32286,7 @@ review-ui-code({ files: [{ path: "components/signup-form.tsx", content: "..." },
31654
32286
  });
31655
32287
 
31656
32288
  // ../shared/src/mcp/figma-prototype.ts
31657
- import { z as z30 } from "zod";
32289
+ import { z as z33 } from "zod";
31658
32290
  function escapeHtml(value) {
31659
32291
  return value.replace(/[&<>"]/g, (char2) => ESCAPES.get(char2) ?? char2);
31660
32292
  }
@@ -31688,63 +32320,63 @@ var colorSchema, paintSchema, sizingSchema, baseSchema, prototypeNodeSchema, pro
31688
32320
  var init_figma_prototype = __esm({
31689
32321
  "../shared/src/mcp/figma-prototype.ts"() {
31690
32322
  "use strict";
31691
- colorSchema = z30.object({ r: z30.number(), g: z30.number(), b: z30.number(), a: z30.number() });
31692
- paintSchema = z30.object({ variable: z30.string().optional(), color: colorSchema.optional() });
31693
- sizingSchema = z30.enum(["FIXED", "HUG", "FILL"]);
31694
- baseSchema = z30.object({
31695
- name: z30.string(),
31696
- width: z30.number(),
31697
- height: z30.number(),
31698
- x: z30.number(),
31699
- y: z30.number(),
31700
- sizing: z30.object({ horizontal: sizingSchema, vertical: sizingSchema }).optional(),
31701
- absolute: z30.boolean().optional(),
31702
- opacity: z30.number().optional()
32323
+ colorSchema = z33.object({ r: z33.number(), g: z33.number(), b: z33.number(), a: z33.number() });
32324
+ paintSchema = z33.object({ variable: z33.string().optional(), color: colorSchema.optional() });
32325
+ sizingSchema = z33.enum(["FIXED", "HUG", "FILL"]);
32326
+ baseSchema = z33.object({
32327
+ name: z33.string(),
32328
+ width: z33.number(),
32329
+ height: z33.number(),
32330
+ x: z33.number(),
32331
+ y: z33.number(),
32332
+ sizing: z33.object({ horizontal: sizingSchema, vertical: sizingSchema }).optional(),
32333
+ absolute: z33.boolean().optional(),
32334
+ opacity: z33.number().optional()
31703
32335
  });
31704
- prototypeNodeSchema = z30.lazy(() => z30.discriminatedUnion("kind", [
32336
+ prototypeNodeSchema = z33.lazy(() => z33.discriminatedUnion("kind", [
31705
32337
  baseSchema.extend({
31706
- kind: z30.literal("instance"),
31707
- auditKey: z30.string(),
31708
- systemId: z30.string(),
31709
- texts: z30.array(z30.string()),
31710
- mainTexts: z30.array(z30.string())
32338
+ kind: z33.literal("instance"),
32339
+ auditKey: z33.string(),
32340
+ systemId: z33.string(),
32341
+ texts: z33.array(z33.string()),
32342
+ mainTexts: z33.array(z33.string())
31711
32343
  }),
31712
32344
  baseSchema.extend({
31713
- kind: z30.literal("frame"),
31714
- layout: z30.object({
31715
- direction: z30.enum(["HORIZONTAL", "VERTICAL"]),
31716
- gap: z30.number(),
31717
- padding: z30.tuple([z30.number(), z30.number(), z30.number(), z30.number()]),
31718
- primary: z30.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]),
31719
- counter: z30.enum(["MIN", "CENTER", "MAX", "BASELINE"]),
31720
- wrap: z30.boolean().optional()
32345
+ kind: z33.literal("frame"),
32346
+ layout: z33.object({
32347
+ direction: z33.enum(["HORIZONTAL", "VERTICAL"]),
32348
+ gap: z33.number(),
32349
+ padding: z33.tuple([z33.number(), z33.number(), z33.number(), z33.number()]),
32350
+ primary: z33.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]),
32351
+ counter: z33.enum(["MIN", "CENTER", "MAX", "BASELINE"]),
32352
+ wrap: z33.boolean().optional()
31721
32353
  }).optional(),
31722
32354
  fill: paintSchema.optional(),
31723
- stroke: z30.object({ paint: paintSchema, weight: z30.number() }).optional(),
31724
- radius: z30.number().optional(),
31725
- clips: z30.boolean().optional(),
31726
- children: z30.array(prototypeNodeSchema)
32355
+ stroke: z33.object({ paint: paintSchema, weight: z33.number() }).optional(),
32356
+ radius: z33.number().optional(),
32357
+ clips: z33.boolean().optional(),
32358
+ children: z33.array(prototypeNodeSchema)
31727
32359
  }),
31728
32360
  baseSchema.extend({
31729
- kind: z30.literal("text"),
31730
- characters: z30.string(),
31731
- fontSize: z30.number(),
31732
- fontWeight: z30.number(),
31733
- lineHeight: z30.number().optional(),
31734
- letterSpacing: z30.number().optional(),
31735
- fontFamily: z30.string().optional(),
31736
- align: z30.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]),
32361
+ kind: z33.literal("text"),
32362
+ characters: z33.string(),
32363
+ fontSize: z33.number(),
32364
+ fontWeight: z33.number(),
32365
+ lineHeight: z33.number().optional(),
32366
+ letterSpacing: z33.number().optional(),
32367
+ fontFamily: z33.string().optional(),
32368
+ align: z33.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]),
31737
32369
  fill: paintSchema.optional()
31738
32370
  }),
31739
32371
  baseSchema.extend({
31740
- kind: z30.literal("shape"),
31741
- svg: z30.string(),
31742
- reason: z30.string()
32372
+ kind: z33.literal("shape"),
32373
+ svg: z33.string(),
32374
+ reason: z33.string()
31743
32375
  })
31744
32376
  ]));
31745
- prototypeRequestSchema = z30.object({
31746
- type: z30.literal("better-design-figma-prototype"),
31747
- id: z30.string(),
32377
+ prototypeRequestSchema = z33.object({
32378
+ type: z33.literal("better-design-figma-prototype"),
32379
+ id: z33.string(),
31748
32380
  frame: prototypeNodeSchema
31749
32381
  });
31750
32382
  ESCAPES = /* @__PURE__ */ new Map([["&", "&amp;"], ["<", "&lt;"], [">", "&gt;"], ['"', "&quot;"]]);
@@ -31930,7 +32562,7 @@ var init_figma_model_prompt = __esm({
31930
32562
  });
31931
32563
 
31932
32564
  // ../shared/src/mcp/figma-model.ts
31933
- import { z as z31 } from "zod";
32565
+ import { z as z34 } from "zod";
31934
32566
  function fenced(text3, language) {
31935
32567
  const match = text3.match(new RegExp("```" + language + "[^\\n]*\\n([\\s\\S]*?)```", "i"));
31936
32568
  return match ? match[1] : null;
@@ -32136,8 +32768,8 @@ var init_figma_model = __esm({
32136
32768
  init_figma_prototype();
32137
32769
  init_html_kit();
32138
32770
  init_preview_html();
32139
- unplacedSchema = z31.object({
32140
- unplaced: z31.array(z31.object({ name: z31.string(), reason: z31.string() }))
32771
+ unplacedSchema = z34.object({
32772
+ unplaced: z34.array(z34.object({ name: z34.string(), reason: z34.string() }))
32141
32773
  });
32142
32774
  UNSAFE_BLOCKS = /<(script|iframe|object|embed|noscript)\b[\s\S]*?(<\/\1[^>]*>|$)/gi;
32143
32775
  UNSAFE_TAGS = /<\/?(meta|link|base|form|style)\b[^>]*(>|$)/gi;
@@ -32238,7 +32870,7 @@ var init_figma_model_handoff = __esm({
32238
32870
  });
32239
32871
 
32240
32872
  // ../shared/src/mcp/figma-model-providers.ts
32241
- import { z as z32 } from "zod";
32873
+ import { z as z35 } from "zod";
32242
32874
  function serverLabel(url) {
32243
32875
  try {
32244
32876
  const parsed = new URL(url);
@@ -32331,19 +32963,19 @@ var init_figma_model_providers = __esm({
32331
32963
  DEFAULT_LOCAL_MODEL = "gemma4:latest";
32332
32964
  DEFAULT_LOCAL_BASE_URL = "http://localhost:11434/v1";
32333
32965
  MAX_OUTPUT_TOKENS = 16e3;
32334
- chatReplySchema = z32.object({
32335
- choices: z32.array(z32.object({
32336
- message: z32.object({ content: z32.string().nullable().optional() }),
32337
- finish_reason: z32.string().nullable().optional()
32966
+ chatReplySchema = z35.object({
32967
+ choices: z35.array(z35.object({
32968
+ message: z35.object({ content: z35.string().nullable().optional() }),
32969
+ finish_reason: z35.string().nullable().optional()
32338
32970
  })).min(1),
32339
- usage: z32.object({ prompt_tokens: z32.number(), completion_tokens: z32.number() }).optional()
32971
+ usage: z35.object({ prompt_tokens: z35.number(), completion_tokens: z35.number() }).optional()
32340
32972
  });
32341
- errorCodeSchema = z32.object({ code: z32.string() });
32973
+ errorCodeSchema = z35.object({ code: z35.string() });
32342
32974
  }
32343
32975
  });
32344
32976
 
32345
32977
  // ../shared/src/mcp/figma-relay.ts
32346
- import { z as z33 } from "zod";
32978
+ import { z as z36 } from "zod";
32347
32979
  function noPluginMessage() {
32348
32980
  return "No Figma plugin is connected. Open the Figma file, run Plugins, Development, Better Design, and leave the panel open.";
32349
32981
  }
@@ -32357,35 +32989,35 @@ var init_figma_relay = __esm({
32357
32989
  "use strict";
32358
32990
  init_figma_prototype();
32359
32991
  FIGMA_RELAY_PORT = 4397;
32360
- relayRequestSchema = z33.object({
32361
- id: z33.string(),
32992
+ relayRequestSchema = z36.object({
32993
+ id: z36.string(),
32362
32994
  /** A frame name as it reads in Figma. Omitted means the current selection. */
32363
- frame: z33.string().optional(),
32995
+ frame: z36.string().optional(),
32364
32996
  /**
32365
32997
  * `model` asks for the serialized frame and its PNG instead of a rendered
32366
32998
  * page, so a model can rebuild a frame that holds no library component.
32367
32999
  * Omitted means the library path, unchanged. [BD-866]
32368
33000
  */
32369
- mode: z33.enum(["library", "model"]).optional()
33001
+ mode: z36.enum(["library", "model"]).optional()
32370
33002
  });
32371
- relayResultSchema = z33.object({
32372
- id: z33.string(),
32373
- html: z33.string().optional(),
32374
- error: z33.string().optional(),
32375
- components: z33.number().optional(),
32376
- report: z33.array(z33.object({ name: z33.string(), reason: z33.string() })).default([]),
32377
- frame: z33.string().optional(),
33003
+ relayResultSchema = z36.object({
33004
+ id: z36.string(),
33005
+ html: z36.string().optional(),
33006
+ error: z36.string().optional(),
33007
+ components: z36.number().optional(),
33008
+ report: z36.array(z36.object({ name: z36.string(), reason: z36.string() })).default([]),
33009
+ frame: z36.string().optional(),
32378
33010
  /** The frames the plugin found, so a missing name can say what is there. */
32379
- frames: z33.array(z33.string()).optional(),
33011
+ frames: z36.array(z36.string()).optional(),
32380
33012
  /** Model mode: the serialized frame, for the model to rebuild. */
32381
33013
  tree: prototypeNodeSchema.optional(),
32382
33014
  /** Model mode: the frame's PNG export, base64. */
32383
- png: z33.string().optional()
33015
+ png: z36.string().optional()
32384
33016
  });
32385
- relayHelloSchema = z33.object({
32386
- file: z33.string(),
33017
+ relayHelloSchema = z36.object({
33018
+ file: z36.string(),
32387
33019
  /** The design system the file holds, when the plugin knows it. */
32388
- system: z33.string().optional()
33020
+ system: z36.string().optional()
32389
33021
  });
32390
33022
  POLL_HOLD_MS = 25e3;
32391
33023
  REQUEST_TIMEOUT_MS = 3e5;
@@ -32553,7 +33185,7 @@ __export(figma_prototype_tool_exports, {
32553
33185
  });
32554
33186
  import { mkdir as mkdir2, readFile, stat, writeFile as writeFile2 } from "fs/promises";
32555
33187
  import { dirname as dirname3, resolve as resolve2 } from "path";
32556
- import { z as z34 } from "zod";
33188
+ import { z as z37 } from "zod";
32557
33189
  async function ensureRelay() {
32558
33190
  if (relay) return relay;
32559
33191
  starting ??= startFigmaRelay().then((started) => {
@@ -32613,30 +33245,30 @@ function registerFigmaPrototypeTool(server) {
32613
33245
  "yourself: the tool returns a prompt file and the frame's image, you write the answer to a file, and",
32614
33246
  "you call this tool again with frameId and answerPath so it checks the page and reports what is missing."
32615
33247
  ].join(" "),
32616
- inputSchema: z34.object({
32617
- frame: z34.string().optional().describe("The frame's name as it reads in Figma. Omit to use the current selection."),
32618
- outputPath: z34.string().optional().describe("Where to write the HTML. Relative paths resolve against the working directory."),
32619
- mode: z34.enum(["library", "model"]).optional().describe(
33248
+ inputSchema: z37.object({
33249
+ frame: z37.string().optional().describe("The frame's name as it reads in Figma. Omit to use the current selection."),
33250
+ outputPath: z37.string().optional().describe("Where to write the HTML. Relative paths resolve against the working directory."),
33251
+ mode: z37.enum(["library", "model"]).optional().describe(
32620
33252
  "library (the default) maps components from a Better Design library. model asks a model to rebuild a frame from any file."
32621
33253
  ),
32622
- system: z34.string().optional().describe("Model mode: the catalog design system to build with, as find-design-system names it."),
32623
- provider: z34.enum(["agent", "openai-compatible"]).optional().describe(
33254
+ system: z37.string().optional().describe("Model mode: the catalog design system to build with, as find-design-system names it."),
33255
+ provider: z37.enum(["agent", "openai-compatible"]).optional().describe(
32624
33256
  "Model mode: agent (the default) hands the frame to you to rebuild; openai-compatible calls a local server, Ollama by default."
32625
33257
  ),
32626
- model: z34.string().optional().describe(`Model mode with openai-compatible: the local model id. Defaults to ${DEFAULT_LOCAL_MODEL}.`),
32627
- frameId: z34.string().optional().describe("Model mode, second call: the frameId the first call returned."),
32628
- answerPath: z34.string().optional().describe("Model mode, second call: the file holding your answer, as the prompt file asks.")
33258
+ model: z37.string().optional().describe(`Model mode with openai-compatible: the local model id. Defaults to ${DEFAULT_LOCAL_MODEL}.`),
33259
+ frameId: z37.string().optional().describe("Model mode, second call: the frameId the first call returned."),
33260
+ answerPath: z37.string().optional().describe("Model mode, second call: the file holding your answer, as the prompt file asks.")
32629
33261
  }),
32630
- outputSchema: z34.object({
32631
- status: z34.enum(["completed", "failed", "needs-answer"]),
32632
- message: z34.string(),
32633
- frameId: z34.string().optional(),
32634
- promptPath: z34.string().optional(),
32635
- imagePath: z34.string().optional(),
32636
- path: z34.string().optional(),
32637
- components: z34.number().optional(),
32638
- report: z34.array(z34.object({ name: z34.string(), reason: z34.string() })).optional(),
32639
- model: z34.string().optional()
33262
+ outputSchema: z37.object({
33263
+ status: z37.enum(["completed", "failed", "needs-answer"]),
33264
+ message: z37.string(),
33265
+ frameId: z37.string().optional(),
33266
+ promptPath: z37.string().optional(),
33267
+ imagePath: z37.string().optional(),
33268
+ path: z37.string().optional(),
33269
+ components: z37.number().optional(),
33270
+ report: z37.array(z37.object({ name: z37.string(), reason: z37.string() })).optional(),
33271
+ model: z37.string().optional()
32640
33272
  }),
32641
33273
  annotations: { readOnlyHint: false, openWorldHint: true }
32642
33274
  },
@@ -32858,6 +33490,8 @@ async function startServer() {
32858
33490
  const { createHttpDataProvider: createHttpDataProvider2 } = await Promise.resolve().then(() => (init_http_data_provider(), http_data_provider_exports));
32859
33491
  const {
32860
33492
  captureDashboardBrowser: captureDashboardBrowser2,
33493
+ readInspiration: readInspiration2,
33494
+ sendAgentFeedback: sendAgentFeedback2,
32861
33495
  trackToolUsage: trackToolUsage2,
32862
33496
  trackMcpInstalled: trackMcpInstalled2
32863
33497
  } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
@@ -32883,7 +33517,9 @@ async function startServer() {
32883
33517
  name: "DesignSystems",
32884
33518
  version: MCP_IMPLEMENTATION_VERSION2,
32885
33519
  onToolUsage,
32886
- captureDashboardBrowser: captureDashboardBrowser2
33520
+ captureDashboardBrowser: captureDashboardBrowser2,
33521
+ readInspiration: readInspiration2,
33522
+ sendFeedback: sendAgentFeedback2
32887
33523
  });
32888
33524
  registerReviewTool2(server, onToolUsage);
32889
33525
  registerFigmaPrototypeTool2(server);