better-design 0.33.0 → 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
@@ -1252,6 +1252,7 @@ var init_tool_names = __esm({
1252
1252
  "review-dashboard-structure",
1253
1253
  "review-illustration",
1254
1254
  "review-ui-code",
1255
+ "send-feedback",
1255
1256
  "set-design-config",
1256
1257
  "update-feature-map"
1257
1258
  ];
@@ -1290,6 +1291,7 @@ var init_tool_names = __esm({
1290
1291
  "review-dashboard-structure",
1291
1292
  "review-illustration",
1292
1293
  "review-ui-code",
1294
+ "send-feedback",
1293
1295
  "set-design-config",
1294
1296
  "update-feature-map"
1295
1297
  ]);
@@ -1389,9 +1391,9 @@ Better Design provides design systems, design principles and UI review checks fo
1389
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.",
1390
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.",
1391
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.",
1392
- "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.",
1393
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.",
1394
- "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.',
1395
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.",
1396
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.",
1397
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.`,
@@ -1410,12 +1412,13 @@ Better Design provides design systems, design principles and UI review checks fo
1410
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.",
1411
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.",
1412
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.",
1413
- "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.",
1414
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`.",
1415
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.",
1416
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.",
1417
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.",
1418
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.",
1419
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."
1420
1423
  };
1421
1424
  }
@@ -4275,12 +4278,15 @@ var init_review_rules_description = __esm({
4275
4278
  "content",
4276
4279
  "comprehension",
4277
4280
  "ai-features",
4281
+ "coverage-gaps",
4278
4282
  "all"
4279
4283
  ];
4280
4284
  REVIEW_RULES_DESCRIPTION = `Retrieves accessibility (WCAG 2.1), visual design, animation, content, comprehension, and AI feature review rules for analyzing code.
4281
4285
 
4282
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.
4283
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
+
4284
4290
  ${REDESIGN_SCOPE_CONTRACT}
4285
4291
 
4286
4292
  ${DESIGN_TRADEOFF_RULE}
@@ -4323,6 +4329,8 @@ ${DESIGN_TRADEOFF_RULE}
4323
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
4324
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
4325
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
+
4326
4334
  Use maxTokens to limit rule content (recommended: 5000\u201310000). Required workflow guidance is always returned in full.`;
4327
4335
  }
4328
4336
  });
@@ -5161,6 +5169,22 @@ These rules were read against source. They cannot see layout. Three checks remai
5161
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.
5162
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.
5163
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
+
5164
5188
  Fix every critical and serious finding before you present.`;
5165
5189
  }
5166
5190
  });
@@ -5184,6 +5208,8 @@ function looksLikeCard(candidate) {
5184
5208
  }
5185
5209
  function redactSecrets(value) {
5186
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]");
5187
5213
  for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, "[redacted]");
5188
5214
  out = out.replace(EMAIL_PATTERN, "[redacted-email]");
5189
5215
  out = out.replace(
@@ -5202,7 +5228,7 @@ function redactUrl(raw) {
5202
5228
  return "[redacted]";
5203
5229
  }
5204
5230
  }
5205
- 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;
5206
5232
  var init_redact = __esm({
5207
5233
  "../shared/src/redact.ts"() {
5208
5234
  "use strict";
@@ -5218,9 +5244,9 @@ var init_redact = __esm({
5218
5244
  /\bwhsec_[A-Za-z0-9]{16,}/g,
5219
5245
  /\bAIza[A-Za-z0-9_-]{30,}/g,
5220
5246
  /\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
5221
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+/g,
5222
- // JWT
5223
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
5247
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g
5248
+ ];
5249
+ CREDENTIAL_VALUE_PATTERNS = [
5224
5250
  // key=value forms for the names that carry credentials. A quoted value is
5225
5251
  // taken whole, so a passphrase with spaces and a short password both go:
5226
5252
  // the old form needed eight characters and no spaces, which let
@@ -5228,8 +5254,13 @@ var init_redact = __esm({
5228
5254
  // through into a corpus held for 180 days. [BD-697]
5229
5255
  /(?:\b|_)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret(?:[_-]?key)?|password|passwd|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9._~+/-]+)/gi,
5230
5256
  // Authorization headers separate the scheme from the token with a space.
5231
- /\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
5232
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;
5233
5264
  EMAIL_PATTERN = /(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
5234
5265
  CARD_CANDIDATE_PATTERN = /\b(?:\d[ -]?){12,18}\d\b/g;
5235
5266
  PHONE_PATTERNS = [
@@ -5363,7 +5394,7 @@ var init_extract = __esm({
5363
5394
  });
5364
5395
 
5365
5396
  // ../shared/src/mcp/comprehension/lexicon.ts
5366
- 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;
5367
5398
  var init_lexicon = __esm({
5368
5399
  "../shared/src/mcp/comprehension/lexicon.ts"() {
5369
5400
  "use strict";
@@ -5472,6 +5503,9 @@ var init_lexicon = __esm({
5472
5503
  MAX_SCREEN_WORDS = 60;
5473
5504
  MAX_ACTIONS = 3;
5474
5505
  EXPLAIN_BUDGET_SECONDS = 15;
5506
+ MAX_SLIDE_WORDS = 40;
5507
+ MAX_SLIDE_LINES = 5;
5508
+ SLIDE_NAME = /\b(slides?|deck|presentation)\b/i;
5475
5509
  }
5476
5510
  });
5477
5511
 
@@ -5546,7 +5580,7 @@ function estimateExplainSeconds(words2, insiderTermCount, actionCount) {
5546
5580
  const choosing = Math.max(0, actionCount - 1) * 3;
5547
5581
  return Math.round(speaking + defining + choosing);
5548
5582
  }
5549
- function fifteenSecondFindings(words2, actions, seconds) {
5583
+ function fifteenSecondFindings(words2, actions, seconds, surface) {
5550
5584
  const findings = [];
5551
5585
  if (seconds > EXPLAIN_BUDGET_SECONDS) {
5552
5586
  findings.push({
@@ -5557,7 +5591,7 @@ function fifteenSecondFindings(words2, actions, seconds) {
5557
5591
  fix: "Cut to one idea and one next step. Move the rest behind a link or a later screen."
5558
5592
  });
5559
5593
  }
5560
- if (words2 > MAX_SCREEN_WORDS) {
5594
+ if (surface === "screen" && words2 > MAX_SCREEN_WORDS) {
5561
5595
  findings.push({
5562
5596
  test: "fifteen-second",
5563
5597
  severity: "serious",
@@ -5618,7 +5652,30 @@ function screenshotFindings(copy, words2) {
5618
5652
  }
5619
5653
  return findings;
5620
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
+ }
5621
5677
  function analyzeComprehension(input) {
5678
+ const surface = input.surface ?? (SLIDE_NAME.test(input.screen) ? "slide" : "screen");
5622
5679
  const copy = resolveCopy(input);
5623
5680
  const known = new Set((input.knownTerms ?? []).map((term) => term.trim().toLowerCase()));
5624
5681
  const visible = [copy.headline ?? "", ...copy.body, ...copy.actions].join(". ");
@@ -5627,13 +5684,16 @@ function analyzeComprehension(input) {
5627
5684
  const estimatedExplainSeconds = estimateExplainSeconds(words2, insiderTerms.length, copy.actions.length);
5628
5685
  const findings = [
5629
5686
  ...momTestFindings(visible, known, insiderTerms),
5630
- ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds),
5631
- ...screenshotFindings(copy, words2)
5687
+ ...fifteenSecondFindings(words2, copy.actions, estimatedExplainSeconds, surface),
5688
+ ...screenshotFindings(copy, words2),
5689
+ ...surface === "slide" ? slideFindings(words2, copy.body) : []
5632
5690
  ];
5691
+ const blocking = findings.some((finding3) => finding3.severity !== "minor");
5633
5692
  const longestSentence = sentences(visible).sort((a, b) => countWords(b) - countWords(a))[0] ?? "";
5634
5693
  return {
5635
5694
  screen: input.screen,
5636
- verdict: findings.some((finding3) => finding3.severity !== "minor") ? "revise" : "pass",
5695
+ surface,
5696
+ verdict: blocking ? "revise" : surface === "slide" ? "needs-screenshot" : "pass",
5637
5697
  findings,
5638
5698
  copy: { headline: copy.headline, body: copy.body, actions: copy.actions },
5639
5699
  stats: {
@@ -5677,7 +5737,8 @@ function formatStats(report) {
5677
5737
  "",
5678
5738
  `- Visible copy: ${words2} words across ${actionCount} action${actionCount === 1 ? "" : "s"}.`,
5679
5739
  `- Insider words found: ${insiderTerms.length > 0 ? insiderTerms.join(", ") : "none"}.`,
5680
- `- 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."
5681
5742
  ].join("\n");
5682
5743
  }
5683
5744
  function formatComprehensionReport(report) {
@@ -5685,8 +5746,11 @@ function formatComprehensionReport(report) {
5685
5746
  (totals, finding3) => ({ ...totals, [finding3.severity]: totals[finding3.severity] + 1 }),
5686
5747
  { critical: 0, serious: 0, minor: 0 }
5687
5748
  );
5688
- 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.`;
5689
- 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")] : [];
5690
5754
  return [
5691
5755
  `# Comprehension check: ${report.screen}`,
5692
5756
  "",
@@ -5697,6 +5761,7 @@ function formatComprehensionReport(report) {
5697
5761
  formatTestSection(report, "fifteen-second"),
5698
5762
  "",
5699
5763
  formatTestSection(report, "screenshot"),
5764
+ ...slideSection,
5700
5765
  "",
5701
5766
  formatStats(report),
5702
5767
  "",
@@ -5705,16 +5770,27 @@ function formatComprehensionReport(report) {
5705
5770
  next
5706
5771
  ].join("\n");
5707
5772
  }
5708
- var TEST_TITLES, SEVERITY_ORDER;
5773
+ var TEST_TITLES, SEVERITY_ORDER, VERDICTS;
5709
5774
  var init_format = __esm({
5710
5775
  "../shared/src/mcp/comprehension/format.ts"() {
5711
5776
  "use strict";
5712
5777
  TEST_TITLES = {
5713
5778
  mom: "Mom test: would someone outside the team understand this?",
5714
5779
  "fifteen-second": "15 second test: can you explain the screen in 15 seconds?",
5715
- 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?"
5716
5782
  };
5717
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
+ };
5718
5794
  }
5719
5795
  });
5720
5796
 
@@ -5739,6 +5815,8 @@ function registerComprehensionTools({ server, provider, track, userId }) {
5739
5815
 
5740
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.
5741
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
+
5742
5820
  Three tests, run on the screen's own words:
5743
5821
  1. Mom test. Would a person outside the team understand this? Flags insider vocabulary, unexplained acronyms and sentences too long to follow.
5744
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.
@@ -5748,6 +5826,8 @@ Pass either the copy or the markup:
5748
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"] })
5749
5827
  - check-comprehension({ screen: "Billing page", code: "<section>\u2026the JSX you wrote\u2026</section>" })
5750
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
+
5751
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.`,
5752
5832
  inputSchema: z9.object({
5753
5833
  screen: z9.string().trim().min(1).max(120).describe("What this screen is, e.g. 'lesson end card', 'billing page'."),
@@ -5756,6 +5836,7 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
5756
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']."),
5757
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."),
5758
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.'),
5759
5840
  context: CONTEXT_ARG
5760
5841
  }),
5761
5842
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -22989,7 +23070,7 @@ function columnToSchema(column, factory) {
22989
23070
  }
22990
23071
  return schema;
22991
23072
  }
22992
- function numberColumnToSchema(column, z37, coerce) {
23073
+ function numberColumnToSchema(column, z38, coerce) {
22993
23074
  let unsigned = column.getSQLType().includes("unsigned");
22994
23075
  let min;
22995
23076
  let max;
@@ -23057,20 +23138,20 @@ function numberColumnToSchema(column, z37, coerce) {
23057
23138
  min = Number.MIN_SAFE_INTEGER;
23058
23139
  max = Number.MAX_SAFE_INTEGER;
23059
23140
  }
23060
- let schema = coerce === true || coerce?.number ? z37.coerce.number() : z37.number();
23141
+ let schema = coerce === true || coerce?.number ? z38.coerce.number() : z38.number();
23061
23142
  schema = schema.min(min).max(max);
23062
23143
  return integer2 ? schema.int() : schema;
23063
23144
  }
23064
- function bigintColumnToSchema(column, z37, coerce) {
23145
+ function bigintColumnToSchema(column, z38, coerce) {
23065
23146
  const unsigned = column.getSQLType().includes("unsigned");
23066
23147
  const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
23067
23148
  const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
23068
- const schema = coerce === true || coerce?.bigint ? z37.coerce.bigint() : z37.bigint();
23149
+ const schema = coerce === true || coerce?.bigint ? z38.coerce.bigint() : z38.bigint();
23069
23150
  return schema.min(min).max(max);
23070
23151
  }
23071
- function stringColumnToSchema(column, z37, coerce) {
23152
+ function stringColumnToSchema(column, z38, coerce) {
23072
23153
  if (isColumnType(column, ["PgUUID"])) {
23073
- return z37.string().uuid();
23154
+ return z38.string().uuid();
23074
23155
  }
23075
23156
  let max;
23076
23157
  let regex;
@@ -23102,7 +23183,7 @@ function stringColumnToSchema(column, z37, coerce) {
23102
23183
  regex = /^[01]+$/;
23103
23184
  max = column.dimensions;
23104
23185
  }
23105
- let schema = coerce === true || coerce?.string ? z37.coerce.string() : z37.string();
23186
+ let schema = coerce === true || coerce?.string ? z38.coerce.string() : z38.string();
23106
23187
  schema = regex ? schema.regex(regex) : schema;
23107
23188
  return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
23108
23189
  }
@@ -23198,15 +23279,74 @@ var init_drizzle_zod = __esm({
23198
23279
  }
23199
23280
  });
23200
23281
 
23201
- // ../shared/src/schema.ts
23282
+ // ../shared/src/agent-feedback.ts
23202
23283
  import { z as z16 } from "zod";
23203
- 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, 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;
23204
23343
  var init_schema2 = __esm({
23205
23344
  "../shared/src/schema.ts"() {
23206
23345
  "use strict";
23207
23346
  init_pg_core();
23208
23347
  init_drizzle_orm();
23209
23348
  init_drizzle_zod();
23349
+ init_agent_feedback();
23210
23350
  vector2 = customType({
23211
23351
  dataType() {
23212
23352
  return "vector(768)";
@@ -23234,7 +23374,7 @@ var init_schema2 = __esm({
23234
23374
  "skip",
23235
23375
  "legacy"
23236
23376
  ];
23237
- onboardingPathSchema = z16.enum(onboardingPathValues);
23377
+ onboardingPathSchema = z17.enum(onboardingPathValues);
23238
23378
  onboardingPathSqlValues = sql.raw(
23239
23379
  onboardingPathValues.map((path6) => `'${path6}'`).join(", ")
23240
23380
  );
@@ -23257,10 +23397,10 @@ var init_schema2 = __esm({
23257
23397
  "not_improved",
23258
23398
  "inconclusive"
23259
23399
  ];
23260
- mcpIssueImprovementStatusSchema = z16.enum(
23400
+ mcpIssueImprovementStatusSchema = z17.enum(
23261
23401
  mcpIssueImprovementStatusValues
23262
23402
  );
23263
- mcpIssueImprovementVerdictSchema = z16.enum(
23403
+ mcpIssueImprovementVerdictSchema = z17.enum(
23264
23404
  mcpIssueImprovementVerdictValues
23265
23405
  );
23266
23406
  mcpIssueImprovementStatusSqlValues = sql.raw(
@@ -23269,38 +23409,38 @@ var init_schema2 = __esm({
23269
23409
  mcpIssueImprovementVerdictSqlValues = sql.raw(
23270
23410
  mcpIssueImprovementVerdictValues.map((verdict) => `'${verdict}'`).join(", ")
23271
23411
  );
23272
- designConfigSchema = z16.object({
23273
- colors: z16.object({
23274
- primary: z16.string(),
23275
- secondary: z16.string(),
23276
- accent: z16.string(),
23277
- muted: z16.string(),
23278
- destructive: z16.string(),
23279
- background: z16.string(),
23280
- foreground: z16.string(),
23281
- card: z16.string(),
23282
- cardForeground: z16.string(),
23283
- popover: z16.string(),
23284
- popoverForeground: z16.string(),
23285
- border: z16.string(),
23286
- input: z16.string(),
23287
- 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()
23288
23428
  }).partial(),
23289
- radius: z16.string(),
23290
- font: z16.object({
23291
- sans: z16.string(),
23292
- mono: z16.string()
23429
+ radius: z17.string(),
23430
+ font: z17.object({
23431
+ sans: z17.string(),
23432
+ mono: z17.string()
23293
23433
  }).partial(),
23294
- icons: z16.object({
23295
- libraryId: z16.string(),
23296
- variant: z16.string().nullable()
23434
+ icons: z17.object({
23435
+ libraryId: z17.string(),
23436
+ variant: z17.string().nullable()
23297
23437
  }).partial()
23298
23438
  }).partial();
23299
- projectMemoryDataSchema = z16.object({
23300
- lockedIn: z16.array(z16.string()),
23301
- rejected: z16.array(z16.string()),
23302
- tone: z16.string().nullable(),
23303
- 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()
23304
23444
  });
23305
23445
  projects = pgTable("projects", {
23306
23446
  id: uuid("id").primaryKey().defaultRandom(),
@@ -23580,6 +23720,43 @@ var init_schema2 = __esm({
23580
23720
  )
23581
23721
  ]
23582
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
+ );
23583
23760
  billingSubscription = pgTable(
23584
23761
  "billing_subscription",
23585
23762
  {
@@ -24386,7 +24563,7 @@ var init_schema2 = __esm({
24386
24563
  });
24387
24564
 
24388
24565
  // ../shared/src/mcp/tools/design-config-tools.ts
24389
- import { z as z17 } from "zod";
24566
+ import { z as z18 } from "zod";
24390
24567
  function registerDesignConfigTools({
24391
24568
  server,
24392
24569
  tracked,
@@ -24437,7 +24614,7 @@ function registerDesignConfigTools({
24437
24614
  {
24438
24615
  title: "Get a Project's Tuned Design Config",
24439
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.`,
24440
- inputSchema: z17.object({
24617
+ inputSchema: z18.object({
24441
24618
  projectId: PROJECT_ID_ARG,
24442
24619
  context: CONTEXT_ARG
24443
24620
  }),
@@ -24466,7 +24643,7 @@ function registerDesignConfigTools({
24466
24643
  {
24467
24644
  title: "Set a Project's Design Config",
24468
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.`,
24469
- inputSchema: z17.object({
24646
+ inputSchema: z18.object({
24470
24647
  projectId: PROJECT_ID_ARG,
24471
24648
  config: CONFIG_ARG,
24472
24649
  context: CONTEXT_ARG
@@ -24517,7 +24694,7 @@ var init_design_config_tools = __esm({
24517
24694
  "use strict";
24518
24695
  init_schema2();
24519
24696
  init_shared();
24520
- 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.");
24521
24698
  CONFIG_ARG = designConfigSchema.describe(
24522
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."
24523
24700
  );
@@ -24525,16 +24702,16 @@ var init_design_config_tools = __esm({
24525
24702
  });
24526
24703
 
24527
24704
  // ../shared/src/mcp/tools/jig-tools.ts
24528
- import { z as z18 } from "zod";
24705
+ import { z as z19 } from "zod";
24529
24706
  function registerJigTools({ server, tracked }) {
24530
24707
  server.registerTool(
24531
24708
  "get-jig-guide",
24532
24709
  {
24533
24710
  title: "Get a Jig for Parameter Work",
24534
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.`,
24535
- inputSchema: z18.object({
24536
- 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."),
24537
- 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'."),
24538
24715
  context: CONTEXT_ARG
24539
24716
  }),
24540
24717
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -25366,7 +25543,7 @@ var init_brand_sources = __esm({
25366
25543
  });
25367
25544
 
25368
25545
  // ../shared/src/mcp/tools/extract-tools.ts
25369
- import { z as z19 } from "zod";
25546
+ import { z as z20 } from "zod";
25370
25547
  function authHeaders2(authContext) {
25371
25548
  if (!authContext) return {};
25372
25549
  const bearer = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
@@ -25403,15 +25580,15 @@ JSON.stringify(${buildRoleSampleScript()})
25403
25580
  - kind "tokens": a design-tokens JSON export ($value entries, W3C format or the Figma variables API shape with 0..1 rgb floats).
25404
25581
 
25405
25582
  Common vendor names (Shopify's --color-button family) are mapped into empty semantic slots automatically and the mapping is reported in the output.`,
25406
- inputSchema: z19.object({
25407
- 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."),
25408
- sources: z19.array(z19.object({
25409
- name: z19.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
25410
- 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."),
25411
- 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)
25412
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."),
25413
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
25414
- 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.")
25415
25592
  }),
25416
25593
  annotations: READ_ONLY_TOOL_ANNOTATIONS
25417
25594
  },
@@ -25442,10 +25619,10 @@ Common vendor names (Shopify's --color-button family) are mapped into empty sema
25442
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.
25443
25620
 
25444
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.`,
25445
- inputSchema: z19.object({
25446
- url: z19.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
25447
- name: z19.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
25448
- 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.")
25449
25626
  }),
25450
25627
  // Reads any public page the user names, so the set of hosts it reaches is
25451
25628
  // the open web, not Better Design's catalog. It stores nothing. [BD-316]
@@ -25495,7 +25672,7 @@ var init_extract_tools = __esm({
25495
25672
  });
25496
25673
 
25497
25674
  // ../shared/src/mcp/tools/illustration-tools.ts
25498
- import { z as z20 } from "zod";
25675
+ import { z as z21 } from "zod";
25499
25676
  function fail(message) {
25500
25677
  return {
25501
25678
  content: [{ type: "text", text: message }],
@@ -25541,11 +25718,11 @@ How to call it:
25541
25718
  4. Stop when \`faults\` is empty.
25542
25719
 
25543
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.`,
25544
- inputSchema: z20.object({
25545
- svg: z20.string().min(1).describe("The complete svg element you drew."),
25546
- intent: z20.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
25547
- animated: z20.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
25548
- 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.")
25549
25726
  }),
25550
25727
  outputSchema: REVIEW_OUTPUT_SCHEMA,
25551
25728
  // A review without a valid reviewToken passes the paywall, so it spends
@@ -25624,16 +25801,16 @@ var init_illustration_tools = __esm({
25624
25801
  init_shared();
25625
25802
  REVIEW_OUTPUT_SCHEMA = {
25626
25803
  /** Empty when nothing is visibly wrong. */
25627
- faults: z20.array(z20.string()),
25804
+ faults: z21.array(z21.string()),
25628
25805
  /** Group ids in paint order, the parts an animation can target. */
25629
- layers: z20.array(z20.string()),
25806
+ layers: z21.array(z21.string()),
25630
25807
  /** False when the markup was refused; faults says why. */
25631
- reviewed: z20.boolean(),
25808
+ reviewed: z21.boolean(),
25632
25809
  /** True when the drawing could not be rendered, so only structure was checked. */
25633
- unseen: z20.boolean(),
25810
+ unseen: z21.boolean(),
25634
25811
  /** Pass this back to review the same drawing again without spending another credit. */
25635
- reviewToken: z20.string(),
25636
- message: z20.string()
25812
+ reviewToken: z21.string(),
25813
+ message: z21.string()
25637
25814
  };
25638
25815
  }
25639
25816
  });
@@ -26995,7 +27172,7 @@ ul.evidence { padding-inline-start: 1rem; }
26995
27172
  });
26996
27173
 
26997
27174
  // ../shared/src/mcp/tools/inspection-tools.ts
26998
- import { z as z21 } from "zod";
27175
+ import { z as z22 } from "zod";
26999
27176
  function coversRecommendedWidth(states, width) {
27000
27177
  return states.some(
27001
27178
  (state) => Math.abs(state.viewport.width - width) <= WIDTH_TOLERANCE
@@ -27051,36 +27228,38 @@ function registerInspectionTools({ server, provider, tracked }) {
27051
27228
 
27052
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.
27053
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
+
27054
27233
  Workflow:
27055
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.
27056
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.
27057
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.
27058
27237
 
27059
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.`,
27060
- inputSchema: z21.object({
27061
- 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."),
27062
- 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."),
27063
27242
  tolerance: finiteNumber.min(0).max(4).optional().describe("Pixel tolerance for grouping measurements. Defaults to 1px."),
27064
- 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."),
27065
27244
  context: CONTEXT_ARG
27066
27245
  }),
27067
- outputSchema: z21.object({
27068
- status: z21.enum(["needs-capture", "completed"]),
27069
- score: z21.number().optional(),
27070
- summary: z21.object({
27071
- critical: z21.number(),
27072
- serious: z21.number(),
27073
- moderate: z21.number(),
27074
- total: z21.number(),
27075
- 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()
27076
27255
  }).optional(),
27077
- findings: z21.array(findingSchema).optional(),
27078
- capturedWidths: z21.array(z21.number()).optional(),
27079
- missingWidths: z21.array(z21.number()).optional(),
27080
- states: z21.array(overlayStateSchema).optional(),
27081
- rubric: z21.array(z21.object({ id: z21.string(), title: z21.string() })).optional(),
27082
- rubricStatus: z21.enum(["loaded", "unavailable"]).optional(),
27083
- 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()
27084
27263
  }),
27085
27264
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
27086
27265
  _meta: {
@@ -27140,7 +27319,8 @@ ${SPACING_CAPTURE_SCRIPT}
27140
27319
  detail === "full" ? `Spacing score: ${result.score}/100.` : "",
27141
27320
  coverageLine,
27142
27321
  findingsText,
27143
- 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
27144
27324
  ].filter(Boolean).join("\n\n");
27145
27325
  return {
27146
27326
  content: [{ type: "text", text: text3 }],
@@ -27157,7 +27337,7 @@ ${SPACING_CAPTURE_SCRIPT}
27157
27337
  )
27158
27338
  );
27159
27339
  }
27160
- 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;
27161
27341
  var init_inspection_tools = __esm({
27162
27342
  "../shared/src/mcp/tools/inspection-tools.ts"() {
27163
27343
  "use strict";
@@ -27165,67 +27345,67 @@ var init_inspection_tools = __esm({
27165
27345
  init_finding_copy();
27166
27346
  init_spacing_inspection_html();
27167
27347
  init_shared();
27168
- finiteNumber = z21.number().finite();
27169
- rectSchema = z21.object({
27348
+ finiteNumber = z22.number().finite();
27349
+ rectSchema = z22.object({
27170
27350
  x: finiteNumber,
27171
27351
  y: finiteNumber,
27172
27352
  width: finiteNumber.nonnegative(),
27173
27353
  height: finiteNumber.nonnegative()
27174
27354
  });
27175
- edgesSchema = z21.object({
27355
+ edgesSchema = z22.object({
27176
27356
  top: finiteNumber,
27177
27357
  right: finiteNumber,
27178
27358
  bottom: finiteNumber,
27179
27359
  left: finiteNumber
27180
27360
  });
27181
- cornerRadiusAxesSchema = z21.object({
27361
+ cornerRadiusAxesSchema = z22.object({
27182
27362
  horizontal: finiteNumber.nonnegative(),
27183
27363
  vertical: finiteNumber.nonnegative()
27184
27364
  });
27185
- cornerRadiusSchema = z21.union([
27365
+ cornerRadiusSchema = z22.union([
27186
27366
  cornerRadiusAxesSchema,
27187
27367
  finiteNumber.nonnegative().transform((value) => ({
27188
27368
  horizontal: value,
27189
27369
  vertical: value
27190
27370
  }))
27191
27371
  ]);
27192
- cornerRadiiSchema = z21.object({
27372
+ cornerRadiiSchema = z22.object({
27193
27373
  topLeft: cornerRadiusSchema,
27194
27374
  topRight: cornerRadiusSchema,
27195
27375
  bottomRight: cornerRadiusSchema,
27196
27376
  bottomLeft: cornerRadiusSchema
27197
27377
  });
27198
- styleSchema = z21.object({
27199
- display: z21.string().max(60).optional(),
27200
- position: z21.string().max(60).optional(),
27201
- flexDirection: z21.string().max(60).optional(),
27202
- flexWrap: z21.string().max(60).optional(),
27203
- alignItems: z21.string().max(60).optional(),
27204
- 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(),
27205
27385
  rowGap: finiteNumber.optional(),
27206
27386
  columnGap: finiteNumber.optional(),
27207
27387
  padding: edgesSchema.optional(),
27208
27388
  margin: edgesSchema.optional(),
27209
- overflowX: z21.string().max(60).optional(),
27210
- overflowY: z21.string().max(60).optional(),
27389
+ overflowX: z22.string().max(60).optional(),
27390
+ overflowY: z22.string().max(60).optional(),
27211
27391
  borderRadius: cornerRadiiSchema.optional(),
27212
- backgroundColor: z21.string().max(160).optional(),
27213
- backgroundImage: z21.string().max(1e3).optional(),
27214
- 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()
27215
27395
  });
27216
- elementSchema = z21.object({
27217
- id: z21.string().min(1).max(300),
27218
- selector: z21.string().max(500).optional(),
27219
- tag: z21.string().max(80).optional(),
27220
- role: z21.string().max(80).optional(),
27221
- label: z21.string().max(160).optional(),
27222
- parentId: z21.string().max(300).optional(),
27223
- groupId: z21.string().max(300).optional(),
27224
- alignmentGroup: z21.string().max(300).optional(),
27225
- alignmentEdge: z21.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
27226
- expectedSymmetry: z21.enum(["horizontal", "vertical", "both"]).optional(),
27227
- compareSize: z21.enum(["width", "height", "both"]).optional(),
27228
- 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(),
27229
27409
  rect: rectSchema,
27230
27410
  style: styleSchema.optional(),
27231
27411
  lineCount: finiteNumber.int().positive().optional(),
@@ -27234,44 +27414,45 @@ var init_inspection_tools = __esm({
27234
27414
  scrollWidth: finiteNumber.nonnegative().optional(),
27235
27415
  scrollHeight: finiteNumber.nonnegative().optional()
27236
27416
  });
27237
- screenshotSchema = z21.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
27238
- spacingStateSchema = z21.object({
27239
- name: z21.string().min(1).max(100),
27240
- 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({
27241
27421
  width: finiteNumber.positive().max(2e4),
27242
27422
  height: finiteNumber.positive().max(2e4),
27243
27423
  devicePixelRatio: finiteNumber.positive().max(10).optional()
27244
27424
  }),
27245
- elements: z21.array(elementSchema).max(1500),
27425
+ elements: z22.array(elementSchema).max(1500),
27246
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.")
27247
27427
  });
27248
- evidenceSchema = z21.object({
27249
- label: z21.string(),
27250
- value: z21.union([z21.number(), z21.string()]),
27251
- 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()
27252
27432
  });
27253
- findingSchema = z21.object({
27254
- id: z21.string(),
27255
- ruleId: z21.string(),
27256
- state: z21.string(),
27257
- severity: z21.enum(["critical", "serious", "moderate"]),
27258
- message: z21.string(),
27259
- fix: z21.string(),
27260
- elementIds: z21.array(z21.string()),
27261
- 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)
27262
27442
  });
27263
- overlayStateSchema = z21.object({
27264
- name: z21.string(),
27265
- viewport: z21.object({
27266
- width: z21.number(),
27267
- height: z21.number(),
27268
- 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()
27269
27449
  }),
27270
- elements: z21.array(z21.object({ id: z21.string(), label: z21.string().optional(), rect: rectSchema })),
27271
- screenshotDataUrl: z21.string().optional()
27450
+ elements: z22.array(z22.object({ id: z22.string(), label: z22.string().optional(), rect: rectSchema })),
27451
+ screenshotDataUrl: z22.string().optional()
27272
27452
  });
27273
27453
  RECOMMENDED_VIEWPORT_WIDTHS = [390, 1440, 1728];
27274
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.";
27275
27456
  }
27276
27457
  });
27277
27458
 
@@ -27310,7 +27491,7 @@ var init_inspection_resource = __esm({
27310
27491
  });
27311
27492
 
27312
27493
  // ../shared/src/mcp/tools/dashboard-review-tools.ts
27313
- import { z as z22 } from "zod";
27494
+ import { z as z23 } from "zod";
27314
27495
  function finding(id, severity, message, fix) {
27315
27496
  return { id, severity, message, fix };
27316
27497
  }
@@ -27817,11 +27998,11 @@ function registerDashboardReviewTools({ server, track }) {
27817
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.
27818
27999
 
27819
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.`,
27820
- inputSchema: z22.object({
27821
- screen: z22.string().trim().min(1).max(120).describe("What this dashboard is, e.g. 'funnel report overview'."),
27822
- 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."),
27823
28004
  fixture: dashboardFixtureSchema.describe("The request's unchanged fixture object. The first step is the overview; remaining steps are dependent task screens."),
27824
- 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."),
27825
28006
  componentSources: dashboardComponentSourcesSchema.optional().describe("Installed Sidebar, StatCard, and Table source files for compound-composition review."),
27826
28007
  context: CONTEXT_ARG
27827
28008
  }),
@@ -27869,34 +28050,34 @@ var init_dashboard_review_tools = __esm({
27869
28050
  "use strict";
27870
28051
  init_review_capture();
27871
28052
  init_shared();
27872
- dashboardFixtureSchema = z22.object({
27873
- stats: z22.array(z22.object({
27874
- label: z22.string().trim().min(1).max(300),
27875
- 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)
27876
28057
  })).max(100).optional(),
27877
- records: z22.array(z22.object({
27878
- title: z22.string().trim().min(1).max(300),
27879
- detail: z22.string().trim().min(1).max(1e3),
27880
- meta: z22.string().trim().min(1).max(300),
27881
- 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)
27882
28063
  })).max(100).optional(),
27883
- steps: z22.array(z22.object({
27884
- title: z22.string().trim().min(1).max(300),
27885
- description: z22.string().trim().min(1).max(1e3),
27886
- 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)
27887
28068
  })).min(2).max(50)
27888
28069
  });
27889
- dashboardComponentSourcesSchema = z22.object({
27890
- sidebar: z22.string().min(1).max(12e4),
27891
- statCard: z22.string().min(1).max(12e4),
27892
- 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)
27893
28074
  });
27894
28075
  }
27895
28076
  });
27896
28077
 
27897
28078
  // ../shared/src/mcp/tools/final-ui-review-tools.ts
27898
28079
  import { createHash as createHash2 } from "crypto";
27899
- import { z as z23 } from "zod";
28080
+ import { z as z24 } from "zod";
27900
28081
  function digest(value) {
27901
28082
  return createHash2("sha256").update(value).digest("hex");
27902
28083
  }
@@ -27907,7 +28088,9 @@ function comprehensionFindings(input) {
27907
28088
  const report = analyzeComprehension({
27908
28089
  screen: input.screen,
27909
28090
  code: input.visibleSource,
27910
- knownTerms: input.knownTerms
28091
+ knownTerms: input.knownTerms,
28092
+ // A dashboard is a screen even when its name mentions slides. [BD-593]
28093
+ surface: "screen"
27911
28094
  });
27912
28095
  return report.findings.flatMap(
27913
28096
  (item, index2) => item.severity === "minor" ? [] : [
@@ -28308,15 +28491,15 @@ function registerFinalUiReviewTools({
28308
28491
  description: `Issue a source-bound receipt for a completed dashboard.
28309
28492
 
28310
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.`,
28311
- inputSchema: z23.object({
28312
- screen: z23.string().trim().min(1).max(120),
28313
- 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:", {
28314
28497
  message: "Use a public HTTPS preview URL."
28315
28498
  }).describe("Public HTTPS URL for the exact deployed dashboard route."),
28316
- source: z23.string().min(1).max(24e4).describe(
28499
+ source: z24.string().min(1).max(24e4).describe(
28317
28500
  "Complete TSX or JSX page source, including its default export."
28318
28501
  ),
28319
- visibleSource: z23.string().min(1).max(12e4).describe(
28502
+ visibleSource: z24.string().min(1).max(12e4).describe(
28320
28503
  "Exact visible screen component copied unchanged from source."
28321
28504
  ),
28322
28505
  fixture: dashboardFixtureSchema.describe(
@@ -28325,7 +28508,7 @@ Submit the public HTTPS preview plus exact source, unchanged data, and installed
28325
28508
  componentSources: dashboardComponentSourcesSchema.describe(
28326
28509
  "Exact installed Sidebar, StatCard, and Table component source files."
28327
28510
  ),
28328
- 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(),
28329
28512
  context: CONTEXT_ARG
28330
28513
  }),
28331
28514
  annotations: READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS
@@ -28396,7 +28579,7 @@ var init_final_ui_review_tools = __esm({
28396
28579
  });
28397
28580
 
28398
28581
  // ../shared/src/mcp/tools/widget-tools.ts
28399
- import { z as z24 } from "zod";
28582
+ import { z as z25 } from "zod";
28400
28583
  function hasIntake(args) {
28401
28584
  return Boolean(args.widgetType || INTAKE_FIELDS.some(({ key }) => args[key]));
28402
28585
  }
@@ -28519,16 +28702,16 @@ var init_widget_tools = __esm({
28519
28702
  WIDGET_GUIDE_TOPIC = "better-widgets";
28520
28703
  WIDGET_GUIDE_TOOL = "get-widget-guide";
28521
28704
  MINIMUM_CONTRACT_HEADING = "## Minimum build contract";
28522
- WIDGET_GUIDE_INPUT_SCHEMA = z24.object({
28523
- widgetType: z24.enum(BETTER_WIDGET_TYPES).optional().describe("Widget kind: calculator, estimator, converter, generator, quiz, or checklist."),
28524
- userGoal: z24.string().trim().min(1).max(500).optional().describe("Who will use the widget and the decision or task it helps them complete."),
28525
- inputs: z24.string().trim().min(1).max(1e3).optional().describe("Required inputs, including units, allowed ranges, and defaults."),
28526
- 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."),
28527
- transformation: z24.string().trim().min(1).max(1500).optional().describe("Formula, scoring rule, conversion, or transformation that produces the result."),
28528
- result: z24.string().trim().min(1).max(750).optional().describe("What the result shows, explains, and lets the user do next."),
28529
- hostFramework: z24.string().trim().min(1).max(300).optional().describe("The host framework and the page or component that will contain the widget."),
28530
- visualContext: z24.string().trim().min(1).max(750).optional().describe("Existing design tokens, components, or brand reference the widget must match."),
28531
- 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."),
28532
28715
  context: CONTEXT_ARG
28533
28716
  });
28534
28717
  INTAKE_FIELDS = [
@@ -28582,7 +28765,7 @@ The completed response combines a scoped build brief with Better Design's stable
28582
28765
  });
28583
28766
 
28584
28767
  // ../shared/src/mcp/feature-map/schema.ts
28585
- import { z as z25 } from "zod";
28768
+ import { z as z26 } from "zod";
28586
28769
  function oneLine(text3) {
28587
28770
  return text3.replace(/\s+/g, " ").trim();
28588
28771
  }
@@ -28617,38 +28800,38 @@ var init_schema3 = __esm({
28617
28800
  "use strict";
28618
28801
  FEATURE_MAP_PATH = ".better-design/feature-map.json";
28619
28802
  FEATURE_MAP_LIMITS = { features: 200, flows: 100, steps: 50, paths: 200, name: 120 };
28620
- NAME = z25.string().trim().min(1).max(FEATURE_MAP_LIMITS.name);
28621
- PATHS = z25.array(z25.string().trim().min(1).max(500)).max(FEATURE_MAP_LIMITS.paths).default([]);
28622
- 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({
28623
28806
  name: NAME,
28624
28807
  /** URL path such as `/settings/billing`. Absent for a dialog or an email. */
28625
- route: z25.string().trim().max(300).optional(),
28808
+ route: z26.string().trim().max(300).optional(),
28626
28809
  /** The file that renders the step, such as the route's page file. */
28627
- screen: z25.string().trim().max(500).optional(),
28810
+ screen: z26.string().trim().max(500).optional(),
28628
28811
  /** What the user does here, such as "confirms the cancellation". */
28629
- action: z25.string().trim().max(300).optional(),
28812
+ action: z26.string().trim().max(300).optional(),
28630
28813
  components: PATHS,
28631
28814
  /** API routes, server actions, hooks and queries the step reads or writes. */
28632
28815
  data: PATHS
28633
28816
  });
28634
- featureMapFlowSchema = z25.object({
28817
+ featureMapFlowSchema = z26.object({
28635
28818
  id: NAME,
28636
28819
  name: NAME,
28637
- description: z25.string().trim().max(500).optional(),
28638
- 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)
28639
28822
  });
28640
- featureMapFeatureSchema = z25.object({
28823
+ featureMapFeatureSchema = z26.object({
28641
28824
  id: NAME,
28642
28825
  name: NAME,
28643
- description: z25.string().trim().max(500).optional(),
28644
- 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),
28645
28828
  /** Components that belong to the feature but to no single step. */
28646
28829
  components: PATHS
28647
28830
  });
28648
- featureMapSchema = z25.object({
28649
- version: z25.literal(1),
28650
- product: z25.string().trim().max(120).optional(),
28651
- 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)
28652
28835
  });
28653
28836
  }
28654
28837
  });
@@ -28926,7 +29109,7 @@ var init_read = __esm({
28926
29109
  });
28927
29110
 
28928
29111
  // ../shared/src/mcp/feature-map/update.ts
28929
- import { z as z26 } from "zod";
29112
+ import { z as z27 } from "zod";
28930
29113
  function find(items, ref) {
28931
29114
  const r = ref.trim();
28932
29115
  const index2 = items.findIndex((i) => i.id === r || i.name.toLowerCase() === r.toLowerCase());
@@ -28989,21 +29172,21 @@ var init_update2 = __esm({
28989
29172
  "../shared/src/mcp/feature-map/update.ts"() {
28990
29173
  "use strict";
28991
29174
  init_schema3();
28992
- NAME2 = z26.string().trim().min(1).max(120);
28993
- 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.");
28994
29177
  FLOW_INPUT = featureMapFlowSchema.omit({ id: true });
28995
- featureMapChangeSchema = z26.discriminatedUnion("op", [
28996
- z26.object({
28997
- op: z26.literal("add-feature"),
29178
+ featureMapChangeSchema = z27.discriminatedUnion("op", [
29179
+ z27.object({
29180
+ op: z27.literal("add-feature"),
28998
29181
  name: NAME2,
28999
- description: z26.string().trim().max(500).optional(),
29000
- 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()
29001
29184
  }),
29002
- z26.object({ op: z26.literal("rename-feature"), feature: REF, name: NAME2 }),
29003
- z26.object({ op: z26.literal("remove-feature"), feature: REF }),
29004
- z26.object({ op: z26.literal("set-flow"), feature: REF, flow: FLOW_INPUT }),
29005
- z26.object({ op: z26.literal("rename-flow"), feature: REF, flow: REF, name: NAME2 }),
29006
- 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 })
29007
29190
  ]);
29008
29191
  idTaken = (items, id, except2) => items.some((item, i) => item.id === id && i !== except2);
29009
29192
  toFlow = (f) => ({ id: slugify(f.name), ...f });
@@ -29022,7 +29205,7 @@ var init_feature_map = __esm({
29022
29205
  });
29023
29206
 
29024
29207
  // ../shared/src/mcp/tools/feature-map-tools.ts
29025
- import { z as z27 } from "zod";
29208
+ import { z as z28 } from "zod";
29026
29209
  function registerFeatureMapTools({ server, tracked }) {
29027
29210
  server.registerTool(
29028
29211
  "build-feature-map",
@@ -29033,9 +29216,9 @@ function registerFeatureMapTools({ server, tracked }) {
29033
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.
29034
29217
 
29035
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").`,
29036
- inputSchema: z27.object({
29037
- 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`."),
29038
- 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."),
29039
29222
  context: CONTEXT_ARG
29040
29223
  }),
29041
29224
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29065,11 +29248,11 @@ ${serializeFeatureMap(map).trimEnd()}
29065
29248
  description: `Find the flows a change touches, with the screen, route, components and data files of every step. ${WHEN_TO_USE}
29066
29249
 
29067
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.`,
29068
- inputSchema: z27.object({
29251
+ inputSchema: z28.object({
29069
29252
  map: MAP_ARG,
29070
- feature: z27.string().trim().max(120).optional().describe("A feature id or part of its name."),
29071
- flow: z27.string().trim().max(120).optional().describe("A flow id or part of its name."),
29072
- 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."),
29073
29256
  context: CONTEXT_ARG
29074
29257
  }),
29075
29258
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29095,7 +29278,7 @@ Changes, one per call:
29095
29278
  - { op: "remove-flow", feature, flow }
29096
29279
 
29097
29280
  \`feature\` and \`flow\` take an id or an exact name. Write the returned file to ${FEATURE_MAP_PATH}.`,
29098
- inputSchema: z27.object({
29281
+ inputSchema: z28.object({
29099
29282
  map: MAP_ARG,
29100
29283
  change: featureMapChangeSchema,
29101
29284
  context: CONTEXT_ARG
@@ -29117,7 +29300,7 @@ var init_feature_map_tools = __esm({
29117
29300
  "use strict";
29118
29301
  init_feature_map();
29119
29302
  init_shared();
29120
- 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}.`);
29121
29304
  text2 = (body, isError = false) => ({
29122
29305
  content: [{ type: "text", text: body }],
29123
29306
  isError
@@ -29139,7 +29322,7 @@ var init_feature_map_tools = __esm({
29139
29322
  });
29140
29323
 
29141
29324
  // ../shared/src/mcp/inspiration.ts
29142
- import { z as z28 } from "zod";
29325
+ import { z as z29 } from "zod";
29143
29326
  function describeSet(set) {
29144
29327
  const heading = `## ${set.title}${set.category ? ` (${set.category})` : ""}`;
29145
29328
  if (!set.notes) return heading;
@@ -29160,40 +29343,40 @@ var init_inspiration = __esm({
29160
29343
  "use strict";
29161
29344
  INSPIRATION_TYPES = ["app-store-screenshots"];
29162
29345
  MAX_INSPIRATION_LIMIT = 6;
29163
- noteSchema = z28.object({
29164
- slug: z28.string(),
29346
+ noteSchema = z29.object({
29347
+ slug: z29.string(),
29165
29348
  /** The day the note was written, as YYYY-MM-DD. A listing can change after it. */
29166
- writtenAt: z28.string(),
29167
- headline: z28.string(),
29168
- order: z28.string(),
29169
- framing: z28.string(),
29170
- color: z28.string()
29349
+ writtenAt: z29.string(),
29350
+ headline: z29.string(),
29351
+ order: z29.string(),
29352
+ framing: z29.string(),
29353
+ color: z29.string()
29171
29354
  });
29172
- inspirationNotesFileSchema = z28.object({
29173
- model: z28.string(),
29174
- notes: z28.array(noteSchema)
29355
+ inspirationNotesFileSchema = z29.object({
29356
+ model: z29.string(),
29357
+ notes: z29.array(noteSchema)
29175
29358
  });
29176
- indexFileSchema = z28.object({
29177
- key: z28.string(),
29178
- type: z28.string(),
29179
- width: z28.number(),
29180
- height: z28.number()
29359
+ indexFileSchema = z29.object({
29360
+ key: z29.string(),
29361
+ type: z29.string(),
29362
+ width: z29.number(),
29363
+ height: z29.number()
29181
29364
  });
29182
- indexEntrySchema = z28.object({
29183
- title: z28.string(),
29184
- slug: z28.string(),
29185
- category: z28.string().nullish(),
29186
- files: z28.array(indexFileSchema)
29365
+ indexEntrySchema = z29.object({
29366
+ title: z29.string(),
29367
+ slug: z29.string(),
29368
+ category: z29.string().nullish(),
29369
+ files: z29.array(indexFileSchema)
29187
29370
  });
29188
- inspirationIndexSchema = z28.object({
29189
- entries: z28.array(indexEntrySchema)
29371
+ inspirationIndexSchema = z29.object({
29372
+ entries: z29.array(indexEntrySchema)
29190
29373
  });
29191
29374
  NOTE_DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
29192
29375
  }
29193
29376
  });
29194
29377
 
29195
29378
  // ../shared/src/mcp/tools/inspiration-tools.ts
29196
- import { z as z29 } from "zod";
29379
+ import { z as z30 } from "zod";
29197
29380
  function describeSets(result) {
29198
29381
  const lines = result.sets.map((set, i) => {
29199
29382
  const category = set.category ? `, ${set.category}` : "";
@@ -29221,10 +29404,10 @@ function registerInspirationTools({ server, tracked }, readInspiration2) {
29221
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.
29222
29405
 
29223
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.`,
29224
- inputSchema: z29.object({
29225
- type: z29.enum(INSPIRATION_TYPES).describe('The kind of reference to send. Today: "app-store-screenshots".'),
29226
- category: z29.string().trim().min(1).max(60).optional().describe('Optional App Store category, for example "Productivity" or "Health & Fitness". Ignores case.'),
29227
- limit: z29.number().int().min(1).max(MAX_INSPIRATION_LIMIT).optional().describe(`How many sets to send, 1 to ${MAX_INSPIRATION_LIMIT}. Defaults to 3.`),
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.`),
29228
29411
  context: CONTEXT_ARG
29229
29412
  }),
29230
29413
  annotations: READ_ONLY_TOOL_ANNOTATIONS
@@ -29261,8 +29444,62 @@ var init_inspiration_tools = __esm({
29261
29444
  }
29262
29445
  });
29263
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
+
29264
29501
  // ../shared/src/mcp/tools.ts
29265
- function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2, readInspiration2) {
29502
+ function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId, captureDashboardBrowser2, readInspiration2, sendFeedback) {
29266
29503
  const { track, tracked } = createTracking(server, onToolUsage);
29267
29504
  const deps = {
29268
29505
  server,
@@ -29289,6 +29526,7 @@ function registerAllTools(server, provider, onToolUsage, authContext, options =
29289
29526
  registerFeatureMapTools(deps);
29290
29527
  registerTeamRulesTools(deps);
29291
29528
  registerInspirationTools(deps, readInspiration2);
29529
+ registerFeedbackTools(deps, sendFeedback);
29292
29530
  registerPreviewResource(deps);
29293
29531
  registerInspectionResource(deps);
29294
29532
  }
@@ -29313,6 +29551,7 @@ var init_tools = __esm({
29313
29551
  init_feature_map_tools();
29314
29552
  init_team_rules_tools();
29315
29553
  init_inspiration_tools();
29554
+ init_feedback_tools();
29316
29555
  init_shared();
29317
29556
  }
29318
29557
  });
@@ -29322,7 +29561,7 @@ var MCP_IMPLEMENTATION_VERSION;
29322
29561
  var init_version2 = __esm({
29323
29562
  "../shared/src/mcp/version.ts"() {
29324
29563
  "use strict";
29325
- MCP_IMPLEMENTATION_VERSION = "0.33.0";
29564
+ MCP_IMPLEMENTATION_VERSION = "0.34.0";
29326
29565
  }
29327
29566
  });
29328
29567
 
@@ -29355,6 +29594,7 @@ __export(mcp_exports, {
29355
29594
  analyzeDashboardStructure: () => analyzeDashboardStructure,
29356
29595
  buildShopifyPrimitiveSet: () => buildShopifyPrimitiveSet,
29357
29596
  canonicalMcpToolName: () => canonicalMcpToolName,
29597
+ createFeedbackSender: () => createFeedbackSender,
29358
29598
  createMcpServer: () => createMcpServer,
29359
29599
  declareAdvertisedSchemaDialect: () => declareAdvertisedSchemaDialect,
29360
29600
  deriveDashboardInterpretation: () => deriveDashboardInterpretation,
@@ -29388,7 +29628,8 @@ function createMcpServer(options) {
29388
29628
  userId,
29389
29629
  captureDashboardBrowser: captureDashboardBrowser2,
29390
29630
  directoryListing = false,
29391
- readInspiration: readInspiration2
29631
+ readInspiration: readInspiration2,
29632
+ sendFeedback
29392
29633
  } = options;
29393
29634
  const server = new McpServer(
29394
29635
  { name, version: version2 },
@@ -29416,7 +29657,8 @@ function createMcpServer(options) {
29416
29657
  apiFetch,
29417
29658
  userId,
29418
29659
  captureDashboardBrowser2,
29419
- readInspiration2
29660
+ readInspiration2,
29661
+ sendFeedback
29420
29662
  );
29421
29663
  return server;
29422
29664
  }
@@ -29433,6 +29675,7 @@ var init_mcp = __esm({
29433
29675
  init_advertised_schema();
29434
29676
  init_tool_names();
29435
29677
  init_principle_tools();
29678
+ init_feedback_tools();
29436
29679
  init_instructions();
29437
29680
  init_session_guidance();
29438
29681
  init_shared();
@@ -29461,6 +29704,7 @@ __export(api_client_exports, {
29461
29704
  searchIconLibraries: () => searchIconLibraries,
29462
29705
  searchIcons: () => searchIcons,
29463
29706
  searchPrinciples: () => searchPrinciples,
29707
+ sendAgentFeedback: () => sendAgentFeedback,
29464
29708
  trackMcpInstalled: () => trackMcpInstalled,
29465
29709
  trackToolUsage: () => trackToolUsage
29466
29710
  });
@@ -29500,6 +29744,9 @@ function readInspiration(input) {
29500
29744
  if (input.limit !== void 0) params.set("limit", String(input.limit));
29501
29745
  return request(`/v1/inspiration?${params}`, { timeoutMs: 3e4 });
29502
29746
  }
29747
+ function sendAgentFeedback(report) {
29748
+ return request("/v1/feedback", { method: "POST", body: report });
29749
+ }
29503
29750
  async function searchDesignSystems(query, limit = 5) {
29504
29751
  return request(
29505
29752
  `/v1/design-systems/search?q=${encodeURIComponent(query)}&limit=${limit}`
@@ -31476,7 +31723,7 @@ var init_engine = __esm({
31476
31723
  });
31477
31724
 
31478
31725
  // ../shared/src/lint/config.ts
31479
- import { z as z30 } from "zod";
31726
+ import { z as z31 } from "zod";
31480
31727
  function mergeReviewConfig(override) {
31481
31728
  if (!override) return DEFAULT_REVIEW_CONFIG;
31482
31729
  return {
@@ -31534,47 +31781,47 @@ var init_config = __esm({
31534
31781
  semanticTokens: { enabled: true }
31535
31782
  }
31536
31783
  };
31537
- reviewFlowStepSchema = z30.discriminatedUnion("action", [
31538
- z30.object({ action: z30.literal("click"), selector: z30.string() }),
31539
- z30.object({ action: z30.literal("type"), selector: z30.string(), value: z30.string() }),
31540
- z30.object({ action: z30.literal("press"), key: z30.string() }),
31541
- z30.object({ action: z30.literal("wait"), milliseconds: z30.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() })
31542
31789
  ]);
31543
- visualReviewConfigSchema = z30.object({
31544
- enabled: z30.boolean().optional(),
31545
- model: z30.object({
31546
- enabled: z30.boolean().optional(),
31547
- maxScreenshots: z30.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()
31548
31795
  }).optional(),
31549
- routes: z30.array(z30.string()).optional(),
31550
- viewports: z30.array(z30.enum(["desktop", "mobile", "reflow"])).optional(),
31551
- flows: z30.array(z30.object({
31552
- name: z30.string(),
31553
- persona: z30.string(),
31554
- path: z30.string().optional(),
31555
- steps: z30.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)
31556
31803
  })).optional(),
31557
- componentGallery: z30.object({
31558
- enabled: z30.boolean().optional(),
31559
- componentSelector: z30.string().optional(),
31560
- canvasSelector: z30.string().optional(),
31561
- labelSelector: z30.string().optional(),
31562
- maxComponents: z30.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()
31563
31810
  }).optional()
31564
31811
  });
31565
- reviewConfigOverrideSchema = z30.object({
31566
- designSystem: z30.string().optional(),
31812
+ reviewConfigOverrideSchema = z31.object({
31813
+ designSystem: z31.string().optional(),
31567
31814
  visual: visualReviewConfigSchema.optional(),
31568
- rules: z30.object({
31569
- elements: z30.record(z30.string(), z30.object({ use: z30.string(), from: z30.string() })).optional(),
31570
- tokens: z30.record(z30.string(), z30.string()).optional(),
31571
- wcag: z30.object({
31572
- enabled: z30.boolean().optional(),
31573
- severity: z30.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()
31574
31821
  }).optional(),
31575
- slop: z30.object({ enabled: z30.boolean().optional() }).optional(),
31576
- copy: z30.object({ enabled: z30.boolean().optional() }).optional(),
31577
- semanticTokens: z30.object({ enabled: z30.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()
31578
31825
  }).optional()
31579
31826
  });
31580
31827
  }
@@ -31950,7 +32197,7 @@ __export(review_tool_exports, {
31950
32197
  isReviewableSourcePath: () => isReviewableSourcePath,
31951
32198
  registerReviewTool: () => registerReviewTool
31952
32199
  });
31953
- import { z as z31 } from "zod";
32200
+ import { z as z32 } from "zod";
31954
32201
  function isReviewableSourcePath(path6) {
31955
32202
  return /\.(tsx|jsx|ts|js|css)$/i.test(path6);
31956
32203
  }
@@ -31974,11 +32221,11 @@ get-review-rules gives you the judgment rules to apply yourself. This tool is th
31974
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.
31975
32222
 
31976
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.`,
31977
- inputSchema: z31.object({
31978
- files: z31.array(
31979
- z31.object({
31980
- path: z31.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
31981
- content: z31.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.")
31982
32229
  })
31983
32230
  ).min(1).max(MAX_FILES).describe("The .tsx, .jsx, .ts, .js, and .css files to review, as written."),
31984
32231
  context: CONTEXT_ARG
@@ -32039,7 +32286,7 @@ review-ui-code({ files: [{ path: "components/signup-form.tsx", content: "..." },
32039
32286
  });
32040
32287
 
32041
32288
  // ../shared/src/mcp/figma-prototype.ts
32042
- import { z as z32 } from "zod";
32289
+ import { z as z33 } from "zod";
32043
32290
  function escapeHtml(value) {
32044
32291
  return value.replace(/[&<>"]/g, (char2) => ESCAPES.get(char2) ?? char2);
32045
32292
  }
@@ -32073,63 +32320,63 @@ var colorSchema, paintSchema, sizingSchema, baseSchema, prototypeNodeSchema, pro
32073
32320
  var init_figma_prototype = __esm({
32074
32321
  "../shared/src/mcp/figma-prototype.ts"() {
32075
32322
  "use strict";
32076
- colorSchema = z32.object({ r: z32.number(), g: z32.number(), b: z32.number(), a: z32.number() });
32077
- paintSchema = z32.object({ variable: z32.string().optional(), color: colorSchema.optional() });
32078
- sizingSchema = z32.enum(["FIXED", "HUG", "FILL"]);
32079
- baseSchema = z32.object({
32080
- name: z32.string(),
32081
- width: z32.number(),
32082
- height: z32.number(),
32083
- x: z32.number(),
32084
- y: z32.number(),
32085
- sizing: z32.object({ horizontal: sizingSchema, vertical: sizingSchema }).optional(),
32086
- absolute: z32.boolean().optional(),
32087
- opacity: z32.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()
32088
32335
  });
32089
- prototypeNodeSchema = z32.lazy(() => z32.discriminatedUnion("kind", [
32336
+ prototypeNodeSchema = z33.lazy(() => z33.discriminatedUnion("kind", [
32090
32337
  baseSchema.extend({
32091
- kind: z32.literal("instance"),
32092
- auditKey: z32.string(),
32093
- systemId: z32.string(),
32094
- texts: z32.array(z32.string()),
32095
- mainTexts: z32.array(z32.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())
32096
32343
  }),
32097
32344
  baseSchema.extend({
32098
- kind: z32.literal("frame"),
32099
- layout: z32.object({
32100
- direction: z32.enum(["HORIZONTAL", "VERTICAL"]),
32101
- gap: z32.number(),
32102
- padding: z32.tuple([z32.number(), z32.number(), z32.number(), z32.number()]),
32103
- primary: z32.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]),
32104
- counter: z32.enum(["MIN", "CENTER", "MAX", "BASELINE"]),
32105
- wrap: z32.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()
32106
32353
  }).optional(),
32107
32354
  fill: paintSchema.optional(),
32108
- stroke: z32.object({ paint: paintSchema, weight: z32.number() }).optional(),
32109
- radius: z32.number().optional(),
32110
- clips: z32.boolean().optional(),
32111
- children: z32.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)
32112
32359
  }),
32113
32360
  baseSchema.extend({
32114
- kind: z32.literal("text"),
32115
- characters: z32.string(),
32116
- fontSize: z32.number(),
32117
- fontWeight: z32.number(),
32118
- lineHeight: z32.number().optional(),
32119
- letterSpacing: z32.number().optional(),
32120
- fontFamily: z32.string().optional(),
32121
- align: z32.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"]),
32122
32369
  fill: paintSchema.optional()
32123
32370
  }),
32124
32371
  baseSchema.extend({
32125
- kind: z32.literal("shape"),
32126
- svg: z32.string(),
32127
- reason: z32.string()
32372
+ kind: z33.literal("shape"),
32373
+ svg: z33.string(),
32374
+ reason: z33.string()
32128
32375
  })
32129
32376
  ]));
32130
- prototypeRequestSchema = z32.object({
32131
- type: z32.literal("better-design-figma-prototype"),
32132
- id: z32.string(),
32377
+ prototypeRequestSchema = z33.object({
32378
+ type: z33.literal("better-design-figma-prototype"),
32379
+ id: z33.string(),
32133
32380
  frame: prototypeNodeSchema
32134
32381
  });
32135
32382
  ESCAPES = /* @__PURE__ */ new Map([["&", "&amp;"], ["<", "&lt;"], [">", "&gt;"], ['"', "&quot;"]]);
@@ -32315,7 +32562,7 @@ var init_figma_model_prompt = __esm({
32315
32562
  });
32316
32563
 
32317
32564
  // ../shared/src/mcp/figma-model.ts
32318
- import { z as z33 } from "zod";
32565
+ import { z as z34 } from "zod";
32319
32566
  function fenced(text3, language) {
32320
32567
  const match = text3.match(new RegExp("```" + language + "[^\\n]*\\n([\\s\\S]*?)```", "i"));
32321
32568
  return match ? match[1] : null;
@@ -32521,8 +32768,8 @@ var init_figma_model = __esm({
32521
32768
  init_figma_prototype();
32522
32769
  init_html_kit();
32523
32770
  init_preview_html();
32524
- unplacedSchema = z33.object({
32525
- unplaced: z33.array(z33.object({ name: z33.string(), reason: z33.string() }))
32771
+ unplacedSchema = z34.object({
32772
+ unplaced: z34.array(z34.object({ name: z34.string(), reason: z34.string() }))
32526
32773
  });
32527
32774
  UNSAFE_BLOCKS = /<(script|iframe|object|embed|noscript)\b[\s\S]*?(<\/\1[^>]*>|$)/gi;
32528
32775
  UNSAFE_TAGS = /<\/?(meta|link|base|form|style)\b[^>]*(>|$)/gi;
@@ -32623,7 +32870,7 @@ var init_figma_model_handoff = __esm({
32623
32870
  });
32624
32871
 
32625
32872
  // ../shared/src/mcp/figma-model-providers.ts
32626
- import { z as z34 } from "zod";
32873
+ import { z as z35 } from "zod";
32627
32874
  function serverLabel(url) {
32628
32875
  try {
32629
32876
  const parsed = new URL(url);
@@ -32716,19 +32963,19 @@ var init_figma_model_providers = __esm({
32716
32963
  DEFAULT_LOCAL_MODEL = "gemma4:latest";
32717
32964
  DEFAULT_LOCAL_BASE_URL = "http://localhost:11434/v1";
32718
32965
  MAX_OUTPUT_TOKENS = 16e3;
32719
- chatReplySchema = z34.object({
32720
- choices: z34.array(z34.object({
32721
- message: z34.object({ content: z34.string().nullable().optional() }),
32722
- finish_reason: z34.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()
32723
32970
  })).min(1),
32724
- usage: z34.object({ prompt_tokens: z34.number(), completion_tokens: z34.number() }).optional()
32971
+ usage: z35.object({ prompt_tokens: z35.number(), completion_tokens: z35.number() }).optional()
32725
32972
  });
32726
- errorCodeSchema = z34.object({ code: z34.string() });
32973
+ errorCodeSchema = z35.object({ code: z35.string() });
32727
32974
  }
32728
32975
  });
32729
32976
 
32730
32977
  // ../shared/src/mcp/figma-relay.ts
32731
- import { z as z35 } from "zod";
32978
+ import { z as z36 } from "zod";
32732
32979
  function noPluginMessage() {
32733
32980
  return "No Figma plugin is connected. Open the Figma file, run Plugins, Development, Better Design, and leave the panel open.";
32734
32981
  }
@@ -32742,35 +32989,35 @@ var init_figma_relay = __esm({
32742
32989
  "use strict";
32743
32990
  init_figma_prototype();
32744
32991
  FIGMA_RELAY_PORT = 4397;
32745
- relayRequestSchema = z35.object({
32746
- id: z35.string(),
32992
+ relayRequestSchema = z36.object({
32993
+ id: z36.string(),
32747
32994
  /** A frame name as it reads in Figma. Omitted means the current selection. */
32748
- frame: z35.string().optional(),
32995
+ frame: z36.string().optional(),
32749
32996
  /**
32750
32997
  * `model` asks for the serialized frame and its PNG instead of a rendered
32751
32998
  * page, so a model can rebuild a frame that holds no library component.
32752
32999
  * Omitted means the library path, unchanged. [BD-866]
32753
33000
  */
32754
- mode: z35.enum(["library", "model"]).optional()
33001
+ mode: z36.enum(["library", "model"]).optional()
32755
33002
  });
32756
- relayResultSchema = z35.object({
32757
- id: z35.string(),
32758
- html: z35.string().optional(),
32759
- error: z35.string().optional(),
32760
- components: z35.number().optional(),
32761
- report: z35.array(z35.object({ name: z35.string(), reason: z35.string() })).default([]),
32762
- frame: z35.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(),
32763
33010
  /** The frames the plugin found, so a missing name can say what is there. */
32764
- frames: z35.array(z35.string()).optional(),
33011
+ frames: z36.array(z36.string()).optional(),
32765
33012
  /** Model mode: the serialized frame, for the model to rebuild. */
32766
33013
  tree: prototypeNodeSchema.optional(),
32767
33014
  /** Model mode: the frame's PNG export, base64. */
32768
- png: z35.string().optional()
33015
+ png: z36.string().optional()
32769
33016
  });
32770
- relayHelloSchema = z35.object({
32771
- file: z35.string(),
33017
+ relayHelloSchema = z36.object({
33018
+ file: z36.string(),
32772
33019
  /** The design system the file holds, when the plugin knows it. */
32773
- system: z35.string().optional()
33020
+ system: z36.string().optional()
32774
33021
  });
32775
33022
  POLL_HOLD_MS = 25e3;
32776
33023
  REQUEST_TIMEOUT_MS = 3e5;
@@ -32938,7 +33185,7 @@ __export(figma_prototype_tool_exports, {
32938
33185
  });
32939
33186
  import { mkdir as mkdir2, readFile, stat, writeFile as writeFile2 } from "fs/promises";
32940
33187
  import { dirname as dirname3, resolve as resolve2 } from "path";
32941
- import { z as z36 } from "zod";
33188
+ import { z as z37 } from "zod";
32942
33189
  async function ensureRelay() {
32943
33190
  if (relay) return relay;
32944
33191
  starting ??= startFigmaRelay().then((started) => {
@@ -32998,30 +33245,30 @@ function registerFigmaPrototypeTool(server) {
32998
33245
  "yourself: the tool returns a prompt file and the frame's image, you write the answer to a file, and",
32999
33246
  "you call this tool again with frameId and answerPath so it checks the page and reports what is missing."
33000
33247
  ].join(" "),
33001
- inputSchema: z36.object({
33002
- frame: z36.string().optional().describe("The frame's name as it reads in Figma. Omit to use the current selection."),
33003
- outputPath: z36.string().optional().describe("Where to write the HTML. Relative paths resolve against the working directory."),
33004
- mode: z36.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(
33005
33252
  "library (the default) maps components from a Better Design library. model asks a model to rebuild a frame from any file."
33006
33253
  ),
33007
- system: z36.string().optional().describe("Model mode: the catalog design system to build with, as find-design-system names it."),
33008
- provider: z36.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(
33009
33256
  "Model mode: agent (the default) hands the frame to you to rebuild; openai-compatible calls a local server, Ollama by default."
33010
33257
  ),
33011
- model: z36.string().optional().describe(`Model mode with openai-compatible: the local model id. Defaults to ${DEFAULT_LOCAL_MODEL}.`),
33012
- frameId: z36.string().optional().describe("Model mode, second call: the frameId the first call returned."),
33013
- answerPath: z36.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.")
33014
33261
  }),
33015
- outputSchema: z36.object({
33016
- status: z36.enum(["completed", "failed", "needs-answer"]),
33017
- message: z36.string(),
33018
- frameId: z36.string().optional(),
33019
- promptPath: z36.string().optional(),
33020
- imagePath: z36.string().optional(),
33021
- path: z36.string().optional(),
33022
- components: z36.number().optional(),
33023
- report: z36.array(z36.object({ name: z36.string(), reason: z36.string() })).optional(),
33024
- model: z36.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()
33025
33272
  }),
33026
33273
  annotations: { readOnlyHint: false, openWorldHint: true }
33027
33274
  },
@@ -33244,6 +33491,7 @@ async function startServer() {
33244
33491
  const {
33245
33492
  captureDashboardBrowser: captureDashboardBrowser2,
33246
33493
  readInspiration: readInspiration2,
33494
+ sendAgentFeedback: sendAgentFeedback2,
33247
33495
  trackToolUsage: trackToolUsage2,
33248
33496
  trackMcpInstalled: trackMcpInstalled2
33249
33497
  } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
@@ -33270,7 +33518,8 @@ async function startServer() {
33270
33518
  version: MCP_IMPLEMENTATION_VERSION2,
33271
33519
  onToolUsage,
33272
33520
  captureDashboardBrowser: captureDashboardBrowser2,
33273
- readInspiration: readInspiration2
33521
+ readInspiration: readInspiration2,
33522
+ sendFeedback: sendAgentFeedback2
33274
33523
  });
33275
33524
  registerReviewTool2(server, onToolUsage);
33276
33525
  registerFigmaPrototypeTool2(server);