better-design 0.7.6 → 0.8.1

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
@@ -284,7 +284,7 @@ var init_constants = __esm({
284
284
  RULE_END = "<!-- better-design:end -->";
285
285
  RULE_BODY = `## Better Design
286
286
 
287
- Always use the Better Design MCP tools for any frontend, UI, UX, interaction, flow, form, navigation, dialog, error, onboarding, component, or styling work, without me having to explicitly ask. Load visual guidance with \`get-ui-principle\`, load flow/behavior guidance with \`get-ux-principle\`, and self-review against \`get-review-rules\` after. Use \`create-design-system\` to generate the user's own brand (the default for building an app), or \`find-design\` / \`get-design-system-docs\` to install an existing catalog system.`;
287
+ Always use the Better Design MCP tools for any frontend, UI, UX, interaction, flow, form, navigation, dialog, error, onboarding, component, or styling work, without me having to explicitly ask. Load visual guidance with \`get-ui-principle\`, load flow/behavior guidance with \`get-ux-principle\`, and self-review against \`get-review-rules\` after. Use \`create-design-system\` to generate the user's own brand (the default for building an app), or \`find-design-system\` / \`get-design-system-docs\` to install an existing catalog system.`;
288
288
  RULE_BLOCK = `${RULE_START}
289
289
  ${RULE_BODY}
290
290
  ${RULE_END}`;
@@ -559,7 +559,7 @@ var init_rule_writer = __esm({
559
559
 
560
560
  // src/setup/args.ts
561
561
  function parseArgs(argv2) {
562
- const args = { help: false };
562
+ const args = { help: false, autoKey: false };
563
563
  for (const arg of argv2) {
564
564
  switch (arg) {
565
565
  case "--global":
@@ -568,6 +568,14 @@ function parseArgs(argv2) {
568
568
  case "--project":
569
569
  args.scope = "project";
570
570
  break;
571
+ // `setup` is the implicit command; tolerate it appearing in argv.
572
+ case "setup":
573
+ break;
574
+ // Non-interactive: skip prompts, auto-mint a key.
575
+ case "-y":
576
+ case "--yes":
577
+ args.autoKey = true;
578
+ break;
571
579
  case "--help":
572
580
  case "-h":
573
581
  args.help = true;
@@ -580,20 +588,25 @@ var HELP_TEXT;
580
588
  var init_args = __esm({
581
589
  "src/setup/args.ts"() {
582
590
  "use strict";
583
- HELP_TEXT = `better-design-mcp setup: configure Better Design in your AI clients
591
+ HELP_TEXT = `better-design: connect Better Design to your AI coding tools
584
592
 
585
593
  Usage:
586
- npx better-design-mcp setup [options]
594
+ npx better-design Set up Better Design (auto-creates a key, no signup)
595
+ npx better-design setup Same as the bare command, with options below
587
596
 
588
597
  Options:
598
+ -y, --yes Non-interactive: skip all prompts and auto-create a key
589
599
  --global Install at user/global scope (default)
590
600
  --project Install at project scope (current directory)
591
601
  --help, -h Show this help
592
602
 
593
- The installer detects your clients and prompts for the rest. Set your API key
594
- via the BETTER_DESIGN_API_KEY env var or the interactive prompt.
603
+ No API key? You don't need one up front. The installer creates a free key for
604
+ you automatically (no account required) so you can start using Better Design
605
+ right away. Create an account later to keep your key and lift the free limit.
595
606
 
596
- Get an API key at https://better-design.com/mcp`;
607
+ Already have a key? Set BETTER_DESIGN_API_KEY and it will be used instead.
608
+
609
+ Manage your account or keys at https://better-design.com/mcp`;
597
610
  }
598
611
  });
599
612
 
@@ -612,11 +625,40 @@ var init_prompts = __esm({
612
625
  }
613
626
  });
614
627
 
628
+ // src/setup/provision.ts
629
+ async function provisionApiKey() {
630
+ const res = await fetch(`${API_URL}/api/api-keys/provision`, {
631
+ method: "POST",
632
+ headers: { "Content-Type": "application/json" },
633
+ signal: AbortSignal.timeout(15e3)
634
+ });
635
+ if (!res.ok) {
636
+ const body2 = await res.json().catch(() => ({}));
637
+ throw new Error(
638
+ body2.message ?? `Could not create a key automatically (HTTP ${res.status}). Create one at ${DASHBOARD_URL}`
639
+ );
640
+ }
641
+ const body = await res.json();
642
+ if (!body.key) {
643
+ throw new Error(`Provisioning endpoint returned no key. Create one at ${DASHBOARD_URL}`);
644
+ }
645
+ return { key: body.key, claimUrl: body.claimUrl ?? "" };
646
+ }
647
+ var API_URL;
648
+ var init_provision = __esm({
649
+ "src/setup/provision.ts"() {
650
+ "use strict";
651
+ init_constants();
652
+ API_URL = process.env.API_URL ?? "https://better-design.com";
653
+ }
654
+ });
655
+
615
656
  // src/setup/index.ts
616
657
  var setup_exports = {};
617
658
  __export(setup_exports, {
618
659
  runSetup: () => runSetup
619
660
  });
661
+ import { spawn } from "child_process";
620
662
  async function runSetup(argv2) {
621
663
  const args = parseArgs(argv2);
622
664
  if (args.help) {
@@ -625,7 +667,7 @@ async function runSetup(argv2) {
625
667
  }
626
668
  const interactive = Boolean(process.stdin.isTTY);
627
669
  clack.intro("Better Design setup");
628
- const apiKey = await getApiKey(interactive);
670
+ const apiKey = await getApiKey(interactive, args.autoKey);
629
671
  const clients = await pickClients(interactive);
630
672
  if (clients.length === 0) {
631
673
  clack.outro("No clients selected. Run again and select at least one client.");
@@ -644,13 +686,55 @@ async function runSetup(argv2) {
644
686
  const ruleResults = rulePlan.files.map((f) => writeRule(f));
645
687
  printSummary(results, ruleResults, rulePlan.manual);
646
688
  }
647
- async function getApiKey(interactive) {
689
+ async function getApiKey(interactive, autoKey) {
648
690
  const fromEnv = process.env.BETTER_DESIGN_API_KEY;
649
691
  if (fromEnv) return fromEnv.trim();
650
- if (!interactive) {
651
- clack.cancel(`No API key. Set BETTER_DESIGN_API_KEY. Get one at ${DASHBOARD_URL}`);
652
- process.exit(1);
692
+ if (!interactive || autoKey) {
693
+ return provisionFlow();
653
694
  }
695
+ const choice = ensure(
696
+ await clack.select({
697
+ message: "No API key found. How do you want to connect?",
698
+ options: [
699
+ { value: "browser", label: "Sign in / create an account", hint: "unlimited queries, recommended" },
700
+ { value: "generate", label: "test without account", hint: "limited to 25 queries" }
701
+ ],
702
+ initialValue: "browser"
703
+ })
704
+ );
705
+ if (choice === "browser") return browserFlow();
706
+ return provisionFlow();
707
+ }
708
+ async function provisionFlow() {
709
+ const spinner = clack.spinner();
710
+ spinner.start("Creating a free Better Design key");
711
+ try {
712
+ const { key, claimUrl } = await provisionApiKey();
713
+ spinner.stop("Created a free Better Design key (no account needed)");
714
+ if (claimUrl) {
715
+ clack.note(
716
+ `You're on the free tier with a limited number of queries.
717
+ Create an account to keep this key and go unlimited:
718
+ ${claimUrl}`,
719
+ "Heads up"
720
+ );
721
+ }
722
+ return key;
723
+ } catch (error) {
724
+ spinner.stop("Couldn't create a key automatically");
725
+ throw error instanceof Error ? error : new Error(String(error));
726
+ }
727
+ }
728
+ async function browserFlow() {
729
+ openUrl(DASHBOARD_URL);
730
+ clack.note(
731
+ `Opened ${DASHBOARD_URL} in your browser.
732
+ Sign in, copy your API key, then paste it here.`,
733
+ "Sign in"
734
+ );
735
+ return pasteFlow();
736
+ }
737
+ async function pasteFlow() {
654
738
  const key = ensure(
655
739
  await clack.password({
656
740
  message: `Paste your Better Design API key (get one at ${DASHBOARD_URL})`,
@@ -659,6 +743,13 @@ async function getApiKey(interactive) {
659
743
  );
660
744
  return key.trim();
661
745
  }
746
+ function openUrl(url) {
747
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
748
+ try {
749
+ spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
750
+ } catch {
751
+ }
752
+ }
662
753
  async function pickClients(interactive) {
663
754
  const detected = detectInstalled();
664
755
  if (!interactive) return detected;
@@ -757,6 +848,7 @@ var init_setup = __esm({
757
848
  init_args();
758
849
  init_prompts();
759
850
  init_constants();
851
+ init_provision();
760
852
  }
761
853
  });
762
854
 
@@ -769,12 +861,12 @@ var init_instructions = __esm({
769
861
 
770
862
  Use these tools for ANY frontend, UI, component, or styling work, without being asked.
771
863
 
772
- Tools (exact names): find-design, get-design-system-docs, preview-design-system (SHOW in-chat), create-design-system, check-design-system-status (poll after create), get-ui-principle, get-ux-principle, get-review-rules, resolve-icon-library, search-icons, install-icons, detect-icon-library.
864
+ Tools (exact names): find-design-system, get-design-system-docs, preview-design-system (SHOW in-chat), create-design-system, get-design-system-status (poll after create), get-ui-principle, get-ux-principle, get-review-rules, find-icon-library, find-icons, install-icons, detect-icon-library.
773
865
 
774
866
  ## Build an app (even if the user never says "design" or "brand")
775
867
  On any "build/make me <app>" request, create the user's BRAND design system FIRST, then build with it:
776
868
  1. create-design-system({ prompt }) \u2014 derive the prompt from the app (name, domain, vibe) -> a custom brand (tokens + components). Share the project URL so the user watches it build.
777
- 2. check-design-system-status({ projectId }) \u2014 poll; it SHOWS the generated brand inline.
869
+ 2. get-design-system-status({ projectId }) \u2014 poll; it SHOWS the generated brand inline.
778
870
  3. Build the app with that brand. Brand first, not raw UI.
779
871
 
780
872
  ## Building / modifying UI
@@ -784,7 +876,7 @@ On any "build/make me <app>" request, create the user's BRAND design system FIRS
784
876
  4. get-review-rules after; fix every critical/serious issue before presenting.
785
877
 
786
878
  ## Only if the user explicitly names an existing catalog DS
787
- Default is create-design-system (above). Just to install a named catalog system: find-design(query) -> get-design-system-docs({ designSystemId }) -> run BOTH bash lines (installs globals.css + components into components/ui/; use only their tokens). Icons: detect-icon-library / resolve-icon-library -> search-icons -> install-icons. Never runtime @iconify/react.
879
+ Default is create-design-system (above). Just to install a named catalog system: find-design-system(query) -> get-design-system-docs({ designSystemId }) -> run BOTH bash lines (installs globals.css + components into components/ui/; use only their tokens). Icons: detect-icon-library / find-icon-library -> find-icons -> install-icons. Never runtime @iconify/react.
788
880
  `.trim();
789
881
  }
790
882
  });
@@ -902,21 +994,20 @@ Emitting a single standalone \`.html\` file as "the app" when you could run the
902
994
  PREVIEW_OUTPUT_SCHEMA = z.object({
903
995
  status: z.string().describe("completed | error | pending | generating | selected"),
904
996
  name: z.string().describe("Display name of the design system or project."),
905
- viewUrl: z.string().optional().describe("Better Design page for this system/project."),
997
+ viewUrl: z.string().optional().describe("Owner-facing Better Design page (the human 'Open' link). May require login."),
998
+ previewUrl: z.string().optional().describe("Public, cookieless showcase URL to embed in an iframe (sandboxed hosts can't carry the user's session). Falls back to viewUrl."),
906
999
  shotUrl: z.string().optional().describe("Real screenshot of the showcase page, for hosts that can't iframe it."),
907
1000
  installCommand: z.string().optional().describe("shadcn install command, when complete."),
908
1001
  step: z.string().optional().describe("Current generation step, while generating."),
909
1002
  tokensCss: z.string().describe("CSS custom-property stylesheet for the system's tokens.")
910
1003
  });
911
1004
  SEARCH_TOOLS = /* @__PURE__ */ new Set([
912
- "find-design",
913
- "resolve-design-system",
914
- // deprecated alias of find-design [BD-152]
1005
+ "find-design-system",
915
1006
  "get-ui-principle",
916
1007
  "get-ux-principle",
917
1008
  "get-review-rules",
918
- "resolve-icon-library",
919
- "search-icons",
1009
+ "find-icon-library",
1010
+ "find-icons",
920
1011
  "create-design-system"
921
1012
  ]);
922
1013
  }
@@ -1356,11 +1447,9 @@ var init_icon_installer = __esm({
1356
1447
  // ../shared/src/mcp/tools/icon-tools.ts
1357
1448
  import { z as z3 } from "zod";
1358
1449
  function registerIconTools({ server, provider, tracked, track }) {
1359
- server.registerTool(
1360
- "resolve-icon-library",
1361
- {
1362
- title: "Resolve Icon Library",
1363
- description: `Search for icon libraries matching a design style or personality using semantic search.
1450
+ const iconLibraryConfig = {
1451
+ title: "Find Icon Library",
1452
+ description: `Search for icon libraries matching a design style or personality using semantic search.
1364
1453
 
1365
1454
  Use this to find an icon library that matches your design system's personality:
1366
1455
  - Style traits (minimal, bold, rounded, sharp, outlined, filled)
@@ -1371,156 +1460,154 @@ Examples:
1371
1460
  - "minimal clean professional" \u2192 Iconoir, Feather Icons
1372
1461
  - "friendly rounded warm" \u2192 Phosphor, MingCute
1373
1462
  - "bold technical enterprise" \u2192 Tabler, Carbon`,
1374
- inputSchema: z3.object({
1375
- query: z3.string().describe("Style description or personality traits to match.")
1376
- }),
1377
- annotations: { readOnlyHint: true }
1378
- },
1379
- async ({ query }) => {
1380
- const start = Date.now();
1381
- try {
1382
- const results = await provider.searchIconLibraries(query, 5);
1383
- const topResults = results.map((result) => ({ id: result.id, score: result.matchScore }));
1384
- if (results.length === 0) {
1385
- return trackSearch(
1386
- track,
1387
- "resolve-icon-library",
1388
- { query, topResults },
1389
- start,
1390
- { content: [{ type: "text", text: "No icon libraries found matching your query." }] }
1391
- );
1392
- }
1393
- const formatted = results.map((r) => {
1394
- const lines = [
1395
- `**ID:** ${r.id}`,
1396
- `**Name:** ${r.name}`,
1397
- `**Prefix:** ${r.prefix}`,
1398
- `**Style:** ${r.tags.join(", ")}`
1399
- ];
1400
- if (r.variants?.length) {
1401
- lines.push(`**Variants:** ${r.variants.join(", ")}`);
1402
- lines.push(`**Default Variant:** ${r.defaultVariant || r.variants[0]}`);
1403
- }
1404
- lines.push(`**Match Score:** ${r.matchScore}%`);
1405
- return lines.join("\n");
1406
- }).join("\n\n----------\n\n");
1463
+ inputSchema: z3.object({
1464
+ query: z3.string().describe("Style description or personality traits to match.")
1465
+ }),
1466
+ annotations: { readOnlyHint: true }
1467
+ };
1468
+ const makeIconLibraryHandler = (toolName) => async ({ query }) => {
1469
+ const start = Date.now();
1470
+ try {
1471
+ const results = await provider.searchIconLibraries(query, 5);
1472
+ const topResults = results.map((result) => ({ id: result.id, score: result.matchScore }));
1473
+ if (results.length === 0) {
1407
1474
  return trackSearch(
1408
1475
  track,
1409
- "resolve-icon-library",
1476
+ toolName,
1410
1477
  { query, topResults },
1411
1478
  start,
1412
- {
1413
- content: [{
1414
- type: "text",
1415
- text: `# Matching Icon Libraries
1479
+ { content: [{ type: "text", text: "No icon libraries found matching your query." }] }
1480
+ );
1481
+ }
1482
+ const formatted = results.map((r) => {
1483
+ const lines = [
1484
+ `**ID:** ${r.id}`,
1485
+ `**Name:** ${r.name}`,
1486
+ `**Prefix:** ${r.prefix}`,
1487
+ `**Style:** ${r.tags.join(", ")}`
1488
+ ];
1489
+ if (r.variants?.length) {
1490
+ lines.push(`**Variants:** ${r.variants.join(", ")}`);
1491
+ lines.push(`**Default Variant:** ${r.defaultVariant || r.variants[0]}`);
1492
+ }
1493
+ lines.push(`**Match Score:** ${r.matchScore}%`);
1494
+ return lines.join("\n");
1495
+ }).join("\n\n----------\n\n");
1496
+ return trackSearch(
1497
+ track,
1498
+ toolName,
1499
+ { query, topResults },
1500
+ start,
1501
+ {
1502
+ content: [{
1503
+ type: "text",
1504
+ text: `# Matching Icon Libraries
1416
1505
 
1417
1506
  Use the prefix with Iconify: \`<Icon icon="prefix:icon-name" />\`
1418
1507
 
1419
1508
  ---
1420
1509
 
1421
1510
  ${formatted}`
1422
- }]
1423
- }
1424
- );
1425
- } catch (error) {
1426
- return trackSearch(
1427
- track,
1428
- "resolve-icon-library",
1429
- { query, topResults: [] },
1430
- start,
1431
- { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1432
- true,
1433
- error instanceof Error ? error.message : String(error)
1434
- );
1435
- }
1511
+ }]
1512
+ }
1513
+ );
1514
+ } catch (error) {
1515
+ return trackSearch(
1516
+ track,
1517
+ toolName,
1518
+ { query, topResults: [] },
1519
+ start,
1520
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1521
+ true,
1522
+ error instanceof Error ? error.message : String(error)
1523
+ );
1436
1524
  }
1437
- );
1438
- server.registerTool(
1439
- "search-icons",
1440
- {
1441
- title: "Search Icons",
1442
- description: `Search for specific icons within an icon library.
1525
+ };
1526
+ server.registerTool("find-icon-library", iconLibraryConfig, makeIconLibraryHandler("find-icon-library"));
1527
+ const iconSearchConfig = {
1528
+ title: "Find Icons",
1529
+ description: `Search for specific icons within an icon library.
1443
1530
 
1444
- Use this after resolve-icon-library to find specific icons by name/concept.
1531
+ Use this after find-icon-library to find specific icons by name/concept.
1445
1532
 
1446
1533
  **Query with a single canonical noun** ("inbox", "calendar", "paperclip"), not a multi-word phrase. The underlying index keyword-matches, so "home" beats "home house dwelling". Multi-word queries still work (they fall back to a per-keyword union) but a single noun gives the best ranking. Search once per concept; you don't need to retry with synonyms.
1447
1534
 
1448
1535
  Examples:
1449
- - search-icons({ query: "home", libraryId: "phosphor" })
1450
- - search-icons({ query: "settings", libraryId: "tabler" })
1536
+ - find-icons({ query: "home", libraryId: "phosphor" })
1537
+ - find-icons({ query: "settings", libraryId: "tabler" })
1451
1538
 
1452
1539
  Returns icons formatted for use with Iconify React. If you pass a variant, Better Design will try standard Iconify naming first, such as \`home-bold\`, then fall back to alternate naming and the bare icon name.`,
1453
- inputSchema: z3.object({
1454
- query: z3.string().describe("What icon you need (e.g., 'home', 'settings', 'user')"),
1455
- libraryId: z3.string().describe("The icon library ID from resolve-icon-library"),
1456
- variant: z3.string().optional().describe("Optional variant style (e.g., 'bold', 'outline', 'duotone').")
1457
- }),
1458
- annotations: { readOnlyHint: true }
1459
- },
1460
- async ({ query, libraryId, variant }) => {
1461
- const start = Date.now();
1462
- try {
1463
- const library = await provider.getIconLibraryById(libraryId);
1464
- if (!library) {
1465
- return trackSearch(
1466
- track,
1467
- "search-icons",
1468
- { query, topResults: [] },
1469
- start,
1470
- { content: [{ type: "text", text: `Icon library '${libraryId}' not found.` }] }
1471
- );
1472
- }
1473
- const icons = await provider.searchIconsInLibrary(query, library, variant, 15);
1474
- const topResults = icons.map((icon2) => ({ id: icon2 }));
1475
- if (icons.length === 0) {
1476
- return trackSearch(
1477
- track,
1478
- "search-icons",
1479
- { query, topResults },
1480
- start,
1481
- { content: [{ type: "text", text: `No icons found in ${library.name}.` }] }
1482
- );
1483
- }
1484
- const lines = [
1485
- `# Icons from ${library.name}`,
1486
- "",
1487
- '**Next step:** call `install-icons` with these names to scaffold them as SSR-clean React component files in the user\'s repo. Avoid the runtime `<Icon icon="..." />` import, it requires a CDN fetch on every page load.',
1488
- "",
1489
- "```ts",
1490
- `install-icons({ libraryId: "${library.id}", names: [${icons.slice(0, 3).map((i) => `"${stripPrefix(i)}"`).join(", ")}${icons.length > 3 ? ", ..." : ""}] })`,
1491
- "```",
1492
- "",
1493
- "Found icons:",
1494
- "",
1495
- ...icons.map((icon2) => `- \`${icon2}\``)
1496
- ];
1540
+ inputSchema: z3.object({
1541
+ query: z3.string().describe("What icon you need (e.g., 'home', 'settings', 'user')"),
1542
+ libraryId: z3.string().describe("The icon library ID from find-icon-library"),
1543
+ variant: z3.string().optional().describe("Optional variant style (e.g., 'bold', 'outline', 'duotone').")
1544
+ }),
1545
+ annotations: { readOnlyHint: true }
1546
+ };
1547
+ const makeIconSearchHandler = (toolName) => async ({ query, libraryId, variant }) => {
1548
+ const start = Date.now();
1549
+ try {
1550
+ const library = await provider.getIconLibraryById(libraryId);
1551
+ if (!library) {
1497
1552
  return trackSearch(
1498
1553
  track,
1499
- "search-icons",
1500
- { query, topResults },
1554
+ toolName,
1555
+ { query, topResults: [] },
1501
1556
  start,
1502
- { content: [{ type: "text", text: lines.join("\n") }] }
1557
+ { content: [{ type: "text", text: `Icon library '${libraryId}' not found.` }] }
1503
1558
  );
1504
- } catch (error) {
1559
+ }
1560
+ const icons = await provider.searchIconsInLibrary(query, library, variant, 15);
1561
+ const topResults = icons.map((icon2) => ({ id: icon2 }));
1562
+ if (icons.length === 0) {
1505
1563
  return trackSearch(
1506
1564
  track,
1507
- "search-icons",
1508
- { query, topResults: [] },
1565
+ toolName,
1566
+ { query, topResults },
1509
1567
  start,
1510
- { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1511
- true,
1512
- error instanceof Error ? error.message : String(error)
1568
+ { content: [{ type: "text", text: `No icons found in ${library.name}.` }] }
1513
1569
  );
1514
1570
  }
1571
+ const lines = [
1572
+ `# Icons from ${library.name}`,
1573
+ "",
1574
+ '**Next step:** call `install-icons` with these names to scaffold them as SSR-clean React component files in the user\'s repo. Avoid the runtime `<Icon icon="..." />` import, it requires a CDN fetch on every page load.',
1575
+ "",
1576
+ "```ts",
1577
+ `install-icons({ libraryId: "${library.id}", names: [${icons.slice(0, 3).map((i) => `"${stripPrefix(i)}"`).join(", ")}${icons.length > 3 ? ", ..." : ""}] })`,
1578
+ "```",
1579
+ "",
1580
+ "Found icons:",
1581
+ "",
1582
+ ...icons.map((icon2) => `- \`${icon2}\``)
1583
+ ];
1584
+ return trackSearch(
1585
+ track,
1586
+ toolName,
1587
+ { query, topResults },
1588
+ start,
1589
+ { content: [{ type: "text", text: lines.join("\n") }] }
1590
+ );
1591
+ } catch (error) {
1592
+ return trackSearch(
1593
+ track,
1594
+ toolName,
1595
+ { query, topResults: [] },
1596
+ start,
1597
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1598
+ true,
1599
+ error instanceof Error ? error.message : String(error)
1600
+ );
1515
1601
  }
1516
- );
1602
+ };
1603
+ server.registerTool("find-icons", iconSearchConfig, makeIconSearchHandler("find-icons"));
1517
1604
  server.registerTool(
1518
1605
  "install-icons",
1519
1606
  {
1520
1607
  title: "Install Icons as React Components",
1521
1608
  description: `Scaffold SVG icons as React component files (.tsx) into the user's repo.
1522
1609
 
1523
- Call this after \`search-icons\` to materialise the chosen icons as owned, SSR-clean component files, with no runtime CDN fetch from \`api.iconify.design\` and no first-paint flash.
1610
+ Call this after \`find-icons\` to materialise the chosen icons as owned, SSR-clean component files, with no runtime CDN fetch from \`api.iconify.design\` and no first-paint flash.
1524
1611
 
1525
1612
  The tool fetches each SVG from the public Iconify API, inlines its viewBox + body into a small \`<Name>Icon\` React component, and returns a list of \`<file path="...">\`-tagged blocks for the agent to write to disk. A barrel \`index.ts\` is also returned.
1526
1613
 
@@ -1532,7 +1619,7 @@ import { HomeIcon } from "@/components/icons";
1532
1619
 
1533
1620
  If \`components/icons/index.ts\` already exists in the project, MERGE the new exports into it rather than overwriting.`,
1534
1621
  inputSchema: z3.object({
1535
- libraryId: z3.string().describe("Icon library ID from resolve-icon-library (e.g. 'tabler', 'phosphor')."),
1622
+ libraryId: z3.string().describe("Icon library ID from find-icon-library (e.g. 'tabler', 'phosphor')."),
1536
1623
  names: z3.array(z3.string()).min(1).describe("Icon names to install. May be bare (e.g. 'home') or prefixed (e.g. 'tabler:home')."),
1537
1624
  targetDir: z3.string().optional().describe("Directory inside the project to write into. Defaults to 'components/icons'."),
1538
1625
  format: z3.enum(["tsx", "svg"]).optional().describe("Output format. 'tsx' (default) generates React components; 'svg' generates raw .svg files."),
@@ -1552,7 +1639,7 @@ If \`components/icons/index.ts\` already exists in the project, MERGE the new ex
1552
1639
  const dir = dirResult.dir;
1553
1640
  const library = await provider.getIconLibraryById(libraryId);
1554
1641
  if (!library) {
1555
- return { content: [{ type: "text", text: `Icon library '${libraryId}' not found. Call resolve-icon-library first.` }] };
1642
+ return { content: [{ type: "text", text: `Icon library '${libraryId}' not found. Call find-icon-library first.` }] };
1556
1643
  }
1557
1644
  const outFormat = format ?? "tsx";
1558
1645
  const prefix = library.prefix;
@@ -1734,11 +1821,11 @@ If \`components/icons/index.ts\` already exists in the project, MERGE the new ex
1734
1821
 
1735
1822
  Reads <targetDir>/.library.json (default targetDir: "components/icons") if it exists and returns the locked library + installed icon list.
1736
1823
 
1737
- Call this BEFORE resolve-icon-library / install-icons. If a library is already locked, use it for any new icon work. Do NOT switch libraries mid-project.
1824
+ Call this BEFORE find-icon-library / install-icons. If a library is already locked, use it for any new icon work. Do NOT switch libraries mid-project.
1738
1825
 
1739
1826
  Returns either:
1740
1827
  - The locked library info + installed icon list, with a hint to call install-icons using that libraryId, OR
1741
- - A "no library detected" response, prompting the agent to call resolve-icon-library normally.`,
1828
+ - A "no library detected" response, prompting the agent to call find-icon-library normally.`,
1742
1829
  inputSchema: z3.object({
1743
1830
  manifestContent: z3.string().optional().describe("Contents of the project's components/icons/.library.json file, if it exists. The agent should attempt to read this file from disk and pass it here. If the file doesn't exist or can't be read, omit this field.")
1744
1831
  }),
@@ -1746,7 +1833,7 @@ Returns either:
1746
1833
  },
1747
1834
  async ({ manifestContent }) => tracked("detect-icon-library", { hasManifest: Boolean(manifestContent) }, async () => {
1748
1835
  if (!manifestContent || !manifestContent.trim()) {
1749
- return { content: [{ type: "text", text: "No `.library.json` manifest found. Proceed by calling `resolve-icon-library` to pick a library, then `install-icons` to scaffold icons. The library you pick will be locked for subsequent installs." }] };
1836
+ return { content: [{ type: "text", text: "No `.library.json` manifest found. Proceed by calling `find-icon-library` to pick a library, then `install-icons` to scaffold icons. The library you pick will be locked for subsequent installs." }] };
1750
1837
  }
1751
1838
  try {
1752
1839
  const parsed = JSON.parse(manifestContent);
@@ -1764,7 +1851,7 @@ Returns either:
1764
1851
  } catch (err) {
1765
1852
  return { content: [{ type: "text", text: `Failed to parse .library.json: ${err instanceof Error ? err.message : "unknown error"}
1766
1853
 
1767
- Proceed by calling \`resolve-icon-library\` to pick a library.` }] };
1854
+ Proceed by calling \`find-icon-library\` to pick a library.` }] };
1768
1855
  }
1769
1856
  })
1770
1857
  );
@@ -2094,29 +2181,38 @@ function componentRadiusCss(files) {
2094
2181
  if (card) rules.push(`.bd-card, .ds-card { border-radius: ${card}; }`);
2095
2182
  return rules.join("\n");
2096
2183
  }
2184
+ function decodeArbitrary(raw) {
2185
+ const value = raw.replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
2186
+ return value && !/[<>{};]/.test(value) ? value : null;
2187
+ }
2188
+ function restingValue(scope, cssProp, utilPrefixes) {
2189
+ for (const prefix of utilPrefixes) {
2190
+ const m = scope.match(new RegExp(`(?<![\\w:.\\-])${prefix}-\\[([^\\]]+)\\]`));
2191
+ if (m) {
2192
+ const v = decodeArbitrary(m[1]);
2193
+ if (v) return v;
2194
+ }
2195
+ }
2196
+ const pm = scope.match(new RegExp(`(?<!:)\\[${cssProp}:([^\\]]+)\\]`));
2197
+ return pm ? decodeArbitrary(pm[1]) : null;
2198
+ }
2199
+ function variantBody(content, name) {
2200
+ const vi = content.search(/variant\s*:\s*\{/);
2201
+ const hay = vi !== -1 ? content.slice(vi) : content;
2202
+ const m = hay.match(
2203
+ new RegExp(`[\\s{,]${name}\\s*:\\s*([\\s\\S]*?)(?:\\n\\s*[A-Za-z_][\\w-]*\\s*:|\\n\\s*\\},)`)
2204
+ );
2205
+ return m ? m[1] : null;
2206
+ }
2097
2207
  function componentDepthCss(files) {
2098
2208
  const btn = files.find((f) => f.path && /(^|\/)button\.(tsx|jsx)$/.test(f.path));
2099
2209
  if (!btn?.content) return "";
2100
2210
  const content = btn.content;
2101
- let scope = content;
2102
- const variantIdx = content.search(/variant\s*:\s*\{/);
2103
- if (variantIdx !== -1) {
2104
- const after = content.slice(variantIdx);
2105
- const dm = after.match(
2106
- /default\s*:\s*([\s\S]*?)(?:\n\s*(?:secondary|destructive|outline|ghost|link|subtle|success|warning|info|danger)\s*:|\n\s*\},)/
2107
- );
2108
- if (dm) scope = dm[1];
2109
- }
2110
- const arbitrary = (prefix) => {
2111
- const m = scope.match(new RegExp(`${prefix}-\\[([^\\]]+)\\]`));
2112
- if (!m) return null;
2113
- const value = m[1].replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
2114
- return value && !/[<>{};]/.test(value) ? value : null;
2115
- };
2116
- const rules = [];
2117
- const shadow = /(?:^|\s|"|')shadow-none(?=\s|"|'|$)/.test(scope) ? "none" : arbitrary("shadow");
2118
- const background = arbitrary("bg");
2211
+ const scope = variantBody(content, "primary") ?? variantBody(content, "default") ?? content;
2212
+ const shadow = /(?:^|\s|"|')shadow-none(?=\s|"|'|$)/.test(scope) ? "none" : restingValue(scope, "box-shadow", ["shadow"]);
2213
+ const background = restingValue(scope, "background", ["bg"]) ?? restingValue(scope, "background-image", []);
2119
2214
  const sel = ".bd-btn-primary, .bd-view, .ds-btn-primary";
2215
+ const rules = [];
2120
2216
  if (shadow) rules.push(`${sel} { box-shadow: ${shadow}; }`);
2121
2217
  if (background) rules.push(`${sel} { background: ${background}; }`);
2122
2218
  return rules.join("\n");
@@ -2138,14 +2234,14 @@ function buildDsComponentCss(files) {
2138
2234
  const card = contentOf(/(^|\/)card\.(tsx|jsx)$/);
2139
2235
  if (card) {
2140
2236
  covered.add("card");
2141
- const shadow = /shadow-none(?=[\s"'])/.test(card) ? "none" : arbitrary(card, /shadow-\[([^\]]+)\]/);
2237
+ const shadow = /shadow-none(?=[\s"'])/.test(card) ? "none" : restingValue(card, "box-shadow", ["shadow"]);
2142
2238
  if (shadow) rules.push(`.ds-card { box-shadow: ${shadow}; }`);
2143
2239
  }
2144
2240
  const sw = contentOf(/(^|\/)switch\.(tsx|jsx)$/);
2145
2241
  if (sw) {
2146
2242
  covered.add("switch");
2147
- const bg = arbitrary(sw, /data-\[state=checked\]:bg-\[([^\]]+)\]/);
2148
- const shadow = arbitrary(sw, /data-\[state=checked\]:shadow-\[([^\]]+)\]/);
2243
+ const bg = arbitrary(sw, /data-\[state=checked\]:bg-\[([^\]]+)\]/) ?? arbitrary(sw, /data-\[state=checked\]:\[background(?:-image)?:([^\]]+)\]/);
2244
+ const shadow = arbitrary(sw, /data-\[state=checked\]:shadow-\[([^\]]+)\]/) ?? arbitrary(sw, /data-\[state=checked\]:\[box-shadow:([^\]]+)\]/);
2149
2245
  if (bg) rules.push(`.ds-switch-track.on { background: ${bg}; }`);
2150
2246
  if (shadow) rules.push(`.ds-switch-track.on { box-shadow: ${shadow}; }`);
2151
2247
  }
@@ -2278,7 +2374,11 @@ var init_preview_html = __esm({
2278
2374
  preview use the system's font everywhere; headings opt into --font-serif. [BD-120] */
2279
2375
  html, body { margin: 0; padding: 0; background: transparent; font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); color: var(--foreground, oklch(0.2 0 0)); }
2280
2376
  ${PREVIEW_BASE_CSS}
2281
- .bd-card-shell { background: var(--background, oklch(1 0 0)); border-radius: 16px; overflow: hidden; box-shadow: 0 1px 3px oklch(0 0 0 / 0.08), 0 8px 24px oklch(0 0 0 / 0.06); }
2377
+ /* Square + flush: no radius, no shadow. The host (claude.ai bubble / ChatGPT
2378
+ widget frame) already provides the card chrome, so an inner rounded+shadowed
2379
+ shell read as an awkward card-in-a-card and clipped the screenshot corners.
2380
+ overflow:hidden stays to contain children. [BD-136] */
2381
+ .bd-card-shell { background: var(--background, oklch(1 0 0)); overflow: hidden; }
2282
2382
  .bd-app { padding: 16px; }
2283
2383
  .bd-cta-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; background: var(--card, var(--background, oklch(1 0 0))); border-top: 1px solid var(--border, oklch(0.92 0 0)); }
2284
2384
  .bd-cta-name { font-weight: 600; font-size: 14px; color: var(--foreground, oklch(0.2 0 0)); font-family: var(--font-serif, var(--font-sans, ui-sans-serif)); }
@@ -2300,21 +2400,11 @@ ${PREVIEW_BASE_CSS}
2300
2400
  /* .bd-row carries margin:12px 0 from the base CSS; reset it inside the sampler so the
2301
2401
  8px column gap is the only spacing (else label<->row reads ~20px, too loose). */
2302
2402
  .bd-sampler .bd-row { margin: 0; }
2303
- /* Generating state: a clearly-loading skeleton, NOT a token-rendered mock \u2014 the
2304
- DS tokens aren't ready yet, so a full vignette would render in flat fallback
2305
- colors and read as a broken finished system. Theme-neutral on purpose. */
2403
+ /* Error state: a static, theme-neutral note (errorBody). No DS tokens are
2404
+ loaded, so it falls back to a light ramp on the white fallback card. */
2306
2405
  .bd-skel-app { padding: 16px; display: flex; flex-direction: column; gap: 12px; font-family: ui-sans-serif, system-ui, sans-serif; }
2307
2406
  .bd-skel-name { font-weight: 600; font-size: 14px; color: var(--muted-foreground, oklch(0.45 0 0)); }
2308
2407
  .bd-skel-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
2309
- .bd-skel-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
2310
- /* Skeleton color tracks the (host or DS) surface tokens, NOT prefers-color-
2311
- scheme: while generating tokensCss is empty so these fall back to a light
2312
- ramp on the white fallback card; if the host applies a dark theme the bars
2313
- follow --muted/--border instead of fighting it. */
2314
- .bd-skel { background: var(--muted, oklch(0.92 0 0)); border-radius: 8px; position: relative; overflow: hidden; }
2315
- .bd-skel::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, oklch(1 0 0 / 0), oklch(1 0 0 / 0.28), oklch(1 0 0 / 0)); animation: bd-shimmer 1.4s ease-in-out infinite; }
2316
- @keyframes bd-shimmer { 100% { transform: translateX(100%); } }
2317
- @media (prefers-reduced-motion: reduce) { .bd-skel::after { animation: none; } }
2318
2408
  .bd-skel-card { border: 1px solid var(--border, oklch(0.9 0 0)); border-radius: 14px; padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; }
2319
2409
  /* No DS font is loaded while generating, so keep the footer name neutral sans
2320
2410
  too \u2014 otherwise a host-injected serif that doesn't load in the sandbox falls
@@ -2511,31 +2601,6 @@ ${DS_KIT_CSS}
2511
2601
  return '<div class="bd-show ds' + (full ? " bd-fs" : "") + '">' + wrapped + '</div>';
2512
2602
  }
2513
2603
 
2514
- // Loading skeleton shown while the system is still generating: its tokens
2515
- // aren't ready, so a token-rendered vignette would paint in flat fallback
2516
- // colors and look like a finished-but-broken DS. Theme-neutral shimmer instead.
2517
- function skeletonBody(out) {
2518
- var nm = esc(out.name || "Design system");
2519
- return '<div class="bd-skel-app">' +
2520
- '<div class="bd-skel-row"><span class="bd-skel-name">' + nm + '</span><span class="bd-skel-name" style="font-weight:400;opacity:0.7">Generating' + (out.step ? ": " + esc(out.step) : "...") + '</span></div>' +
2521
- '<div class="bd-skel-card">' +
2522
- '<div class="bd-skel" style="height:18px;width:60%"></div>' +
2523
- '<div class="bd-skel" style="height:11px;width:90%"></div>' +
2524
- '<div class="bd-skel" style="height:11px;width:75%"></div>' +
2525
- '<div class="bd-skel-row" style="margin-top:6px;justify-content:flex-start;gap:10px">' +
2526
- '<div class="bd-skel" style="height:34px;width:104px"></div>' +
2527
- '<div class="bd-skel" style="height:34px;width:104px"></div>' +
2528
- '</div>' +
2529
- '</div>' +
2530
- '<div class="bd-skel-grid">' +
2531
- '<div class="bd-skel" style="height:62px"></div>' +
2532
- '<div class="bd-skel" style="height:62px"></div>' +
2533
- '<div class="bd-skel" style="height:62px"></div>' +
2534
- '</div>' +
2535
- '<div class="bd-skel" style="height:44px"></div>' +
2536
- '</div>';
2537
- }
2538
-
2539
2604
  // Generation failed: a static (non-shimmer) note. A shimmering "loading"
2540
2605
  // skeleton would falsely read as still-in-progress, and the token vignette
2541
2606
  // can't render (no tokens). The CTA below links to the project page to retry.
@@ -2656,16 +2721,20 @@ ${DS_KIT_CSS}
2656
2721
  // our own: both hosts render a native close button in fullscreen (ChatGPT's
2657
2722
  // top bar, claude.ai's native header), so our own Back bar was redundant and
2658
2723
  // visually broken (misaligned, disliked chevron). [BD-136]
2659
- function iframeBody(out, view, full) {
2724
+ // embed = the PUBLIC, cookieless URL we iframe (the sandbox carries no
2725
+ // session cookie, so an owner-scoped page would 404 inside it). open = the
2726
+ // owner-facing page for the human "Open" link (they have the cookie). [BD-164]
2727
+ function iframeBody(out, embed, open, full) {
2660
2728
  var nm = esc(out.name || "Design system");
2661
- var frame = '<iframe class="bd-frame" src="' + esc(embedUrl(view)) + '" title="' + nm + ' showcase" referrerpolicy="no-referrer-when-downgrade"></iframe>';
2729
+ var frame = '<iframe class="bd-frame" src="' + esc(embedUrl(embed)) + '" title="' + nm + ' showcase" referrerpolicy="no-referrer-when-downgrade"></iframe>';
2662
2730
  if (full) {
2663
2731
  return '<div class="bd-frame-wrap bd-fs">' + frame + '</div>';
2664
2732
  }
2733
+ var openHref = open || embed;
2665
2734
  return '<div class="bd-frame-wrap">' + frame +
2666
2735
  '<div class="bd-frame-foot"><span class="bd-cta-name">' + nm + '</span><span class="bd-cta-actions">' +
2667
2736
  (canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
2668
- '<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a>' +
2737
+ '<a class="bd-cta" data-bd-open href="' + esc(openHref) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a>' +
2669
2738
  '</span></div>' +
2670
2739
  '</div>';
2671
2740
  }
@@ -2707,6 +2776,12 @@ ${DS_KIT_CSS}
2707
2776
  var img = root.querySelector("[data-bd-shot]");
2708
2777
  if (!img) return;
2709
2778
  img.addEventListener("error", function () { shotFailed = true; if (lastOut) render(lastOut); });
2779
+ // The screenshot loads async; its real height only exists after it decodes.
2780
+ // claude.ai reads the widget's documentElement height from the DOM, so re-pin
2781
+ // the iframe height once the image has dimensions (a cached image may already
2782
+ // be complete before this runs). [BD-136]
2783
+ img.addEventListener("load", scheduleResize);
2784
+ if (img.complete && img.naturalHeight) scheduleResize();
2710
2785
  }
2711
2786
 
2712
2787
  function render(out) {
@@ -2716,6 +2791,10 @@ ${DS_KIT_CSS}
2716
2791
  syncHostFonts();
2717
2792
  // Only an http(s) URL is usable; reject any other scheme up front.
2718
2793
  var view = safeUrl(out.viewUrl);
2794
+ // The iframe must load a PUBLIC, cookieless URL (the sandbox has no session
2795
+ // cookie). previewUrl points at /preview/<fragmentId>; fall back to view if
2796
+ // the host didn't supply one (e.g. catalog DSes, whose view is public). [BD-164]
2797
+ var embed = safeUrl(out.previewUrl) || view;
2719
2798
  var isError = out.status === "error";
2720
2799
  var generating = out.status && out.status !== "completed" && !isError;
2721
2800
  var completed = !isError && !generating;
@@ -2723,10 +2802,18 @@ ${DS_KIT_CSS}
2723
2802
  // render if there's no shotUrl or the image failed to load.
2724
2803
  var useShot = !shotFailed && !!safeUrl(out.shotUrl);
2725
2804
 
2805
+ // ChatGPT's fullscreen "Open in <app>" button defaults to the iframe's
2806
+ // current path, which is now the PUBLIC /preview page. Repoint it at the
2807
+ // owner's editable /projects/<id> (they have the session cookie on the web,
2808
+ // so it loads for them) so fullscreen Open still reaches their project. [BD-164]
2809
+ if (completed && view && window.openai && window.openai.setOpenInAppUrl) {
2810
+ try { window.openai.setOpenInAppUrl({ href: view }); } catch (e) {}
2811
+ }
2812
+
2726
2813
  // Fullscreen: ChatGPT iframes the real page; claude.ai shows the full
2727
2814
  // screenshot (scrollable) or the token showcase. [BD-136]
2728
2815
  if (displayMode === "fullscreen" && completed) {
2729
- if (canIframe() && view) { root.innerHTML = iframeBody(out, view, true); return; }
2816
+ if (canIframe() && embed) { root.innerHTML = iframeBody(out, embed, view, true); return; }
2730
2817
  if (useShot) { root.innerHTML = '<div class="bd-card-shell bd-fs">' + shotBody(out, true) + footHtml(out) + '</div>'; wireShotFallback(); }
2731
2818
  else { root.innerHTML = showcaseBody(out, true); wireShowcase(); }
2732
2819
  return;
@@ -2735,8 +2822,8 @@ ${DS_KIT_CSS}
2735
2822
  // Inline completed: ChatGPT = real-page iframe; claude.ai = real screenshot
2736
2823
  // (capped) or token showcase, + a footer (Fullscreen + Open). [BD-136]
2737
2824
  if (completed) {
2738
- if (canIframe() && view) {
2739
- root.innerHTML = '<div class="bd-card-shell">' + iframeBody(out, view, false) + '</div>';
2825
+ if (canIframe() && embed) {
2826
+ root.innerHTML = '<div class="bd-card-shell">' + iframeBody(out, embed, view, false) + '</div>';
2740
2827
  return;
2741
2828
  }
2742
2829
  var body = useShot ? shotBody(out, false) : showcaseBody(out, false);
@@ -2745,8 +2832,11 @@ ${DS_KIT_CSS}
2745
2832
  return;
2746
2833
  }
2747
2834
 
2748
- // Generating / error / no-url: skeleton or static note + optional open link.
2749
- var hero = isError ? errorBody(out) : generating ? skeletonBody(out) : errorBody(out);
2835
+ // Generating: render nothing \u2014 no fake skeleton. The widget stays empty
2836
+ // until the system is completed (or errors). [BD-164]
2837
+ if (generating) { root.innerHTML = ""; return; }
2838
+ // Error / no-url: static note + optional open link.
2839
+ var hero = errorBody(out);
2750
2840
  var cta = view
2751
2841
  ? '<div class="bd-cta-row"><span class="bd-cta-name">' + esc(out.name || "Design system") + '</span>' +
2752
2842
  '<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a></div>'
@@ -2754,9 +2844,56 @@ ${DS_KIT_CSS}
2754
2844
  root.innerHTML = '<div class="bd-card-shell">' + hero + cta + "</div>";
2755
2845
  }
2756
2846
 
2757
- // Auto-resize is handled by the MCP Apps client (setupSizeChangedNotifications:
2758
- // ResizeObserver on documentElement + body -> ui/notifications/size-changed),
2759
- // mirroring ext-apps' App. The widget doesn't manage sizing itself. [BD-150]
2847
+ // \u2500\u2500 Sizing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2848
+ // The MCP Apps client emits ui/notifications/size-changed (ResizeObserver on
2849
+ // documentElement + body); ChatGPT honors that and auto-sizes its widget.
2850
+ // claude.ai web does NOT honor size-changed \u2014 it reads the widget's
2851
+ // documentElement height straight from the DOM and pins the iframe to whatever
2852
+ // inline height the document carries. With no explicit height the iframe stays
2853
+ // at 0 (verified live: a single size-changed{0,0} then the iframe sits at 0px;
2854
+ // claude-ai-mcp#69). So on the DOM-reading hosts we set
2855
+ // document.documentElement.style.height ourselves, AFTER content + the async
2856
+ // screenshot have real height. [BD-136]
2857
+ var lastAppliedH = 0;
2858
+ function measureCardHeight() {
2859
+ var card = root.firstElementChild; // .bd-card-shell / fullscreen shell
2860
+ return Math.ceil(Math.max(
2861
+ (card && card.scrollHeight) || 0,
2862
+ root.scrollHeight || 0,
2863
+ document.body ? document.body.scrollHeight : 0,
2864
+ ));
2865
+ }
2866
+ function applyClaudeHeight() {
2867
+ // ChatGPT (window.openai) auto-sizes via its own bridge \u2014 never fight it.
2868
+ if (window.openai) return;
2869
+ var de = document.documentElement;
2870
+ // Fullscreen content is position:fixed (out of flow), so its scrollHeight is
2871
+ // meaningless \u2014 clear the pin and let the host own the viewport.
2872
+ if (displayMode === "fullscreen") { de.style.height = ""; lastAppliedH = 0; return; }
2873
+ var h = measureCardHeight();
2874
+ if (h > 0 && Math.abs(h - lastAppliedH) > 1) {
2875
+ lastAppliedH = h;
2876
+ de.style.height = h + "px";
2877
+ }
2878
+ }
2879
+ // Measure across two frames (layout settle + image decode) so we never pin a
2880
+ // pre-render / pre-image height that claude.ai would latch onto.
2881
+ function scheduleResize() {
2882
+ if (typeof requestAnimationFrame !== "function") { applyClaudeHeight(); return; }
2883
+ requestAnimationFrame(function () { applyClaudeHeight(); requestAnimationFrame(applyClaudeHeight); });
2884
+ }
2885
+ // Re-pin on every render swap (#bd-root mutations) and on any late layout growth
2886
+ // (fonts, reflow). The screenshot's own load event is wired in wireShotFallback.
2887
+ try {
2888
+ if (typeof MutationObserver === "function") {
2889
+ new MutationObserver(scheduleResize).observe(root, { childList: true, subtree: true });
2890
+ }
2891
+ } catch (e) {}
2892
+ try {
2893
+ if (typeof ResizeObserver === "function") {
2894
+ new ResizeObserver(scheduleResize).observe(root);
2895
+ }
2896
+ } catch (e) {}
2760
2897
 
2761
2898
  // The widget runs in a sandboxed iframe (claude.ai); a bare <a target=_blank>
2762
2899
  // cannot open a tab or navigate the top frame, so the CTA looks dead. Route the
@@ -2917,7 +3054,7 @@ function registerDesignSystemTools({
2917
3054
  const searchInputSchema = z4.object({
2918
3055
  query: z4.string().describe("Natural language query describing the type of UI you're building.")
2919
3056
  });
2920
- const searchDescription = (deprecated) => `${deprecated ? "DEPRECATED alias of find-design \u2014 prefer find-design. " : ""}Search the Better Design catalog for an EXISTING design system via semantic search, and return its ID.
3057
+ const searchDescription = () => `Search the Better Design catalog for an EXISTING design system via semantic search, and return its ID.
2921
3058
 
2922
3059
  For building an app, generate the user's OWN brand with create-design-system instead. Use this only to browse or inspect catalog systems (the user names one, or wants to preview before installing).
2923
3060
 
@@ -2988,24 +3125,14 @@ ${formatted}`
2988
3125
  }
2989
3126
  };
2990
3127
  server.registerTool(
2991
- "find-design",
3128
+ "find-design-system",
2992
3129
  {
2993
3130
  title: "Find Design System",
2994
- description: searchDescription(false),
2995
- inputSchema: searchInputSchema,
2996
- annotations: { readOnlyHint: true }
2997
- },
2998
- makeSearchHandler("find-design")
2999
- );
3000
- server.registerTool(
3001
- "resolve-design-system",
3002
- {
3003
- title: "Resolve Design System (deprecated alias of find-design)",
3004
- description: searchDescription(true),
3131
+ description: searchDescription(),
3005
3132
  inputSchema: searchInputSchema,
3006
3133
  annotations: { readOnlyHint: true }
3007
3134
  },
3008
- makeSearchHandler("resolve-design-system")
3135
+ makeSearchHandler("find-design-system")
3009
3136
  );
3010
3137
  server.registerTool(
3011
3138
  "get-design-system-docs",
@@ -3013,7 +3140,7 @@ ${formatted}`
3013
3140
  title: "Get Design System Documentation",
3014
3141
  description: `Scaffolds a design system into the user's project.
3015
3142
 
3016
- You must call 'find-design' first to obtain the design system ID.
3143
+ You must call 'find-design-system' first to obtain the design system ID.
3017
3144
 
3018
3145
  **Default behavior (recommended): install the full design system.**
3019
3146
  Call with only a designSystemId and this tool returns ONE shadcn command that
@@ -3025,7 +3152,7 @@ Pass 'component' only when the user explicitly asks to inspect or customise a
3025
3152
  specific component's source code. In that mode the tool returns the requested
3026
3153
  components, a scoped install command, and their source code.`,
3027
3154
  inputSchema: z4.object({
3028
- designSystemId: z4.string().describe("The design system ID from find-design"),
3155
+ designSystemId: z4.string().describe("The design system ID from find-design-system"),
3029
3156
  component: z4.union([z4.string(), z4.array(z4.string())]).optional().describe('Optional. Only pass this when the user asks to inspect specific component source code. Examples: "button", ["button", "card"]. Omit to scaffold the entire design system.')
3030
3157
  }),
3031
3158
  outputSchema: PREVIEW_OUTPUT_SCHEMA,
@@ -3167,17 +3294,17 @@ components, a scoped install command, and their source code.`,
3167
3294
  title: "Create a New Design System",
3168
3295
  description: `Generate a brand new design system tailored to the user's product, and return a single shadcn install command that lands the entire set of components + tokens (globals.css) into the user's Next.js project.
3169
3296
 
3170
- **Use this PROACTIVELY on any "build/make me <app>" request \u2014 even when the user never mentions design or branding.** A design system IS the app's brand/visual identity, so creating one first is the right first step for building an app: derive the \`prompt\` from the app (its name, domain, and vibe), generate the brand, show it with \`check-design-system-status\`, then build the app with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via 'get-design-system-docs'.
3297
+ **Use this PROACTIVELY on any "build/make me <app>" request \u2014 even when the user never mentions design or branding.** A design system IS the app's brand/visual identity, so creating one first is the right first step for building an app: derive the \`prompt\` from the app (its name, domain, and vibe), generate the brand, show it with \`get-design-system-status\`, then build the app with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via 'get-design-system-docs'.
3171
3298
 
3172
3299
  How it works:
3173
3300
  1. Sends the user's prompt to Better Design's generation pipeline (same flow the web chat input uses) and returns IMMEDIATELY with the project id + project URL.
3174
3301
  2. Generation runs in the background and typically takes 1-3 minutes.
3175
- 3. Call \`check-design-system-status\` with the returned project id (wait ~60 seconds first) to get the live preview URL and the final \`npx shadcn add <registry-url>\` install command once it completes.
3302
+ 3. Call \`get-design-system-status\` with the returned project id (wait ~60 seconds first) to get the live preview URL and the final \`npx shadcn add <registry-url>\` install command once it completes.
3176
3303
 
3177
3304
  Share the project URL with the user right away so they can watch the generation live.`,
3178
3305
  inputSchema: z4.object({
3179
3306
  prompt: z4.string().min(1).max(1e4).describe("Describe the design system the user wants. E.g. 'A warm, minimal design system for a personal finance app, soft pastels, rounded corners' or 'A brutalist developer tool aesthetic with sharp edges and monospace type'."),
3180
- baseDesignSystemId: z4.string().optional().describe("Optional. Pass a design system ID from `find-design` to bias generation toward an existing aesthetic (e.g. apply a theme tweak on top of Linear). Leave empty for a fully custom DS.")
3307
+ baseDesignSystemId: z4.string().optional().describe("Optional. Pass a design system ID from `find-design-system` to bias generation toward an existing aesthetic (e.g. apply a theme tweak on top of Linear). Leave empty for a fully custom DS.")
3181
3308
  }),
3182
3309
  annotations: {
3183
3310
  readOnlyHint: false,
@@ -3223,126 +3350,128 @@ Share the project URL with the user right away so they can watch the generation
3223
3350
  ...authContext ? [`Saved to your Better Design account. Watch it generate live:`] : [`Watch it generate live:`],
3224
3351
  projectUrl,
3225
3352
  "",
3226
- `Generation takes 1-3 minutes. Share the project link with the user now, wait ~60 seconds, then call \`check-design-system-status\` with the project id to get the live preview URL and the final install command.`,
3353
+ `Generation takes 1-3 minutes. Share the project link with the user now, wait ~60 seconds, then call \`get-design-system-status\` with the project id to get the live preview URL and the final install command.`,
3227
3354
  inlineBuildMd(projectId)
3228
3355
  ];
3229
3356
  return { content: [{ type: "text", text: lines.join("\n") }] };
3230
3357
  })
3231
3358
  );
3232
- server.registerTool(
3233
- "check-design-system-status",
3234
- {
3235
- title: "Check Design System Generation Status",
3236
- description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready.`,
3237
- inputSchema: z4.object({
3238
- projectId: z4.string().min(1).describe("The project id returned by create-design-system.")
3239
- }),
3240
- outputSchema: PREVIEW_OUTPUT_SCHEMA,
3241
- annotations: {
3242
- readOnlyHint: true,
3243
- destructiveHint: false,
3244
- openWorldHint: false
3245
- },
3246
- _meta: {
3247
- ui: { resourceUri: DS_PREVIEW_WIDGET_URI },
3248
- "ui/resourceUri": DS_PREVIEW_WIDGET_URI,
3249
- "openai/outputTemplate": DS_PREVIEW_WIDGET_URI,
3250
- "openai/toolInvocation/invoking": "Checking your design system...",
3251
- "openai/toolInvocation/invoked": "Design system status ready"
3252
- }
3253
- },
3254
- async ({ projectId }) => tracked("check-design-system-status", {}, async () => {
3255
- const headers = {};
3256
- if (authContext) {
3257
- const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
3258
- headers.Authorization = `Bearer ${bearer2}`;
3259
- }
3260
- const statusUrl = `${APP_BASE_URL}/api/projects/${projectId}`;
3261
- let project;
3262
- try {
3263
- const res = await fetch(statusUrl, { headers });
3264
- if (!res.ok) {
3265
- return { content: [{ type: "text", text: `Could not read project ${projectId} (HTTP ${res.status}). Check the id, or view it at ${APP_BASE_URL}/projects/${projectId}.` }] };
3266
- }
3267
- project = await res.json();
3268
- } catch (err) {
3269
- return { content: [{ type: "text", text: `Network error calling ${statusUrl}: ${err instanceof Error ? err.message : String(err)}` }] };
3359
+ const runDesignSystemStatus = async (projectId) => {
3360
+ const headers = {};
3361
+ if (authContext) {
3362
+ const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
3363
+ headers.Authorization = `Bearer ${bearer2}`;
3364
+ }
3365
+ const statusUrl = `${APP_BASE_URL}/api/projects/${projectId}`;
3366
+ let project;
3367
+ try {
3368
+ const res = await fetch(statusUrl, { headers });
3369
+ if (!res.ok) {
3370
+ return { content: [{ type: "text", text: `Could not read project ${projectId} (HTTP ${res.status}). Check the id, or view it at ${APP_BASE_URL}/projects/${projectId}.` }] };
3270
3371
  }
3271
- const projectUrl = `${APP_BASE_URL}/projects/${projectId}`;
3272
- if (project.designSystemStatus === "COMPLETED") {
3273
- const registryUrl = `${APP_BASE_URL}/api/projects/${projectId}/registry/all.json`;
3274
- let body = `\`\`\`bash
3372
+ project = await res.json();
3373
+ } catch (err) {
3374
+ return { content: [{ type: "text", text: `Network error calling ${statusUrl}: ${err instanceof Error ? err.message : String(err)}` }] };
3375
+ }
3376
+ const projectUrl = `${APP_BASE_URL}/projects/${projectId}`;
3377
+ if (project.designSystemStatus === "COMPLETED") {
3378
+ const registryUrl = `${APP_BASE_URL}/api/projects/${projectId}/registry/all.json`;
3379
+ let body = `\`\`\`bash
3275
3380
  npx shadcn@latest add ${registryUrl}
3276
3381
  \`\`\``;
3277
- let tokensCss = "";
3278
- let dsName = `Project ${projectId.slice(0, 8)}`;
3279
- try {
3280
- const res = await fetch(registryUrl);
3281
- if (res.ok) {
3282
- const json = await res.json();
3283
- if (json.meta?.installInstructions) body = json.meta.installInstructions;
3284
- tokensCss = tokensCssFromRegistry(json);
3285
- if (project.name) dsName = project.name;
3286
- else if (json.name) dsName = json.name;
3287
- }
3288
- } catch {
3382
+ let tokensCss = "";
3383
+ let dsName = `Project ${projectId.slice(0, 8)}`;
3384
+ try {
3385
+ const res = await fetch(registryUrl);
3386
+ if (res.ok) {
3387
+ const json = await res.json();
3388
+ if (json.meta?.installInstructions) body = json.meta.installInstructions;
3389
+ tokensCss = tokensCssFromRegistry(json);
3390
+ if (project.name) dsName = project.name;
3391
+ else if (json.name) dsName = json.name;
3289
3392
  }
3290
- const lines = [
3291
- `# Design system ready`,
3292
- "",
3293
- ...authContext ? [`Saved to your Better Design account. View, preview, and re-grab the install command anytime here:`] : [`View and preview your design system anytime here:`],
3294
- projectUrl,
3295
- "",
3296
- `Project id: \`${projectId}\``,
3297
- "",
3298
- `To install it into a local project (Cursor, Claude Code, a checked-out repo):`,
3299
- "",
3300
- REQUIREMENTS_MD,
3301
- body,
3302
- ANTI_MOCKUP_MD,
3303
- inlineBuildMd(projectId),
3304
- "",
3305
- `In a real codebase, run the install above; do NOT hand-build an HTML/React copy of the components. The preview widget already shows it, and ${projectUrl} has the full showcase.`
3306
- ];
3307
- return {
3308
- content: [{ type: "text", text: lines.join("\n") }],
3309
- structuredContent: {
3310
- status: "completed",
3311
- name: dsName,
3312
- viewUrl: projectUrl,
3313
- installCommand: `npx shadcn@latest add ${registryUrl}`,
3314
- tokensCss
3315
- }
3316
- };
3317
- }
3318
- if (project.designSystemStatus === "ERROR") {
3319
- return {
3320
- content: [{ type: "text", text: `Generation failed for project ${projectId}. Last step: ${project.currentStep ?? "(none)"}. The user can retry from ${projectUrl}.` }],
3321
- structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: projectUrl, tokensCss: "" }
3322
- };
3393
+ } catch {
3323
3394
  }
3324
- const step = project.currentStep ? project.currentStep.replace(/^[a-z]+:/, "") : void 0;
3325
- const stepNote = step ? ` Current step: ${step}.` : "";
3395
+ const lines = [
3396
+ `# Design system ready`,
3397
+ "",
3398
+ ...authContext ? [`Saved to your Better Design account. View, preview, and re-grab the install command anytime here:`] : [`View and preview your design system anytime here:`],
3399
+ projectUrl,
3400
+ "",
3401
+ `Project id: \`${projectId}\``,
3402
+ "",
3403
+ `To install it into a local project (Cursor, Claude Code, a checked-out repo):`,
3404
+ "",
3405
+ REQUIREMENTS_MD,
3406
+ body,
3407
+ ANTI_MOCKUP_MD,
3408
+ inlineBuildMd(projectId),
3409
+ "",
3410
+ `In a real codebase, run the install above; do NOT hand-build an HTML/React copy of the components. The preview widget already shows it, and ${projectUrl} has the full showcase.`
3411
+ ];
3412
+ const previewUrl = project.previewFragmentId ? `${APP_BASE_URL}/preview/${project.previewFragmentId}` : projectUrl;
3413
+ const shotUrl = project.previewFragmentId ? `${APP_BASE_URL}/api/og/preview?fragment=${project.previewFragmentId}` : void 0;
3326
3414
  return {
3327
- content: [{ type: "text", text: `Still generating (status: ${project.designSystemStatus ?? "PENDING"}).${stepNote}
3328
- Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }],
3415
+ content: [{ type: "text", text: lines.join("\n") }],
3329
3416
  structuredContent: {
3330
- status: (project.designSystemStatus ?? "PENDING").toLowerCase(),
3331
- name: `Project ${projectId.slice(0, 8)}`,
3417
+ status: "completed",
3418
+ name: dsName,
3332
3419
  viewUrl: projectUrl,
3333
- step,
3334
- tokensCss: ""
3420
+ previewUrl,
3421
+ ...shotUrl ? { shotUrl } : {},
3422
+ installCommand: `npx shadcn@latest add ${registryUrl}`,
3423
+ tokensCss
3335
3424
  }
3336
3425
  };
3337
- })
3426
+ }
3427
+ if (project.designSystemStatus === "ERROR") {
3428
+ return {
3429
+ content: [{ type: "text", text: `Generation failed for project ${projectId}. Last step: ${project.currentStep ?? "(none)"}. The user can retry from ${projectUrl}.` }],
3430
+ structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: projectUrl, tokensCss: "" }
3431
+ };
3432
+ }
3433
+ const step = project.currentStep ? project.currentStep.replace(/^[a-z]+:/, "") : void 0;
3434
+ const stepNote = step ? ` Current step: ${step}.` : "";
3435
+ return {
3436
+ content: [{ type: "text", text: `Still generating (status: ${project.designSystemStatus ?? "PENDING"}).${stepNote}
3437
+ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }],
3438
+ structuredContent: {
3439
+ status: (project.designSystemStatus ?? "PENDING").toLowerCase(),
3440
+ name: `Project ${projectId.slice(0, 8)}`,
3441
+ viewUrl: projectUrl,
3442
+ step,
3443
+ tokensCss: ""
3444
+ }
3445
+ };
3446
+ };
3447
+ const statusConfig = {
3448
+ title: "Get Design System Generation Status",
3449
+ description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready.`,
3450
+ inputSchema: z4.object({
3451
+ projectId: z4.string().min(1).describe("The project id returned by create-design-system.")
3452
+ }),
3453
+ outputSchema: PREVIEW_OUTPUT_SCHEMA,
3454
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
3455
+ _meta: {
3456
+ ui: { resourceUri: DS_PREVIEW_WIDGET_URI },
3457
+ "ui/resourceUri": DS_PREVIEW_WIDGET_URI,
3458
+ "openai/outputTemplate": DS_PREVIEW_WIDGET_URI,
3459
+ "openai/toolInvocation/invoking": "Checking your design system...",
3460
+ "openai/toolInvocation/invoked": "Design system status ready"
3461
+ }
3462
+ };
3463
+ server.registerTool(
3464
+ "get-design-system-status",
3465
+ statusConfig,
3466
+ ({ projectId }) => tracked("get-design-system-status", {}, () => runDesignSystemStatus(projectId))
3338
3467
  );
3339
3468
  server.registerTool(
3340
3469
  "preview-design-system",
3341
3470
  {
3342
3471
  title: "Preview a Design System",
3343
- description: `Show the user a visual preview of a design system from the catalog (use the id from 'find-design'). Returns a token-true HTML preview: palette, typography, buttons, card, input, badge, plus a link to the full live showcase. Use this when the user wants to SEE a design system before installing it.`,
3472
+ description: `Show the user a visual preview of a design system from the catalog (use the id from 'find-design-system'). Returns a token-true HTML preview: palette, typography, buttons, card, input, badge, plus a link to the full live showcase. Use this when the user wants to SEE a design system before installing it.`,
3344
3473
  inputSchema: z4.object({
3345
- designSystemId: z4.string().min(1).describe("Design system id from find-design, e.g. 'linear', 'editorial-warm'.")
3474
+ designSystemId: z4.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'.")
3346
3475
  }),
3347
3476
  outputSchema: PREVIEW_OUTPUT_SCHEMA,
3348
3477
  annotations: {
@@ -3362,7 +3491,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
3362
3491
  const ds = await provider.getDesignSystemById(designSystemId);
3363
3492
  if (!ds) {
3364
3493
  return {
3365
- content: [{ type: "text", text: `Design system "${designSystemId}" not found. Call find-design first to get a valid id.` }],
3494
+ content: [{ type: "text", text: `Design system "${designSystemId}" not found. Call find-design-system first to get a valid id.` }],
3366
3495
  structuredContent: { status: "error", name: designSystemId, tokensCss: "" }
3367
3496
  };
3368
3497
  }
@@ -3403,12 +3532,12 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
3403
3532
  title: "Get a Design System as HTML/CSS",
3404
3533
  description: `Export a design system as plain, framework-free HTML + CSS for building an app INLINE (ChatGPT canvas / Claude artifact) where npm and a Tailwind build are unavailable. Returns the system's real design tokens (globals.css) plus a token-driven component CSS kit (button, input, card, badge, alert, etc.) and an example to riff on.
3405
3534
 
3406
- Works for BOTH a catalog system (pass \`designSystemId\` from find-design) AND a custom system you just generated (pass \`projectId\` from create-design-system / check-design-system-status).
3535
+ Works for BOTH a catalog system (pass \`designSystemId\` from find-design-system) AND a custom system you just generated (pass \`projectId\` from create-design-system / get-design-system-status).
3407
3536
 
3408
3537
  ${BUILD_TARGET_RULE}`,
3409
3538
  inputSchema: z4.object({
3410
- designSystemId: z4.string().min(1).optional().describe("Catalog design system id from find-design, e.g. 'linear', 'editorial-warm'. Pass this OR projectId."),
3411
- projectId: z4.string().min(1).optional().describe("Project id from create-design-system / check-design-system-status, for a custom generated DS. Pass this OR designSystemId.")
3539
+ designSystemId: z4.string().min(1).optional().describe("Catalog design system id from find-design-system, e.g. 'linear', 'editorial-warm'. Pass this OR projectId."),
3540
+ projectId: z4.string().min(1).optional().describe("Project id from create-design-system / get-design-system-status, for a custom generated DS. Pass this OR designSystemId.")
3412
3541
  }),
3413
3542
  annotations: {
3414
3543
  readOnlyHint: true,
@@ -3431,17 +3560,31 @@ ${BUILD_TARGET_RULE}`,
3431
3560
  }
3432
3561
  if (!designSystemId) {
3433
3562
  return {
3434
- content: [{ type: "text", text: "Pass either a catalog designSystemId (from find-design) or a projectId (from create-design-system)." }]
3563
+ content: [{ type: "text", text: "Pass either a catalog designSystemId (from find-design-system) or a projectId (from create-design-system)." }]
3435
3564
  };
3436
3565
  }
3437
3566
  const ds = await provider.getDesignSystemById(designSystemId);
3438
3567
  if (!ds) {
3439
3568
  return {
3440
- content: [{ type: "text", text: `Design system "${designSystemId}" not found. Call find-design first to get a valid id, or pass a projectId for a generated system.` }]
3569
+ content: [{ type: "text", text: `Design system "${designSystemId}" not found. Call find-design-system first to get a valid id, or pass a projectId for a generated system.` }]
3441
3570
  };
3442
3571
  }
3443
- const { tokensCss, files } = await fetchCatalog(designSystemId);
3444
3572
  const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
3573
+ const host = codingHostName(server);
3574
+ const steer = host ? `> \u26A0\uFE0F **You appear to be in ${host}, a coding environment with a filesystem.** Build this as real React components: call \`get-design-system-docs\` and run the shadcn install instead of shipping a standalone HTML file. The kit below is only for inline chat canvases (ChatGPT / Claude artifacts) that cannot run a build.
3575
+
3576
+ ` : "";
3577
+ if (ds.metadata.html) {
3578
+ const intro = `Framework-free HTML/CSS version of **${ds.metadata.title}**. One self-contained document: every component rendered with the exact compiled CSS inline, no JS, no build. Render it directly in a ChatGPT canvas / Claude artifact, or lift the markup + \`<style>\` you need. Full live showcase: ${viewUrl}
3579
+
3580
+ `;
3581
+ const longestRun = Math.max(0, ...[...ds.metadata.html.matchAll(/`+/g)].map((m) => m[0].length));
3582
+ const fence2 = "`".repeat(Math.max(3, longestRun + 1));
3583
+ return { content: [{ type: "text", text: `${steer}${intro}${fence2}html
3584
+ ${ds.metadata.html}
3585
+ ${fence2}` }] };
3586
+ }
3587
+ const { tokensCss, files } = await fetchCatalog(designSystemId);
3445
3588
  if (!tokensCss) {
3446
3589
  return {
3447
3590
  content: [{ type: "text", text: `Could not load tokens for "${designSystemId}". The full live showcase is at ${viewUrl}.` }]
@@ -3449,10 +3592,6 @@ ${BUILD_TARGET_RULE}`,
3449
3592
  }
3450
3593
  const componentKit = buildDsComponentCss(files);
3451
3594
  const text = buildDsHtmlKit({ name: ds.metadata.title, tokensCss, viewUrl, componentKit });
3452
- const host = codingHostName(server);
3453
- const steer = host ? `> \u26A0\uFE0F **You appear to be in ${host}, a coding environment with a filesystem.** Build this as real React components: call \`get-design-system-docs\` and run the shadcn install instead of shipping a standalone HTML file. The kit below is only for inline chat canvases (ChatGPT / Claude artifacts) that cannot run a build.
3454
-
3455
- ` : "";
3456
3595
  return { content: [{ type: "text", text: steer + text }] };
3457
3596
  })
3458
3597
  );
@@ -3598,9 +3737,12 @@ __export(api_client_exports, {
3598
3737
  import { createHash as createHash2 } from "crypto";
3599
3738
  async function request(path4) {
3600
3739
  if (!API_KEY) {
3601
- throw new ApiError(401, "BETTER_DESIGN_API_KEY is not set. Get one at https://better-design.com/mcp");
3740
+ throw new ApiError(
3741
+ 401,
3742
+ "BETTER_DESIGN_API_KEY is not set. Run `npx better-design` to create a free key (no account needed), or get one at https://better-design.com/mcp"
3743
+ );
3602
3744
  }
3603
- const res = await fetch(`${API_URL}/api${path4}`, {
3745
+ const res = await fetch(`${API_URL2}/api${path4}`, {
3604
3746
  headers: {
3605
3747
  Authorization: `Bearer ${API_KEY}`,
3606
3748
  "Content-Type": "application/json"
@@ -3661,7 +3803,7 @@ async function getReviewRules(category = "all") {
3661
3803
  return request(`/v1/review-rules${params}`);
3662
3804
  }
3663
3805
  function postAlert(body) {
3664
- fetch(`${API_URL}/api/internal/alert`, {
3806
+ fetch(`${API_URL2}/api/internal/alert`, {
3665
3807
  method: "POST",
3666
3808
  headers: { "Content-Type": "application/json" },
3667
3809
  body: JSON.stringify(body),
@@ -3693,11 +3835,11 @@ function trackMcpInstalled(opts) {
3693
3835
  mcpVersion: process.env.npm_package_version
3694
3836
  });
3695
3837
  }
3696
- var API_URL, API_KEY, API_KEY_HASH, ApiError;
3838
+ var API_URL2, API_KEY, API_KEY_HASH, ApiError;
3697
3839
  var init_api_client = __esm({
3698
3840
  "src/lib/api-client.ts"() {
3699
3841
  "use strict";
3700
- API_URL = process.env.API_URL ?? "https://better-design.com";
3842
+ API_URL2 = process.env.API_URL ?? "https://better-design.com";
3701
3843
  API_KEY = process.env.BETTER_DESIGN_API_KEY;
3702
3844
  API_KEY_HASH = API_KEY ? createHash2("sha256").update(API_KEY).digest("hex") : void 0;
3703
3845
  ApiError = class extends Error {
@@ -3784,9 +3926,9 @@ var init_http_data_provider = __esm({
3784
3926
  // src/index.ts
3785
3927
  var argv = process.argv.slice(2);
3786
3928
  var command = argv[0];
3787
- if (command === "setup" || !command) {
3929
+ if (!command || command.startsWith("-") || command === "setup") {
3788
3930
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
3789
- runSetup2(argv.slice(1)).catch((error) => {
3931
+ runSetup2(argv).catch((error) => {
3790
3932
  console.error(error);
3791
3933
  process.exit(1);
3792
3934
  });