@greatstore/cli 0.1.0 → 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.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: greatstore
3
+ description: Build AI-powered shopping experiences with GreatStore on a merchant's website and store. Use when installing the GreatStore chat widget on a site, generating AI content for custom UI with generateStructuredContent, adding chat entry points (sendMessage), letting the assistant act on the page via WebMCP page tools (document.modelContext.registerTool), authoring custom in-chat React components with the gs CLI, administering the store from the CLI (gs configure, gs connectors — origin allowlists, CSP hosts, MCP connectors), setting up web push re-engagement, or connecting AI agents to a store's MCP endpoints. Covers setup, schema design, caching behavior, component authoring, store administration, and ready-made recipes.
4
+ ---
5
+
6
+ # Building with GreatStore
7
+
8
+ GreatStore gives a store an AI shopping assistant on two surfaces: a hosted
9
+ storefront at `https://<slug>.greatstore.ai/`, and an embedded chat widget on
10
+ the merchant's own site, installed with one script tag:
11
+
12
+ ```html
13
+ <script src="https://my-store.greatstore.ai/embed.js"></script>
14
+ ```
15
+
16
+ Everything else a site can do with GreatStore is documented in the
17
+ references below. Read the one that matches the task before writing code —
18
+ each facet has non-obvious rules (caching, grounding, result shapes, design
19
+ constraints) that the references spell out.
20
+
21
+ ## Index
22
+
23
+ | Goal | Use | Read |
24
+ |---|---|---|
25
+ | Install the widget; control the panel; readiness, events, troubleshooting | `window.GreatStore` SDK | [references/embed-api.md](references/embed-api.md) |
26
+ | AI-generated, catalog-grounded content rendered in **your own HTML/CSS** (highlights, comparisons, FAQs, gift guides) | `generateStructuredContent(schema, prompt)` | [references/structured-content.md](references/structured-content.md) |
27
+ | Copy-paste on-page experiences | recipes built on the SDK | the [Recipes](#recipes) table below |
28
+ | Contextual **conversation entry points** anywhere on the page | `sendMessage(text)`, `open()`, `?gs_chat=open` | [references/embed-api.md](references/embed-api.md) |
29
+ | Feed the assistant **invisible page/component state** (current product, cart, selected variant) | `updateModelContext(text)`; component `onUpdateModelContext` | [references/embed-api.md](references/embed-api.md), [references/chat-components.md](references/chat-components.md) |
30
+ | Let the assistant **act on the page** (cart, navigation, filters) | WebMCP: `document.modelContext.registerTool(...)` | [references/embed-api.md](references/embed-api.md) |
31
+ | Custom **interactive UI inside the chat** (configurators, quizzes, size guides, booking forms) | remote components shipped with the `gs` CLI | [references/chat-components.md](references/chat-components.md) |
32
+ | Expand a component's image in an **on-brand full-screen lightbox** | component `onShowLightbox({ src, originRect })` | [references/chat-components.md](references/chat-components.md) |
33
+ | **Re-engage shoppers** with browser notifications | merchant-hosted `gs.js` + `enableNotifications()` | [references/push-notifications.md](references/push-notifications.md) |
34
+ | Connect **AI agents** to the store (shopping tools over MCP, CLI docs for coding agents) | the store's MCP endpoints | [references/agents-and-mcp.md](references/agents-and-mcp.md) |
35
+ | **Administer the store** — read config/connectors to ground decisions; self-serve origin allowlists and CSP hosts | `gs configure`, `gs connectors` | [references/store-admin.md](references/store-admin.md) |
36
+
37
+ ## Three things to know before any of it
38
+
39
+ - **Every code sample in this skill is a reference implementation, not a
40
+ drop-in.** Samples are framework-free vanilla JS so they stay portable —
41
+ re-express the same logic in the conventions of the repo you're working
42
+ in (React, Vue, Shopify Liquid sections, Svelte, …); never retrofit the
43
+ sample as-is into a codebase with its own framework.
44
+ - The page's domain **must be in the store's allowed domains**. If it isn't,
45
+ nothing works and the console shows
46
+ `[GreatStore] Chat is unavailable on <origin>…` — check this first whenever
47
+ the embed appears dead. You can verify and fix it yourself with the `gs`
48
+ CLI (read-merge-write on `extraOrigins` — see
49
+ [references/store-admin.md](references/store-admin.md)).
50
+ - Every SDK call is safe immediately after the script tag — pre-mount calls
51
+ queue and replay in order, and the SDK pre-warms itself in the background.
52
+
53
+ ## Recipes
54
+
55
+ Complete, framework-free implementations, one per file. Shared conventions:
56
+ containers start `hidden` and reveal only on success (a failed generation
57
+ changes nothing); generated strings render via `textContent`, never
58
+ `innerHTML`; every recipe guards on `window.GreatStore`; prompts stay
59
+ deterministic per page so repeat renders hit the cache.
60
+ Inline real product/page data into prompts where the platform exposes it.
61
+
62
+ | Recipe | What it builds | Use it for |
63
+ |---|---|---|
64
+ | [GreatStore launchers](recipes/launchers.md) | Horizontally scrollable AI-generated chips — engaging first-person questions about the current page; tap to ask the assistant. | Instant engagement on any page type — product, collection, blog, home. Simple yet effective; start here. |
65
+ | [Ask-about-this entry points](recipes/ask-about-this.md) | One-line `sendMessage` buttons wired to existing page elements. | Size guides, shipping rows, out-of-stock badges — anywhere a shopper hesitates. |
66
+ | [Product FAQ accordion](recipes/product-faq.md) | Grounded pre-purchase Q&A with an "ask us" handoff into chat. | Product pages; answering objections before they cost the sale. |
67
+ | [Comparison table](recipes/comparison-table.md) | AI-picked representative products compared on category-relevant criteria. | Collection pages where shoppers weigh options. |
68
+ | [Complete the look](recipes/complete-the-look.md) | Catalog-grounded cross-sell strip with a reason per pick. | Product pages; raising order value with genuine pairings. |
69
+ | [Campaign hero](recipes/campaign-hero.md) | Seasonal homepage hero copy, cache-keyed to the ISO week. | Fresh homepage/campaign copy without manual rewrites. |
70
+ | [Gift finder funnel](recipes/gift-finder-funnel.md) | Quiz teaser → chat handoff → page-tool navigation; the full funnel. | Gifting seasons, guided discovery, homepage engagement. |
71
+ | [Page-action suite](recipes/page-action-suite.md) | WebMCP cart/page tools every conversation can use. | Any site where the assistant should act, not just advise. |
72
+ | [Custom chat button](recipes/custom-chat-button.md) | Branded launcher synced via `ready` + `open`/`close` events. | Replacing the default launcher with the site's own UI. |
73
+
74
+ ## How the facets combine
75
+
76
+ The strongest pattern is the **teaser → conversation → action** funnel:
77
+ `generateStructuredContent` renders a grounded teaser in the merchant's
78
+ design; each option's click handler calls `sendMessage` with the shopper's
79
+ choice, dropping them into a conversation with momentum; WebMCP page tools
80
+ and custom chat components let that conversation actually do things — add to
81
+ cart, configure a product, book a slot — so it ends in a conversion, not a
82
+ copy-paste. The [gift finder funnel](recipes/gift-finder-funnel.md)
83
+ recipe is this funnel end to end.
@@ -0,0 +1,31 @@
1
+ # "Ask about this" entry points (`sendMessage` only)
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ Zero-generation, instant, and often the biggest engagement win per line of
10
+ code. Sprinkle context-aware buttons wherever a shopper hesitates:
11
+
12
+ ```js
13
+ const gs = window.GreatStore;
14
+ if (gs) {
15
+ sizeGuideLink.addEventListener("click", (e) => {
16
+ e.preventDefault();
17
+ gs.sendMessage(`How does the sizing run on "${productName}"? I usually wear a medium.`);
18
+ });
19
+
20
+ shippingRow.querySelector(".ask").addEventListener("click", () => {
21
+ gs.sendMessage(`What are the shipping options and times for "${productName}"?`);
22
+ });
23
+
24
+ outOfStockBadge?.addEventListener("click", () => {
25
+ gs.sendMessage(`"${productName}" looks out of stock — is there anything similar in stock?`);
26
+ });
27
+ }
28
+ ```
29
+
30
+ Write each message as something the shopper would plausibly say — it appears
31
+ in the transcript as their message.
@@ -0,0 +1,43 @@
1
+ # Campaign hero with deliberate variation
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ Cache-friendly variation: key the prompt to a low-cardinality period, not to
10
+ time itself.
11
+
12
+ ```js
13
+ // ISO week number → one generation per store per week, shared by everyone.
14
+ const week = (d => {
15
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
16
+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
17
+ return `${t.getUTCFullYear()}-W${Math.ceil((((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5) + 1) / 7)}`;
18
+ })(new Date());
19
+
20
+ const data = await window.GreatStore.generateStructuredContent(
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ headline: { type: "string", maxLength: 60 },
25
+ subline: { type: "string", maxLength: 120 },
26
+ featuredProductName: { type: "string", nullable: true },
27
+ ctaChatMessage: { type: "string", maxLength: 120 },
28
+ },
29
+ required: ["headline", "subline", "ctaChatMessage"],
30
+ },
31
+ `Variant ${week}. Write a homepage hero for this store: a headline and ` +
32
+ `subline spotlighting a real product or category that fits the current ` +
33
+ `season, plus ctaChatMessage — the first-person message a shopper ` +
34
+ `would send to start shopping for it.`
35
+ );
36
+
37
+ heroHeadline.textContent = data.headline;
38
+ heroSubline.textContent = data.subline;
39
+ heroCta.addEventListener("click", () =>
40
+ window.GreatStore.sendMessage(data.ctaChatMessage)
41
+ );
42
+ hero.hidden = false;
43
+ ```
@@ -0,0 +1,80 @@
1
+ # Collection-page comparison table
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ ```html
10
+ <section id="gs-compare" hidden>
11
+ <h3>Quick comparison</h3>
12
+ <table><thead id="gsc-head"></thead><tbody id="gsc-body"></tbody></table>
13
+ </section>
14
+
15
+ <script>
16
+ (async () => {
17
+ if (!window.GreatStore?.generateStructuredContent) return;
18
+ const collection = "winter jackets"; // ← your collection name
19
+ try {
20
+ const data = await window.GreatStore.generateStructuredContent(
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ criteria: {
25
+ type: "array", minItems: 2, maxItems: 4,
26
+ items: { type: "string", maxLength: 25 },
27
+ },
28
+ rows: {
29
+ type: "array", minItems: 2, maxItems: 4,
30
+ items: {
31
+ type: "object",
32
+ properties: {
33
+ productName: { type: "string" },
34
+ bestFor: { type: "string", maxLength: 60 },
35
+ values: {
36
+ type: "array",
37
+ items: { type: "string", maxLength: 60 },
38
+ },
39
+ },
40
+ required: ["productName", "bestFor", "values"],
41
+ },
42
+ },
43
+ },
44
+ required: ["criteria", "rows"],
45
+ },
46
+ `The shopper is browsing the "${collection}" collection. Pick the ` +
47
+ `3-4 most representative products and compare them. Choose the ` +
48
+ `criteria a shopper actually decides on for this category. ` +
49
+ `"values" must align with "criteria" by index. Add a one-line ` +
50
+ `"bestFor" verdict per product. Only use real products and facts.`
51
+ );
52
+
53
+ const head = document.getElementById("gsc-head");
54
+ const hr = document.createElement("tr");
55
+ for (const h of ["Product", ...data.criteria, "Best for"]) {
56
+ const th = document.createElement("th");
57
+ th.textContent = h;
58
+ hr.append(th);
59
+ }
60
+ head.append(hr);
61
+
62
+ const body = document.getElementById("gsc-body");
63
+ for (const row of data.rows) {
64
+ const tr = document.createElement("tr");
65
+ const cells = [row.productName, ...(row.values ?? []), row.bestFor];
66
+ for (let i = 0; i < data.criteria.length + 2; i++) {
67
+ const td = document.createElement("td");
68
+ td.textContent = cells[i] ?? "—";
69
+ tr.append(td);
70
+ }
71
+ body.append(tr);
72
+ }
73
+ document.getElementById("gs-compare").hidden = false;
74
+ } catch {}
75
+ })();
76
+ </script>
77
+ ```
78
+
79
+ Note the index-aligned `values`/`criteria` trick and the `?? "—"` guard —
80
+ grounding means a value the catalog can't support may be missing.
@@ -0,0 +1,59 @@
1
+ # "Complete the look" cross-sell strip
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ ```js
10
+ const data = await window.GreatStore.generateStructuredContent(
11
+ {
12
+ type: "object",
13
+ properties: {
14
+ intro: { type: "string", maxLength: 90 },
15
+ picks: {
16
+ type: "array", minItems: 2, maxItems: 4,
17
+ items: {
18
+ type: "object",
19
+ properties: {
20
+ productName: { type: "string" },
21
+ reason: { type: "string", maxLength: 90 },
22
+ imageUrl: { type: "string", nullable: true },
23
+ productUrl: { type: "string", nullable: true },
24
+ },
25
+ required: ["productName", "reason"],
26
+ },
27
+ },
28
+ },
29
+ required: ["picks"],
30
+ },
31
+ `The shopper is viewing "${productName}". From the store's real catalog, ` +
32
+ `pick 2-4 products that genuinely pair with it and say why each one ` +
33
+ `completes the look or use-case. Include image and product URLs only ` +
34
+ `if known.`
35
+ );
36
+
37
+ for (const pick of data.picks) {
38
+ const card = document.createElement("a");
39
+ if (pick.productUrl) card.href = pick.productUrl;
40
+ if (pick.imageUrl) {
41
+ const img = document.createElement("img");
42
+ img.src = pick.imageUrl;
43
+ img.alt = pick.productName;
44
+ img.loading = "lazy";
45
+ card.append(img);
46
+ }
47
+ const name = document.createElement("strong");
48
+ name.textContent = pick.productName;
49
+ const why = document.createElement("p");
50
+ why.textContent = pick.reason;
51
+ card.append(name, why);
52
+ strip.append(card);
53
+ }
54
+ strip.hidden = false;
55
+ ```
56
+
57
+ `imageUrl`/`productUrl` are `nullable` and optional in the render — the
58
+ grounding contract means they're only present when the catalog actually has
59
+ them. Never `require` URLs.
@@ -0,0 +1,25 @@
1
+ # Custom chat button synced to panel state
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ Replace the default launcher with your own UI using the lifecycle surface:
10
+
11
+ ```js
12
+ const gs = window.GreatStore;
13
+ const btn = document.getElementById("my-chat-button");
14
+
15
+ gs.ready.then(() => { btn.hidden = false; });
16
+ btn.addEventListener("click", () => gs.toggle());
17
+
18
+ gs.on("open", () => btn.setAttribute("aria-expanded", "true"));
19
+ gs.on("close", () => btn.setAttribute("aria-expanded", "false"));
20
+ ```
21
+
22
+ `ready` resolves even when `.then()` is attached after mount, so script
23
+ ordering doesn't matter. The `open`/`close` events also fire for opens the
24
+ SDK triggers itself (`sendMessage`, `?gs_chat=open`), keeping your button
25
+ state honest.
@@ -0,0 +1,97 @@
1
+ # Gift finder funnel (teaser → conversation → action)
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ The flagship pattern: an AI-generated quiz card in your design, whose answers
10
+ drop the shopper into a chat that can act on the page. Uses the
11
+ `onGreatStoreReady` helper from the
12
+ [embed API reference](../references/embed-api.md#availability) — define
13
+ that once and it works for every use on the page.
14
+
15
+ ```html
16
+ <section id="gift-finder" hidden>
17
+ <h3 id="gf-question"></h3>
18
+ <div id="gf-options"></div>
19
+ </section>
20
+
21
+ <script>
22
+ // Depending on where this script tag sits relative to the embed script
23
+ // tag, this inline script could run either before embed.js has executed
24
+ // at all, or after it's already mounted — checking `window.GreatStore`
25
+ // synchronously and bailing if it's not there yet would silently skip
26
+ // the whole funnel in the first case, and a bare
27
+ // `addEventListener("greatstore:ready", ...)` would silently miss the
28
+ // (one-shot, already-fired) event in the second.
29
+ // onGreatStoreReady (see the embed API reference) handles both.
30
+ onGreatStoreReady(async (gs) => {
31
+ // Tools the resulting conversation can use — discovered on its next
32
+ // turn automatically.
33
+ document.modelContext.registerTool({
34
+ name: "go_to_product",
35
+ description:
36
+ "Navigate the shopper to a product page on this site. Use when " +
37
+ "the shopper picks a product they want to see.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: { url: { type: "string" } },
41
+ required: ["url"],
42
+ },
43
+ execute({ url }) {
44
+ const u = new URL(String(url), location.origin);
45
+ if (u.origin !== location.origin) throw new Error("Only same-site URLs allowed");
46
+ location.assign(u.href);
47
+ return { content: [{ type: "text", text: "Navigating." }] };
48
+ },
49
+ });
50
+
51
+ try {
52
+ const data = await gs.generateStructuredContent(
53
+ {
54
+ type: "object",
55
+ properties: {
56
+ question: { type: "string", maxLength: 80 },
57
+ options: {
58
+ type: "array",
59
+ minItems: 3,
60
+ maxItems: 4,
61
+ items: {
62
+ type: "object",
63
+ properties: {
64
+ label: { type: "string", maxLength: 30 },
65
+ chatMessage: { type: "string", maxLength: 140 },
66
+ },
67
+ required: ["label", "chatMessage"],
68
+ },
69
+ },
70
+ },
71
+ required: ["question", "options"],
72
+ },
73
+ "Create one engaging gift-finder opening question for this store, " +
74
+ "with 3-4 answer options that map to real areas of the catalog. " +
75
+ "For each option also write chatMessage: the message a shopper " +
76
+ "would send to a shopping assistant after picking it, phrased in " +
77
+ "first person (e.g. \"I'm shopping for my dad who loves hiking\")."
78
+ );
79
+
80
+ document.getElementById("gf-question").textContent = data.question;
81
+ const wrap = document.getElementById("gf-options");
82
+ for (const opt of data.options) {
83
+ const btn = document.createElement("button");
84
+ btn.type = "button";
85
+ btn.textContent = opt.label;
86
+ btn.addEventListener("click", () => gs.sendMessage(opt.chatMessage));
87
+ wrap.append(btn);
88
+ }
89
+ document.getElementById("gift-finder").hidden = false;
90
+ } catch {}
91
+ });
92
+ </script>
93
+ ```
94
+
95
+ Why it works: the teaser costs one cached generation per page, each
96
+ click opens a conversation that already has direction, and `go_to_product`
97
+ lets the conversation end on a product page instead of in a dead end.
@@ -0,0 +1,109 @@
1
+ # GreatStore launchers — AI question chips
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ A horizontally scrollable row of chips, each a highly engaging first-person
10
+ question about the current page. Tapping a chip sends that question to the
11
+ assistant — `generateStructuredContent` writes the questions, `sendMessage`
12
+ fires them. Simple yet effective: it works on every page type, costs one
13
+ cached generation per page, and every tap starts a conversation that already
14
+ has a great opening line.
15
+
16
+ ```html
17
+ <div id="gs-launchers" hidden></div>
18
+
19
+ <style>
20
+ #gs-launchers {
21
+ display: flex;
22
+ gap: 0.5em;
23
+ overflow-x: auto;
24
+ -webkit-overflow-scrolling: touch;
25
+ scrollbar-width: none;
26
+ padding: 0.5em 1em;
27
+ }
28
+ #gs-launchers::-webkit-scrollbar { display: none; }
29
+ #gs-launchers button {
30
+ flex: 0 0 auto;
31
+ white-space: nowrap;
32
+ border: 1px solid #ddd;
33
+ border-radius: 999px;
34
+ padding: 0.5em 0.9em;
35
+ background: #fff;
36
+ cursor: pointer;
37
+ }
38
+ </style>
39
+
40
+ <script>
41
+ (async () => {
42
+ const gs = window.GreatStore;
43
+ if (!gs?.generateStructuredContent) return;
44
+ try {
45
+ const data = await gs.generateStructuredContent(
46
+ {
47
+ type: "object",
48
+ properties: {
49
+ launchers: {
50
+ type: "array", minItems: 4, maxItems: 6,
51
+ items: {
52
+ type: "object",
53
+ properties: {
54
+ chip: { type: "string", maxLength: 32 },
55
+ question: { type: "string", maxLength: 140 },
56
+ },
57
+ required: ["chip", "question"],
58
+ },
59
+ },
60
+ },
61
+ required: ["launchers"],
62
+ },
63
+ `The shopper is on the page "${document.title}". Write 4-6 launcher ` +
64
+ `chips for a shopping assistant. For each, "question" is a highly ` +
65
+ `engaging first-person question this shopper would genuinely want ` +
66
+ `answered on this page — specific to its product, category, or ` +
67
+ `content, never generic — and "chip" is a 2-4 word teaser of it. ` +
68
+ `Vary the angles: fit and use, comparisons, gifting, care, what's ` +
69
+ `popular.`
70
+ );
71
+
72
+ const row = document.getElementById("gs-launchers");
73
+ for (const { chip, question } of data.launchers) {
74
+ const btn = document.createElement("button");
75
+ btn.type = "button";
76
+ btn.textContent = chip;
77
+ btn.title = question;
78
+ btn.addEventListener("click", () => gs.sendMessage(question));
79
+ row.append(btn);
80
+ }
81
+ row.hidden = false;
82
+ } catch {}
83
+ })();
84
+ </script>
85
+ ```
86
+
87
+ Why it works:
88
+
89
+ - **The chip is the teaser, the question is the payload.** A 2-4 word chip
90
+ scans instantly; the full first-person question lands in the transcript
91
+ reading like something the shopper typed, and gives the assistant a
92
+ well-formed prompt. The `title` attribute previews the full question on
93
+ hover.
94
+ - **Per-page for free.** The page URL is part of the generation context and
95
+ the cache key, so one site-wide snippet yields different chips on every
96
+ page — each served from cache on repeat renders.
97
+ - **Placement is the lever.** Under the product title, above the grid on
98
+ collections, at the end of a blog post — wherever a shopper pauses to
99
+ wonder, the chips name the question for them.
100
+
101
+ Tips:
102
+
103
+ - Inline the product or collection name into the prompt when the platform
104
+ exposes it — it beats relying on `document.title`.
105
+ - Restyle the chips to the site's design system; the CSS above is just the
106
+ scroll mechanics (flex row, `overflow-x: auto`, hidden scrollbars,
107
+ `white-space: nowrap`).
108
+ - Resist adding more than ~6 chips — a launcher row is an invitation, not a
109
+ sitemap.
@@ -0,0 +1,90 @@
1
+ # Page-action suite (WebMCP)
2
+
3
+ > **Treat this code as a reference, not a drop-in.** Samples are
4
+ > framework-free vanilla JS so they stay portable — re-express the same
5
+ > logic in the conventions of the repo you're working in (React, Vue,
6
+ > Shopify Liquid sections, Svelte, …) instead of retrofitting the sample
7
+ > as-is.
8
+
9
+ Give every conversation on the site real capabilities. Register once in a
10
+ shared snippet, after the SDK is ready (which guarantees
11
+ `document.modelContext` exists) — using the `onGreatStoreReady` helper from
12
+ the [embed API reference](../references/embed-api.md#availability), which
13
+ handles both load orderings (embed script not run yet vs. already mounted)
14
+ that a plain `window.GreatStore?.ready.then()` or a bare
15
+ `addEventListener("greatstore:ready", ...)` each only cover one side of:
16
+
17
+ ```js
18
+ onGreatStoreReady(() => {
19
+ const text = (value) => ({
20
+ content: [{ type: "text", text: JSON.stringify(value) }],
21
+ });
22
+
23
+ document.modelContext.registerTool({
24
+ name: "get_cart",
25
+ description:
26
+ "Read the shopper's current cart on this site: items, quantities, " +
27
+ "and totals. Use before answering any cart question.",
28
+ inputSchema: { type: "object", properties: {} },
29
+ async execute() {
30
+ return text(await (await fetch("/cart.js")).json());
31
+ },
32
+ });
33
+
34
+ document.modelContext.registerTool({
35
+ name: "add_to_cart",
36
+ description:
37
+ "Add a product variant to the cart on this site. Use when the " +
38
+ "shopper asks to add or buy something. Confirm the variant with " +
39
+ "the shopper first if ambiguous.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ variantId: { type: "string" },
44
+ quantity: { type: "integer", minimum: 1, maximum: 10 },
45
+ },
46
+ required: ["variantId"],
47
+ },
48
+ async execute({ variantId, quantity }) {
49
+ if (!variantId) throw new Error("variantId is required");
50
+ const res = await fetch("/cart/add.js", {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),
54
+ });
55
+ if (!res.ok) throw new Error(`Could not add to cart (${res.status})`);
56
+ document.dispatchEvent(new CustomEvent("cart:refresh"));
57
+ return text(await res.json());
58
+ },
59
+ });
60
+
61
+ document.modelContext.registerTool({
62
+ name: "get_current_page",
63
+ description:
64
+ "Read what page the shopper is currently on, including structured " +
65
+ "product data when on a product page.",
66
+ inputSchema: { type: "object", properties: {} },
67
+ execute() {
68
+ return text({
69
+ url: location.href,
70
+ title: document.title,
71
+ productJson: document.querySelector("#product-json")?.textContent ?? null,
72
+ });
73
+ },
74
+ });
75
+ });
76
+ ```
77
+
78
+ Principles at work: throw on failure (the assistant explains and recovers),
79
+ return fresh state after mutations (the assistant confirms accurately), cap
80
+ quantities in the schema, and notify your own UI (`cart:refresh`) so the
81
+ page reflects what the AI did.
82
+
83
+ For a product-page-only tool, register with an `AbortSignal` and abort on
84
+ SPA navigation:
85
+
86
+ ```js
87
+ const ac = new AbortController();
88
+ document.modelContext.registerTool(reviewsTool, { signal: ac.signal });
89
+ router.onLeave("/products/:handle", () => ac.abort());
90
+ ```