@sellhapi/mcp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ Go to **sellhapi.com** → Settings → API Keys → Create Key.
42
42
  "mcpServers": {
43
43
  "sellhapi": {
44
44
  "command": "npx",
45
- "args": ["-y", "@sellhapi/mcp", "--api-key", "sk_live_your_key_here"]
45
+ "args": ["-y", "@sellhapi/mcp@latest", "--api-key", "sk_live_your_key_here"]
46
46
  }
47
47
  }
48
48
  }
@@ -54,43 +54,29 @@ Or set the environment variable instead of `--api-key`:
54
54
  export SELLHAPI_API_KEY=sk_live_your_key_here
55
55
  ```
56
56
 
57
- ## Setup — hosted
57
+ ## Setup — hosted (ChatGPT, Claude web)
58
58
 
59
- ### 1. Build
59
+ The SellHapi MCP server is hosted at:
60
60
 
61
- ```bash
62
- npm install
63
- npm run build
64
61
  ```
65
-
66
- ### 2. Configure environment
67
-
68
- ```bash
69
- cp .env.example .env
70
- # Edit .env with your values
62
+ https://sellhapi-mcp.onrender.com/mcp
71
63
  ```
72
64
 
73
- Required env vars:
65
+ No installation required — just connect your AI client.
74
66
 
75
- | Variable | Description | Default |
76
- |----------|-------------|---------|
77
- | `PORT` | HTTP port | `3100` |
78
- | `SELLHAPI_API_URL` | sell_api base URL | production |
79
- | `MCP_SERVER_URL` | Public URL of this server (for OAuth metadata) | `http://localhost:PORT` |
67
+ ### Claude web
80
68
 
81
- ### 3. Run
69
+ 1. Go to **claude.ai** → Settings → **Integrations** → Add integration
70
+ 2. Enter: `https://sellhapi-mcp.onrender.com/mcp`
71
+ 3. Click Connect — you'll be redirected to SellHapi to authorise access
72
+ 4. Once authorised, all SellHapi tools are available in your Claude conversations
82
73
 
83
- ```bash
84
- npm run start:hosted
85
- ```
86
-
87
- Endpoints:
74
+ ### ChatGPT
88
75
 
89
- - `POST /mcp` — MCP Streamable HTTP (initialize + messages)
90
- - `GET /mcp` — SSE stream
91
- - `DELETE /mcp` — session teardown
92
- - `GET /health` — health check
93
- - `/.well-known/oauth-authorization-server` — OAuth metadata
76
+ 1. Go to **chatgpt.com** → your profile → **My GPTs** → Create a GPT (or edit an existing one)
77
+ 2. In the GPT editor → **Configure** → **Actions** → Add action
78
+ 3. Set the server URL to: `https://sellhapi-mcp.onrender.com/mcp`
79
+ 4. Authentication: OAuth 2.0 — the server handles the full flow automatically
94
80
 
95
81
  ### sell_api env vars
96
82
 
@@ -103,12 +89,27 @@ The sell_api server needs these for OAuth:
103
89
 
104
90
  ## Tools
105
91
 
106
- ### Studio (5)
92
+ ### Studio (15)
107
93
  - `list_projects` — list vendor's Studio projects
108
94
  - `get_project` — get project details with code
109
- - `create_project` — create a new project
110
- - `push_code` — push React code (validated by BuildGuard)
95
+ - `create_project` — create a new project (returns workflow guide + build rules)
96
+ - `apply_design_direction` — store a full DesignDirection (colors, fonts, pages) — zero credits
97
+ - `set_design_direction` — ask SellHapi's Claude to generate a direction from a brief (costs credits)
98
+ - `analyze_design_reference` — extract design DNA from an image, website URL, or Pinterest pins
99
+ - `import_website_content` — scrape existing website copy into the project
100
+ - `analyze_reference` — fetch OG metadata + theme color from any URL
101
+ - `apply_wireframe` — store approved mid-fidelity wireframes per page — zero credits
102
+ - `push_code` — push React code (validated by BuildGuard) — zero credits
103
+ - `patch_code` — push only changed files, merged into existing code — zero credits
111
104
  - `deploy_website` — publish project to live storefront
105
+ - `get_preview` — get current sandbox preview URL
106
+ - `list_revisions` — list revision history
107
+ - `get_revision` — get a specific revision's code
108
+ - `restore_revision` — restore project to a previous revision
109
+
110
+ ### Context
111
+ - `get_vendor_context` — call this first — returns capabilities, store slug, product/service counts in one call
112
+ - `suggest_capabilities` — given a business brief, returns ranked capability IDs to activate
112
113
 
113
114
  ### Commerce (8)
114
115
  - `list_products`, `list_digital_products`, `list_service_listings`
@@ -121,6 +122,10 @@ The sell_api server needs these for OAuth:
121
122
  ### Intelligence (2)
122
123
  - `list_orders`, `get_wallet_balance`
123
124
 
125
+ ### Reference
126
+ - `get_build_rules` — full BuildGuard validation rules (call when push_code fails)
127
+ - `get_runtime_hooks` — complete @sellhapi/studio-runtime hook reference
128
+
124
129
  ## Resources
125
130
 
126
131
  - `sellhapi://runtime-hooks` — available React hooks from @sellhapi/studio-runtime
package/dist/hosted.js CHANGED
@@ -38,7 +38,7 @@ const PORT = parseInt(process.env.PORT ?? "3100", 10);
38
38
  const API_URL = process.env.SELLHAPI_API_URL ?? "https://sell-api-xl2a.onrender.com";
39
39
  const SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`;
40
40
  const sessions = new Map();
41
- const SESSION_TTL_MS = 30 * 60 * 1000;
41
+ const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours — long enough for a full AI conversation
42
42
  const MAX_SESSIONS = 1000;
43
43
  setInterval(() => {
44
44
  const now = Date.now();
@@ -127,8 +127,8 @@ app.use((0, router_js_1.mcpAuthRouter)({
127
127
  scopesSupported: ["studio", "commerce", "content", "intelligence"],
128
128
  serviceDocumentationUrl: new URL("https://sellhapi.com/mcp"),
129
129
  }));
130
- // MCP endpoint — JSON body parsing
131
- app.use("/mcp", express_1.default.json());
130
+ // MCP endpoint — JSON body parsing (10mb to handle large push_code payloads)
131
+ app.use("/mcp", express_1.default.json({ limit: "10mb" }));
132
132
  // MCP POST — initialize or send message
133
133
  app.post("/mcp", async (req, res) => {
134
134
  const apiKey = resolveApiKeyFromAuth(req);
@@ -218,12 +218,19 @@ app.delete("/mcp", (req, res) => {
218
218
  res.sendStatus(204);
219
219
  });
220
220
  // ── Keep-alive ping (prevent Render free tier sleep) ────────────────────────
221
- // Ping sell_api every 10 minutes to keep it warm for tool calls
221
+ // Ping both sell_api and this MCP server every 10 minutes.
222
+ // sell_api: keeps it warm for tool calls.
223
+ // MCP server (self): prevents Render from sleeping the process, which would
224
+ // wipe the in-memory sessions Map and force all clients to reinitialize.
222
225
  setInterval(async () => {
223
226
  try {
224
227
  await fetch(API_URL);
225
228
  }
226
229
  catch { /* ignore */ }
230
+ try {
231
+ await fetch(`${SERVER_URL}/health`);
232
+ }
233
+ catch { /* ignore */ }
227
234
  }, 10 * 60 * 1000).unref();
228
235
  // ── Start ────────────────────────────────────────────────────────────────────
229
236
  app.listen(PORT, () => {
@@ -2,4 +2,4 @@
2
2
  * BuildGuard rules — extracted from buildGuard.service.ts.
3
3
  * Code that violates these rules will be rejected by push_code.
4
4
  */
5
- export declare const BUILD_RULES = "\n# SellHapi BuildGuard Rules\n\nYour code MUST pass all these rules or push_code will reject it with specific violation details.\n\n## APPROVED IMPORTS (only these are allowed)\n\nExact matches:\n @sellhapi/studio-runtime\n react\n react-dom\n react-dom/client\n react/jsx-runtime\n react/jsx-dev-runtime\n framer-motion\n clsx\n date-fns\n zod\n lucide-react\n\nSubpath imports allowed:\n date-fns/*\n react-dom/*\n react/*\n\nRelative imports (./ and ../) are always allowed between project files.\n\nEVERYTHING ELSE IS BLOCKED \u2014 no axios, no tailwindcss, no lodash, no node builtins.\n\n## HARD VIOLATIONS (build fails)\n\n1. DISALLOWED IMPORTS\n Any import not in the approved list above.\n Violation: \"Import from X is not allowed.\"\n\n2. DYNAMIC REQUIRE\n require() with non-string argument or unapproved package.\n Violation: \"Dynamic require() is not allowed.\"\n\n3. DYNAMIC IMPORT\n import() expressions (as opposed to static import statements).\n Violation: \"Dynamic import() is not allowed in generated storefront code.\"\n\n4. CODE INJECTION\n eval(), new Function(), Function() \u2014 all blocked.\n Violation: \"eval()/Function() is not allowed \u2014 code injection risk.\"\n\n5. RAW NETWORK ACCESS\n fetch(), new XMLHttpRequest(), new WebSocket() \u2014 all blocked.\n Violation: \"Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead.\"\n All data must flow through runtime hooks.\n\n6. DANGEROUS GLOBALS\n Blocked property access:\n - process.env\n - document.cookie, document.write(), document.writeln(), document.domain, document.execCommand()\n - window.fetch, window.eval, window.Function\n - window.XMLHttpRequest, window.WebSocket\n - window.localStorage, window.sessionStorage, window.indexedDB\n - navigator.sendBeacon\n - Standalone: localStorage, sessionStorage, indexedDB\n Violation: \"X is not allowed in storefront code.\"\n\n7. SUBSCRIPT ACCESS ON GLOBALS\n window[\"fetch\"], globalThis[\"eval\"], self[\"XMLHttpRequest\"] \u2014 all blocked.\n Violation: \"Subscript access on 'window' is not allowed.\"\n\n8. DANGEROUS JSX\n dangerouslySetInnerHTML \u2014 blocked.\n Violation: \"dangerouslySetInnerHTML is not allowed \u2014 XSS vector.\"\n\n9. MISSING CART DRAWER\n If any file uses useCart or addItem, <CartDrawer /> MUST be rendered somewhere.\n Violation: \"useCart/addItem is used but <CartDrawer /> is not rendered anywhere.\"\n\n## SOFT WARNINGS (build passes, but flagged)\n\n1. product.images[N].url \u2014 WRONG. images is string[], each element IS the URL.\n Fix: use images[N] directly or product.imageUrl for primary.\n\n2. addItem() without type field \u2014 CartItem.type is required.\n Fix: add type: 'physical' | 'digital' to every addItem() call.\n\n3. campaign.raised / campaign.goalAmount \u2014 WRONG field names.\n Fix: use campaign.raisedAmountKobo / campaign.goalAmountKobo (divide by 100 for display).\n\n4. ticketType.soldCount \u2014 WRONG. Use ticketType.sold.\n\n## REQUIRED: data-region ATTRIBUTES\n\nEvery section that renders live data MUST have a data-region attribute on its outermost container element.\nThis is required for Studio's region selector, Layers panel, and entity injection to work.\n\nRequired attributes:\n data-region=\"<slug>\" \u2014 unique kebab-case identifier, e.g. \"hero\", \"products\", \"appointments\"\n data-label=\"<name>\" \u2014 human-readable label shown in Studio UI, e.g. \"Products Section\"\n data-scope=\"content\" \u2014 always \"content\" for data-driven sections\n\nExample:\n <section data-region=\"products\" data-label=\"Products\" data-scope=\"content\">\n {/* useProducts() renders here */}\n </section>\n\n <section data-region=\"appointments\" data-label=\"Consultations\" data-scope=\"content\">\n {/* useServices() renders here */}\n </section>\n\nALSO add data-region to static layout sections (hero, footer, nav) \u2014 these don't need data hooks but must still be selectable:\n <section data-region=\"hero\" data-label=\"Hero\" data-scope=\"layout\">\n <header data-region=\"navigation\" data-label=\"Navigation\" data-scope=\"navigation\">\n <footer data-region=\"footer\" data-label=\"Footer\" data-scope=\"layout\">\n\nIf you use ANY data hook (useProducts, useServices, useEvents, etc.) without ANY data-region attributes, the build will warn you.\n\n## FILE STRUCTURE\n\nRequired: App.tsx (or App.jsx) as entry point.\nRecommended structure:\n App.tsx \u2014 main component with routing\n components/ \u2014 reusable UI components\n pages/ \u2014 route-level pages (ProductPage, EventPage, etc.)\n styles/ \u2014 optional shared styles\n\n## BUNDLE SIZE LIMITS\n\nHard limit: 4 MB (build fails above this)\nWarning: 3.5 MB (runtime baseline is ~2.1 MB \u2014 aim to keep user code under 1.4 MB)\n\n## STYLING\n\nUse inline styles (React style objects) \u2014 this is the convention.\nCSS modules work but are not typical.\nTailwind is NOT available (not in approved imports).\nframer-motion is available for animations.\n\n## KEY PATTERNS\n\n1. All data comes from hooks. NEVER hardcode product names, prices, or content.\n2. Show loading states: if (loading) return <div>Loading...</div>\n3. Show empty states: if (data.length === 0) return <div>No items yet</div>\n4. Format prices: (priceKobo / 100).toLocaleString() for Naira display\n5. Use useHashRouter() for multi-page navigation\n6. Commerce actions return { paymentLink } \u2014 redirect: window.location.href = result.paymentLink\n";
5
+ export declare const BUILD_RULES = "\n# SellHapi BuildGuard Rules\n\nYour code MUST pass all these rules or push_code will reject it with specific violation details.\n\n## APPROVED IMPORTS (only these are allowed)\n\nExact matches:\n @sellhapi/studio-runtime\n react\n react-dom\n react-dom/client\n react/jsx-runtime\n react/jsx-dev-runtime\n framer-motion\n clsx\n date-fns\n zod\n lucide-react\n\nSubpath imports allowed:\n date-fns/*\n react-dom/*\n react/*\n\nRelative imports (./ and ../) are always allowed between project files.\n\nEVERYTHING ELSE IS BLOCKED \u2014 no axios, no tailwindcss, no lodash, no node builtins.\n\n## HARD VIOLATIONS (build fails)\n\n1. DISALLOWED IMPORTS\n Any import not in the approved list above.\n Violation: \"Import from X is not allowed.\"\n\n2. DYNAMIC REQUIRE\n require() with non-string argument or unapproved package.\n Violation: \"Dynamic require() is not allowed.\"\n\n3. DYNAMIC IMPORT\n import() expressions (as opposed to static import statements).\n Violation: \"Dynamic import() is not allowed in generated storefront code.\"\n\n4. CODE INJECTION\n eval(), new Function(), Function() \u2014 all blocked.\n Violation: \"eval()/Function() is not allowed \u2014 code injection risk.\"\n\n5. RAW NETWORK ACCESS\n fetch(), new XMLHttpRequest(), new WebSocket() \u2014 all blocked.\n Violation: \"Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead.\"\n All data must flow through runtime hooks.\n\n ALLOWED: static external image URLs in <img src=\"...\"> are NOT fetch() calls and are permitted.\n Use Unsplash (https://images.unsplash.com/...) or picsum (https://picsum.photos/...) as placeholder\n fallbacks for hero images, category covers, or any section where the vendor has no uploaded image.\n Example: <img src={product.imageUrl || \"https://images.unsplash.com/photo-1517048676732-d65bc937f952?w=800\"} />\n This is valid \u2014 only JavaScript fetch()/XHR/WebSocket calls are blocked, not <img src> URLs.\n\n6. DANGEROUS GLOBALS\n Blocked property access:\n - process.env\n - document.cookie, document.write(), document.writeln(), document.domain, document.execCommand()\n - window.fetch, window.eval, window.Function\n - window.XMLHttpRequest, window.WebSocket\n - window.localStorage, window.sessionStorage, window.indexedDB\n - navigator.sendBeacon\n - Standalone: localStorage, sessionStorage, indexedDB\n Violation: \"X is not allowed in storefront code.\"\n\n7. SUBSCRIPT ACCESS ON GLOBALS\n window[\"fetch\"], globalThis[\"eval\"], self[\"XMLHttpRequest\"] \u2014 all blocked.\n Violation: \"Subscript access on 'window' is not allowed.\"\n\n8. DANGEROUS JSX\n dangerouslySetInnerHTML \u2014 blocked.\n Violation: \"dangerouslySetInnerHTML is not allowed \u2014 XSS vector.\"\n\n9. MISSING CART DRAWER\n If any file uses useCart or addItem, <CartDrawer /> MUST be rendered somewhere.\n Violation: \"useCart/addItem is used but <CartDrawer /> is not rendered anywhere.\"\n\n## SOFT WARNINGS (build passes, but flagged)\n\n1. product.images[N].url \u2014 WRONG. images is string[], each element IS the URL.\n Fix: use images[N] directly or product.imageUrl for primary.\n\n2. addItem() without type field \u2014 CartItem.type is required.\n Fix: add type: 'physical' | 'digital' to every addItem() call.\n\n3. campaign.raised / campaign.goalAmount \u2014 WRONG field names.\n Fix: use campaign.raisedAmountKobo / campaign.goalAmountKobo (divide by 100 for display).\n\n4. ticketType.soldCount \u2014 WRONG. Use ticketType.sold.\n\n## REQUIRED: data-region ATTRIBUTES\n\nEvery section that renders live data MUST have a data-region attribute on its outermost container element.\nThis is required for Studio's region selector, Layers panel, and entity injection to work.\n\nRequired attributes:\n data-region=\"<slug>\" \u2014 unique kebab-case identifier, e.g. \"hero\", \"products\", \"appointments\"\n data-label=\"<name>\" \u2014 human-readable label shown in Studio UI, e.g. \"Products Section\"\n data-scope=\"content\" \u2014 always \"content\" for data-driven sections\n\nExample:\n <section data-region=\"products\" data-label=\"Products\" data-scope=\"content\">\n {/* useProducts() renders here */}\n </section>\n\n <section data-region=\"appointments\" data-label=\"Consultations\" data-scope=\"content\">\n {/* useServices() renders here */}\n </section>\n\nALSO add data-region to static layout sections (hero, footer, nav) \u2014 these don't need data hooks but must still be selectable:\n <section data-region=\"hero\" data-label=\"Hero\" data-scope=\"layout\">\n <header data-region=\"navigation\" data-label=\"Navigation\" data-scope=\"navigation\">\n <footer data-region=\"footer\" data-label=\"Footer\" data-scope=\"layout\">\n\nIf you use ANY data hook (useProducts, useServices, useEvents, etc.) without ANY data-region attributes, the build will warn you.\n\n## FILE STRUCTURE\n\nRequired: App.tsx (or App.jsx) as entry point.\nRecommended structure:\n App.tsx \u2014 main component with routing\n components/ \u2014 reusable UI components\n pages/ \u2014 route-level pages (ProductPage, EventPage, etc.)\n styles/ \u2014 optional shared styles\n\n## BUNDLE SIZE LIMITS\n\nHard limit: 4 MB (build fails above this)\nWarning: 3.5 MB (runtime baseline is ~2.1 MB \u2014 aim to keep user code under 1.4 MB)\n\n## STYLING\n\nUse inline styles (React style objects) \u2014 this is the convention.\nCSS modules work but are not typical.\nTailwind is NOT available (not in approved imports).\nframer-motion is available for animations.\n\n## KEY PATTERNS\n\n1. All data comes from hooks. NEVER hardcode product names, prices, or content.\n2. Show loading states: if (loading) return <div>Loading...</div>\n3. Show empty states: if (data.length === 0) return <div>No items yet</div>\n4. Format prices: (priceKobo / 100).toLocaleString() for Naira display\n5. Use useHashRouter() for multi-page navigation\n6. Commerce actions return { paymentLink } \u2014 redirect: window.location.href = result.paymentLink\n";
@@ -57,6 +57,12 @@ EVERYTHING ELSE IS BLOCKED — no axios, no tailwindcss, no lodash, no node buil
57
57
  Violation: "Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead."
58
58
  All data must flow through runtime hooks.
59
59
 
60
+ ALLOWED: static external image URLs in <img src="..."> are NOT fetch() calls and are permitted.
61
+ Use Unsplash (https://images.unsplash.com/...) or picsum (https://picsum.photos/...) as placeholder
62
+ fallbacks for hero images, category covers, or any section where the vendor has no uploaded image.
63
+ Example: <img src={product.imageUrl || "https://images.unsplash.com/photo-1517048676732-d65bc937f952?w=800"} />
64
+ This is valid — only JavaScript fetch()/XHR/WebSocket calls are blocked, not <img src> URLs.
65
+
60
66
  6. DANGEROUS GLOBALS
61
67
  Blocked property access:
62
68
  - process.env
@@ -157,8 +157,9 @@ Hooks: useServices(), useAvailableDates({ serviceId, month }), useAvailability({
157
157
  Flow:
158
158
  1. Show services list (useServices) — each card shows name, duration, price
159
159
  2. Customer picks a service → show month calendar (useAvailableDates gives available YYYY-MM-DD dates)
160
- 3. Customer picks a date → show time slots: useAvailability({ serviceId, from: date, to: date })
161
- NOTE: to must be the SAME date as from — the hook internally extends it to end-of-day
160
+ 3. Customer picks a date → show time slots: useAvailability({ serviceId, from: date, to: nextDay })
161
+ to = new Date(+new Date(date) + 86400000).toISOString().slice(0, 10) // always from + 1 day
162
+ Filter result: only render slots where slot.status === "available"
162
163
  4. Customer picks a slot — render slot.startTime formatted for display
163
164
  5. Show booking form (name, email, phone)
164
165
  6. Submit: useBookAppointment().book({ serviceId, dateTime: slot.startTime, customerName, customerEmail, customerPhone })
@@ -169,7 +170,12 @@ Price display:
169
170
  ALWAYS wrap in Number(): Number(service.priceKobo) before any arithmetic or comparison.
170
171
  Display: ₦{(Number(service.priceKobo) / 100).toLocaleString()}
171
172
 
172
- Slot filtering: DO NOT filter slots by status. useAvailability returns only bookable slots — pass them all through.
173
+ Slot filtering: useAvailability returns slots with multiple statuses ("available", "claimed", "booked", "blocked").
174
+ ONLY show slots where status === "available" — filter the result before rendering.
175
+ If filtered result is empty → show "No slots available for this date. Try another date."
176
+
177
+ useAvailability date range: to must always be from + 1 day — NEVER set from === to.
178
+ to = new Date(+new Date(from) + 86400000).toISOString().slice(0, 10)
173
179
 
174
180
  Anti-patterns:
175
181
  - NEVER call initiate() for appointments — always use book(). useBookAppointment returns { book, initiate (alias), loading, error }
@@ -177,7 +183,8 @@ Anti-patterns:
177
183
  - NEVER use slot.durationMinutes — it doesn't exist on AvailabilitySlot
178
184
  - NEVER hardcode service names/prices — always use useServices()
179
185
  - NEVER skip the date→slot two-step — must pick date first, then load slots for that date
180
- - NEVER filter slots with slot.status === 'available' — drops all slots; let the hook decide
186
+ - NEVER set from === to in useAvailability — to must be from + 1 day or you get zero results
187
+ - NEVER show slots where status !== "available" — always filter the hook result
181
188
 
182
189
  ---
183
190
 
@@ -298,15 +305,92 @@ Anti-patterns:
298
305
 
299
306
  ---
300
307
 
308
+ ## ENTITY LAYOUT PHILOSOPHY
309
+
310
+ Entities DO NOT have to be displayed as uniform rectangle cards in a grid.
311
+ Choose the layout that fits the business type, visual direction, and the entity's purpose.
312
+ The wireframe is your opportunity to propose the right layout — the user can approve or change it.
313
+
314
+ Layout options by entity type:
315
+
316
+ MEMBERSHIPS — premium tiers deserve premium presentation:
317
+ - Large image tile (full-width image, title + price + CTA below) — ideal for lifestyle/luxury brands
318
+ - Horizontal comparison table — ideal for SaaS-style or tiered feature sets
319
+ - Feature-rich card with expanded benefits checklist — ideal when benefits are the selling point
320
+ - Avoid generic equal-height rectangle cards unless the business explicitly has many tiers
321
+
322
+ SERVICES — editorial presentation conveys quality better than a card grid:
323
+ - Alternating split sections (image left / text + CTA right, alternates per service)
324
+ - Full-width editorial rows with large background image + text overlay
325
+ - Icon + heading + description list for minimalist service studios
326
+ - Grid cards are acceptable only when services are many and uniform (e.g. a menu)
327
+
328
+ EVENTS — presentation should match the energy of the event:
329
+ - Poster-style: large image, event name as headline, date + location below — theatrical feel
330
+ - Horizontal scroll strip: compact cards in a row — good for many events
331
+ - Featured hero card (first event large, rest smaller) — highlights the primary event
332
+ - Calendar or timeline view when date ordering matters most
333
+
334
+ DONATIONS — campaigns are stories, not products:
335
+ - Hero campaign section: full-width image, campaign name, progress bar, donate CTA
336
+ - Stacked editorial rows: image + story side-by-side, alternating
337
+ - Card grid is acceptable when there are multiple parallel campaigns of equal importance
338
+
339
+ APPOINTMENTS — scheduling flows need clarity, not decoration:
340
+ - Service list → select → calendar → time slot: linear step-by-step (most common, best UX)
341
+ - Service cards with inline "Book now" that opens a side panel
342
+ - Minimal: service name + duration + price in a clean list, not a card grid
343
+
344
+ PRODUCTS (homepage teaser) — this is an editorial/marketing section:
345
+ - 2-col editorial layout: large image takes 60% width, product name + price + CTA in remaining 40%
346
+ - 4-col editorial grid with large images and minimal text overlay
347
+ - Featured product hero (one product at full width) + 3 supporting products below
348
+ - NOT the same component or layout as the /products page — it must look visually distinct
349
+
350
+ PRODUCTS (grid/browse page) — standard grid is correct here:
351
+ - 3-col or 4-col responsive grid with image, name, price, Add to Cart
352
+ - Consistent card height, pagination or Load More at bottom
353
+ - This is the browsing context — uniformity and scannability are correct here
354
+
355
+ Key rules:
356
+ - Match layout to visualDirection.layoutPattern if set — it explicitly describes the intended pattern
357
+ - "glassmorphism" or "soft-shadow" cardStyle + premium business → prefer large image tiles, not small cards
358
+ - The wireframe section names you choose (e.g. "featured-membership-tiles", "editorial-services", "campaign-hero")
359
+ signal your layout intent — use descriptive names, not generic ones like "section1"
360
+ - BuildGuard does NOT enforce layout — only data-region attributes and banned JS APIs are checked
361
+ - The visual representation is yours to decide based on brand and context
362
+
363
+ ---
364
+
301
365
  ## UNIVERSAL RULES
302
366
 
303
367
  1. Price display: always (priceKobo / 100).toLocaleString() — prefix with ₦
304
368
  2. Empty states: always handle empty arrays gracefully — never show blank sections
305
369
  3. Loading states: always show a loading indicator while data is being fetched
306
- 4. Images: always handle null imageUrl — show placeholder or hide the image element
370
+ 4. Images: always handle null imageUrl — use an Unsplash or picsum URL as fallback, never hide sections because an image is missing.
371
+ Allowed pattern: <img src={item.imageUrl || "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800"} />
372
+ Pick a contextually appropriate Unsplash photo (products, food, fashion, etc.) — do not reuse the same fallback for every image type.
373
+ Static external URLs in <img src> are NOT blocked by BuildGuard — only fetch()/XHR/WebSocket in JS are blocked.
307
374
  5. Hash routing: use useHashRouter() for navigation between pages
308
- 6. Commerce results: all initiate/checkout functions return { paymentLink } — redirect with window.location.href
375
+ 6. Commerce results: all initiate/checkout functions return { paymentLink } — redirect with window.location.href = result.paymentLink
309
376
  7. Cart pattern: addItem() → openCart() → CartDrawer shows → customer proceeds to checkout
377
+ 8. window.location.href: ALLOWED and REQUIRED for payment redirects — do not avoid it
378
+ 9. localStorage/sessionStorage: NOT allowed for general state — use React state and hooks only
379
+
380
+ ## FORM IDs
381
+
382
+ useContentForm(formId) requires a real form ID from the vendor's account.
383
+ NEVER hardcode a form ID or guess one. Before using any form hook:
384
+ 1. Call list_forms to get the vendor's available forms and their IDs
385
+ 2. Match the form to its purpose (contact, waitlist, enquiry, etc.)
386
+ 3. Only then pass the ID to useContentForm(formId) and useSubmitForm(formId)
387
+ If no suitable form exists, render a plain HTML form or omit the section — do not use a placeholder ID.
388
+
389
+ ## STOREFRONT MODE — PAGE GUIDANCE
390
+
391
+ The patterns below are DEFAULTS for a general storefront. The vendor brief overrides them.
392
+ If the brief explicitly requires standalone Events, Appointments, or Services pages — include them.
393
+ The patterns describe what a typical storefront looks like without specific requirements, not BuildGuard rules.
310
394
 
311
395
  ## LAYOUT ALIGNMENT — MANDATORY
312
396
 
@@ -2,4 +2,4 @@
2
2
  * Runtime hooks reference — extracted from @sellhapi/studio-runtime.
3
3
  * This resource teaches AI clients the exact hook signatures and return shapes.
4
4
  */
5
- export declare const RUNTIME_HOOKS = "\n# @sellhapi/studio-runtime \u2014 Hook Reference\n\nAll imports come from \"@sellhapi/studio-runtime\". No other network/data access is allowed.\n\n## IMPORTANT RULES\n- All prices are in kobo (divide by 100 for Naira display): product.priceKobo / 100\n- product.images is string[] \u2014 each element IS the URL. NEVER access .url on it.\n- Always include type: 'physical' | 'digital' when calling addItem()\n- If you use useCart/addItem, you MUST render <CartDrawer /> in App.tsx\n- FetchState<T> = { data: T; loading: boolean; error: string | null }\n\n---\n\n## ROUTING (required for multi-page storefronts)\n\nuseHashRouter()\n Returns: { route: string; params: Record<string, string>; navigate: (route: string) => void }\n Usage: const { route, navigate } = useHashRouter();\n Routes: \"#/\" (home), \"#/product/:id\", \"#/digital/:id\", \"#/services/:id\", etc.\n\n---\n\n## PHYSICAL PRODUCTS\n\nuseProducts(options?)\n Options: { limit?: number; categoryId?: string; collectionId?: string; sectionId?: string; sort?: \"newest\"|\"price_asc\"|\"price_desc\"; search?: string; ids?: string[] }\n Returns: FetchState<StoreProduct[]>\n StoreProduct: { id, name, priceKobo, compareAtPriceKobo, imageUrl, images: string[], category, type: \"physical\"|\"digital\", variants: StoreProductVariant[], inStock }\n\nuseProduct(productId: string)\n Returns: FetchState<StoreProductDetail | null>\n StoreProductDetail: extends StoreProduct + { description, variants: StoreProductVariant[] }\n StoreProductVariant: { id, size, color, priceKobo, compareAtPriceKobo, stock, imageUrl }\n\nuseCategories()\n Returns: FetchState<StoreCategory[]>\n StoreCategory: { name: string; count: number }\n\nuseProductCollections()\n Returns: FetchState<ProductCollection[]>\n ProductCollection: { id, name, coverImage: string | null }\n\nuseSections()\n Returns: FetchState<StoreSection[]>\nuseSection(sectionId: string)\n Returns: FetchState<StoreProduct[]>\n\n---\n\n## CART & CHECKOUT\n\nuseCart()\n Returns: {\n items: CartItem[]; count: number; totalKobo: number; savingsKobo: number; isEmpty: boolean;\n addItem: (item: { productId, name, priceKobo, imageUrl?, variantId?, variantLabel?, quantity?, type: \"physical\"|\"digital\" }) => void;\n removeItem: (productId, variantId?) => void;\n updateQty: (productId, quantity, variantId?) => void;\n clear: () => void;\n openCart: () => void; // MUST call after addItem \u2014 opens the cart drawer\n openCheckout: () => void;\n }\n\nCartDrawer \u2014 REQUIRED component if using cart. Import from \"@sellhapi/studio-runtime\".\n Usage: <CartDrawer /> in App.tsx root. Without it, openCart() does nothing.\n\nuseCartCheckout()\n Returns: { initiate: (customer: { name, email, phone, address? }, options?: { deliveryMethod?, shippingRateId?, discountCode?, note? }) => Promise<CommerceActionResult>; loading; error }\n CommerceActionResult: { paymentLink: string } \u2014 redirect to paymentLink\n\nuseProductCheckout()\n Returns: { initiate: (input: { productId, quantity, variantId?, customer: { name, email, phone, address? }, deliveryMethod? }) => Promise<CommerceActionResult>; loading; error }\n Use for single-product \"Buy Now\" flows only. For multi-product stores, use cart.\n\nuseShippingRates()\n Returns: FetchState<ShippingRate[]>\n ShippingRate: { id, name, amountKobo, estimatedDays }\n\nuseValidateDiscount()\n Returns: { validate: (code: string) => Promise<DiscountValidationResult>; validating; error }\n\nuseCheckoutConfig()\n Returns: FetchState<CheckoutConfig>\n CheckoutConfig: { deliveryEnabled, pickupEnabled, deliveryOnly, pickupOnly }\n\n---\n\n## DIGITAL PRODUCTS\n\nuseDigitalProducts(options?)\n Options: { limit?: number }\n Returns: FetchState<StoreProduct[]> (filtered to type=\"digital\")\n\nuseDigitalProduct(productId: string)\n Returns: FetchState<StoreProductDetail | null>\n\n---\n\n## SERVICES\n\nuseServiceListings(options?)\n Options: { limit?: number; category?: string }\n Returns: FetchState<StorefrontServiceListing[]>\n StorefrontServiceListing: { id, title, description, coverImage, category, priceKobo, availability, addOns: ServiceAddOn[], subServices: SubService[] }\n\nuseServiceListing(listingId: string)\n Returns: FetchState<StorefrontServiceListing | null>\n\nuseCheckoutService()\n Returns: { checkout: (input: { listingId, customer: { name, email, phone }, addOnIds?, subServiceIds? }) => Promise<{ paymentLink }>; loading; error }\n\nuseInitiateEngagement()\n Returns: { submit: (input: { listingId, customer: { name, email, phone }, message? }) => Promise<void>; submitting; submitted; error }\n\n---\n\n## APPOINTMENTS\n\nuseServices(options?)\n Options: { limit?: number }\n Returns: FetchState<StorefrontService[]>\n StorefrontService: { id, name, description, durationMinutes, priceKobo, imageUrl, category }\n\nuseAvailableDates(options)\n Options: { serviceId: string; month: string } (month format: \"YYYY-MM\")\n Returns: FetchState<string[]> \u2014 array of date strings \"YYYY-MM-DD\"\n\nuseAvailability(options)\n Options: { serviceId: string; date: string } (date format: \"YYYY-MM-DD\")\n Returns: FetchState<AvailabilitySlot[]>\n AvailabilitySlot: { id, startTime, endTime, status }\n\nuseBookAppointment()\n Returns: { initiate: (input: { serviceId, slotId, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\n---\n\n## EVENTS\n\nuseEvents(options?)\n Options: { limit?: number }\n Returns: FetchState<StorefrontEvent[]>\n StorefrontEvent: { id, name, description, date, endDate, location, imageUrl, ticketTypes: StorefrontTicketType[] }\n StorefrontTicketType: { id, name, priceKobo, quantity, sold, available }\n\nuseEvent(eventId: string)\n Returns: FetchState<StorefrontEvent | null>\n\nusePurchaseTickets()\n Returns: { initiate: (input: { eventId, tickets: [{ ticketTypeId, quantity }], customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\nuseCreateRSVP()\n Returns: { submit: (input: { eventId, name, email, phone? }) => Promise<void>; loading; success; error }\n\n---\n\n## DONATIONS\n\nuseDonationCampaigns(options?)\n Options: { limit?: number; ids?: string[] }\n Returns: FetchState<StorefrontDonationCampaign[]>\n StorefrontDonationCampaign: { id, name, description, imageUrl, goalAmountKobo: number|null, raisedAmountKobo: number }\n WARNING: .raised, .raisedAmount, .goalAmount DO NOT EXIST. Use .raisedAmountKobo and .goalAmountKobo only.\n\nuseDonationCampaign(campaignId: string)\n Returns: FetchState<StorefrontDonationCampaign | null>\n\nuseDonate()\n Returns: { initiate: (input: { campaignId, amountKobo, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\n---\n\n## MEMBERSHIPS\n\nuseMemberships()\n Returns: FetchState<StorefrontMembershipTier[]>\n StorefrontMembershipTier: { id, name, description, priceKobo, billingPeriod: \"monthly\"|\"yearly\"|\"one_time\", benefits: string[]|null, imageUrl: string|null, isFeatured }\n WARNING: .price, .amount, .features, .perks DO NOT EXIST. Use .priceKobo and .benefits only.\n WARNING: billingPeriod is \"yearly\" not \"annual\".\n\nuseSubscribeMembership()\n Returns: { initiate: (input: { tierId, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\n---\n\n## CONTENT & CMS\n\nuseContentItems(options: { type: string })\n Type values: \"image\", \"video\", \"testimonial\", \"faq\", \"stat\", \"feature\", \"team_member\", \"logo_cloud\", \"pricing\"\n Returns: FetchState<ContentItem[]>\n\nConvenience aliases (all return FetchState<ContentItem[]>):\n useContentImages(), useContentVideos(), useContentTestimonials(), useContentFaqs(),\n useContentStats(), useContentFeatures(), useContentTeam(), useContentLogoCloud(), useContentPricing()\n\nuseArticles(options?)\n Options: { limit?: number; category?: string; tag?: string }\n Returns: FetchState<StorefrontArticle[]>\n\nuseArticle(slug: string)\n Returns: FetchState<StorefrontArticle | null>\n\nuseContentForm(formId: string)\n Returns: { form: ContentFormSchema | null; loading; error }\n ContentFormSchema: { id, name, fields: ContentFormField[] }\n ContentFormField: { id, label, type, required, options?, placeholder? }\n\nuseSubmitForm(formId: string)\n Returns: { submit: (data: Record<string, unknown>) => Promise<void>; submitting; submitted; error }\n\nuseMailingListSubscribe()\n Returns: { subscribe: (email: string) => Promise<void>; subscribing; subscribed; error }\n\n---\n\n## LISTINGS\n\nuseListings(options?)\n Options: { listingTypeId?: string; page?: number; limit?: number; filters?: Record<string, string> }\n Returns: { listings: StorefrontListing[]; total; page; totalPages; loading; error }\n\nuseListingDetail(slugOrId: string)\n Returns: { listing: StorefrontListingDetail | null; related: StorefrontListing[]; loading; error }\n\nuseListingFilters(listingTypeId?: string)\n Returns: { fieldDefinitions; params; setParam; reset; loading; error }\n\nAction hooks (each takes listingId):\n useListingEnquiry(id) \u2014 { submit: ({ name, email?, phone?, message? }) => Promise<void>; loading; success; error }\n useListingApplication(id) \u2014 { submit: ({ name, email, phone?, coverNote?, resumeUrl? }) => Promise<void>; loading; success; error }\n useListingPayment(id) \u2014 { checkout: (customer, fieldData?) => Promise<void>; loading; error }\n useListingReserve(id) \u2014 { reserve: (data?) => Promise<void>; loading; success; alreadyReserved; error }\n useListingDownload(id) \u2014 { download: () => Promise<void>; downloadUrl; loading; error }\n\n---\n\n## CUSTOMER ACCOUNTS\n\nuseCustomerAuth()\n Returns: { customer: { id, name, email, phone, address } | null; isLoggedIn: boolean; isLoading: boolean;\n signUp: (name, email, password) => Promise<void>; signIn: (email, password) => Promise<void>; signOut: () => void; error }\n WARNING: field is isLoggedIn, NOT isAuthenticated.\n\nuseMyOrders() \u2014 { orders: MyOrder[]; loading; error }\nuseMyAppointments() \u2014 { appointments: MyAppointment[]; loading; error }\nuseMyPurchases() \u2014 { purchases: MyPurchase[]; loading; error }\nuseMyMembership() \u2014 { membership: MyMembership | null; loading; error }\nuseMyProfile() \u2014 { profile: { name, email, phone, address } | null; update: ({ name, phone, address }) => Promise<void>; updating; updateError }\n\n---\n\n## ORDER TRACKING\n\nuseOrderStatus() \u2014 { status: OrderStatusData | null; loading; error }\nuseOrderLookup() \u2014 { lookup: (orderRef, email) => Promise<OrderLookupItem[]>; items; loading; error }\n\n---\n\n## COMPONENTS (import from \"@sellhapi/studio-runtime\")\n\nCartDrawer \u2014 renders the slide-out cart drawer. REQUIRED if using useCart().\n";
5
+ export declare const RUNTIME_HOOKS = "\n# @sellhapi/studio-runtime \u2014 Hook Reference\n\nAll imports come from \"@sellhapi/studio-runtime\". No other network/data access is allowed.\n\n## IMPORTANT RULES\n- All prices are in kobo (divide by 100 for Naira display): product.priceKobo / 100\n- product.images is string[] \u2014 each element IS the URL. NEVER access .url on it.\n- Always include type: 'physical' | 'digital' when calling addItem()\n- If you use useCart/addItem, you MUST render <CartDrawer /> in App.tsx\n- FetchState<T> = { data: T; loading: boolean; error: string | null }\n\n---\n\n## ROUTING (required for multi-page storefronts)\n\nuseHashRouter()\n Returns: { route: string; params: Record<string, string>; navigate: (route: string) => void }\n Usage: const { route, navigate } = useHashRouter();\n Routes: \"#/\" (home), \"#/product/:id\", \"#/digital/:id\", \"#/services/:id\", etc.\n\n---\n\n## PHYSICAL PRODUCTS\n\nuseProducts(options?)\n Options: { limit?: number; categoryId?: string; collectionId?: string; sectionId?: string; sort?: \"newest\"|\"price_asc\"|\"price_desc\"; search?: string; ids?: string[] }\n Returns: FetchState<StoreProduct[]>\n StoreProduct: {\n id, name, priceKobo, imageUrl, images: string[], category,\n type: \"physical\"|\"digital\", inStock,\n originalPriceKobo: number | undefined, // strikethrough price \u2014 show when > priceKobo\n discountBadge: string | null, // pre-formatted e.g. \"20% Off\" \u2014 show as badge\n priceRange: { minKobo: number; maxKobo: number } | null, // when variants differ in price\n variants: StoreProductVariant[],\n }\n WARNING: compareAtPriceKobo does NOT exist. Use originalPriceKobo for strikethrough price.\n WARNING: images is string[] \u2014 each element IS the URL. Never access .url on it.\n WARNING: some image URLs contain spaces \u2014 always encode: u.includes(' ') ? u.split('/').map(encodeURIComponent).join('/') : u\n\nuseProduct(productId: string)\n Returns: FetchState<StoreProductDetail | null>\n StoreProductDetail: extends StoreProduct + { description, variants: StoreProductVariant[] }\n StoreProductVariant: { id, options: Array<{ field: string; value: string }>, priceKobo?: number, stock?: number | null }\n WARNING: variants have NO size/color/name fields \u2014 iterate options array for labels\n Variant label helper: v.options?.map(o => o.value).join(' / ') || 'Standard'\n\nuseCategories()\n Returns: FetchState<StoreCategory[]>\n StoreCategory: { name: string; count: number }\n\nuseProductCollections()\n Returns: FetchState<ProductCollection[]>\n ProductCollection: { id, name, coverImage: string | null }\n\nuseSections()\n Returns: FetchState<StoreSection[]>\nuseSection(sectionId: string)\n Returns: FetchState<StoreProduct[]>\n\n---\n\n## CART & CHECKOUT\n\nuseCart()\n Returns: {\n items: CartItem[]; count: number; totalKobo: number; savingsKobo: number; isEmpty: boolean;\n addItem: (item: { productId, name, priceKobo, imageUrl?, variantId?, variantLabel?, quantity?, type: \"physical\"|\"digital\" }) => void;\n removeItem: (productId, variantId?) => void;\n updateQty: (productId, quantity, variantId?) => void;\n clear: () => void;\n openCart: () => void; // MUST call after addItem \u2014 opens the cart drawer\n openCheckout: () => void;\n }\n\nCartDrawer \u2014 REQUIRED component if using cart. Import from \"@sellhapi/studio-runtime\".\n Usage: <CartDrawer /> in App.tsx root. Without it, openCart() does nothing.\n\nuseCartCheckout()\n Returns: { initiate: (customer: { name, email, phone, address? }, options?: { deliveryMethod?, shippingRateId?, discountCode?, note? }) => Promise<CommerceActionResult>; loading; error }\n CommerceActionResult: { paymentLink: string } \u2014 redirect to paymentLink\n\nuseProductCheckout()\n Returns: { initiate: (input: { productId, quantity, variantId?, customer: { name, email, phone, address? }, deliveryMethod? }) => Promise<CommerceActionResult>; loading; error }\n Use for single-product \"Buy Now\" flows only. For multi-product stores, use cart.\n\nuseShippingRates()\n Returns: FetchState<ShippingRate[]>\n ShippingRate: { id, name, amountKobo, estimatedDays }\n\nuseValidateDiscount()\n Returns: { validate: (code: string) => Promise<DiscountValidationResult>; validating; error }\n\nuseCheckoutConfig()\n Returns: FetchState<CheckoutConfig>\n CheckoutConfig: { deliveryEnabled, pickupEnabled, deliveryOnly, pickupOnly }\n\n---\n\n## DIGITAL PRODUCTS\n\nuseDigitalProducts(options?)\n Options: { limit?: number }\n Returns: FetchState<StoreProduct[]> (filtered to type=\"digital\")\n\nuseDigitalProduct(productId: string)\n Returns: FetchState<StoreProductDetail | null>\n\n---\n\n## SERVICES\n\nuseServiceListings(options?)\n Options: { limit?: number; category?: string }\n Returns: FetchState<StorefrontServiceListing[]>\n StorefrontServiceListing: { id, title, description, coverImage, category, priceKobo, availability, addOns: ServiceAddOn[], subServices: SubService[] }\n\nuseServiceListing(listingId: string)\n Returns: FetchState<StorefrontServiceListing | null>\n\nuseCheckoutService()\n Returns: { checkout: (input: { listingId, customer: { name, email, phone }, addOnIds?, subServiceIds? }) => Promise<{ paymentLink }>; loading; error }\n\nuseInitiateEngagement()\n Returns: { submit: (input: { listingId, customer: { name, email, phone }, message? }) => Promise<void>; submitting; submitted; error }\n\n---\n\n## APPOINTMENTS\n\nuseServices(options?)\n Options: { limit?: number }\n Returns: FetchState<StorefrontService[]>\n StorefrontService: { id, name, description, durationMinutes, priceKobo, imageUrl, category }\n\nuseAvailableDates(options)\n Options: { serviceId: string; month: string } (month format: \"YYYY-MM\")\n Returns: FetchState<string[]> \u2014 array of date strings \"YYYY-MM-DD\"\n\nuseAvailability(options)\n Options: { serviceId: string; from: string; to: string } (both \"YYYY-MM-DD\")\n CRITICAL: to must always be from + 1 day \u2014 NEVER set from === to, it returns no slots\n to = new Date(+new Date(from) + 86400000).toISOString().slice(0, 10)\n Returns: FetchState<AvailabilitySlot[]>\n AvailabilitySlot: { id, startTime, endTime, status: \"available\"|\"claimed\"|\"booked\"|\"blocked\" }\n FILTER: only show slots where status === \"available\" \u2014 the hook returns all statuses\n\nuseBookAppointment()\n Returns: { book: (input: { serviceId, dateTime: string, customerName, customerEmail, customerPhone }) => Promise<CommerceActionResult>; loading; error }\n dateTime: slot.startTime (ISO datetime string from useAvailability result)\n WARNING: NEVER pass slotId \u2014 pass dateTime: slot.startTime\n WARNING: NEVER use initiate() for appointments \u2014 use book(). Returns { paymentLink } \u2014 redirect with window.location.href\n\n---\n\n## EVENTS\n\nuseEvents(options?)\n Options: { limit?: number }\n Returns: FetchState<StorefrontEvent[]>\n StorefrontEvent: { id, name, description, startsAt, endsAt, location, imageUrl, ticketTypes: StorefrontTicketType[] }\n WARNING: event.date and event.endDate do NOT exist \u2014 use startsAt and endsAt (ISO strings)\n StorefrontTicketType: { id, name, priceKobo, quantity, sold, available }\n\nuseEvent(eventId: string)\n Returns: FetchState<StorefrontEvent | null>\n\nusePurchaseTickets()\n Returns: { initiate: (input: { eventId, ticketOrders: [{ ticketTypeId: string, quantity: number }], customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n WARNING: param is ticketOrders not tickets\n\nuseCreateRSVP()\n Returns: { submit: (input: { eventId, name, email, phone? }) => Promise<void>; loading; success; error }\n\n---\n\n## DONATIONS\n\nuseDonationCampaigns(options?)\n Options: { limit?: number; ids?: string[] }\n Returns: FetchState<StorefrontDonationCampaign[]>\n StorefrontDonationCampaign: { id, name, description, imageUrl, goalAmountKobo: number|null, raisedAmountKobo: number }\n WARNING: .raised, .raisedAmount, .goalAmount DO NOT EXIST. Use .raisedAmountKobo and .goalAmountKobo only.\n\nuseDonationCampaign(campaignId: string)\n Returns: FetchState<StorefrontDonationCampaign | null>\n\nuseDonate()\n Returns: { initiate: (input: { campaignId, amountKobo, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\n---\n\n## MEMBERSHIPS\n\nuseMemberships()\n Returns: FetchState<StorefrontMembershipTier[]>\n StorefrontMembershipTier: { id, name, description, priceKobo, billingPeriod: \"monthly\"|\"yearly\"|\"one_time\", benefits: string[]|null, imageUrl: string|null, isFeatured }\n WARNING: .price, .amount, .features, .perks DO NOT EXIST. Use .priceKobo and .benefits only.\n WARNING: billingPeriod is \"yearly\" not \"annual\".\n\nuseSubscribeMembership()\n Returns: { initiate: (input: { tierId, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }\n\n---\n\n## CONTENT & CMS\n\nuseContentItems(options: { type: string })\n Type values: \"image\", \"video\", \"testimonial\", \"faq\", \"stat\", \"feature\", \"team_member\", \"logo_cloud\", \"pricing\"\n Returns: FetchState<ContentItem[]>\n\nConvenience aliases (all return FetchState<ContentItem[]>):\n useContentImages(), useContentVideos(), useContentTestimonials(), useContentFaqs(),\n useContentStats(), useContentFeatures(), useContentTeamMembers(), useContentLogoCloud(), useContentPricing()\n WARNING: useContentTeam() does NOT exist \u2014 use useContentTeamMembers()\n\nuseArticles(options?)\n Options: { limit?: number; category?: string; tag?: string }\n Returns: FetchState<StorefrontArticle[]>\n\nuseArticle(slug: string)\n Returns: FetchState<StorefrontArticle | null>\n\nuseContentForm(formId: string)\n Returns: { form: ContentFormSchema | null; loading; error }\n ContentFormSchema: { id, name, fields: ContentFormField[] }\n ContentFormField: { id, label, type, required, options?, placeholder? }\n\nuseSubmitForm(formId: string)\n Returns: { submit: (data: Record<string, unknown>) => Promise<void>; submitting; submitted; error }\n\nuseMailingListSubscribe()\n Returns: { subscribe: (email: string) => Promise<void>; subscribing; subscribed; error }\n\n---\n\n## LISTINGS\n\nuseListings(options?)\n Options: { listingTypeId?: string; page?: number; limit?: number; filters?: Record<string, string> }\n Returns: { listings: StorefrontListing[]; total; page; totalPages; loading; error }\n\nuseListingDetail(slugOrId: string)\n Returns: { listing: StorefrontListingDetail | null; related: StorefrontListing[]; loading; error }\n\nuseListingFilters(listingTypeId?: string)\n Returns: { fieldDefinitions; params; setParam; reset; loading; error }\n\nAction hooks (each takes listingId):\n useListingEnquiry(id) \u2014 { submit: ({ name, email?, phone?, message? }) => Promise<void>; loading; success; error }\n useListingApplication(id) \u2014 { submit: ({ name, email, phone?, coverNote?, resumeUrl? }) => Promise<void>; loading; success; error }\n useListingPayment(id) \u2014 { checkout: (customer, fieldData?) => Promise<void>; loading; error }\n useListingReserve(id) \u2014 { reserve: (data?) => Promise<void>; loading; success; alreadyReserved; error }\n useListingDownload(id) \u2014 { download: () => Promise<void>; downloadUrl; loading; error }\n\n---\n\n## CUSTOMER ACCOUNTS\n\nuseCustomerAuth()\n Returns: { customer: { id, name, email, phone, address } | null; isLoggedIn: boolean; isLoading: boolean;\n signUp: (name, email, password) => Promise<void>; signIn: (email, password) => Promise<void>; signOut: () => void; error }\n WARNING: field is isLoggedIn, NOT isAuthenticated.\n\nuseMyOrders() \u2014 { orders: MyOrder[]; loading; error }\nuseMyAppointments() \u2014 { appointments: MyAppointment[]; loading; error }\nuseMyPurchases() \u2014 { purchases: MyPurchase[]; loading; error }\nuseMyMembership() \u2014 { membership: MyMembership | null; loading; error }\nuseMyProfile() \u2014 { profile: { name, email, phone, address } | null; update: ({ name, phone, address }) => Promise<void>; updating; updateError }\n\n---\n\n## ORDER TRACKING\n\nuseOrderStatus() \u2014 { status: OrderStatusData | null; loading; error }\nuseOrderLookup() \u2014 { lookup: (orderRef, email) => Promise<OrderLookupItem[]>; items; loading; error }\n\n---\n\n## COMPONENTS (import from \"@sellhapi/studio-runtime\")\n\nCartDrawer \u2014 renders the slide-out cart drawer. REQUIRED if using useCart().\n";
@@ -33,12 +33,24 @@ useHashRouter()
33
33
  useProducts(options?)
34
34
  Options: { limit?: number; categoryId?: string; collectionId?: string; sectionId?: string; sort?: "newest"|"price_asc"|"price_desc"; search?: string; ids?: string[] }
35
35
  Returns: FetchState<StoreProduct[]>
36
- StoreProduct: { id, name, priceKobo, compareAtPriceKobo, imageUrl, images: string[], category, type: "physical"|"digital", variants: StoreProductVariant[], inStock }
36
+ StoreProduct: {
37
+ id, name, priceKobo, imageUrl, images: string[], category,
38
+ type: "physical"|"digital", inStock,
39
+ originalPriceKobo: number | undefined, // strikethrough price — show when > priceKobo
40
+ discountBadge: string | null, // pre-formatted e.g. "20% Off" — show as badge
41
+ priceRange: { minKobo: number; maxKobo: number } | null, // when variants differ in price
42
+ variants: StoreProductVariant[],
43
+ }
44
+ WARNING: compareAtPriceKobo does NOT exist. Use originalPriceKobo for strikethrough price.
45
+ WARNING: images is string[] — each element IS the URL. Never access .url on it.
46
+ WARNING: some image URLs contain spaces — always encode: u.includes(' ') ? u.split('/').map(encodeURIComponent).join('/') : u
37
47
 
38
48
  useProduct(productId: string)
39
49
  Returns: FetchState<StoreProductDetail | null>
40
50
  StoreProductDetail: extends StoreProduct + { description, variants: StoreProductVariant[] }
41
- StoreProductVariant: { id, size, color, priceKobo, compareAtPriceKobo, stock, imageUrl }
51
+ StoreProductVariant: { id, options: Array<{ field: string; value: string }>, priceKobo?: number, stock?: number | null }
52
+ WARNING: variants have NO size/color/name fields — iterate options array for labels
53
+ Variant label helper: v.options?.map(o => o.value).join(' / ') || 'Standard'
42
54
 
43
55
  useCategories()
44
56
  Returns: FetchState<StoreCategory[]>
@@ -133,12 +145,18 @@ useAvailableDates(options)
133
145
  Returns: FetchState<string[]> — array of date strings "YYYY-MM-DD"
134
146
 
135
147
  useAvailability(options)
136
- Options: { serviceId: string; date: string } (date format: "YYYY-MM-DD")
148
+ Options: { serviceId: string; from: string; to: string } (both "YYYY-MM-DD")
149
+ CRITICAL: to must always be from + 1 day — NEVER set from === to, it returns no slots
150
+ to = new Date(+new Date(from) + 86400000).toISOString().slice(0, 10)
137
151
  Returns: FetchState<AvailabilitySlot[]>
138
- AvailabilitySlot: { id, startTime, endTime, status }
152
+ AvailabilitySlot: { id, startTime, endTime, status: "available"|"claimed"|"booked"|"blocked" }
153
+ FILTER: only show slots where status === "available" — the hook returns all statuses
139
154
 
140
155
  useBookAppointment()
141
- Returns: { initiate: (input: { serviceId, slotId, customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }
156
+ Returns: { book: (input: { serviceId, dateTime: string, customerName, customerEmail, customerPhone }) => Promise<CommerceActionResult>; loading; error }
157
+ dateTime: slot.startTime (ISO datetime string from useAvailability result)
158
+ WARNING: NEVER pass slotId — pass dateTime: slot.startTime
159
+ WARNING: NEVER use initiate() for appointments — use book(). Returns { paymentLink } — redirect with window.location.href
142
160
 
143
161
  ---
144
162
 
@@ -147,14 +165,16 @@ useBookAppointment()
147
165
  useEvents(options?)
148
166
  Options: { limit?: number }
149
167
  Returns: FetchState<StorefrontEvent[]>
150
- StorefrontEvent: { id, name, description, date, endDate, location, imageUrl, ticketTypes: StorefrontTicketType[] }
168
+ StorefrontEvent: { id, name, description, startsAt, endsAt, location, imageUrl, ticketTypes: StorefrontTicketType[] }
169
+ WARNING: event.date and event.endDate do NOT exist — use startsAt and endsAt (ISO strings)
151
170
  StorefrontTicketType: { id, name, priceKobo, quantity, sold, available }
152
171
 
153
172
  useEvent(eventId: string)
154
173
  Returns: FetchState<StorefrontEvent | null>
155
174
 
156
175
  usePurchaseTickets()
157
- Returns: { initiate: (input: { eventId, tickets: [{ ticketTypeId, quantity }], customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }
176
+ Returns: { initiate: (input: { eventId, ticketOrders: [{ ticketTypeId: string, quantity: number }], customer: { name, email, phone } }) => Promise<CommerceActionResult>; loading; error }
177
+ WARNING: param is ticketOrders not tickets
158
178
 
159
179
  useCreateRSVP()
160
180
  Returns: { submit: (input: { eventId, name, email, phone? }) => Promise<void>; loading; success; error }
@@ -198,7 +218,8 @@ useContentItems(options: { type: string })
198
218
 
199
219
  Convenience aliases (all return FetchState<ContentItem[]>):
200
220
  useContentImages(), useContentVideos(), useContentTestimonials(), useContentFaqs(),
201
- useContentStats(), useContentFeatures(), useContentTeam(), useContentLogoCloud(), useContentPricing()
221
+ useContentStats(), useContentFeatures(), useContentTeamMembers(), useContentLogoCloud(), useContentPricing()
222
+ WARNING: useContentTeam() does NOT exist — use useContentTeamMembers()
202
223
 
203
224
  useArticles(options?)
204
225
  Options: { limit?: number; category?: string; tag?: string }
@@ -5,4 +5,4 @@
5
5
  * apply_wireframe. The AI client generates mid-fidelity wireframes per page,
6
6
  * presents each one to the user for review, then submits the approved set.
7
7
  */
8
- export declare const WIREFRAME_SCHEMA = "\n# SellHapi Wireframe Schema\n\nWireframes are mid-fidelity HTML page layouts \u2014 one per storefront page.\nThey are the USER'S checkpoint before code is written: present each page\nwireframe in the conversation, collect feedback, revise if needed, then\ncall apply_wireframe only after the user approves the full set.\n\n---\n\n## Data shape\n\n```\n{ [pageSlug: string]: htmlString }\n```\n\n- Each key is a page slug that MUST match one of the slugs in designDirection.pages\n- Each value is a valid HTML string for that page's mid-fidelity wireframe\n- All pages from designDirection.pages should be included (missing pages means no wireframe guidance for code generation on that page)\n\n---\n\n## What a wireframe IS\n\nMid-fidelity structural HTML. It communicates:\n- Which sections appear on the page and in what order\n- What content each section contains (headings, CTAs, placeholder data)\n- Navigation structure (links, cart icon, CTA button)\n- Footer structure (columns, links)\n- Which runtime hook each section will use\n\nIt does NOT include:\n- Real CSS (use only inline style for basic layout structure if needed)\n- JavaScript or React\n- Actual product/service data (use descriptive placeholders)\n- Final colour values or fonts (those come from designDirection.visualDirection)\n\n---\n\n## Structure rules\n\n### Navigation \u2014 MANDATORY on every page\n```html\n<nav>\n <div class=\"nav-logo\">[Business Name]</div>\n <ul class=\"nav-links\">\n <li><a href=\"#/home\">Home</a></li>\n <li><a href=\"#/shop\">Shop</a></li>\n <!-- one <li> per page in designDirection.pages -->\n </ul>\n <!-- If physical_products or digital_products in capabilityIds: -->\n <button class=\"nav-cart\">Cart (0)</button>\n <button class=\"nav-cta\">[Primary CTA]</button>\n</nav>\n```\n\n- Navigation HTML must be **identical across all pages** \u2014 copy verbatim after the first page\n- Highlight the current page's link as active (add class=\"active\" to that <li>)\n- Cart icon only when capability includes physical_products or digital_products\n\n### Footer \u2014 MANDATORY on every page\n```html\n<footer>\n <div class=\"footer-col\">\n <div class=\"footer-logo\">[Business Name]</div>\n <p>[tagline]</p>\n </div>\n <div class=\"footer-col\">\n <h4>NAVIGATE</h4>\n <ul><!-- one <li> per page --></ul>\n </div>\n <div class=\"footer-col\">\n <h4>RESOURCES</h4>\n <ul><!-- relevant links for this business type --></ul>\n </div>\n <div class=\"footer-col\">\n <h4>CONNECT</h4>\n <!-- social platform links -->\n </div>\n <div class=\"footer-bottom\">\u00A9 [year] [Business Name]. All rights reserved.</div>\n</footer>\n```\n\n- Footer HTML must be **identical across all pages** \u2014 copy verbatim after the first page\n\n### Sections\nEach section gets a wrapper with a comment and data-hook attribute:\n\n```html\n<!-- Hero Section -->\n<section data-section=\"hero\" data-hook=\"none\">\n <h1>[Headline \u2014 use the business tagline]</h1>\n <p>[Supporting copy]</p>\n <button>[Primary CTA]</button>\n <div class=\"hero-image\">[Hero image placeholder]</div>\n</section>\n\n<!-- Product Grid Section -->\n<section data-section=\"product-grid\" data-hook=\"useProducts\">\n <h2>Our Products</h2>\n <div class=\"product-grid\">\n <div class=\"product-card\">\n <div class=\"product-image\">[Product image]</div>\n <h3>[Product name]</h3>\n <p>[Price in \u20A6]</p>\n <button>Add to Cart</button>\n </div>\n <!-- repeat 2 more placeholder cards -->\n </div>\n</section>\n\n<!-- Testimonials Section -->\n<section data-section=\"testimonials\" data-hook=\"useContentTestimonials\">\n <h2>What Customers Say</h2>\n <div class=\"testimonials-grid\">\n <blockquote>[Testimonial 1]</blockquote>\n <blockquote>[Testimonial 2]</blockquote>\n <blockquote>[Testimonial 3]</blockquote>\n </div>\n</section>\n```\n\n---\n\n## data-hook values by capability\n\nUse these exact hook names in data-hook attributes so code generation\nmatches the correct runtime hook:\n\n| Section type | data-hook |\n|-------------------------------|---------------------------------|\n| Product listing / grid | useProducts |\n| Product collections | useProductCollections |\n| Single collection | useCollection |\n| Category sections | useSections |\n| Service listings | useServiceListings |\n| Appointment booking | useBookAppointment |\n| Available dates | useAvailableDates |\n| Events listing | useEvents |\n| Donation campaigns | useDonationCampaigns |\n| Membership tiers | useMemberships |\n| Content images | useContentImages |\n| Testimonials | useContentTestimonials |\n| FAQs | useContentFaqs |\n| Team members | useContentTeam |\n| Stats / social proof | useContentStats |\n| Feature highlights | useContentFeatures |\n| Contact / enquiry form | useSubmitForm |\n| Listings (classifieds/jobs) | useListings |\n| Non-data sections (hero, about, footer, nav) | none |\n\n---\n\n## Presenting wireframes to the user\n\nBefore calling apply_wireframe, show each page to the user like this:\n\n```\n\uD83D\uDCC4 HOME PAGE\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n[NAV] Logo | Home \u00B7 Shop \u00B7 About \u00B7 Contact [Cart] [Shop Now]\n\n[HERO]\n \"Authentic Nigerian fashion for the bold\"\n Subheading + CTA button\n Full-width hero image\n\n[FEATURED PRODUCTS] (useProducts)\n 3-column product grid \u2014 6 products\n Each card: image, name, price, Add to Cart\n\n[COLLECTIONS] (useProductCollections)\n 4 collection tiles with cover images\n\n[TESTIMONIALS] (useContentTestimonials)\n 3 customer quotes in a row\n\n[NEWSLETTER]\n Email capture form\n\n[FOOTER] 4-column footer\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nDoes this structure look right? Any sections to add, remove, or reorder?\n```\n\nWait for explicit user approval before proceeding to apply_wireframe.\nIf the user requests changes, update the affected page(s) and show again.\n\n---\n\n## Submission\n\nOnce ALL pages are approved by the user:\n\n```\napply_wireframe(projectId, {\n \"home\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"shop\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"about\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"contact\": \"<nav>...</nav><main>...</main><footer>...</footer>\"\n})\n```\n\nReturns: `{ pageCount: 4, slugs: [\"home\", \"shop\", \"about\", \"contact\"] }`\n\nAfter this, proceed to generate React code using the wireframe as your\nstructural reference. The wireframe defines WHAT to build \u2014 runtime-hooks\nand build-rules define HOW to build it in React.\n";
8
+ export declare const WIREFRAME_SCHEMA = "\n# SellHapi Wireframe Schema\n\nWireframes are mid-fidelity HTML page layouts \u2014 one per storefront page.\nThey are the USER'S checkpoint before code is written: present each page\nwireframe in the conversation, collect feedback, revise if needed, then\ncall apply_wireframe only after the user approves the full set.\n\n---\n\n## Data shape\n\n```\n{ [pageSlug: string]: htmlString }\n```\n\n- Each key is a page slug that MUST match one of the slugs in designDirection.pages\n- Each value is a valid HTML string for that page's mid-fidelity wireframe\n- All pages from designDirection.pages should be included (missing pages means no wireframe guidance for code generation on that page)\n\n---\n\n## What a wireframe IS\n\nMid-fidelity structural HTML. It communicates:\n- Which sections appear on the page and in what order\n- What content each section contains (headings, CTAs, placeholder data)\n- Navigation structure (links, cart icon, CTA button)\n- Footer structure (columns, links)\n- Which runtime hook each section will use\n\nIt does NOT include:\n- Real CSS (use only inline style for basic layout structure if needed)\n- JavaScript or React\n- Actual product/service data (use descriptive placeholders)\n- Final colour values or fonts (those come from designDirection.visualDirection)\n\n---\n\n## Structure rules\n\n### Navigation \u2014 MANDATORY on every page\n```html\n<nav>\n <div class=\"nav-logo\">[Business Name]</div>\n <ul class=\"nav-links\">\n <li><a href=\"#/home\">Home</a></li>\n <li><a href=\"#/shop\">Shop</a></li>\n <!-- one <li> per page in designDirection.pages -->\n </ul>\n <!-- If physical_products or digital_products in capabilityIds: -->\n <button class=\"nav-cart\">Cart (0)</button>\n <button class=\"nav-cta\">[Primary CTA]</button>\n</nav>\n```\n\n- Navigation HTML must be **identical across all pages** \u2014 copy verbatim after the first page\n- Highlight the current page's link as active (add class=\"active\" to that <li>)\n- Cart icon only when capability includes physical_products or digital_products\n\n### Footer \u2014 MANDATORY on every page\n```html\n<footer>\n <div class=\"footer-col\">\n <div class=\"footer-logo\">[Business Name]</div>\n <p>[tagline]</p>\n </div>\n <div class=\"footer-col\">\n <h4>NAVIGATE</h4>\n <ul><!-- one <li> per page --></ul>\n </div>\n <div class=\"footer-col\">\n <h4>RESOURCES</h4>\n <ul><!-- relevant links for this business type --></ul>\n </div>\n <div class=\"footer-col\">\n <h4>CONNECT</h4>\n <!-- social platform links -->\n </div>\n <div class=\"footer-bottom\">\u00A9 [year] [Business Name]. All rights reserved.</div>\n</footer>\n```\n\n- Footer HTML must be **identical across all pages** \u2014 copy verbatim after the first page\n\n### Sections\nEach section gets a wrapper with a comment and data-hook attribute:\n\n```html\n<!-- Hero Section -->\n<section data-section=\"hero\" data-hook=\"none\">\n <h1>[Headline \u2014 use the business tagline]</h1>\n <p>[Supporting copy]</p>\n <button>[Primary CTA]</button>\n <div class=\"hero-image\">[Hero image placeholder]</div>\n</section>\n\n<!-- Product Grid Section -->\n<section data-section=\"product-grid\" data-hook=\"useProducts\">\n <h2>Our Products</h2>\n <div class=\"product-grid\">\n <div class=\"product-card\">\n <div class=\"product-image\">[Product image]</div>\n <h3>[Product name]</h3>\n <p>[Price in \u20A6]</p>\n <button>Add to Cart</button>\n </div>\n <!-- repeat 2 more placeholder cards -->\n </div>\n</section>\n\n<!-- Testimonials Section -->\n<section data-section=\"testimonials\" data-hook=\"useContentTestimonials\">\n <h2>What Customers Say</h2>\n <div class=\"testimonials-grid\">\n <blockquote>[Testimonial 1]</blockquote>\n <blockquote>[Testimonial 2]</blockquote>\n <blockquote>[Testimonial 3]</blockquote>\n </div>\n</section>\n```\n\n---\n\n## data-hook values by capability\n\nUse these exact hook names in data-hook attributes so code generation\nmatches the correct runtime hook:\n\n| Section type | data-hook |\n|-------------------------------|---------------------------------|\n| Product listing / grid | useProducts |\n| Product collections | useProductCollections |\n| Single collection | useCollection |\n| Category sections | useSections |\n| Service listings | useServiceListings |\n| Appointment booking | useBookAppointment |\n| Available dates | useAvailableDates |\n| Events listing | useEvents |\n| Donation campaigns | useDonationCampaigns |\n| Membership tiers | useMemberships |\n| Content images | useContentImages |\n| Testimonials | useContentTestimonials |\n| FAQs | useContentFaqs |\n| Team members | useContentTeamMembers |\n| Stats / social proof | useContentStats |\n| Feature highlights | useContentFeatures |\n| Contact / enquiry form | useSubmitForm |\n| Listings (classifieds/jobs) | useListings |\n| Non-data sections (hero, about, footer, nav) | none |\n\n---\n\n## Presenting wireframes to the user\n\nBefore calling apply_wireframe, show each page to the user like this:\n\n```\n\uD83D\uDCC4 HOME PAGE\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n[NAV] Logo | Home \u00B7 Shop \u00B7 About \u00B7 Contact [Cart] [Shop Now]\n\n[HERO]\n \"Authentic Nigerian fashion for the bold\"\n Subheading + CTA button\n Full-width hero image\n\n[FEATURED PRODUCTS] (useProducts)\n 3-column product grid \u2014 6 products\n Each card: image, name, price, Add to Cart\n\n[COLLECTIONS] (useProductCollections)\n 4 collection tiles with cover images\n\n[TESTIMONIALS] (useContentTestimonials)\n 3 customer quotes in a row\n\n[NEWSLETTER]\n Email capture form\n\n[FOOTER] 4-column footer\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\nDoes this structure look right? Any sections to add, remove, or reorder?\n```\n\nWait for explicit user approval before proceeding to apply_wireframe.\nIf the user requests changes, update the affected page(s) and show again.\n\n---\n\n## Submission\n\nOnce ALL pages are approved by the user:\n\n```\napply_wireframe(projectId, {\n \"home\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"shop\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"about\": \"<nav>...</nav><main>...</main><footer>...</footer>\",\n \"contact\": \"<nav>...</nav><main>...</main><footer>...</footer>\"\n})\n```\n\nReturns: `{ pageCount: 4, slugs: [\"home\", \"shop\", \"about\", \"contact\"] }`\n\nAfter this, proceed to generate React code using the wireframe as your\nstructural reference. The wireframe defines WHAT to build \u2014 runtime-hooks\nand build-rules define HOW to build it in React.\n";
@@ -152,7 +152,7 @@ matches the correct runtime hook:
152
152
  | Content images | useContentImages |
153
153
  | Testimonials | useContentTestimonials |
154
154
  | FAQs | useContentFaqs |
155
- | Team members | useContentTeam |
155
+ | Team members | useContentTeamMembers |
156
156
  | Stats / social proof | useContentStats |
157
157
  | Feature highlights | useContentFeatures |
158
158
  | Contact / enquiry form | useSubmitForm |
@@ -1,9 +1,6 @@
1
1
  /**
2
- * Commerce tools — read-only access to products, services, events, memberships, donations, appointments.
3
- *
4
- * All URLs point to /api/mcp/* (API key auth). Write endpoints are not yet
5
- * available on the MCP route layer — create tools will be added when backend
6
- * write proxies are shipped.
2
+ * Commerce tools — read and write access to products, services, events,
3
+ * memberships, donations, appointments, and forms.
7
4
  */
8
5
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
6
  export declare function registerCommerceTools(server: McpServer): void;
@@ -1,13 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
- * Commerce tools — read-only access to products, services, events, memberships, donations, appointments.
4
- *
5
- * All URLs point to /api/mcp/* (API key auth). Write endpoints are not yet
6
- * available on the MCP route layer — create tools will be added when backend
7
- * write proxies are shipped.
3
+ * Commerce tools — read and write access to products, services, events,
4
+ * memberships, donations, appointments, and forms.
8
5
  */
9
6
  Object.defineProperty(exports, "__esModule", { value: true });
10
7
  exports.registerCommerceTools = registerCommerceTools;
8
+ const v4_1 = require("zod/v4");
11
9
  const client_js_1 = require("../client.js");
12
10
  const util_js_1 = require("../util.js");
13
11
  function registerCommerceTools(server) {
@@ -74,6 +72,134 @@ function registerCommerceTools(server) {
74
72
  const data = await (0, client_js_1.get)("/api/mcp/appointments/services");
75
73
  return JSON.stringify(data.services, null, 2);
76
74
  }));
75
+ // ── Write: Create product ─────────────────────────────────────────────────
76
+ server.registerTool("create_product", {
77
+ title: "Create Product",
78
+ description: "Create a physical or digital product. Call list_capabilities first to confirm the relevant capability (physical_products or digital_products) is active. Prices must be in kobo (₦1 = 100 kobo). imageUrl is optional — use an external URL if the vendor has one, or omit it.",
79
+ inputSchema: {
80
+ name: v4_1.z.string().describe("Product name"),
81
+ description: v4_1.z.string().optional().describe("Product description"),
82
+ priceKobo: v4_1.z.number().int().min(0).describe("Price in kobo (₦1 = 100 kobo)"),
83
+ type: v4_1.z.enum(["physical", "digital"]).optional().default("physical").describe("Product type — physical (default) or digital"),
84
+ stock: v4_1.z.number().int().min(0).optional().describe("Initial stock quantity (0 = out of stock). Omit for untracked stock."),
85
+ imageUrl: v4_1.z.string().optional().describe("External image URL for the product"),
86
+ },
87
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
88
+ const data = await (0, client_js_1.post)("/api/mcp/products", input);
89
+ return JSON.stringify(data, null, 2);
90
+ }));
91
+ // ── Write: Create event ───────────────────────────────────────────────────
92
+ server.registerTool("create_event", {
93
+ title: "Create Event",
94
+ description: "Create an event with optional ticket types in a single call. Events are created as draft — they become visible on the storefront when published from the dashboard. Call list_capabilities first to confirm events capability is active.",
95
+ inputSchema: {
96
+ title: v4_1.z.string().describe("Event title"),
97
+ description: v4_1.z.string().optional().describe("Event description"),
98
+ startsAt: v4_1.z.string().describe("Start datetime — ISO 8601 string e.g. '2025-03-15T10:00:00'"),
99
+ endsAt: v4_1.z.string().describe("End datetime — ISO 8601 string"),
100
+ location: v4_1.z.string().optional().describe("Venue or location name"),
101
+ imageUrl: v4_1.z.string().optional().describe("Event cover image URL"),
102
+ tickets: v4_1.z.array(v4_1.z.object({
103
+ name: v4_1.z.string().describe("Ticket type name e.g. 'General Admission'"),
104
+ priceKobo: v4_1.z.number().int().min(0).describe("Ticket price in kobo (0 = free)"),
105
+ capacity: v4_1.z.number().int().min(1).describe("Total number of tickets available"),
106
+ maxPerOrder: v4_1.z.number().int().optional().describe("Max tickets per order"),
107
+ description: v4_1.z.string().optional(),
108
+ })).optional().default([]).describe("Ticket types to create inline with the event"),
109
+ },
110
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
111
+ const data = await (0, client_js_1.post)("/api/mcp/events", input);
112
+ return JSON.stringify(data, null, 2);
113
+ }));
114
+ // ── Write: Create membership tier ─────────────────────────────────────────
115
+ server.registerTool("create_membership_tier", {
116
+ title: "Create Membership Tier",
117
+ description: "Create a membership tier with benefits list and billing period. Call list_capabilities first to confirm memberships capability is active. Prices in kobo.",
118
+ inputSchema: {
119
+ name: v4_1.z.string().describe("Tier name e.g. 'Gold Member'"),
120
+ description: v4_1.z.string().optional().describe("Tier description"),
121
+ priceKobo: v4_1.z.number().int().min(0).describe("Price in kobo per billing period"),
122
+ billingPeriod: v4_1.z.enum(["monthly", "yearly", "one_time"]).optional().default("monthly"),
123
+ benefits: v4_1.z.array(v4_1.z.string()).optional().describe("List of benefits e.g. ['Free delivery', 'Early access to drops']"),
124
+ imageUrl: v4_1.z.string().optional().describe("Tier cover image URL"),
125
+ maxMembers: v4_1.z.number().int().optional().describe("Maximum members for this tier (null = unlimited)"),
126
+ },
127
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
128
+ const data = await (0, client_js_1.post)("/api/mcp/memberships/tiers", input);
129
+ return JSON.stringify(data, null, 2);
130
+ }));
131
+ // ── Write: Create donation campaign ───────────────────────────────────────
132
+ server.registerTool("create_donation_campaign", {
133
+ title: "Create Donation Campaign",
134
+ description: "Create a fundraising campaign. Campaigns are live immediately after creation. Call list_capabilities first to confirm donations capability is active.",
135
+ inputSchema: {
136
+ title: v4_1.z.string().describe("Campaign title"),
137
+ description: v4_1.z.string().optional().describe("Campaign story / description"),
138
+ goalAmountKobo: v4_1.z.number().int().optional().describe("Fundraising goal in kobo (omit for open-ended campaigns)"),
139
+ imageUrl: v4_1.z.string().optional().describe("Campaign cover image URL"),
140
+ endsAt: v4_1.z.string().optional().describe("Campaign end date — ISO 8601 string (omit for no end date)"),
141
+ presetAmounts: v4_1.z.array(v4_1.z.number().int().min(0)).optional().describe("Preset donation amount options in kobo e.g. [500000, 1000000, 2000000]"),
142
+ },
143
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
144
+ const data = await (0, client_js_1.post)("/api/mcp/donations/campaigns", input);
145
+ return JSON.stringify(data, null, 2);
146
+ }));
147
+ // ── Write: Create appointment service ────────────────────────────────────
148
+ server.registerTool("create_appointment_service", {
149
+ title: "Create Appointment Service",
150
+ description: "Create a bookable appointment service. Note: staff members and their weekly schedules must be set up in the Appointments dashboard before customers can book. Call list_capabilities first to confirm appointments capability is active.",
151
+ inputSchema: {
152
+ name: v4_1.z.string().describe("Service name e.g. 'Hair Consultation'"),
153
+ description: v4_1.z.string().optional(),
154
+ durationMinutes: v4_1.z.number().int().min(1).describe("Service duration in minutes"),
155
+ priceKobo: v4_1.z.number().int().min(0).describe("Price in kobo"),
156
+ imageUrl: v4_1.z.string().optional(),
157
+ bufferMinutes: v4_1.z.number().int().min(0).optional().describe("Buffer time between bookings in minutes (default 0)"),
158
+ maxConcurrentBookings: v4_1.z.number().int().min(1).optional().describe("Max simultaneous bookings for this service (default 1)"),
159
+ },
160
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
161
+ const data = await (0, client_js_1.post)("/api/mcp/appointments/services", input);
162
+ return JSON.stringify(data, null, 2);
163
+ }));
164
+ // ── Write: Create service listing ────────────────────────────────────────
165
+ server.registerTool("create_service_listing", {
166
+ title: "Create Service Listing",
167
+ description: "Create a service listing (freelance/agency-style services with a coverImage, description, and purchase flow). Different from appointment services — service listings are for deliverable-based work like design, photography, consulting. Call list_capabilities first to confirm services capability is active.",
168
+ inputSchema: {
169
+ title: v4_1.z.string().describe("Listing title e.g. 'Brand Identity Package'"),
170
+ description: v4_1.z.string().optional(),
171
+ priceKobo: v4_1.z.number().int().min(0).describe("Price in kobo"),
172
+ category: v4_1.z.string().optional().describe("Service category e.g. 'Design', 'Photography'"),
173
+ coverImageUrl: v4_1.z.string().optional(),
174
+ duration: v4_1.z.string().optional().describe("Delivery timeframe label e.g. '3–5 days'"),
175
+ deliveryTime: v4_1.z.string().optional().describe("Delivery time e.g. '7 days'"),
176
+ },
177
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
178
+ const data = await (0, client_js_1.post)("/api/mcp/service-listings", input);
179
+ return JSON.stringify(data, null, 2);
180
+ }));
181
+ // ── Write: Create form ───────────────────────────────────────────────────
182
+ server.registerTool("create_form", {
183
+ title: "Create Form",
184
+ description: "Create a published storefront form. Returns the form ID — use it in useContentForm(formId) and useSubmitForm(formId) in storefront code. Call list_capabilities first to confirm content capability is active. Field types: text, email, phone, textarea, select, checkbox, date, number.",
185
+ inputSchema: {
186
+ title: v4_1.z.string().describe("Form name e.g. 'Contact Us'"),
187
+ description: v4_1.z.string().optional(),
188
+ fields: v4_1.z.array(v4_1.z.object({
189
+ label: v4_1.z.string().describe("Field label shown to the user"),
190
+ type: v4_1.z.enum(["text", "email", "phone", "textarea", "select", "checkbox", "date", "number"]).describe("Field input type"),
191
+ required: v4_1.z.boolean().optional().default(false),
192
+ placeholder: v4_1.z.string().optional(),
193
+ options: v4_1.z.array(v4_1.z.string()).optional().describe("Options for select fields"),
194
+ })).describe("Form fields in order"),
195
+ submitButtonText: v4_1.z.string().optional().describe("Submit button label (default: 'Submit')"),
196
+ successMessage: v4_1.z.string().optional().describe("Message shown after successful submission"),
197
+ storefrontPurpose: v4_1.z.enum(["contact", "waitlist", "enquiry", "feedback", "application"]).optional().describe("Semantic purpose — helps the storefront and Studio identify this form"),
198
+ },
199
+ }, async (input) => (0, util_js_1.safeCall)(async () => {
200
+ const data = await (0, client_js_1.post)("/api/mcp/content/forms", input);
201
+ return JSON.stringify(data, null, 2);
202
+ }));
77
203
  // ── Capabilities ───────────────────────────────────────────────────────────
78
204
  server.registerTool("list_capabilities", {
79
205
  title: "List Capabilities",
@@ -9,6 +9,8 @@ const client_js_1 = require("../client.js");
9
9
  const util_js_1 = require("../util.js");
10
10
  const runtime_hooks_js_1 = require("../resources/runtime-hooks.js");
11
11
  const build_rules_js_1 = require("../resources/build-rules.js");
12
+ const design_direction_schema_js_1 = require("../resources/design-direction-schema.js");
13
+ const wireframe_schema_js_1 = require("../resources/wireframe-schema.js");
12
14
  function registerStudioTools(server) {
13
15
  // ── List projects ──────────────────────────────────────────────────────────
14
16
  server.registerTool("list_projects", {
@@ -52,13 +54,13 @@ function registerStudioTools(server) {
52
54
  step: 1,
53
55
  tool: "apply_design_direction",
54
56
  required: true,
55
- description: "Generate and store a full DesignDirection — colors, fonts, pages, business context. Do this NOW before anything else. Read sellhapi://design-direction-schema for the exact shape. Present the direction summary to the user (palette, pages, mood) and get confirmation before calling apply_design_direction. Alternatively use set_design_direction to let SellHapi's Claude generate it (costs credits).",
57
+ description: "BEFORE generating any direction JSON, have a SHORT design conversation with the user. Ask: (1) preferred brand colors — or let the AI suggest based on the business type; (2) font style — modern/minimal, editorial/serif, warm/handcrafted, bold/display, playful; (3) if physical_products is active: which display modes to use (product grid / collections / bundles / category sections); (4) overall mood / reference brands they like. Then generate the DesignDirection JSON and present a summary (palette, fonts, pages, mood) for user confirmation before calling apply_design_direction. The design_direction_schema in required_reading has the full shape. Alternatively use set_design_direction to let SellHapi's Claude generate it (costs credits).",
56
58
  },
57
59
  {
58
60
  step: 2,
59
61
  tool: "apply_wireframe",
60
62
  required: false,
61
- description: "Optional but recommended. Generate mid-fidelity HTML wireframes for each page, present them section-by-section to the user, collect feedback, then store the approved wireframes. Read sellhapi://wireframe-schema first.",
63
+ description: "Optional but recommended. Generate mid-fidelity HTML wireframes for each page, present them section-by-section to the user, collect feedback, then store the approved wireframes. The wireframe_schema in required_reading has full conventions and data-hook reference.",
62
64
  },
63
65
  {
64
66
  step: 3,
@@ -75,7 +77,9 @@ function registerStudioTools(server) {
75
77
  ],
76
78
  },
77
79
  required_reading: {
78
- instruction: "Read build_rules and runtime_hooks in full before generating any storefront code. Violating build_rules causes push_code to fail with a BuildGuard error.",
80
+ instruction: "Read ALL four documents below before generating any storefront code. design_direction_schema and wireframe_schema must be read before Step 1. build_rules and runtime_hooks must be read before Step 3 (push_code). Violating build_rules causes push_code to fail with a BuildGuard error.",
81
+ design_direction_schema: design_direction_schema_js_1.DESIGN_DIRECTION_SCHEMA,
82
+ wireframe_schema: wireframe_schema_js_1.WIREFRAME_SCHEMA,
79
83
  build_rules: build_rules_js_1.BUILD_RULES,
80
84
  runtime_hooks: runtime_hooks_js_1.RUNTIME_HOOKS,
81
85
  },
@@ -120,7 +124,7 @@ function registerStudioTools(server) {
120
124
  // ── Push code (zero credits) ───────────────────────────────────────────────
121
125
  server.registerTool("push_code", {
122
126
  title: "Push Code",
123
- description: "Push client-generated React code to a project. Runs BuildGuard validation + esbuild bundling and returns a preview URL. ZERO AI credit cost — you generate the code, SellHapi only builds it. CRITICAL RULES: (1) import ONLY from @sellhapi/studio-runtime and react — NO external packages; (2) every top-level JSX section must have data-region, data-label, data-scope attributes; (3) NO inline style={{}} — use Tailwind only; (4) NO localStorage/sessionStorage/document/window; (5) prices via formatPrice() hook — never hardcode currency. Call get_build_rules or get_runtime_hooks for full reference.",
127
+ description: "Push client-generated React code to a project. Runs BuildGuard validation + esbuild bundling and returns a preview URL. ZERO AI credit cost — you generate the code, SellHapi only builds it. CRITICAL RULES: (1) import ONLY from @sellhapi/studio-runtime and react — NO external packages; (2) every top-level JSX section must have data-region, data-label, data-scope attributes; (3) NO inline style={{}} — use Tailwind only; (4) NO localStorage/sessionStorage for state management — EXCEPTION: window.location.href IS required and allowed for payment redirects (all commerce hooks return { paymentLink } — redirect with window.location.href = result.paymentLink); (5) prices via priceKobo / 100 — never hardcode currency. Call get_build_rules or get_runtime_hooks for full reference.",
124
128
  inputSchema: {
125
129
  projectId: v4_1.z.string().describe("Studio project ID"),
126
130
  files: v4_1.z.record(v4_1.z.string(), v4_1.z.string()).describe("Map of filename → code content. Must include App.tsx as entry point."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellhapi/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "SellHapi MCP server — build and manage SellHapi storefronts from AI tools",
5
5
  "bin": {
6
6
  "sellhapi-mcp": "./dist/index.js"