better-design 0.34.0 → 0.35.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
@@ -1403,7 +1403,7 @@ Better Design provides design systems, design principles and UI review checks fo
1403
1403
  "find-design-system": "Searches Better Design's catalog of design systems by industry, personality or a description of the interface, and returns ranked matches with a preview link for each. Use it when the user wants a design system or a visual direction for an app, site, page, dashboard or presentation.",
1404
1404
  "get-design-system-kit": "Returns the build kit for a catalog design system or a generated project. `target` picks the format: `react` returns one shadcn install command for every component and the tokens, `html` returns framework-free token and component CSS for inline canvases, `react-native` and `swiftui` return mobile kits, `shopify` returns Liquid theme files, `dtcg` returns a W3C Design Tokens document, and `store-screenshots` returns slide files and a render script for App Store and Google Play screenshots. `component` narrows the React install to one component.",
1405
1405
  "get-inspiration": 'Returns real reference designs from Better Design\'s library as images. `type` picks the collection: today `app-store-screenshots`, the App Store screenshot sets of shipped apps, each with its title and category. `category`, such as "Productivity", narrows the sets, and `limit` sets how many come back, 1 to 6.',
1406
- "create-design-system": "Starts generating a custom design system from a description of the product and its look, and returns the project id and project URL at once. Generation runs in the background and typically takes 1 to 3 minutes. get-design-system-status reports progress and returns the install command when it finishes. `baseDesignSystemId` starts from a catalog system, and `brandCss` builds from an existing brand token sheet, such as the output of extract-design-system. Each call uses one credit from the user's Better Design account. Anyone with the registry or preview URL can view the generated source and previews.",
1406
+ "create-design-system": "Starts generating a custom design system from a description of the product and its look, and returns the project id and project URL at once. Generation runs in the background and typically takes 1 to 3 minutes. get-design-system-status reports progress and returns the install command when it finishes. `baseDesignSystemId` starts from a catalog system the user picked, `userConfirmedCustom` records that the user chose fully custom, and `brandCss` builds from an existing brand token sheet, such as the output of extract-design-system. With none of the three, it generates nothing and returns the top catalog matches to show the user. Each call uses one credit from the user's Better Design account. Anyone with the registry or preview URL can view the generated source and previews.",
1407
1407
  "get-design-system-status": "Reports the progress of a design system started with create-design-system. While generation runs it returns the current stage. When generation finishes it returns the live preview URL and the `npx shadcn add` install command.",
1408
1408
  "preview-design-system": "Returns a visual preview of a catalog design system: its palette, typography, buttons, card, input and badge, with a link to the full live showcase. Takes an id from find-design-system.",
1409
1409
  "get-design-config": "Returns the design config saved on a generated project: exact color tokens, radius, fonts and icon library, including values the user tuned on the project page.",
@@ -13676,7 +13676,7 @@ function parseShopifyPrimitiveSet(value) {
13676
13676
  const parsed = shopifyPrimitiveSetSchema.safeParse(value);
13677
13677
  return parsed.success ? parsed.data : null;
13678
13678
  }
13679
- var SHOPIFY_PRIMITIVE_CONTRACT_VERSION, SHOPIFY_ICON_MATERIAL_VERSION, shopifyThemeFileSchema, primitiveFields, iconLibrarySchema, currentShopifyPrimitiveSetSchema, shopifyPrimitiveSetSchema, localImport;
13679
+ var SHOPIFY_PRIMITIVE_CONTRACT_VERSION, SHOPIFY_ICON_MATERIAL_VERSION, SHOPIFY_PILOT_REQUIRED, shopifyThemeFileSchema, primitiveFields, iconLibrarySchema, currentShopifyPrimitiveSetSchema, shopifyPrimitiveSetSchema, localImport;
13680
13680
  var init_shopify_primitives = __esm({
13681
13681
  "../shared/src/mcp/shopify-primitives.ts"() {
13682
13682
  "use strict";
@@ -13688,6 +13688,7 @@ var init_shopify_primitives = __esm({
13688
13688
  init_shopify_icons();
13689
13689
  SHOPIFY_PRIMITIVE_CONTRACT_VERSION = 1;
13690
13690
  SHOPIFY_ICON_MATERIAL_VERSION = 2;
13691
+ SHOPIFY_PILOT_REQUIRED = "SHOPIFY_PILOT_REQUIRED";
13691
13692
  shopifyThemeFileSchema = z12.object({
13692
13693
  path: z12.string().min(1),
13693
13694
  content: z12.string().min(1)
@@ -13848,6 +13849,9 @@ function parseProjectShopifyPrimitives(projectId, name, value) {
13848
13849
  primitives
13849
13850
  };
13850
13851
  }
13852
+ function shopifyPilotDenied(name, subject, viewUrl) {
13853
+ return `Shopify target unavailable. The Shopify kit is a private pilot and this API key is not in it, so the stored theme for ${name} was withheld. Nothing is wrong with the ${subject}. To join the pilot, email info@better-design.com. Until then, use target "react" or "html". See ${viewUrl}.`;
13854
+ }
13851
13855
  async function fetchProjectShopifyPrimitives(projectId, apiFetch, authContext) {
13852
13856
  try {
13853
13857
  const res = await apiFetch(
@@ -13966,6 +13970,32 @@ Examples:
13966
13970
  - "fintech startup" \u2192 Stripe design system
13967
13971
  - "playful education app" \u2192 Duolingo design system
13968
13972
  - "minimal developer tools" \u2192 Linear design system`;
13973
+ const formatCatalogMatches = (results) => results.map(
13974
+ (r) => [
13975
+ // Markdown link so the NAME is clickable in the host (Claude Code,
13976
+ // ChatGPT, claude.ai all render markdown links). Keep it intact when
13977
+ // presenting so the user can open the preview in one click.
13978
+ `### [${r.title}](${APP_BASE_URL}/design-systems/${r.id})`,
13979
+ r.description,
13980
+ `_${r.personality.join(", ")}_ \xB7 id: \`${r.id}\``
13981
+ ].join("\n")
13982
+ ).join("\n\n");
13983
+ const askForBaseChoice = async (prompt) => {
13984
+ const heading = "# Choose a starting point first\n\nNothing was generated.";
13985
+ try {
13986
+ const results = await provider.searchDesignSystems(prompt, 4);
13987
+ if (results.length === 0) {
13988
+ return `${heading} No catalog system matched this prompt. Ask the user whether to go fully custom, then call create-design-system again with \`userConfirmedCustom: true\`.`;
13989
+ }
13990
+ return `${heading} Show the user these options, keeping each name as its clickable link, and ask which to start from or whether to go fully custom. Then call create-design-system again with the chosen \`id\` as \`baseDesignSystemId\`, or with \`userConfirmedCustom: true\` if the user chose fully custom. Do not pick for the user.
13991
+
13992
+ ---
13993
+
13994
+ ${formatCatalogMatches(results)}`;
13995
+ } catch {
13996
+ return `${heading} The catalog search failed, so call \`find-design-system\` with the app description, show the user the top matches, and ask which to start from or whether to go fully custom.`;
13997
+ }
13998
+ };
13969
13999
  const makeSearchHandler = (toolName, includeBuildRules = false) => async ({ query }) => {
13970
14000
  const start = Date.now();
13971
14001
  const finish = (response, topResults, isError = false, errorMessage) => {
@@ -13993,27 +14023,20 @@ ${includeBuildRules ? BUILD_TARGET_RULE : DESIGN_TRADEOFF_RULE}`
13993
14023
  topResults
13994
14024
  );
13995
14025
  }
13996
- const formatted = results.map(
13997
- (r) => [
13998
- // Markdown link so the NAME is clickable in the host (Claude Code,
13999
- // ChatGPT, claude.ai all render markdown links). Keep it intact when
14000
- // presenting so the user can open the preview in one click.
14001
- `### [${r.title}](${APP_BASE_URL}/design-systems/${r.id})`,
14002
- `${r.description}`,
14003
- `_${r.personality.join(", ")}_ \xB7 id: \`${r.id}\``
14004
- ].join("\n")
14005
- ).join("\n\n");
14026
+ const productPagePointer = COMMERCIAL_QUERY.test(query) ? `
14027
+
14028
+ ${PRODUCT_PAGE_POINTER}` : "";
14006
14029
  return finish(
14007
14030
  {
14008
14031
  content: [{
14009
14032
  type: "text",
14010
14033
  text: `# Catalog matches
14011
14034
 
14012
- Keep any already selected design system and its assigned icons; do not reopen that choice unless requested. If none is selected, present these as options, keeping each name as its clickable markdown link (no scores) so they can open the preview. Explain each option's expected buyer fit and trade-offs using known project context; do not claim conversion effects without evidence. Recommend by buyer, never by the product's material or subject (a steel tool does not need a steel-dark UI). For ecommerce and DTC product pages, default to light, warm, photography-led systems unless the brand already owns a dark identity. Ask which to start their brand from, or to go fully custom, then pass the chosen \`id\` as \`baseDesignSystemId\` to create-design-system.
14035
+ Keep any already selected design system and its assigned icons; do not reopen that choice unless requested. If none is selected, present these as options, keeping each name as its clickable markdown link (no scores) so they can open the preview. Explain each option's expected buyer fit and trade-offs using known project context; do not claim conversion effects without evidence. Recommend by buyer, never by the product's material or subject (a steel tool does not need a steel-dark UI). For ecommerce and DTC product pages, default to light, warm, photography-led systems unless the brand already owns a dark identity. Ask which to start their brand from, or to go fully custom, then pass the chosen \`id\` as \`baseDesignSystemId\` to create-design-system, or pass \`userConfirmedCustom: true\` if they choose fully custom.${productPagePointer}
14013
14036
 
14014
14037
  ---
14015
14038
 
14016
- ${formatted}
14039
+ ${formatCatalogMatches(results)}
14017
14040
 
14018
14041
  ---
14019
14042
 
@@ -14358,14 +14381,18 @@ ${fence3}` }],
14358
14381
  if (projectId) {
14359
14382
  const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
14360
14383
  const providerResult = await provider.getProjectShopifyPrimitives?.(projectId);
14361
- const result = providerResult ? parseProjectShopifyPrimitives(
14384
+ const denied = Boolean(providerResult && "shopifyAccess" in providerResult);
14385
+ const result = denied ? null : providerResult && "shopifyPrimitives" in providerResult ? parseProjectShopifyPrimitives(
14362
14386
  projectId,
14363
14387
  providerResult.name,
14364
14388
  providerResult.shopifyPrimitives
14365
14389
  ) : await fetchProjectShopifyPrimitives(projectId, apiFetch, authContext);
14366
14390
  if (!result) {
14367
14391
  return {
14368
- content: [{ type: "text", text: `Shopify version unavailable. Project "${projectId}" has no maintained Shopify primitive set yet. It may still be generating, belong to another account, or predate Shopify persistence. Check ${viewUrl2}.` }],
14392
+ content: [{
14393
+ type: "text",
14394
+ text: denied ? shopifyPilotDenied(`project "${projectId}"`, "project", viewUrl2) : `Shopify version unavailable. Project "${projectId}" has no maintained Shopify primitive set yet. It may still be generating, belong to another account, or predate Shopify persistence. Check ${viewUrl2}.`
14395
+ }],
14369
14396
  structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
14370
14397
  };
14371
14398
  }
@@ -14395,7 +14422,7 @@ ${fence3}` }],
14395
14422
  const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
14396
14423
  const primitives = parseShopifyPrimitiveSet(ds.metadata.shopifyPrimitives);
14397
14424
  if (!primitives) {
14398
- const text3 = ds.metadata.shopifyAccess === "denied" ? `Shopify target unavailable. The Shopify kit is a private pilot and this API key is not in it, so the stored theme for "${designSystemId}" was withheld. Nothing is wrong with the design system. Ask for pilot access, or use target "react" or "html". See ${viewUrl}.` : `Shopify version unavailable. Design system "${designSystemId}" has no maintained Shopify primitive set. Shopify output is unavailable until that system is seeded and certified; no generic fallback was returned. See ${viewUrl}.`;
14425
+ const text3 = ds.metadata.shopifyAccess === "denied" ? shopifyPilotDenied(`"${designSystemId}"`, "design system", viewUrl) : `Shopify version unavailable. Design system "${designSystemId}" has no maintained Shopify primitive set. Shopify output is unavailable until that system is seeded and certified; no generic fallback was returned. See ${viewUrl}.`;
14399
14426
  return {
14400
14427
  content: [{ type: "text", text: text3 }],
14401
14428
  structuredContent: { status: "error", name: ds.metadata.title, viewUrl, tokensCss: "" }
@@ -14705,7 +14732,7 @@ _${installNote}_`,
14705
14732
 
14706
14733
  ${DESIGN_TRADEOFF_RULE}
14707
14734
 
14708
- **Use this for a new custom brand, even when the user never mentions design or branding** (a design system IS the app's brand). Keep an already selected system and its assigned icons unless the user requests a different direction; a redesign alone does not require regenerating the brand. When no system is selected, first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build 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-kit\`.
14735
+ **Use this for a new custom brand, even when the user never mentions design or branding** (a design system IS the app's brand). Keep an already selected system and its assigned icons unless the user requests a different direction; a redesign alone does not require regenerating the brand. When no system is selected, first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom (passed here as \`userConfirmedCustom: true\`). A call with neither, and no \`brandCss\`, generates nothing: it returns those matches for you to show the user. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build 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-kit\`.
14709
14736
 
14710
14737
  **When the user has an existing brand** (their site, Figma file, or stylesheet): run \`extract-design-system\` or \`extract-from-url\` first and pass the resulting token sheet (the fenced \`\`\`css block, verbatim: the \`:root { ... }\` and optional \`.dark { ... }\` blocks) as \`brandCss\`. The generation is then pinned to that brand: no catalog aesthetic is retrieved, the sheet's colors, fonts and radii override everything, and later design-system switches are refused. Copy the block exactly; do not retype or trim it.
14711
14738
 
@@ -14718,6 +14745,7 @@ Share the project URL with the user right away so they can watch the generation
14718
14745
  inputSchema: z14.object({
14719
14746
  prompt: z14.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'."),
14720
14747
  baseDesignSystemId: z14.string().optional().describe("Optional. The catalog id the USER picked from the `find-design-system` matches you showed them, used to bias generation toward that aesthetic (e.g. a theme tweak on top of Linear). Leave empty only when the user chose fully custom."),
14748
+ userConfirmedCustom: z14.boolean().optional().describe("Set true only when the user saw the find-design-system matches and chose fully custom. Without it, baseDesignSystemId or brandCss, the tool returns the matches and generates nothing."),
14721
14749
  brandCss: z14.string().max(5e4).optional().describe("Optional. The user's extracted brand token sheet, verbatim: the fenced css block from extract-design-system / extract-from-url (`:root { --background: ...; }` plus optional `.dark { ... }`). When set, generation builds FROM this brand instead of a catalog look-alike, and baseDesignSystemId is ignored (a catalog base would leak its identity past the brand tokens).")
14722
14750
  }),
14723
14751
  // Each call starts a new generation and returns a new project id, so it
@@ -14731,7 +14759,10 @@ Share the project URL with the user right away so they can watch the generation
14731
14759
  openWorldHint: true
14732
14760
  }
14733
14761
  },
14734
- async ({ prompt, baseDesignSystemId, brandCss }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId), hasBrand: Boolean(brandCss) }, async () => {
14762
+ async ({ prompt, baseDesignSystemId, brandCss, userConfirmedCustom }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId), hasBrand: Boolean(brandCss), userConfirmedCustom: Boolean(userConfirmedCustom) }, async () => {
14763
+ if (!baseDesignSystemId && !brandCss && !userConfirmedCustom) {
14764
+ return { content: [{ type: "text", text: await askForBaseChoice(prompt) }] };
14765
+ }
14735
14766
  const startUrl = `${APP_BASE_URL}/api/projects/anonymous`;
14736
14767
  let projectId;
14737
14768
  const requestHeaders = { "Content-Type": "application/json" };
@@ -14971,7 +15002,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
14971
15002
  })
14972
15003
  );
14973
15004
  }
14974
- var SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, DS_INDEX_SCHEMA;
15005
+ var SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, DS_INDEX_SCHEMA, COMMERCIAL_QUERY, PRODUCT_PAGE_POINTER;
14975
15006
  var init_design_system_tools = __esm({
14976
15007
  "../shared/src/mcp/tools/design-system-tools.ts"() {
14977
15008
  "use strict";
@@ -14995,6 +15026,8 @@ var init_design_system_tools = __esm({
14995
15026
  DS_INDEX_SCHEMA = z14.object({
14996
15027
  baseComponents: z14.array(z14.object({ name: z14.string() })).optional()
14997
15028
  });
15029
+ COMMERCIAL_QUERY = /\b(product pages?|pdp|shops?|(?<!app )stores?|storefront|e-?commerce|webshop|dtc|d2c|shopify|checkout|buy box)\b|[$€£]\s?\d/i;
15030
+ PRODUCT_PAGE_POINTER = 'This looks like a product page or a store. Before you build, call `get-ui-principle({ topic: "product-pages" })` for section order by buyer objection, the squint test and buyer-matched tone. For a catalog or checkout, also call `get-ux-principle` with topic `commerce-catalog` or `commerce-checkout`.';
14998
15031
  }
14999
15032
  });
15000
15033
 
@@ -23339,7 +23372,7 @@ var init_agent_feedback = __esm({
23339
23372
 
23340
23373
  // ../shared/src/schema.ts
23341
23374
  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;
23375
+ 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, adminProjectReadRouteValues, adminProjectReads, 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;
23343
23376
  var init_schema2 = __esm({
23344
23377
  "../shared/src/schema.ts"() {
23345
23378
  "use strict";
@@ -24103,6 +24136,25 @@ var init_schema2 = __esm({
24103
24136
  },
24104
24137
  (t) => [index("project_events_project_id_id_idx").on(t.projectId, t.id)]
24105
24138
  );
24139
+ adminProjectReadRouteValues = ["project", "messages", "events"];
24140
+ adminProjectReads = pgTable(
24141
+ "admin_project_reads",
24142
+ {
24143
+ id: uuid("id").primaryKey().defaultRandom(),
24144
+ adminUserId: text("admin_user_id").notNull(),
24145
+ /** The project's owner at the time of the read. */
24146
+ targetUserId: text("target_user_id").notNull(),
24147
+ projectId: uuid("project_id").notNull(),
24148
+ route: text("route", { enum: adminProjectReadRouteValues }).notNull(),
24149
+ /** The UTC hour the read fell in. */
24150
+ readHour: timestamp("read_hour", { withTimezone: true }).notNull(),
24151
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull()
24152
+ },
24153
+ (t) => [
24154
+ index("admin_project_reads_created_at_idx").on(t.createdAt),
24155
+ uniqueIndex("admin_project_reads_hour_uniq").on(t.adminUserId, t.projectId, t.route, t.readHour)
24156
+ ]
24157
+ );
24106
24158
  iconLibraries = pgTable("icon_libraries", {
24107
24159
  id: text("id").primaryKey(),
24108
24160
  name: text("name").notNull(),
@@ -29561,7 +29613,7 @@ var MCP_IMPLEMENTATION_VERSION;
29561
29613
  var init_version2 = __esm({
29562
29614
  "../shared/src/mcp/version.ts"() {
29563
29615
  "use strict";
29564
- MCP_IMPLEMENTATION_VERSION = "0.34.0";
29616
+ MCP_IMPLEMENTATION_VERSION = "0.35.1";
29565
29617
  }
29566
29618
  });
29567
29619
 
@@ -29727,7 +29779,8 @@ async function request(path6, options = {}) {
29727
29779
  });
29728
29780
  if (!res.ok) {
29729
29781
  const body = await res.json().catch(() => ({ message: res.statusText }));
29730
- throw new ApiError(res.status, body.message ?? `API error ${res.status}`);
29782
+ const { message, code } = body;
29783
+ throw new ApiError(res.status, message ?? `API error ${res.status}`, code);
29731
29784
  }
29732
29785
  return res.json();
29733
29786
  }
@@ -29777,7 +29830,12 @@ async function getProjectMobilePrimitives(projectId) {
29777
29830
  return request(`/projects/${encodeURIComponent(projectId)}/mobile/all.json`);
29778
29831
  }
29779
29832
  async function getProjectShopifyPrimitives(projectId) {
29780
- return request(`/projects/${encodeURIComponent(projectId)}/shopify/all.json`);
29833
+ try {
29834
+ return await request(`/projects/${encodeURIComponent(projectId)}/shopify/all.json`);
29835
+ } catch (err) {
29836
+ if (err instanceof ApiError && err.code === SHOPIFY_PILOT_REQUIRED) return { shopifyAccess: "denied" };
29837
+ throw err;
29838
+ }
29781
29839
  }
29782
29840
  async function listPrinciples(area) {
29783
29841
  const params = area ? `?area=${area}` : "";
@@ -29857,14 +29915,16 @@ var API_URL2, API_KEY, API_KEY_HASH, ApiError;
29857
29915
  var init_api_client = __esm({
29858
29916
  "src/lib/api-client.ts"() {
29859
29917
  "use strict";
29918
+ init_shopify_primitives();
29860
29919
  init_api_url();
29861
29920
  API_URL2 = resolveApiUrl();
29862
29921
  API_KEY = process.env.BETTER_DESIGN_API_KEY;
29863
29922
  API_KEY_HASH = API_KEY ? createHash3("sha256").update(API_KEY).digest("hex") : void 0;
29864
29923
  ApiError = class extends Error {
29865
- constructor(status, message) {
29924
+ constructor(status, message, code) {
29866
29925
  super(message);
29867
29926
  this.status = status;
29927
+ this.code = code;
29868
29928
  this.name = "ApiError";
29869
29929
  }
29870
29930
  };