@greatstore/cli 0.0.38 → 0.0.40

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/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to `@greatstore/cli` are recorded here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## 0.0.40 — 2026-07-24
7
+
8
+ ### Removed
9
+ - `gs configure set` no longer accepts `--salesGuide` or
10
+ `--salesGuideFile`, and `gs configure show` no longer lists the field.
11
+
12
+ ## 0.0.39 — 2026-07-21
13
+
14
+ ### Added
15
+ - Components now receive an `Image` prop — a drop-in for `<img>` that
16
+ serves images at the size they're displayed. Render `<Image src=… />`
17
+ instead of `<img>`; pass `Image={"img"}` to preview a component
18
+ outside a store. Scaffolded into `gs apps init` and documented in
19
+ `AGENTS.md`.
20
+
6
21
  ## 0.0.38 — 2026-07-13
7
22
 
8
23
  ### Added
package/README.md CHANGED
@@ -36,7 +36,7 @@ with the same field names and behaviour.
36
36
  | Command | What it does |
37
37
  | ---------------------------------------- | ---------------------------------------------------------- |
38
38
  | `gs configure [--json]` | Show the store configuration. |
39
- | `gs configure set --<field> <value>` | Edit it. Same fields as the dashboard: `--displayName`, `--assistantName`, `--salesGuide` (or `--salesGuideFile <path>`), `--storeLink`, `--extraOrigins a,b`, `--theme '<json>'` (or `--themeFile <path>`), `--cspScriptHosts a,b`, `--cspConnectHosts a,b`. Pass `""` to clear a field. |
39
+ | `gs configure set --<field> <value>` | Edit it. Same fields as the dashboard: `--displayName`, `--assistantName`, `--storeLink`, `--extraOrigins a,b`, `--theme '<json>'` (or `--themeFile <path>`), `--cspScriptHosts a,b`, `--cspConnectHosts a,b`. Pass `""` to clear a field. |
40
40
  | `gs configure upload <kind> <file>` | Upload `icon`, `logoLight`, or `logoDark` (.png/.jpg/.webp). |
41
41
  | `gs configure clear <kind>` | Remove an uploaded asset. |
42
42
 
package/dist/cli.js CHANGED
@@ -139,6 +139,7 @@ function waitForCallback(server, expectedState, timeoutMs) {
139
139
  }
140
140
  const params = url.searchParams;
141
141
  const error = params.get("error");
142
+ const errorDescription = params.get("error_description");
142
143
  const token = params.get("code");
143
144
  const returnedState = params.get("state");
144
145
  if (returnedState !== expectedState) {
@@ -152,7 +153,9 @@ function waitForCallback(server, expectedState, timeoutMs) {
152
153
  res.writeHead(400, { "content-type": "text/html" });
153
154
  res.end(FAILURE_HTML);
154
155
  clearTimeout(timer);
155
- settle(() => reject(new LoopbackError("Sign-in failed. Please try again.")));
156
+ settle(
157
+ () => reject(new LoopbackError(errorDescription ?? "Sign-in failed. Please try again."))
158
+ );
156
159
  return;
157
160
  }
158
161
  res.writeHead(200, { "content-type": "text/html" });
@@ -664,7 +667,7 @@ import { fileURLToPath } from "url";
664
667
  var cache = null;
665
668
  function loadTemplate() {
666
669
  if (cache) return cache;
667
- cache = true ? JSON.parse('{"component/component.tsx":"import React from \\"react\\";\\n\\n// Add your tool args (matching manifest.json#inputSchema.properties)\\n// alongside the GreatStore-injected lifecycle props. See AGENTS.md for\\n// what each lifecycle prop does.\\ninterface Props {\\n onSendMessage: (text: string) => void;\\n onCallTool: (name: string, args: Record<string, unknown>) => void;\\n onUpdateModelContext: (context: string) => void;\\n onShowLightbox: (options: { src: string; originRect?: DOMRect }) => void;\\n onClose: () => void;\\n}\\n\\n// See AGENTS.md for the design rules (em-based sizing, brand CSS\\n// variables), the lifecycle props, and how to write an async,\\n// backend-backed component.\\nexport default function __GS_PASCAL__(_props: Props): React.ReactElement {\\n return (\\n <div\\n style={{\\n padding: \\"1em\\",\\n border: \\"1px solid var(--color-border-default)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n background: \\"var(--color-surface)\\",\\n color: \\"var(--color-foreground)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n >\\n <strong>__GS_NAME__</strong> \u2014 hello from your component!\\n </div>\\n );\\n}\\n","component/manifest.json":"{\\n \\"name\\": \\"__GS_NAME__\\",\\n \\"displayName\\": \\"__GS_DISPLAY_NAME__\\",\\n \\"description\\": \\"Renders the __GS_NAME__ widget.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {}\\n }\\n}\\n","root/.gitignore":"node_modules/\\ncomponents/*/bundle.js\\ncomponents/*/.gssync.json\\n*.tsbuildinfo\\n.DS_Store\\n","root/.gsrc":"{\\n \\"store\\": \\"__GS_STORE__\\"\\n}\\n","root/AGENTS.md":"# AGENTS.md\\n\\nGuidance for AI coding agents working on the components in this project.\\n`CLAUDE.md` and `GEMINI.md` are symlinks to this file.\\n\\nThese components render inside arbitrary publisher websites (the embed\\nwidget mounts on a host page) as well as the GreatStore storefront. You\\ncontrol neither the host page\'s base font size nor its color scheme, and\\nthe store owner controls the look from GreatStore theme settings. The\\nrules below keep components portable across both.\\n\\n## Design requirements\\n\\n### Never use `rem` for fonts and sizing \u2014 always use `em`\\n\\n`rem` resolves against the host page\'s root font size, which is\\narbitrary and outside our control. A host that sets `html { font-size:\\n8px }` (or 200%) will shrink or blow up every `rem`-based dimension.\\n`em` resolves against the component\'s own font size, so it stays\\nself-consistent wherever the component is mounted.\\n\\nUse `em` for font sizes, padding, margins, gaps, widths, heights,\\nand any other length. Borders may stay in `px` (hairlines should not\\nscale with text).\\n\\n```tsx\\n// Bad \u2014 breaks on hosts with a non-default root font size\\n<div style={{ padding: \\"1rem\\", fontSize: \\"0.875rem\\" }} />\\n\\n// Good\\n<div style={{ padding: \\"1em\\", fontSize: \\"0.875em\\" }} />\\n```\\n\\n### Don\'t hardcode colors, fonts, or corner radius \u2014 use GreatStore brand variables\\n\\nThe store owner themes the assistant from GreatStore settings. Hardcoded\\nvalues ignore that and make the component clash with the rest of the\\nstore. Read from the CSS variables GreatStore injects on the surface\\ninstead, so the component restyles itself when the theme changes.\\n\\nAvailable variables:\\n\\n| Purpose | Variable |\\n| --- | --- |\\n| Surface background | `--color-surface` |\\n| Secondary surface | `--color-surface-secondary` |\\n| Accent surface | `--color-surface-accent` |\\n| Hover surface | `--color-surface-hover` |\\n| Foreground text | `--color-foreground` |\\n| Secondary text | `--color-foreground-secondary` |\\n| Muted text | `--color-foreground-muted` |\\n| Accent text | `--color-foreground-accent` |\\n| Border | `--color-border-default` |\\n| Focus border | `--color-border-focus` |\\n| Primary / brand | `--color-primary` |\\n| Text on primary | `--color-primary-foreground` |\\n| Primary hover | `--color-primary-hover` |\\n| Primary muted | `--color-primary-muted` |\\n| Text on primary muted | `--color-primary-muted-foreground` |\\n| Primary tint | `--color-primary-tint` |\\n| Link | `--color-link` |\\n| Font family | `--font-primary` |\\n| Secondary font family | `--font-secondary` |\\n| Corner radius (scale) | `--radius-xs` \u2026 `--radius-4xl`, `--radius-pill` |\\n\\nThe store owner can set a secondary font family in their GreatStore\\ntheme settings, exposed as `--font-secondary` (it falls back to\\n`--font-primary` when unset). Reach for it to add a tasteful second\\nlayer of typography \u2014 pair `--font-primary` for body copy with\\n`--font-secondary` for headings, prices, or other accents \u2014 so the\\ncomponent reflects the store\'s full type system instead of a single\\nface.\\n\\n```tsx\\n// Bad \u2014 hardcoded, ignores the store\'s theme\\n<button style={{ background: \\"#4f46e5\\", borderRadius: 8, fontFamily: \\"Inter\\" }} />\\n\\n// Good\\n<button\\n style={{\\n background: \\"var(--color-primary)\\",\\n color: \\"var(--color-primary-foreground)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n/>\\n```\\n\\nProvide a sensible fallback only when a value might be absent, e.g.\\n`var(--color-primary, currentColor)`.\\n\\n## Component props\\n\\nEach component receives its tool args (the fields you declare in\\n`manifest.json#inputSchema.properties`) plus these GreatStore-injected\\nlifecycle props, which are always present:\\n\\n| Prop | What it does |\\n| --- | --- |\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it. |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject invisible background context for the model (selected variant, configured options, current step). Replaces the prior value \u2014 never appends; pass `\\"\\"` to clear. Read on the next chat turn. Use it when the assistant should *know* in-component state without a visible message; use `onSendMessage` when you want a turn to happen now. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay your component can\'t render itself (it\'s boxed inside its own bounds and shadow root). Pass the image `src`; for a smooth zoom, also pass the clicked element\'s `getBoundingClientRect()` as `originRect` (omit it and the image grows from the viewport centre). |\\n| `onClose()` | Dismiss the host slot. Over-input clears the overlay, fullscreen reverts the pane, inline is a no-op. |\\n\\n## Async components (backend-backed, render-blocking data)\\n\\nIf a component must load data from a backend/API before it can render\\ncorrectly, make it async \u2014 don\'t render an empty shell and fetch in a\\n`useEffect`. Set `\\"async\\": true` in `manifest.json` and export an async\\ndefault. GreatStore waits for your promise (showing a normal loading\\nstate, so you don\'t render your own placeholder), then renders what it\\nresolves to. Components that render purely from their props stay\\nsynchronous.\\n\\nA thrown error is a **retry signal**: the in-store AI sees it and\\nusually re-calls the tool. So only throw when a *different* call could\\nhelp:\\n\\n1. `await` the backend call, then return the finished JSX.\\n2. Validate the AI-passed props first and throw on bad input \u2014 the AI\\n can fix the args and retry. (Don\'t validate the API\'s *output* and\\n throw: the AI can\'t fix the backend, it\'ll just loop.)\\n3. Broadcast a backend failure ONLY when retrying differently could\\n succeed, and say what to change (e.g. empty search \u2192 \\"try a broader\\n keyword\\"). For idempotent failures (500, timeout, missing record)\\n re-running the same call changes nothing \u2014 render a graceful fallback\\n instead of throwing.\\n\\n```tsx\\nexport default async function Example(props: Props) {\\n if (!props.query?.trim()) throw new Error(\\"missing required prop: query\\");\\n const res = await fetch(`/api/search?q=${encodeURIComponent(props.query)}`);\\n if (res.ok) {\\n const { results } = await res.json();\\n if (results.length === 0)\\n throw new Error(`no results for \\"${props.query}\\" \u2014 try a broader keyword`);\\n return <ul>{/* render results */}</ul>;\\n }\\n return <p>Couldn\'t load results right now.</p>; // idempotent: don\'t throw\\n}\\n```\\n","root/README.md":"# GreatStore components\\n\\nCustom React components published to your GreatStore tenant. Each\\ncomponent lives in its own folder under `components/`.\\n\\n```\\nnpm install\\ngs apps init <component_name> # add a new component\\ngs apps build # builds every components/<name>/bundle.js\\ngs apps push # uploads every changed component as a draft\\ngs apps publish <component_name> # promote a specific component to live\\n```\\n\\n- `gs apps push` (no args) hashes each component and only uploads the ones\\n that have changed since the last sync.\\n- `gs apps pull` (no args) refreshes every remote component into\\n `components/<name>/`. Components with unsaved local edits are skipped\\n with a warning; pass `--force` to overwrite.\\n\\nSee `AGENTS.md` for the design rules every component must follow\\n(em-based sizing, brand CSS variables) \u2014 it doubles as guidance for AI\\ncoding agents (`CLAUDE.md` / `GEMINI.md` symlink to it).\\n","root/package.json":"{\\n \\"name\\": \\"greatstore-components\\",\\n \\"version\\": \\"0.0.1\\",\\n \\"private\\": true,\\n \\"type\\": \\"module\\",\\n \\"scripts\\": {\\n \\"build\\": \\"gs apps build\\",\\n \\"push\\": \\"gs apps build && gs apps push\\"\\n },\\n \\"dependencies\\": {\\n \\"react\\": \\"^19.0.0\\",\\n \\"react-dom\\": \\"^19.0.0\\"\\n },\\n \\"devDependencies\\": {\\n \\"@types/react\\": \\"^19.0.0\\",\\n \\"@types/react-dom\\": \\"^19.0.0\\",\\n \\"@vitejs/plugin-react\\": \\"^4.3.0\\",\\n \\"typescript\\": \\"^5.6.0\\",\\n \\"vite\\": \\"^5.4.0\\"\\n }\\n}\\n","root/tsconfig.json":"{\\n \\"compilerOptions\\": {\\n \\"target\\": \\"ES2022\\",\\n \\"module\\": \\"ESNext\\",\\n \\"moduleResolution\\": \\"Bundler\\",\\n \\"jsx\\": \\"react-jsx\\",\\n \\"lib\\": [\\"ES2022\\", \\"DOM\\"],\\n \\"strict\\": true,\\n \\"esModuleInterop\\": true,\\n \\"skipLibCheck\\": true,\\n \\"isolatedModules\\": true,\\n \\"noEmit\\": true\\n },\\n \\"include\\": [\\"components/**/component.tsx\\", \\"vite.config.ts\\"]\\n}\\n","root/vite.config.ts":"import { defineConfig } from \\"vite\\";\\nimport react from \\"@vitejs/plugin-react\\";\\n\\n// Real builds happen in `gs build` (one Vite invocation per\\n// component, externals + runtime shim paths owned by the CLI). This\\n// file exists only so editors / language servers can resolve the\\n// React plugin when inspecting components/*/component.tsx.\\nexport default defineConfig({\\n plugins: [react()],\\n});\\n"}') : readTreeFromDisk(new URL("../template/", import.meta.url));
670
+ cache = true ? JSON.parse('{"component/component.tsx":"import React from \\"react\\";\\n\\n// Add your tool args (matching manifest.json#inputSchema.properties)\\n// alongside the GreatStore-injected lifecycle props. See AGENTS.md for\\n// what each lifecycle prop does.\\ninterface Props {\\n onSendMessage: (text: string) => void;\\n onCallTool: (name: string, args: Record<string, unknown>) => void;\\n onUpdateModelContext: (context: string) => void;\\n onShowLightbox: (options: { src: string; originRect?: DOMRect }) => void;\\n onClose: () => void;\\n // Drop-in for <img>: `<Image src=\u2026 alt=\u2026 />`. See AGENTS.md.\\n Image: React.ElementType<React.ComponentProps<\\"img\\">>;\\n}\\n\\n// See AGENTS.md for the design rules (em-based sizing, brand CSS\\n// variables), the lifecycle props, and how to write an async,\\n// backend-backed component.\\nexport default function __GS_PASCAL__(_props: Props): React.ReactElement {\\n return (\\n <div\\n style={{\\n padding: \\"1em\\",\\n border: \\"1px solid var(--color-border-default)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n background: \\"var(--color-surface)\\",\\n color: \\"var(--color-foreground)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n >\\n <strong>__GS_NAME__</strong> \u2014 hello from your component!\\n </div>\\n );\\n}\\n","component/manifest.json":"{\\n \\"name\\": \\"__GS_NAME__\\",\\n \\"displayName\\": \\"__GS_DISPLAY_NAME__\\",\\n \\"description\\": \\"Renders the __GS_NAME__ widget.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {}\\n }\\n}\\n","root/.gitignore":"node_modules/\\ncomponents/*/bundle.js\\ncomponents/*/.gssync.json\\n*.tsbuildinfo\\n.DS_Store\\n","root/.gsrc":"{\\n \\"store\\": \\"__GS_STORE__\\"\\n}\\n","root/AGENTS.md":"# AGENTS.md\\n\\nGuidance for AI coding agents working on the components in this project.\\n`CLAUDE.md` and `GEMINI.md` are symlinks to this file.\\n\\nThese components render inside arbitrary publisher websites (the embed\\nwidget mounts on a host page) as well as the GreatStore storefront. You\\ncontrol neither the host page\'s base font size nor its color scheme, and\\nthe store owner controls the look from GreatStore theme settings. The\\nrules below keep components portable across both.\\n\\n## Design requirements\\n\\n### Never use `rem` for fonts and sizing \u2014 always use `em`\\n\\n`rem` resolves against the host page\'s root font size, which is\\narbitrary and outside our control. A host that sets `html { font-size:\\n8px }` (or 200%) will shrink or blow up every `rem`-based dimension.\\n`em` resolves against the component\'s own font size, so it stays\\nself-consistent wherever the component is mounted.\\n\\nUse `em` for font sizes, padding, margins, gaps, widths, heights,\\nand any other length. Borders may stay in `px` (hairlines should not\\nscale with text).\\n\\n```tsx\\n// Bad \u2014 breaks on hosts with a non-default root font size\\n<div style={{ padding: \\"1rem\\", fontSize: \\"0.875rem\\" }} />\\n\\n// Good\\n<div style={{ padding: \\"1em\\", fontSize: \\"0.875em\\" }} />\\n```\\n\\n### Don\'t hardcode colors, fonts, or corner radius \u2014 use GreatStore brand variables\\n\\nThe store owner themes the assistant from GreatStore settings. Hardcoded\\nvalues ignore that and make the component clash with the rest of the\\nstore. Read from the CSS variables GreatStore injects on the surface\\ninstead, so the component restyles itself when the theme changes.\\n\\nAvailable variables:\\n\\n| Purpose | Variable |\\n| --- | --- |\\n| Surface background | `--color-surface` |\\n| Secondary surface | `--color-surface-secondary` |\\n| Accent surface | `--color-surface-accent` |\\n| Hover surface | `--color-surface-hover` |\\n| Foreground text | `--color-foreground` |\\n| Secondary text | `--color-foreground-secondary` |\\n| Muted text | `--color-foreground-muted` |\\n| Accent text | `--color-foreground-accent` |\\n| Border | `--color-border-default` |\\n| Focus border | `--color-border-focus` |\\n| Primary / brand | `--color-primary` |\\n| Text on primary | `--color-primary-foreground` |\\n| Primary hover | `--color-primary-hover` |\\n| Primary muted | `--color-primary-muted` |\\n| Text on primary muted | `--color-primary-muted-foreground` |\\n| Primary tint | `--color-primary-tint` |\\n| Link | `--color-link` |\\n| Font family | `--font-primary` |\\n| Secondary font family | `--font-secondary` |\\n| Corner radius (scale) | `--radius-xs` \u2026 `--radius-4xl`, `--radius-pill` |\\n\\nThe store owner can set a secondary font family in their GreatStore\\ntheme settings, exposed as `--font-secondary` (it falls back to\\n`--font-primary` when unset). Reach for it to add a tasteful second\\nlayer of typography \u2014 pair `--font-primary` for body copy with\\n`--font-secondary` for headings, prices, or other accents \u2014 so the\\ncomponent reflects the store\'s full type system instead of a single\\nface.\\n\\n```tsx\\n// Bad \u2014 hardcoded, ignores the store\'s theme\\n<button style={{ background: \\"#4f46e5\\", borderRadius: 8, fontFamily: \\"Inter\\" }} />\\n\\n// Good\\n<button\\n style={{\\n background: \\"var(--color-primary)\\",\\n color: \\"var(--color-primary-foreground)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n/>\\n```\\n\\nProvide a sensible fallback only when a value might be absent, e.g.\\n`var(--color-primary, currentColor)`.\\n\\n## Component props\\n\\nEach component receives its tool args (the fields you declare in\\n`manifest.json#inputSchema.properties`) plus these GreatStore-injected\\nlifecycle props, which are always present:\\n\\n| Prop | What it does |\\n| --- | --- |\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it. |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject invisible background context for the model (selected variant, configured options, current step). Replaces the prior value \u2014 never appends; pass `\\"\\"` to clear. Read on the next chat turn. Use it when the assistant should *know* in-component state without a visible message; use `onSendMessage` when you want a turn to happen now. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay your component can\'t render itself (it\'s boxed inside its own bounds and shadow root). Pass the image `src`; for a smooth zoom, also pass the clicked element\'s `getBoundingClientRect()` as `originRect` (omit it and the image grows from the viewport centre). |\\n| `onClose()` | Dismiss the host slot. Over-input clears the overlay, fullscreen reverts the pane, inline is a no-op. |\\n| `Image` | A drop-in for `<img>`: render `<Image src=\u2026 alt=\u2026 />` instead of `<img>` and your images are automatically served at the right size for the store. Takes the same props as `<img>` (`src`, `alt`, `style`, `loading`, `onClick`, \u2026). Previewing the component outside the store? Pass `Image={\\"img\\"}` (or your own component) to render a plain image. |\\n\\n## Async components (backend-backed, render-blocking data)\\n\\nIf a component must load data from a backend/API before it can render\\ncorrectly, make it async \u2014 don\'t render an empty shell and fetch in a\\n`useEffect`. Set `\\"async\\": true` in `manifest.json` and export an async\\ndefault. GreatStore waits for your promise (showing a normal loading\\nstate, so you don\'t render your own placeholder), then renders what it\\nresolves to. Components that render purely from their props stay\\nsynchronous.\\n\\nA thrown error is a **retry signal**: the in-store AI sees it and\\nusually re-calls the tool. So only throw when a *different* call could\\nhelp:\\n\\n1. `await` the backend call, then return the finished JSX.\\n2. Validate the AI-passed props first and throw on bad input \u2014 the AI\\n can fix the args and retry. (Don\'t validate the API\'s *output* and\\n throw: the AI can\'t fix the backend, it\'ll just loop.)\\n3. Broadcast a backend failure ONLY when retrying differently could\\n succeed, and say what to change (e.g. empty search \u2192 \\"try a broader\\n keyword\\"). For idempotent failures (500, timeout, missing record)\\n re-running the same call changes nothing \u2014 render a graceful fallback\\n instead of throwing.\\n\\n```tsx\\nexport default async function Example(props: Props) {\\n if (!props.query?.trim()) throw new Error(\\"missing required prop: query\\");\\n const res = await fetch(`/api/search?q=${encodeURIComponent(props.query)}`);\\n if (res.ok) {\\n const { results } = await res.json();\\n if (results.length === 0)\\n throw new Error(`no results for \\"${props.query}\\" \u2014 try a broader keyword`);\\n return <ul>{/* render results */}</ul>;\\n }\\n return <p>Couldn\'t load results right now.</p>; // idempotent: don\'t throw\\n}\\n```\\n","root/README.md":"# GreatStore components\\n\\nCustom React components published to your GreatStore tenant. Each\\ncomponent lives in its own folder under `components/`.\\n\\n```\\nnpm install\\ngs apps init <component_name> # add a new component\\ngs apps build # builds every components/<name>/bundle.js\\ngs apps push # uploads every changed component as a draft\\ngs apps publish <component_name> # promote a specific component to live\\n```\\n\\n- `gs apps push` (no args) hashes each component and only uploads the ones\\n that have changed since the last sync.\\n- `gs apps pull` (no args) refreshes every remote component into\\n `components/<name>/`. Components with unsaved local edits are skipped\\n with a warning; pass `--force` to overwrite.\\n\\nSee `AGENTS.md` for the design rules every component must follow\\n(em-based sizing, brand CSS variables) \u2014 it doubles as guidance for AI\\ncoding agents (`CLAUDE.md` / `GEMINI.md` symlink to it).\\n","root/package.json":"{\\n \\"name\\": \\"greatstore-components\\",\\n \\"version\\": \\"0.0.1\\",\\n \\"private\\": true,\\n \\"type\\": \\"module\\",\\n \\"scripts\\": {\\n \\"build\\": \\"gs apps build\\",\\n \\"push\\": \\"gs apps build && gs apps push\\"\\n },\\n \\"dependencies\\": {\\n \\"react\\": \\"^19.0.0\\",\\n \\"react-dom\\": \\"^19.0.0\\"\\n },\\n \\"devDependencies\\": {\\n \\"@types/react\\": \\"^19.0.0\\",\\n \\"@types/react-dom\\": \\"^19.0.0\\",\\n \\"@vitejs/plugin-react\\": \\"^4.3.0\\",\\n \\"typescript\\": \\"^5.6.0\\",\\n \\"vite\\": \\"^5.4.0\\"\\n }\\n}\\n","root/tsconfig.json":"{\\n \\"compilerOptions\\": {\\n \\"target\\": \\"ES2022\\",\\n \\"module\\": \\"ESNext\\",\\n \\"moduleResolution\\": \\"Bundler\\",\\n \\"jsx\\": \\"react-jsx\\",\\n \\"lib\\": [\\"ES2022\\", \\"DOM\\"],\\n \\"strict\\": true,\\n \\"esModuleInterop\\": true,\\n \\"skipLibCheck\\": true,\\n \\"isolatedModules\\": true,\\n \\"noEmit\\": true\\n },\\n \\"include\\": [\\"components/**/component.tsx\\", \\"vite.config.ts\\"]\\n}\\n","root/vite.config.ts":"import { defineConfig } from \\"vite\\";\\nimport react from \\"@vitejs/plugin-react\\";\\n\\n// Real builds happen in `gs build` (one Vite invocation per\\n// component, externals + runtime shim paths owned by the CLI). This\\n// file exists only so editors / language servers can resolve the\\n// React plugin when inspecting components/*/component.tsx.\\nexport default defineConfig({\\n plugins: [react()],\\n});\\n"}') : readTreeFromDisk(new URL("../template/", import.meta.url));
668
671
  return cache;
669
672
  }
670
673
  var TOKENS = {
@@ -693,7 +696,7 @@ var SKILL_DIR_NAME = "greatstore";
693
696
  var cache2 = null;
694
697
  function loadSkill() {
695
698
  if (cache2) return cache2;
696
- cache2 = true ? JSON.parse('{"SKILL.md":"---\\nname: greatstore\\ndescription: 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 \u2014 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.\\n---\\n\\n# Building with GreatStore\\n\\nGreatStore gives a store an AI shopping assistant on two surfaces: a hosted\\nstorefront at `https://<slug>.greatstore.ai/`, and an embedded chat widget on\\nthe merchant\'s own site, installed with one script tag:\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nEverything else a site can do with GreatStore is documented in the\\nreferences below. Read the one that matches the task before writing code \u2014\\neach facet has non-obvious rules (caching, grounding, result shapes, design\\nconstraints) that the references spell out.\\n\\n## Index\\n\\n| Goal | Use | Read |\\n|---|---|---|\\n| Install the widget; control the panel; readiness, events, troubleshooting | `window.GreatStore` SDK | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Copy-paste on-page experiences | recipes built on the SDK | the [Recipes](#recipes) table below |\\n| Contextual **conversation entry points** anywhere on the page | `sendMessage(text)`, `open()`, `?gs_chat=open` | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Let the assistant **act on the page** (cart, navigation, filters) | WebMCP: `document.modelContext.registerTool(...)` | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Expand a component\'s image in an **on-brand full-screen lightbox** | component `onShowLightbox({ src, originRect })` | [references/chat-components.md](references/chat-components.md) |\\n| **Re-engage shoppers** with browser notifications | merchant-hosted `gs.js` + `enableNotifications()` | [references/push-notifications.md](references/push-notifications.md) |\\n| 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) |\\n| **Administer the store** \u2014 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) |\\n\\n## Three things to know before any of it\\n\\n- **Every code sample in this skill is a reference implementation, not a\\n drop-in.** Samples are framework-free vanilla JS so they stay portable \u2014\\n re-express the same logic in the conventions of the repo you\'re working\\n in (React, Vue, Shopify Liquid sections, Svelte, \u2026); never retrofit the\\n sample as-is into a codebase with its own framework.\\n- The page\'s domain **must be in the store\'s allowed domains**. If it isn\'t,\\n nothing works and the console shows\\n `[GreatStore] Chat is unavailable on <origin>\u2026` \u2014 check this first whenever\\n the embed appears dead. You can verify and fix it yourself with the `gs`\\n CLI (read-merge-write on `extraOrigins` \u2014 see\\n [references/store-admin.md](references/store-admin.md)).\\n- Every SDK call is safe immediately after the script tag \u2014 pre-mount calls\\n queue and replay in order, and the SDK pre-warms itself in the background.\\n\\n## Recipes\\n\\nComplete, framework-free implementations, one per file. Shared conventions:\\ncontainers start `hidden` and reveal only on success (a failed generation\\nchanges nothing); generated strings render via `textContent`, never\\n`innerHTML`; every recipe guards on `window.GreatStore`; prompts stay\\ndeterministic per page so repeat renders hit the cache.\\nInline real product/page data into prompts where the platform exposes it.\\n\\n| Recipe | What it builds | Use it for |\\n|---|---|---|\\n| [GreatStore launchers](recipes/launchers.md) | Horizontally scrollable AI-generated chips \u2014 engaging first-person questions about the current page; tap to ask the assistant. | Instant engagement on any page type \u2014 product, collection, blog, home. Simple yet effective; start here. |\\n| [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 \u2014 anywhere a shopper hesitates. |\\n| [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. |\\n| [Comparison table](recipes/comparison-table.md) | AI-picked representative products compared on category-relevant criteria. | Collection pages where shoppers weigh options. |\\n| [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. |\\n| [Campaign hero](recipes/campaign-hero.md) | Seasonal homepage hero copy, cache-keyed to the ISO week. | Fresh homepage/campaign copy without manual rewrites. |\\n| [Gift finder funnel](recipes/gift-finder-funnel.md) | Quiz teaser \u2192 chat handoff \u2192 page-tool navigation; the full funnel. | Gifting seasons, guided discovery, homepage engagement. |\\n| [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. |\\n| [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. |\\n\\n## How the facets combine\\n\\nThe strongest pattern is the **teaser \u2192 conversation \u2192 action** funnel:\\n`generateStructuredContent` renders a grounded teaser in the merchant\'s\\ndesign; each option\'s click handler calls `sendMessage` with the shopper\'s\\nchoice, dropping them into a conversation with momentum; WebMCP page tools\\nand custom chat components let that conversation actually do things \u2014 add to\\ncart, configure a product, book a slot \u2014 so it ends in a conversion, not a\\ncopy-paste. The [gift finder funnel](recipes/gift-finder-funnel.md)\\nrecipe is this funnel end to end.\\n","recipes/ask-about-this.md":"# \\"Ask about this\\" entry points (`sendMessage` only)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nZero-generation, instant, and often the biggest engagement win per line of\\ncode. Sprinkle context-aware buttons wherever a shopper hesitates:\\n\\n```js\\nconst gs = window.GreatStore;\\nif (gs) {\\n sizeGuideLink.addEventListener(\\"click\\", (e) => {\\n e.preventDefault();\\n gs.sendMessage(`How does the sizing run on \\"${productName}\\"? I usually wear a medium.`);\\n });\\n\\n shippingRow.querySelector(\\".ask\\").addEventListener(\\"click\\", () => {\\n gs.sendMessage(`What are the shipping options and times for \\"${productName}\\"?`);\\n });\\n\\n outOfStockBadge?.addEventListener(\\"click\\", () => {\\n gs.sendMessage(`\\"${productName}\\" looks out of stock \u2014 is there anything similar in stock?`);\\n });\\n}\\n```\\n\\nWrite each message as something the shopper would plausibly say \u2014 it appears\\nin the transcript as their message.\\n","recipes/campaign-hero.md":"# Campaign hero with deliberate variation\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nCache-friendly variation: key the prompt to a low-cardinality period, not to\\ntime itself.\\n\\n```js\\n// ISO week number \u2192 one generation per store per week, shared by everyone.\\nconst week = (d => {\\n const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\\n t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));\\n return `${t.getUTCFullYear()}-W${Math.ceil((((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5) + 1) / 7)}`;\\n})(new Date());\\n\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n headline: { type: \\"string\\", maxLength: 60 },\\n subline: { type: \\"string\\", maxLength: 120 },\\n featuredProductName: { type: \\"string\\", nullable: true },\\n ctaChatMessage: { type: \\"string\\", maxLength: 120 },\\n },\\n required: [\\"headline\\", \\"subline\\", \\"ctaChatMessage\\"],\\n },\\n `Variant ${week}. Write a homepage hero for this store: a headline and ` +\\n `subline spotlighting a real product or category that fits the current ` +\\n `season, plus ctaChatMessage \u2014 the first-person message a shopper ` +\\n `would send to start shopping for it.`\\n);\\n\\nheroHeadline.textContent = data.headline;\\nheroSubline.textContent = data.subline;\\nheroCta.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(data.ctaChatMessage)\\n);\\nhero.hidden = false;\\n```\\n","recipes/comparison-table.md":"# Collection-page comparison table\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```html\\n<section id=\\"gs-compare\\" hidden>\\n <h3>Quick comparison</h3>\\n <table><thead id=\\"gsc-head\\"></thead><tbody id=\\"gsc-body\\"></tbody></table>\\n</section>\\n\\n<script>\\n (async () => {\\n if (!window.GreatStore?.generateStructuredContent) return;\\n const collection = \\"winter jackets\\"; // \u2190 your collection name\\n try {\\n const data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n criteria: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: { type: \\"string\\", maxLength: 25 },\\n },\\n rows: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n bestFor: { type: \\"string\\", maxLength: 60 },\\n values: {\\n type: \\"array\\",\\n items: { type: \\"string\\", maxLength: 60 },\\n },\\n },\\n required: [\\"productName\\", \\"bestFor\\", \\"values\\"],\\n },\\n },\\n },\\n required: [\\"criteria\\", \\"rows\\"],\\n },\\n `The shopper is browsing the \\"${collection}\\" collection. Pick the ` +\\n `3-4 most representative products and compare them. Choose the ` +\\n `criteria a shopper actually decides on for this category. ` +\\n `\\"values\\" must align with \\"criteria\\" by index. Add a one-line ` +\\n `\\"bestFor\\" verdict per product. Only use real products and facts.`\\n );\\n\\n const head = document.getElementById(\\"gsc-head\\");\\n const hr = document.createElement(\\"tr\\");\\n for (const h of [\\"Product\\", ...data.criteria, \\"Best for\\"]) {\\n const th = document.createElement(\\"th\\");\\n th.textContent = h;\\n hr.append(th);\\n }\\n head.append(hr);\\n\\n const body = document.getElementById(\\"gsc-body\\");\\n for (const row of data.rows) {\\n const tr = document.createElement(\\"tr\\");\\n const cells = [row.productName, ...(row.values ?? []), row.bestFor];\\n for (let i = 0; i < data.criteria.length + 2; i++) {\\n const td = document.createElement(\\"td\\");\\n td.textContent = cells[i] ?? \\"\u2014\\";\\n tr.append(td);\\n }\\n body.append(tr);\\n }\\n document.getElementById(\\"gs-compare\\").hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nNote the index-aligned `values`/`criteria` trick and the `?? \\"\u2014\\"` guard \u2014\\ngrounding means a value the catalog can\'t support may be missing.\\n","recipes/complete-the-look.md":"# \\"Complete the look\\" cross-sell strip\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n intro: { type: \\"string\\", maxLength: 90 },\\n picks: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n reason: { type: \\"string\\", maxLength: 90 },\\n imageUrl: { type: \\"string\\", nullable: true },\\n productUrl: { type: \\"string\\", nullable: true },\\n },\\n required: [\\"productName\\", \\"reason\\"],\\n },\\n },\\n },\\n required: [\\"picks\\"],\\n },\\n `The shopper is viewing \\"${productName}\\". From the store\'s real catalog, ` +\\n `pick 2-4 products that genuinely pair with it and say why each one ` +\\n `completes the look or use-case. Include image and product URLs only ` +\\n `if known.`\\n);\\n\\nfor (const pick of data.picks) {\\n const card = document.createElement(\\"a\\");\\n if (pick.productUrl) card.href = pick.productUrl;\\n if (pick.imageUrl) {\\n const img = document.createElement(\\"img\\");\\n img.src = pick.imageUrl;\\n img.alt = pick.productName;\\n img.loading = \\"lazy\\";\\n card.append(img);\\n }\\n const name = document.createElement(\\"strong\\");\\n name.textContent = pick.productName;\\n const why = document.createElement(\\"p\\");\\n why.textContent = pick.reason;\\n card.append(name, why);\\n strip.append(card);\\n}\\nstrip.hidden = false;\\n```\\n\\n`imageUrl`/`productUrl` are `nullable` and optional in the render \u2014 the\\ngrounding contract means they\'re only present when the catalog actually has\\nthem. Never `require` URLs.\\n","recipes/custom-chat-button.md":"# Custom chat button synced to panel state\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nReplace the default launcher with your own UI using the lifecycle surface:\\n\\n```js\\nconst gs = window.GreatStore;\\nconst btn = document.getElementById(\\"my-chat-button\\");\\n\\ngs.ready.then(() => { btn.hidden = false; });\\nbtn.addEventListener(\\"click\\", () => gs.toggle());\\n\\ngs.on(\\"open\\", () => btn.setAttribute(\\"aria-expanded\\", \\"true\\"));\\ngs.on(\\"close\\", () => btn.setAttribute(\\"aria-expanded\\", \\"false\\"));\\n```\\n\\n`ready` resolves even when `.then()` is attached after mount, so script\\nordering doesn\'t matter. The `open`/`close` events also fire for opens the\\nSDK triggers itself (`sendMessage`, `?gs_chat=open`), keeping your button\\nstate honest.\\n","recipes/gift-finder-funnel.md":"# Gift finder funnel (teaser \u2192 conversation \u2192 action)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nThe flagship pattern: an AI-generated quiz card in your design, whose answers\\ndrop the shopper into a chat that can act on the page.\\n\\n```html\\n<section id=\\"gift-finder\\" hidden>\\n <h3 id=\\"gf-question\\"></h3>\\n <div id=\\"gf-options\\"></div>\\n</section>\\n\\n<script>\\n (async () => {\\n const gs = window.GreatStore;\\n if (!gs?.generateStructuredContent) return;\\n\\n // Tools the resulting conversation can use. Registered once the\\n // SDK is ready (so document.modelContext exists); the assistant\\n // discovers them on its next turn automatically.\\n gs.ready.then(() => {\\n document.modelContext.registerTool({\\n name: \\"go_to_product\\",\\n description:\\n \\"Navigate the shopper to a product page on this site. Use when \\" +\\n \\"the shopper picks a product they want to see.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: { url: { type: \\"string\\" } },\\n required: [\\"url\\"],\\n },\\n execute({ url }) {\\n const u = new URL(String(url), location.origin);\\n if (u.origin !== location.origin) throw new Error(\\"Only same-site URLs allowed\\");\\n location.assign(u.href);\\n return { content: [{ type: \\"text\\", text: \\"Navigating.\\" }] };\\n },\\n });\\n });\\n\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 80 },\\n options: {\\n type: \\"array\\",\\n minItems: 3,\\n maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n label: { type: \\"string\\", maxLength: 30 },\\n chatMessage: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"label\\", \\"chatMessage\\"],\\n },\\n },\\n },\\n required: [\\"question\\", \\"options\\"],\\n },\\n \\"Create one engaging gift-finder opening question for this store, \\" +\\n \\"with 3-4 answer options that map to real areas of the catalog. \\" +\\n \\"For each option also write chatMessage: the message a shopper \\" +\\n \\"would send to a shopping assistant after picking it, phrased in \\" +\\n \\"first person (e.g. \\\\\\"I\'m shopping for my dad who loves hiking\\\\\\").\\"\\n );\\n\\n document.getElementById(\\"gf-question\\").textContent = data.question;\\n const wrap = document.getElementById(\\"gf-options\\");\\n for (const opt of data.options) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = opt.label;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(opt.chatMessage));\\n wrap.append(btn);\\n }\\n document.getElementById(\\"gift-finder\\").hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nWhy it works: the teaser costs one cached generation per page, each\\nclick opens a conversation that already has direction, and `go_to_product`\\nlets the conversation end on a product page instead of in a dead end.\\n","recipes/launchers.md":"# GreatStore launchers \u2014 AI question chips\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nA horizontally scrollable row of chips, each a highly engaging first-person\\nquestion about the current page. Tapping a chip sends that question to the\\nassistant \u2014 `generateStructuredContent` writes the questions, `sendMessage`\\nfires them. Simple yet effective: it works on every page type, costs one\\ncached generation per page, and every tap starts a conversation that already\\nhas a great opening line.\\n\\n```html\\n<div id=\\"gs-launchers\\" hidden></div>\\n\\n<style>\\n #gs-launchers {\\n display: flex;\\n gap: 0.5em;\\n overflow-x: auto;\\n -webkit-overflow-scrolling: touch;\\n scrollbar-width: none;\\n padding: 0.5em 1em;\\n }\\n #gs-launchers::-webkit-scrollbar { display: none; }\\n #gs-launchers button {\\n flex: 0 0 auto;\\n white-space: nowrap;\\n border: 1px solid #ddd;\\n border-radius: 999px;\\n padding: 0.5em 0.9em;\\n background: #fff;\\n cursor: pointer;\\n }\\n</style>\\n\\n<script>\\n (async () => {\\n const gs = window.GreatStore;\\n if (!gs?.generateStructuredContent) return;\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n launchers: {\\n type: \\"array\\", minItems: 4, maxItems: 6,\\n items: {\\n type: \\"object\\",\\n properties: {\\n chip: { type: \\"string\\", maxLength: 32 },\\n question: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"chip\\", \\"question\\"],\\n },\\n },\\n },\\n required: [\\"launchers\\"],\\n },\\n `The shopper is on the page \\"${document.title}\\". Write 4-6 launcher ` +\\n `chips for a shopping assistant. For each, \\"question\\" is a highly ` +\\n `engaging first-person question this shopper would genuinely want ` +\\n `answered on this page \u2014 specific to its product, category, or ` +\\n `content, never generic \u2014 and \\"chip\\" is a 2-4 word teaser of it. ` +\\n `Vary the angles: fit and use, comparisons, gifting, care, what\'s ` +\\n `popular.`\\n );\\n\\n const row = document.getElementById(\\"gs-launchers\\");\\n for (const { chip, question } of data.launchers) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = chip;\\n btn.title = question;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(question));\\n row.append(btn);\\n }\\n row.hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nWhy it works:\\n\\n- **The chip is the teaser, the question is the payload.** A 2-4 word chip\\n scans instantly; the full first-person question lands in the transcript\\n reading like something the shopper typed, and gives the assistant a\\n well-formed prompt. The `title` attribute previews the full question on\\n hover.\\n- **Per-page for free.** The page URL is part of the generation context and\\n the cache key, so one site-wide snippet yields different chips on every\\n page \u2014 each served from cache on repeat renders.\\n- **Placement is the lever.** Under the product title, above the grid on\\n collections, at the end of a blog post \u2014 wherever a shopper pauses to\\n wonder, the chips name the question for them.\\n\\nTips:\\n\\n- Inline the product or collection name into the prompt when the platform\\n exposes it \u2014 it beats relying on `document.title`.\\n- Restyle the chips to the site\'s design system; the CSS above is just the\\n scroll mechanics (flex row, `overflow-x: auto`, hidden scrollbars,\\n `white-space: nowrap`).\\n- Resist adding more than ~6 chips \u2014 a launcher row is an invitation, not a\\n sitemap.\\n","recipes/page-action-suite.md":"# Page-action suite (WebMCP)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nGive every conversation on the site real capabilities. Register once in a\\nshared snippet, after the SDK is ready (which guarantees\\n`document.modelContext` exists):\\n\\n```js\\nwindow.GreatStore?.ready.then(() => {\\n const text = (value) => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(value) }],\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_cart\\",\\n description:\\n \\"Read the shopper\'s current cart on this site: items, quantities, \\" +\\n \\"and totals. Use before answering any cart question.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n async execute() {\\n return text(await (await fetch(\\"/cart.js\\")).json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the cart on this site. Use when the \\" +\\n \\"shopper asks to add or buy something. Confirm the variant with \\" +\\n \\"the shopper first if ambiguous.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1, maximum: 10 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n if (!variantId) throw new Error(\\"variantId is required\\");\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Could not add to cart (${res.status})`);\\n document.dispatchEvent(new CustomEvent(\\"cart:refresh\\"));\\n return text(await res.json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_current_page\\",\\n description:\\n \\"Read what page the shopper is currently on, including structured \\" +\\n \\"product data when on a product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute() {\\n return text({\\n url: location.href,\\n title: document.title,\\n productJson: document.querySelector(\\"#product-json\\")?.textContent ?? null,\\n });\\n },\\n });\\n});\\n```\\n\\nPrinciples at work: throw on failure (the assistant explains and recovers),\\nreturn fresh state after mutations (the assistant confirms accurately), cap\\nquantities in the schema, and notify your own UI (`cart:refresh`) so the\\npage reflects what the AI did.\\n\\nFor a product-page-only tool, register with an `AbortSignal` and abort on\\nSPA navigation:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(reviewsTool, { signal: ac.signal });\\nrouter.onLeave(\\"/products/:handle\\", () => ac.abort());\\n```\\n","recipes/product-faq.md":"# Product FAQ accordion\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n faqs: {\\n type: \\"array\\", minItems: 3, maxItems: 5,\\n items: {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 90 },\\n answer: { type: \\"string\\", maxLength: 300 },\\n },\\n required: [\\"question\\", \\"answer\\"],\\n },\\n },\\n },\\n required: [\\"faqs\\"],\\n },\\n `Generate the questions shoppers most plausibly have before buying the ` +\\n `product \\"${productName}\\", with accurate answers grounded in the real ` +\\n `product details and store policies. Skip any question the store data ` +\\n `can\'t answer confidently.`\\n);\\n\\nconst wrap = document.getElementById(\\"gs-faq\\");\\nfor (const { question, answer } of data.faqs) {\\n const details = document.createElement(\\"details\\");\\n const summary = document.createElement(\\"summary\\");\\n summary.textContent = question;\\n const p = document.createElement(\\"p\\");\\n p.textContent = answer;\\n details.append(summary, p);\\n wrap.append(details);\\n}\\nwrap.hidden = false;\\n```\\n\\nEngagement bonus \u2014 append a hand-off row so unanswered questions become\\nconversations:\\n\\n```js\\nconst ask = document.createElement(\\"button\\");\\nask.type = \\"button\\";\\nask.textContent = \\"Have a different question? Ask us\\";\\nask.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(`I have a question about \\"${productName}\\".`)\\n);\\nwrap.append(ask);\\n```\\n","references/agents-and-mcp.md":"# AI agents and the store\'s MCP endpoint\\n\\nEvery GreatStore store publishes a standard MCP server card, and the\\nplatform runs one MCP endpoint for coding agents \u2014 no authentication\\nrequired for either:\\n\\n| URL | What it is |\\n|---|---|\\n| `https://<slug>.greatstore.ai/.well-known/mcp/server-card.json` | Standard MCP server card \u2014 machine-readable discovery document for the store. |\\n| `https://admin.greatstore.ai/mcp` | A **documentation server for coding agents**: its tools return usage docs for the `gs` CLI. Same URL for every store. |\\n\\n## CLI docs for coding agents \u2014 `/mcp`\\n\\nA stateless HTTP MCP whose tools hand back markdown documentation for `gs`\\nCLI commands.\\n\\n```\\nclaude mcp add --transport http greatstore-admin https://admin.greatstore.ai/mcp\\n```\\n\\nUseful when a coding agent is shipping chat components\\n([chat-components.md](chat-components.md)) and needs the exact command for\\nthe next step. If this skill is installed, the agent already has the\\nworkflow \u2014 the MCP is the self-serve alternative for agents that don\'t.\\n\\n## What to use when\\n\\n- **Building the merchant\'s site** \u2192 this skill\'s other references (the SDK,\\n structured content, page tools).\\n- **A coding agent shipping chat components** \u2192 the `gs` CLI, with `/mcp`\\n as its built-in documentation.\\n- **Reading or changing store settings** (origin allowlists, CSP hosts, MCP\\n connectors, brand config) \u2192 the `gs` CLI\'s admin commands \u2014\\n [store-admin.md](store-admin.md), including which changes are safe to make\\n without asking the merchant.\\n","references/chat-components.md":"# Custom chat components \u2014 authoring with the `gs` CLI\\n\\nRemote components are React components the assistant renders **inside the\\nconversation** \u2014 product configurators, quizzes, size guides, booking forms,\\nanything richer than text. Each component is an AI-callable tool: the\\nmanifest\'s `description` tells the assistant *when* to show it, its\\n`inputSchema` declares the props the assistant fills in, and a `displayMode`\\npicks where it appears.\\n\\nAuthoring requires store-owner access (`gs login` signs in with the store\\nowner\'s account).\\n\\n## Workflow\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # browser sign-in\\ngs apps init --store my-store # scaffold a project root\\ncd <project> && npm install\\ngs apps init size_guide # scaffold components/size_guide/\\n# \u2026 edit components/size_guide/{component.tsx,manifest.json} \u2026\\ngs apps build # bundle every component\\ngs apps push # upload changed components as drafts\\ngs apps publish size_guide # promote to live\\n```\\n\\n`gs apps list` shows what\'s deployed (with dashboard links); `gs apps pull`\\nround-trips remote components back to disk. `gs apps push` hashes components\\nand only uploads what changed. (The subcommands also work at the top level \u2014\\n`gs push` == `gs apps push`.)\\n\\nThe scaffold writes an `AGENTS.md` into the project (with `CLAUDE.md` /\\n`GEMINI.md` symlinked) containing the complete design rules and brand\\nvariable table \u2014 your coding agent picks it up automatically when working in\\nthe project. `https://admin.greatstore.ai/mcp` serves the same CLI docs to\\nagents over MCP (see [agents-and-mcp.md](agents-and-mcp.md)).\\n\\n## `manifest.json`\\n\\n```json\\n{\\n \\"name\\": \\"size_guide\\",\\n \\"displayName\\": \\"Size guide\\",\\n \\"description\\": \\"Interactive size guide. Show when the shopper asks about sizing or fit for apparel.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {\\n \\"productName\\": { \\"type\\": \\"string\\" },\\n \\"category\\": { \\"type\\": \\"string\\" }\\n },\\n \\"required\\": [\\"productName\\"]\\n }\\n}\\n```\\n\\n| Field | Meaning |\\n|---|---|\\n| `name` | Tool name, snake_case (`^[a-z][a-z0-9_]*$`), matches the folder under `components/`. |\\n| `displayName` | Friendly label shown in chat UI. |\\n| `description` | **Load-bearing** \u2014 how the assistant decides when to render the component. Say what it shows *and* when to use it, like any good tool description. |\\n| `displayMode` | Where it renders \u2014 see below. |\\n| `inputSchema` | JSON Schema for the props the assistant fills. Keep it tight; required fields the AI can\'t infer cause bad calls. |\\n| `async` | Set `true` for backend-backed components (see Async below). |\\n\\n### Display modes\\n\\n- `inline` \u2014 a bubble inside the chat transcript; persists with the message\\n log.\\n- `over-input` \u2014 floats above the chat input (like a question overlay);\\n cleared by the next user turn or an explicit close.\\n- `fullscreen` \u2014 takes over the full preview surface; persists until the\\n next widget-emitting tool call or an explicit close.\\n\\n## The component contract\\n\\n`component.tsx` default-exports a React component. Its props are the\\n`inputSchema` fields the assistant filled, plus five GreatStore-injected\\nlifecycle props (always present):\\n\\n| Prop | What it does |\\n|---|---|\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it \u2014 lets the component drive the conversation (\\"Selected size M, what\'s the return policy?\\"). |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject **invisible** background context for the model \u2014 the variant the shopper selected, the options they configured, the step they\'re on. Replaces the prior value (never appends); pass `\\"\\"` to clear. Read on the next chat turn. Use this instead of `onSendMessage` when the assistant should *know* state without a message appearing in the transcript. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay you can\'t render yourself (your component is boxed inside its own bounds and shadow root). Pass the image `src`; for a smooth zoom, also pass the clicked element\'s `getBoundingClientRect()` as `originRect` (omit it and the image grows from the viewport centre). |\\n| `onClose()` | Dismiss the host slot. `over-input` clears the overlay, `fullscreen` reverts the pane, `inline` is a no-op. |\\n\\nReach for `onUpdateModelContext` when the shopper changes something inside\\nthe component (picks a size, configures a build, advances a quiz) and you\\nwant the assistant to factor it into the *next* thing they ask \u2014 without\\nspamming the chat with a visible \\"I selected M\\" message. Use `onSendMessage`\\nwhen you actually want a turn to happen now.\\n\\nReach for `onShowLightbox` whenever your component shows imagery the shopper\\nmight want to inspect closely \u2014 product photos, swatches, a size chart. A\\nthumbnail\'s `onClick` handler is the natural place to call it. Don\'t build\\nyour own full-screen modal: a remote component is sandboxed inside its own\\nbounds and shadow root, so a self-rendered overlay can\'t cover the chat. The\\nshared lightbox escapes those bounds and themes itself from the store\'s CSS\\nvariables.\\n\\n```tsx\\nimport React from \\"react\\";\\n\\ninterface Props {\\n productName: string;\\n category?: string;\\n onSendMessage: (text: string) => void;\\n onCallTool: (name: string, args: Record<string, unknown>) => void;\\n onUpdateModelContext: (context: string) => void;\\n onShowLightbox: (options: { src: string; originRect?: DOMRect }) => void;\\n onClose: () => void;\\n}\\n\\nexport default function SizeGuide({\\n productName,\\n onSendMessage,\\n onUpdateModelContext,\\n onShowLightbox,\\n}: Props) {\\n return (\\n <div\\n style={{\\n padding: \\"1em\\",\\n border: \\"1px solid var(--color-border-default)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n background: \\"var(--color-surface)\\",\\n color: \\"var(--color-foreground)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n >\\n {/* \u2026 sizes for {productName} \u2026 */}\\n <img\\n src={`/size-charts/${productName}.png`}\\n alt={`${productName} size chart`}\\n style={{ cursor: \\"zoom-in\\", width: \\"100%\\" }}\\n onClick={(e) =>\\n onShowLightbox({\\n src: `/size-charts/${productName}.png`,\\n originRect: e.currentTarget.getBoundingClientRect(),\\n })\\n }\\n />\\n <button\\n onClick={() =>\\n // Silent: the assistant now knows the pick for the shopper\'s\\n // next question, with nothing added to the transcript.\\n onUpdateModelContext(`Shopper selected size M of \\"${productName}\\".`)\\n }\\n >\\n Select size M\\n </button>\\n <button onClick={() => onSendMessage(`Size M of \\"${productName}\\" \u2014 is it in stock?`)}>\\n Ask about size M\\n </button>\\n </div>\\n );\\n}\\n```\\n\\n## Design rules (non-negotiable)\\n\\nComponents render inside arbitrary publisher pages *and* the GreatStore\\nstorefront; you control neither the host\'s root font size nor its colors.\\n\\n1. **Size in `em`, never `rem`** \u2014 `rem` resolves against the host page\'s\\n root font size, which is arbitrary (`html { font-size: 8px }` breaks every\\n `rem` dimension). `em` stays self-consistent anywhere. Borders may stay\\n `px`.\\n2. **Never hardcode colors, fonts, or radii** \u2014 read the brand CSS variables\\n GreatStore injects (`--color-primary`, `--color-surface`,\\n `--color-foreground`, `--color-border-default`, `--font-primary`,\\n `--radius-lg`, \u2026) so the component restyles itself with the store\'s\\n theme. The scaffolded `AGENTS.md` has the full variable table.\\n\\n## Async components (backend-backed data)\\n\\nIf a component must load data before it can render correctly, don\'t render a\\nshell and fetch in `useEffect` \u2014 set `\\"async\\": true` in the manifest and\\nexport an **async** default. GreatStore shows its own loading state, awaits\\nyour promise, and renders what it resolves to.\\n\\nA thrown error is a **retry signal**: the assistant sees it and usually\\nre-calls the tool. So only throw when a *different* call could help:\\n\\n1. Validate the AI-passed props first and throw on bad input \u2014 the AI can\\n fix the args and retry. (Don\'t validate the API\'s *output* and throw: the\\n AI can\'t fix your backend, it\'ll just loop.)\\n2. Throw on failures where retrying differently could succeed, and say what\\n to change (e.g. empty search \u2192 `\\"no results for X \u2014 try a broader keyword\\"`).\\n3. For idempotent failures (500, timeout, missing record) render a graceful\\n fallback instead of throwing \u2014 re-running the same call changes nothing.\\n\\n```tsx\\nexport default async function Results(props: Props) {\\n if (!props.query?.trim()) throw new Error(\\"missing required prop: query\\");\\n const res = await fetch(`/api/search?q=${encodeURIComponent(props.query)}`);\\n if (res.ok) {\\n const { results } = await res.json();\\n if (results.length === 0)\\n throw new Error(`no results for \\"${props.query}\\" \u2014 try a broader keyword`);\\n return <ul>{/* render results */}</ul>;\\n }\\n return <p>Couldn\'t load results right now.</p>; // idempotent: don\'t throw\\n}\\n```\\n\\n## When to build a component vs. the other facets\\n\\n- Content for the **merchant\'s page** \u2192 `generateStructuredContent`\\n ([structured-content.md](structured-content.md)).\\n- Letting the assistant **act on the page** \u2192 WebMCP page tools\\n ([embed-api.md](embed-api.md)).\\n- Rich, interactive UI **inside the conversation itself**, available on the\\n storefront and every embed without page changes \u2192 a chat component.\\n","references/embed-api.md":"# `window.GreatStore` API reference\\n\\n## Setup\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nOne script tag, anywhere on the page (end of `<body>` preferred), with the\\nstore\'s slug in the host. The `window.GreatStore` object exists synchronously\\nonce the script executes; every method below is safe to call before the chat\\nUI has finished loading \u2014 pre-mount calls are queued and replayed in order\\nonce it mounts. The SDK pre-warms its chat bundle in the background\\nautomatically; the panel stays closed until `open()` / `toggle()` /\\n`sendMessage()` is called or the shopper clicks the launcher.\\n\\nRequirements:\\n\\n- The page\'s domain must be in the store\'s **allowed domains** (GreatStore\\n store settings) \u2014 see Troubleshooting below for the failure signature.\\n- If the store also wants push notifications, host `gs.js` at the site root\\n and load that instead of `embed.js` \u2014 it injects the embed for you. See\\n [push-notifications.md](push-notifications.md).\\n\\nCode samples throughout are framework-free reference implementations \u2014\\nre-express them in the host repo\'s framework (React, Vue, Shopify Liquid,\\n\u2026) rather than retrofitting them as-is.\\n\\n## Properties\\n\\n| Property | Type | Description |\\n|---|---|---|\\n| `slug` | `string` | The store identifier the script was loaded for. |\\n| `host` | `string` | `\\"greatstore.ai\\"`. |\\n| `embedHost` | `string` | Origin the embed assets load from, e.g. `https://<slug>.greatstore.ai`. |\\n| `ready` | `Promise<void>` | Resolves when the chat UI has mounted and `open()` would render instantly. Resolved promises replay, so `.then()` works no matter when it\'s attached. The readiness signal to gate on. |\\n\\nA `greatstore:ready` `CustomEvent` (with the SDK object as `detail`) is also\\ndispatched on `window` at the moment `ready` resolves, for declarative\\ntooling. Unlike the promise, the listener must be attached before mount\\ncompletes \u2014 attach it before (or immediately after) the embed script tag.\\n\\n## Methods\\n\\n### `load(): void`\\n\\nPre-warms the chat bundle and identity in the background without opening the\\npanel. Called automatically when `embed.js` runs, so you rarely need it.\\nIdempotent.\\n\\n### `open(): void` / `close(): void` / `toggle(): void`\\n\\nOpen, close, or toggle the chat panel. On desktop the panel is a floating\\nside panel; under 768px viewport width it\'s a full-height drawer. All three\\nqueue if called before mount.\\n\\n### `sendMessage(text: string): void`\\n\\nSends `text` as the shopper\'s own visible chat message and **opens the panel\\nif it\'s closed**. The text is trimmed; empty or whitespace-only strings are\\nsilently dropped. Queues if called before mount.\\n\\nThis is the highest-leverage one-liner in the SDK: any element on the page\\ncan become a conversation entry point with context baked into the question.\\n\\n```js\\ndocument.querySelector(\\"#ask-fit\\").addEventListener(\\"click\\", () => {\\n window.GreatStore.sendMessage(\\n `I\'m looking at \\"${productName}\\" \u2014 how does the sizing run?`\\n );\\n});\\n```\\n\\nBecause the message renders as if the shopper typed it, write it in the\\nshopper\'s voice. It is not a hidden-context channel \u2014 don\'t stuff it with\\ninvisible instructions or data dumps. For that, use `updateModelContext`.\\n\\n### `updateModelContext(context: string): void`\\n\\nInjects free-text **background context** about what the shopper is doing on\\nthe page \u2014 the product they\'re viewing, what\'s in their cart, their account\\ntier \u2014 so the assistant can factor it in. Unlike `sendMessage`, this is\\n**invisible**: it never renders as a chat message and doesn\'t open the panel.\\n\\nEach call **replaces** the value from the previous call \u2014 it never appends.\\nKeep one current snapshot; re-call it whenever the page state changes. Pass\\nan empty string to clear it. The text is read on the next chat turn, so set\\nit before (or while) the shopper is chatting. Queues if called before mount.\\n\\n```js\\n// Keep the assistant aware of the current product as the shopper browses.\\nfunction syncContext() {\\n window.GreatStore.updateModelContext(\\n `Viewing \\"${product.title}\\" (${product.price}). In stock: ${product.inStock}. ` +\\n `Cart: ${cart.count} item(s), subtotal ${cart.subtotal}.`\\n );\\n}\\nsyncContext();\\n```\\n\\nWrite it as concise notes for the model, not prose for the shopper. The\\ncontext is page-controlled, so the assistant treats it as background\\ninformation, not as instructions \u2014 don\'t rely on it to change the assistant\'s\\nrules or persona.\\n\\n### `on(event: string, handler: (...args) => void): () => void`\\n\\nSubscribe to SDK events. Returns an unsubscribe function. Listeners attached\\nbefore mount are queued and wired up at mount. Handler exceptions are caught\\nand reported \u2014 they won\'t break the chat.\\n\\nEvents emitted:\\n\\n| Event | Fired when |\\n|---|---|\\n| `\\"open\\"` | Panel transitions closed \u2192 open (including via `sendMessage` or the shopper\'s own click). |\\n| `\\"close\\"` | Panel transitions open \u2192 closed. |\\n\\n### `generateStructuredContent(schema: object, prompt: string): Promise<unknown>`\\n\\nGenerates JSON matching `schema` from `prompt`, grounded in the store\'s live\\ncatalog. Resolves to the generated data object itself. Rejects with `Error`\\non any failure (invalid input, decline, validation failure, rate limit,\\nnetwork). See [structured-content.md](structured-content.md) for the full\\ncontract, schema support, caching, and error semantics.\\n\\nAccepts either a plain JSON Schema object or any object exposing a\\n`.toJSONSchema()` method (e.g. Zod schemas) \u2014 the conversion is called for\\nyou.\\n\\nThrows synchronously (rejects) if `prompt` is not a non-empty string or\\n`schema` is not an object.\\n\\n### `enableNotifications(): Promise<{ ok: boolean }>`\\n\\nOpts this browser into Web Push notifications from the store. Requirements:\\n\\n- Must be called from a user gesture (e.g. a click handler).\\n- The site must host GreatStore\'s `gs.js` service-worker file. By default the\\n SDK looks for it at `/gs.js`; if it\'s hosted elsewhere, point to it via an\\n attribute on the embed script tag:\\n `<script src=\\"\u2026/embed.js\\" data-push-sw-path=\\"/path/to/gs.js\\"></script>`.\\n\\nResolves `{ ok: true }` on success and `{ ok: false }` on any failure\\n(unsupported browser, no service worker hosted, permission denied). It never\\nrejects.\\n\\n## Page tools \u2014 WebMCP (`document.modelContext`)\\n\\nThe recommended way to expose page capabilities to the assistant is the\\nWebMCP standard. The GreatStore assistant discovers every tool registered on\\n`document.modelContext`, re-reading the list on each conversational turn \u2014\\nso tools registered mid-session appear on the next message without a reload.\\n\\n### Availability\\n\\nIf the browser implements WebMCP natively, `document.modelContext` is just\\nthere. Otherwise the SDK installs a spec-tracking polyfill on the page\\nautomatically \u2014 but asynchronously, so at your script\'s first run\\n`document.modelContext` may not exist yet. Two robust patterns:\\n\\n```js\\n// 1. Register once GreatStore is ready (polyfill is in place by then):\\nwindow.GreatStore?.ready.then(() => {\\n document.modelContext.registerTool(/* \u2026 */);\\n});\\n\\n// 2. Or ship your own polyfill (npm: @mcp-b/webmcp-polyfill) and register\\n// immediately \u2014 the SDK detects an existing implementation and uses it.\\n```\\n\\n(`navigator.modelContext` is a deprecated alias for the same object; use\\n`document.modelContext` in new code.)\\n\\n### `registerTool(tool, options?)`\\n\\n```ts\\ndocument.modelContext.registerTool(\\n {\\n name: string, // required, non-empty, unique on the page\\n description: string, // required \u2014 how the AI decides when to call it\\n inputSchema?: object, // JSON Schema for execute\'s args;\\n // defaults to { type: \\"object\\", properties: {} }\\n execute(args): Result | Promise<Result>,\\n },\\n options?: { signal?: AbortSignal }, // abort to unregister\\n);\\n```\\n\\n- **Result shape**: `execute` returns MCP content blocks \u2014\\n `{ content: [{ type: \\"text\\", text: \\"\u2026\\" }] }`. For structured data,\\n `JSON.stringify` it into `text`. Add `isError: true` to mark a handled\\n failure.\\n- **Errors**: a thrown error or rejected promise is delivered to the\\n assistant as a *failed* tool call carrying the error message \u2014 the\\n assistant can explain or adapt. Errors never escape into your page.\\n- **Duplicate names throw.** To replace a tool, abort its registration first.\\n- **Unregistration is `AbortSignal`-driven**: pass `{ signal }` and call\\n `abort()` when the tool\'s context goes away (SPA navigation, modal close).\\n A pre-aborted signal skips registration. (A legacy\\n `unregisterTool(name)` exists but is deprecated in the spec.)\\n- **Treat `args` as untrusted input**: values are AI-generated. Validate\\n before passing to your own APIs, and never `eval` anything from them.\\n\\nA complete tool, registered once GreatStore is ready:\\n\\n```js\\nwindow.GreatStore?.ready.then(() => {\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the shopper\'s cart on this site. \\" +\\n \\"Use when the shopper asks to add, buy, or get a product.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Cart add failed (${res.status})`);\\n const cart = await res.json();\\n return { content: [{ type: \\"text\\", text: JSON.stringify(cart) }] };\\n },\\n });\\n});\\n```\\n\\nReturning the fresh cart state after the mutation lets the assistant confirm\\naccurately. Good tool families: cart (`get_cart`, `add_to_cart`), navigation\\n(`go_to_page`), page state (`get_current_product`, `apply_filters`), UI\\n(`highlight_section`, `scroll_to_reviews`).\\n\\nAnd a context-scoped tool, unregistered via `AbortSignal`:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(\\n {\\n name: \\"get_product_reviews\\",\\n description: \\"Read the reviews shown on the current product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute: () => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(collectReviews()) }],\\n }),\\n },\\n { signal: ac.signal },\\n);\\n\\n// On SPA route change away from the product page:\\nac.abort();\\n```\\n\\n## URL parameter: `?gs_chat=open`\\n\\nWhen the page URL carries `gs_chat=open`, the panel opens automatically once\\nthe embed mounts. The param is consumed and stripped from the URL via\\n`history.replaceState`, so a manual reload doesn\'t re-open the panel. Use it\\nin campaign links, emails, and post-login redirects.\\n\\n## Troubleshooting\\n\\n| Symptom | Likely cause |\\n|---|---|\\n| Console: `[GreatStore] Chat is unavailable on <origin>\u2026 this domain isn\'t in the store\'s allowed domains` | The page\'s origin isn\'t in the store\'s allowed domains. Add it in store settings. Until then every SDK network call fails. |\\n| Console: `[GreatStore] Embed script must be loaded from <slug>.greatstore.ai/embed.js` | The script was copied/self-hosted instead of loaded from the store\'s embed URL. Always load it from `https://<slug>.greatstore.ai/embed.js`. |\\n| `generateStructuredContent` rejects with a rate-limit message | More than ~20 requests/minute from one visitor. Consolidate calls into fewer, richer schemas. |\\n| Panel won\'t auto-open on mobile after returning to the page | Intentional: the mobile drawer never auto-opens on resume \u2014 it would cover the content the shopper is reading. The transcript is preserved; they\'ll see it when they tap the launcher. |\\n| Tools registered but the assistant doesn\'t use them | Check the `description` \u2014 it\'s the only signal for *when* to call. Also confirm the registration ran (`document.modelContext` exists after `ready`) on the same page the conversation is on. |\\n","references/push-notifications.md":"# Web push re-engagement\\n\\nShoppers who opt in receive browser notifications from the store \u2014 under the\\nmerchant\'s own domain and branding, with the permission prompt shown inline\\non the merchant\'s page. Setup is two pieces: a single file hosted at the site\\nroot, and an opt-in button.\\n\\n## 1. Host `gs.js` at the site root\\n\\nDownload the store\'s loader and serve it at `/gs.js` on the merchant\'s\\ndomain:\\n\\n```\\nhttps://<slug>.greatstore.ai/gs.js \u2192 https://www.merchant-site.com/gs.js\\n```\\n\\nAlways download it from the **store\'s own subdomain** \u2014 the file is built for\\nthat store; don\'t copy one from elsewhere.\\n\\nThen load it with one tag (replacing the `embed.js` tag \u2014 `gs.js` injects the\\nembed for you and registers itself as the service worker):\\n\\n```html\\n<script src=\\"/gs.js\\"></script>\\n```\\n\\nHosting this file is what enables push. Without it, push is simply off \u2014\\n`enableNotifications()` returns `{ ok: false }` and nothing else changes.\\n\\n### Non-root hosting\\n\\nIf the platform can\'t serve files at the site root (e.g. Shopify themes\\nserve assets under a path), keep the regular `embed.js` tag and point it at\\nwhere the file lives \u2014 the path must be on the merchant\'s own origin:\\n\\n```html\\n<script\\n src=\\"https://my-store.greatstore.ai/embed.js\\"\\n data-push-sw-path=\\"/cdn/shop/files/gs.js\\"\\n></script>\\n```\\n\\n## 2. Offer the opt-in from a user gesture\\n\\n```js\\noptInButton.addEventListener(\\"click\\", async () => {\\n const { ok } = await window.GreatStore.enableNotifications();\\n optInButton.hidden = ok; // done \u2014 or quietly keep the button\\n});\\n```\\n\\nRules that make this work well:\\n\\n- **Always call it from a click** \u2014 browsers ignore or penalize permission\\n prompts that aren\'t user-initiated, and the call is designed for gesture\\n context.\\n- **Never prompt on page load.** Tie the button to a moment where\\n notifications have obvious value (\\"Notify me when this is back in stock\\",\\n post-purchase, after a chat conversation).\\n- `{ ok: false }` covers every failure the same way \u2014 unsupported browser, no\\n `gs.js` hosted, permission denied. It never rejects, and there\'s no popup\\n fallback. Design the button so a decline just leaves the page as it was;\\n don\'t show an error.\\n- The promise resolving `{ ok: true }` means this browser is subscribed.\\n There\'s nothing else to wire \u2014 notification delivery is handled by\\n GreatStore.\\n","references/store-admin.md":"# Store administration from the CLI\\n\\nThe `gs` CLI is a full admin surface for a GreatStore store, mirroring the\\nmerchant\'s admin dashboard one-to-one: `gs configure` \u2194 the Configure panel,\\n`gs connectors` \u2194 the Connectors panel, `gs apps` \u2194 the Apps panel. Same\\nfields, same behaviour \u2014 anything you change is what the merchant sees in\\ntheir dashboard.\\n\\nThat makes the CLI the way a coding agent grounds and unblocks its own work:\\nread the store\'s configuration to make better decisions, and make the narrow\\nclass of additive, integration-enabling changes yourself instead of telling\\nthe user to go click through a dashboard.\\n\\n## Setup\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # one-time browser sign-in (needs a human)\\n```\\n\\nCredentials persist across runs. Admin commands take `--store <slug>`\\ndirectly, or read the nearest `.gsrc` (`{\\"store\\":\\"my-store\\"}`) \u2014 so they work\\nfrom any repo, not just a scaffolded component project. Every read supports\\n`--json` for machine-readable output.\\n\\n## Read freely \u2014 always safe\\n\\nReading store state is never destructive. Do it whenever it would improve a\\ndecision:\\n\\n```\\ngs configure --store my-store --json # brand config\\ngs connectors --store my-store --json # MCP connectors feeding the assistant\\ngs connectors health --store my-store # live-probe them (reports tool counts)\\ngs apps list --store my-store --json # deployed custom chat components\\n```\\n\\nWhat each read is good for:\\n\\n- **`gs configure`** \u2014 the store\'s `extraOrigins` (is the site you\'re\\n integrating actually allowlisted? \u2014 see the embed troubleshooting note in\\n [embed-api.md](embed-api.md)), `theme` (match generated on-page UI to the\\n brand\'s colors/radius), `storeLink`, and CSP host lists.\\n- **`gs connectors`** \u2014 which MCP connectors the assistant has, and whether\\n the Maker MCP is on. This tells you what the assistant can actually *do*:\\n check it before building features that lean on catalog data (e.g.\\n [structured content](structured-content.md) grounding), and before\\n registering WebMCP page tools that would duplicate a capability a\\n server-side connector already provides.\\n- **`gs connectors health`** \u2014 a connector being listed doesn\'t mean it\'s\\n reachable; this probes each one live. Run it when assistant behaviour\\n suggests a tool isn\'t responding.\\n\\n## Safe to change without asking\\n\\nWhen the task the user gave you requires it, these changes are yours to\\nmake: they\'re additive, easy to undo, and invisible to shoppers.\\n\\n- **`extraOrigins`** \u2014 the origin allowlist for the embed. Installing the\\n widget on a new site/staging domain? Add that origin yourself instead of\\n reporting \\"ask the merchant to allowlist it\\".\\n- **`cspScriptHosts` / `cspConnectHosts`** \u2014 hosts the storefront accepts\\n for scripts/connections. Add a host when a custom chat component\\n legitimately needs a third-party SDK or API.\\n\\n**List flags replace the stored list \u2014 always read-merge-write.** Fetch the\\ncurrent value, append yours, write the union:\\n\\n```\\ngs configure --store my-store --json\\n# extraOrigins is [\\"https://shop.example.com\\"] \u2192 write both, comma-separated:\\ngs configure set --store my-store \\\\\\n --extraOrigins \\"https://shop.example.com,https://staging.example.com\\"\\n```\\n\\nNever drop an entry you didn\'t add, and say what you changed (and why) when\\nyou report back to the user.\\n\\n## Ask the merchant first\\n\\nEverything below is shopper-visible or changes what the live assistant can\\ndo for every shopper. Propose it, don\'t do it unprompted:\\n\\n- **Brand-visible config** \u2014 `displayName`, `assistantName`, `salesGuide`,\\n `storeLink`, `theme`, and asset uploads\\n (`gs configure upload|clear icon|logoLight|logoDark`).\\n- **Connector mutations** \u2014 `gs connectors add|remove|enable|disable` and\\n `gs connectors maker on|off`. (`add` probes the connector for tool\\n discovery before saving and refuses if it doesn\'t answer; `--force`\\n overrides. Still: adding capabilities to the merchant\'s assistant is the\\n merchant\'s call.)\\n- **Anything that clears** \u2014 passing `\\"\\"` to empty a field, removing list\\n entries, `clear`ing assets.\\n\\nWhen the user has *explicitly asked* for one of these (\\"set the assistant\'s\\nname to Voyager\\", \\"connect this MCP server\\"), that\'s the go-ahead \u2014 do it\\nand confirm the result with a read.\\n\\n## Command reference\\n\\n```\\ngs configure [--json] show configuration\\ngs configure set --<field> <value> displayName, assistantName,\\n salesGuide (--salesGuideFile <path>),\\n storeLink, extraOrigins a,b,\\n theme \'<json>\' (--themeFile <path>),\\n cspScriptHosts a,b, cspConnectHosts a,b\\n (\\"\\" clears a field)\\ngs configure upload <kind> <file> icon | logoLight | logoDark (.png/.jpg/.webp)\\ngs configure clear <kind> remove an uploaded asset\\n\\ngs connectors [--json] list Maker toggle + custom connectors\\ngs connectors add <name> --url <url> [--token <t>] [--profileUrl <u>]\\n [--disabled] [--force]\\ngs connectors remove <name|id>\\ngs connectors enable|disable <name|id>\\ngs connectors maker on|off\\ngs connectors health [<name|id>] [--json]\\n\\ngs apps <init|build|list|pull|push|publish|unpublish|delete>\\n see chat-components.md for the workflow\\n```\\n","references/structured-content.md":"# `generateStructuredContent` deep dive\\n\\nAI-generated, catalog-grounded JSON for UI you render yourself \u2014 the chat\\npanel is not involved.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(schema, prompt);\\n```\\n\\n- `schema` \u2014 a JSON Schema describing the output (or a Zod schema exposing\\n `.toJSONSchema()`).\\n- `prompt` \u2014 what to generate.\\n- Resolves to **the generated JSON object itself**, matching the schema.\\n Rejects with an `Error` on any failure.\\n\\nCode samples here are framework-free reference implementations \u2014 re-express\\nthem in the host repo\'s framework (React, Vue, Shopify Liquid, \u2026) rather\\nthan retrofitting them as-is.\\n\\n## The rules that make it work well\\n\\nEach is unpacked in the sections below; this is the checklist.\\n\\n1. **Top level must be an object.** Want a list? Wrap it:\\n `{ type: \\"object\\", properties: { items: { type: \\"array\\", \u2026 } }, required: [\\"items\\"] }`.\\n2. **Steer with the prompt, not schema `description`s** \u2014 free-text schema\\n fields are stripped before the AI sees them. Use self-explanatory\\n property names (`benefitHeadline`, not `text1`).\\n3. **Only `require` what\'s guaranteed.** `required` is strictly enforced; if\\n the catalog can\'t ground a required field the whole call can fail. Require\\n structural fields, keep per-product details (image URLs, prices) optional,\\n and make rendering tolerate missing values.\\n4. **Point, don\'t paste.** GreatStore researches the store\'s live catalog on\\n its own \u2014 name the entity (`` `\u2026the product \\"${productName}\\" (SKU ${sku})` ``)\\n and let it look the facts up. Don\'t fetch specs/prices/descriptions\\n yourself and paste them into the prompt. The one thing it *can\'t* see is\\n your page, so page-only context (which page the shopper is on, what the\\n section is for) does belong in the prompt.\\n5. **Research is bounded by the store\'s connectors \u2014 validate before you\\n build.** The AI can only look up what the store\'s MCP connectors actually\\n provide. Ask for something outside them \u2014 currency conversion, live\\n shipping rates, review data the store never wired up \u2014 and it has nothing\\n to ground on, so it will decline, omit\u2026 or hallucinate. Before writing a\\n prompt or schema that depends on a data capability, run `gs connectors`\\n (and `gs connectors health`, [store-admin.md](store-admin.md)) and confirm\\n a connector for it exists; if it doesn\'t, don\'t ask for it.\\n6. **Responses are cached** for up to ~24h per page + prompt + schema \u2014\\n shared across anonymous visitors, per-shopper for identified ones. Keep\\n prompts deterministic per page \u2014 no timestamps, random values, or\\n per-visitor data (GreatStore already knows who the shopper is; see below).\\n7. **Progressive enhancement, always.** Generate after the page renders into\\n a hidden container, reveal on success, leave the fallback on error. Render\\n generated strings via `textContent`, never `innerHTML`.\\n8. **One rich call beats many small ones** \u2014 there\'s a per-visitor rate limit\\n (~20 requests/minute); fetch multiple surfaces with one combined schema.\\n\\n## What actually happens\\n\\n1. The SDK posts your schema + prompt to the store\'s GreatStore endpoint,\\n along with the current **page URL and page title** (sent automatically \u2014\\n you don\'t pass them, and you can\'t override them).\\n2. GreatStore first **researches**: it looks up real data from the store\'s\\n live catalog (products, prices, availability, store info) using read-only\\n lookups. The page URL/title serve as hints about which product or category\\n to look up \u2014 they are *not* treated as a source of product data, and the\\n page\'s DOM is never read. Research happens on GreatStore\'s side \u2014 your\\n prompt only needs to *point* it at the right SKU, product, or collection,\\n not carry the material. Its reach is exactly the store\'s MCP connectors:\\n validate with `gs connectors` ([store-admin.md](store-admin.md)) that a\\n connector for the data you want actually exists before you build on it \u2014\\n research can\'t exceed the wired-up connectors, and prompts that assume\\n otherwise invite hallucinated filler.\\n3. The AI then fills your schema from the researched data, under a strict\\n grounding contract: it must not invent product names, prices, images, IDs,\\n or descriptions. Fields it can\'t ground are omitted or `null`. For an\\n identified shopper this step also sees their shopper profile \u2014 the same\\n identity the chat assistant has \u2014 so the output can be subtly personalized\\n without you passing anything about the visitor.\\n4. The output is validated against your schema (with internal retries) before\\n being returned and cached.\\n\\nGreatStore already knows who\'s reading: identity rides the request the same\\nway it does for chat, and responses for identified shoppers are cached just\\nfor them (anonymous visitors share one entry). The practical consequence:\\nnever put shopper data in the prompt \u2014 it\'s redundant, and it poisons the\\ncache key.\\n\\n## Schema support\\n\\nTop level **must describe an object**: `type: \\"object\\"` (or a bare\\n`properties` / `anyOf`). To get a list, wrap it in an object property.\\n\\nSupported keywords (anything else is tolerated but ignored):\\n\\n- Types: `object`, `array`, `string`, `number`, `integer`, `boolean`, `null`\\n- Structure: `properties`, `required`, `items`, `additionalProperties`\\n- Choice: `enum`, `const`, `anyOf`, `nullable`\\n- Constraints: `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`,\\n `maxItems`, `pattern`, `format`, `default`\\n\\nValidation of the output is real: `required` is enforced, `enum`/`const`\\nmust match, numeric and length bounds are checked, and\\n`additionalProperties: false` rejects extra keys. Constraints are therefore a\\n*tool* \u2014 `maxItems: 4` reliably caps a list, `enum` reliably restricts a\\nfield \u2014 but every constraint is also a way for generation to fail, so apply\\nthem only where you\'d rather have no content than non-conforming content.\\n\\nZod schemas (or anything with a `.toJSONSchema()` method) are accepted and\\nconverted automatically.\\n\\n### Free-text schema fields are stripped\\n\\n`description`, `title`, and `example` are removed from the schema before the\\nAI sees it (they\'re a prompt-injection surface, so they\'re filtered\\nserver-side). Consequences:\\n\\n- Schema descriptions **cannot** steer generation. All steering lives in the\\n prompt string.\\n- Property *names* are the only in-schema signal of intent \u2014 make them\\n self-documenting: `ctaLabel`, `warmthRating`, `priceJustification`.\\n\\n## Prompting guide\\n\\nThe prompt is the entire instruction channel. A good prompt states, in order:\\n\\n1. **Context** \u2014 what page/situation the shopper is in, naming the entity so\\n research targets the right thing (the AI can\'t see your DOM, so identify\\n it explicitly):\\n `The shopper is viewing the product \\"Aurora Down Parka\\" (SKU AUR-021) on its product page.`\\n2. **Task** \u2014 what to generate, mapped loosely onto your schema\'s fields:\\n `Write a heading and 3 reasons to love it; each reason has a short title and one supporting sentence.`\\n3. **Grounding expectations** \u2014 what store data to draw on:\\n `Base every claim on the product\'s real materials, features, and price.`\\n4. **Voice** \u2014 tone and constraints:\\n `Warm and concrete. No exclamation marks, no generic marketing filler.`\\n\\nNote what\'s *not* in that prompt: no pasted specs, prices, or descriptions.\\nPointing at the SKU is enough \u2014 GreatStore researches the rest itself, from\\ndata that\'s live rather than whatever was true when you wrote the prompt.\\n\\nAnti-patterns:\\n\\n- **Pasting researched material into the prompt** (specs, prices,\\n descriptions you fetched from your platform\'s API). GreatStore researches\\n the live catalog itself \u2014 point it at the SKU/product/collection and let\\n it look things up. Pasted facts go stale, bloat the cache key, and compete\\n with the fresher data research returns.\\n- **Per-visitor or per-moment data in the prompt** (names, cart contents,\\n timestamps, `Math.random()`): destroys caching, so every visitor pays full\\n generation latency and the store pays for every call \u2014 and it\'s redundant,\\n because GreatStore already knows the shopper and personalizes for\\n identified ones server-side. If you need per-shopper *interaction*, that\'s\\n what `sendMessage` and the chat panel are for.\\n- **Asking for data no connector provides** (\\"convert the price to EUR\\",\\n \\"estimate delivery to the shopper\'s city\\"): research can\'t exceed the\\n store\'s MCP connectors, and the AI may hallucinate plausible-looking\\n values rather than leave the field empty. Validate first \u2014 `gs connectors`\\n shows what\'s wired up ([store-admin.md](store-admin.md)); if there\'s no\\n connector for it, don\'t ask for it.\\n- **Asking it to read the page** (\\"summarize the reviews shown below\\") \u2014 it\\n can\'t. Page-only data (review snippets, UGC, things that exist nowhere but\\n the DOM) is the one kind worth inlining \u2014 keep it stable per page so\\n caching still works.\\n- **Asking for minute-fresh operational data** (exact live stock counts,\\n delivery countdowns). Even when a connector could answer, responses cache\\n for up to ~24h \u2014 display operational data from your own platform APIs and\\n use GreatStore for *editorial intelligence over the catalog*.\\n- **Burying instructions in schema descriptions** \u2014 stripped, see above.\\n\\n## Caching: design for it\\n\\nResponses are cached server-side for up to **24 hours**, keyed by the\\ncombination of page URL + page title + prompt + schema. Anonymous visitors\\nshare one entry per key; identified shoppers each get their own (their\\noutput may be personalized, so it\'s only ever served back to them).\\n(Tracking query params like `utm_*`/`gclid` and the URL fragment are\\nignored, so ad-tagged visits share the campaign-free page\'s cache entry.\\nMeaningful params like `?product=123` are part of the key.)\\n\\nPractical consequences:\\n\\n- **The first render pays, repeats fly.** Expect a few seconds on a cache\\n miss and near-instant responses after \u2014 shared across all anonymous\\n traffic, per-shopper for identified traffic (the research underneath is\\n cached briefly and shared, so even those misses are cheaper than cold).\\n Design loading states for the miss case.\\n- **Same call on different pages = different content**, automatically \u2014 the\\n page URL is in the key and in the AI\'s hints. A single site-wide snippet\\n with a constant prompt yields per-page content for free.\\n- **Content refreshes roughly daily.** Don\'t build experiences that assume\\n minute-level freshness.\\n- **To force different content, change the prompt or schema** (e.g. a\\n campaign variant string that changes weekly \u2014 deliberate, low-cardinality\\n variation is fine; per-visitor cardinality is not).\\n\\nThe catalog research underneath is also cached briefly, so several distinct\\nsurfaces on the same page (different prompts/schemas) stay cheap even on\\ncold cache.\\n\\n## Errors and how to handle them\\n\\nThe promise rejects with `new Error(message)`. The message is\\ndeveloper-facing \u2014 never render it to shoppers. Cases:\\n\\n| Case | Message you\'ll see | Retry? |\\n|---|---|---|\\n| Bad input (empty prompt, non-object schema) | thrown immediately by the SDK | Fix the call |\\n| Invalid schema shape | `Invalid schema: \u2026` | Fix the schema |\\n| AI declined the request | `The assistant declined to generate content for this request.` | No \u2014 permanent for that prompt/schema. Rework the prompt. |\\n| Output couldn\'t satisfy the schema | `Failed to produce valid structured content` | No \u2014 usually `required`/constraints demand data the catalog lacks. Loosen the schema. |\\n| Rate limit (~20/min per visitor) | rate-limit message | Later \u2014 and consolidate calls |\\n| Network / server | varies | Next page load |\\n\\nThe uniform shopper-facing strategy: render into a hidden-by-default\\ncontainer, reveal on success, leave hidden (or show your static fallback) on\\nany rejection. One `try/catch`, no case analysis needed unless you\'re\\nlogging.\\n\\n## Performance pattern\\n\\nFire generation as early as possible without blocking render \u2014 top of your\\ndeferred script, before other work:\\n\\n```js\\nconst highlightsPromise = window.GreatStore?.generateStructuredContent\\n ? window.GreatStore.generateStructuredContent(schema, prompt).catch(() => null)\\n : Promise.resolve(null);\\n\\n// \u2026rest of page setup\u2026\\n\\nconst data = await highlightsPromise;\\nif (data) renderHighlights(data);\\n```\\n\\nThe `.catch(() => null)` attached immediately avoids unhandled-rejection\\nnoise while keeping a single render path.\\n\\nFor multiple surfaces on one page, prefer **one call with a combined\\nschema** over parallel calls \u2014 it\'s one research pass, one cache entry, and\\nno rate-limit pressure:\\n\\n```js\\nconst schema = {\\n type: \\"object\\",\\n properties: {\\n highlights: { /* \u2026 */ },\\n faq: { /* \u2026 */ },\\n crossSell: { /* \u2026 */ },\\n },\\n required: [\\"highlights\\"],\\n};\\n```\\n"}') : readTreeFromDisk(new URL("../../gs-skill/", import.meta.url));
699
+ cache2 = true ? JSON.parse('{"SKILL.md":"---\\nname: greatstore\\ndescription: 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 \u2014 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.\\n---\\n\\n# Building with GreatStore\\n\\nGreatStore gives a store an AI shopping assistant on two surfaces: a hosted\\nstorefront at `https://<slug>.greatstore.ai/`, and an embedded chat widget on\\nthe merchant\'s own site, installed with one script tag:\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nEverything else a site can do with GreatStore is documented in the\\nreferences below. Read the one that matches the task before writing code \u2014\\neach facet has non-obvious rules (caching, grounding, result shapes, design\\nconstraints) that the references spell out.\\n\\n## Index\\n\\n| Goal | Use | Read |\\n|---|---|---|\\n| Install the widget; control the panel; readiness, events, troubleshooting | `window.GreatStore` SDK | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Copy-paste on-page experiences | recipes built on the SDK | the [Recipes](#recipes) table below |\\n| Contextual **conversation entry points** anywhere on the page | `sendMessage(text)`, `open()`, `?gs_chat=open` | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Let the assistant **act on the page** (cart, navigation, filters) | WebMCP: `document.modelContext.registerTool(...)` | [references/embed-api.md](references/embed-api.md) |\\n| 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) |\\n| Expand a component\'s image in an **on-brand full-screen lightbox** | component `onShowLightbox({ src, originRect })` | [references/chat-components.md](references/chat-components.md) |\\n| **Re-engage shoppers** with browser notifications | merchant-hosted `gs.js` + `enableNotifications()` | [references/push-notifications.md](references/push-notifications.md) |\\n| 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) |\\n| **Administer the store** \u2014 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) |\\n\\n## Three things to know before any of it\\n\\n- **Every code sample in this skill is a reference implementation, not a\\n drop-in.** Samples are framework-free vanilla JS so they stay portable \u2014\\n re-express the same logic in the conventions of the repo you\'re working\\n in (React, Vue, Shopify Liquid sections, Svelte, \u2026); never retrofit the\\n sample as-is into a codebase with its own framework.\\n- The page\'s domain **must be in the store\'s allowed domains**. If it isn\'t,\\n nothing works and the console shows\\n `[GreatStore] Chat is unavailable on <origin>\u2026` \u2014 check this first whenever\\n the embed appears dead. You can verify and fix it yourself with the `gs`\\n CLI (read-merge-write on `extraOrigins` \u2014 see\\n [references/store-admin.md](references/store-admin.md)).\\n- Every SDK call is safe immediately after the script tag \u2014 pre-mount calls\\n queue and replay in order, and the SDK pre-warms itself in the background.\\n\\n## Recipes\\n\\nComplete, framework-free implementations, one per file. Shared conventions:\\ncontainers start `hidden` and reveal only on success (a failed generation\\nchanges nothing); generated strings render via `textContent`, never\\n`innerHTML`; every recipe guards on `window.GreatStore`; prompts stay\\ndeterministic per page so repeat renders hit the cache.\\nInline real product/page data into prompts where the platform exposes it.\\n\\n| Recipe | What it builds | Use it for |\\n|---|---|---|\\n| [GreatStore launchers](recipes/launchers.md) | Horizontally scrollable AI-generated chips \u2014 engaging first-person questions about the current page; tap to ask the assistant. | Instant engagement on any page type \u2014 product, collection, blog, home. Simple yet effective; start here. |\\n| [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 \u2014 anywhere a shopper hesitates. |\\n| [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. |\\n| [Comparison table](recipes/comparison-table.md) | AI-picked representative products compared on category-relevant criteria. | Collection pages where shoppers weigh options. |\\n| [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. |\\n| [Campaign hero](recipes/campaign-hero.md) | Seasonal homepage hero copy, cache-keyed to the ISO week. | Fresh homepage/campaign copy without manual rewrites. |\\n| [Gift finder funnel](recipes/gift-finder-funnel.md) | Quiz teaser \u2192 chat handoff \u2192 page-tool navigation; the full funnel. | Gifting seasons, guided discovery, homepage engagement. |\\n| [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. |\\n| [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. |\\n\\n## How the facets combine\\n\\nThe strongest pattern is the **teaser \u2192 conversation \u2192 action** funnel:\\n`generateStructuredContent` renders a grounded teaser in the merchant\'s\\ndesign; each option\'s click handler calls `sendMessage` with the shopper\'s\\nchoice, dropping them into a conversation with momentum; WebMCP page tools\\nand custom chat components let that conversation actually do things \u2014 add to\\ncart, configure a product, book a slot \u2014 so it ends in a conversion, not a\\ncopy-paste. The [gift finder funnel](recipes/gift-finder-funnel.md)\\nrecipe is this funnel end to end.\\n","recipes/ask-about-this.md":"# \\"Ask about this\\" entry points (`sendMessage` only)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nZero-generation, instant, and often the biggest engagement win per line of\\ncode. Sprinkle context-aware buttons wherever a shopper hesitates:\\n\\n```js\\nconst gs = window.GreatStore;\\nif (gs) {\\n sizeGuideLink.addEventListener(\\"click\\", (e) => {\\n e.preventDefault();\\n gs.sendMessage(`How does the sizing run on \\"${productName}\\"? I usually wear a medium.`);\\n });\\n\\n shippingRow.querySelector(\\".ask\\").addEventListener(\\"click\\", () => {\\n gs.sendMessage(`What are the shipping options and times for \\"${productName}\\"?`);\\n });\\n\\n outOfStockBadge?.addEventListener(\\"click\\", () => {\\n gs.sendMessage(`\\"${productName}\\" looks out of stock \u2014 is there anything similar in stock?`);\\n });\\n}\\n```\\n\\nWrite each message as something the shopper would plausibly say \u2014 it appears\\nin the transcript as their message.\\n","recipes/campaign-hero.md":"# Campaign hero with deliberate variation\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nCache-friendly variation: key the prompt to a low-cardinality period, not to\\ntime itself.\\n\\n```js\\n// ISO week number \u2192 one generation per store per week, shared by everyone.\\nconst week = (d => {\\n const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\\n t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));\\n return `${t.getUTCFullYear()}-W${Math.ceil((((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5) + 1) / 7)}`;\\n})(new Date());\\n\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n headline: { type: \\"string\\", maxLength: 60 },\\n subline: { type: \\"string\\", maxLength: 120 },\\n featuredProductName: { type: \\"string\\", nullable: true },\\n ctaChatMessage: { type: \\"string\\", maxLength: 120 },\\n },\\n required: [\\"headline\\", \\"subline\\", \\"ctaChatMessage\\"],\\n },\\n `Variant ${week}. Write a homepage hero for this store: a headline and ` +\\n `subline spotlighting a real product or category that fits the current ` +\\n `season, plus ctaChatMessage \u2014 the first-person message a shopper ` +\\n `would send to start shopping for it.`\\n);\\n\\nheroHeadline.textContent = data.headline;\\nheroSubline.textContent = data.subline;\\nheroCta.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(data.ctaChatMessage)\\n);\\nhero.hidden = false;\\n```\\n","recipes/comparison-table.md":"# Collection-page comparison table\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```html\\n<section id=\\"gs-compare\\" hidden>\\n <h3>Quick comparison</h3>\\n <table><thead id=\\"gsc-head\\"></thead><tbody id=\\"gsc-body\\"></tbody></table>\\n</section>\\n\\n<script>\\n (async () => {\\n if (!window.GreatStore?.generateStructuredContent) return;\\n const collection = \\"winter jackets\\"; // \u2190 your collection name\\n try {\\n const data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n criteria: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: { type: \\"string\\", maxLength: 25 },\\n },\\n rows: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n bestFor: { type: \\"string\\", maxLength: 60 },\\n values: {\\n type: \\"array\\",\\n items: { type: \\"string\\", maxLength: 60 },\\n },\\n },\\n required: [\\"productName\\", \\"bestFor\\", \\"values\\"],\\n },\\n },\\n },\\n required: [\\"criteria\\", \\"rows\\"],\\n },\\n `The shopper is browsing the \\"${collection}\\" collection. Pick the ` +\\n `3-4 most representative products and compare them. Choose the ` +\\n `criteria a shopper actually decides on for this category. ` +\\n `\\"values\\" must align with \\"criteria\\" by index. Add a one-line ` +\\n `\\"bestFor\\" verdict per product. Only use real products and facts.`\\n );\\n\\n const head = document.getElementById(\\"gsc-head\\");\\n const hr = document.createElement(\\"tr\\");\\n for (const h of [\\"Product\\", ...data.criteria, \\"Best for\\"]) {\\n const th = document.createElement(\\"th\\");\\n th.textContent = h;\\n hr.append(th);\\n }\\n head.append(hr);\\n\\n const body = document.getElementById(\\"gsc-body\\");\\n for (const row of data.rows) {\\n const tr = document.createElement(\\"tr\\");\\n const cells = [row.productName, ...(row.values ?? []), row.bestFor];\\n for (let i = 0; i < data.criteria.length + 2; i++) {\\n const td = document.createElement(\\"td\\");\\n td.textContent = cells[i] ?? \\"\u2014\\";\\n tr.append(td);\\n }\\n body.append(tr);\\n }\\n document.getElementById(\\"gs-compare\\").hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nNote the index-aligned `values`/`criteria` trick and the `?? \\"\u2014\\"` guard \u2014\\ngrounding means a value the catalog can\'t support may be missing.\\n","recipes/complete-the-look.md":"# \\"Complete the look\\" cross-sell strip\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n intro: { type: \\"string\\", maxLength: 90 },\\n picks: {\\n type: \\"array\\", minItems: 2, maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n productName: { type: \\"string\\" },\\n reason: { type: \\"string\\", maxLength: 90 },\\n imageUrl: { type: \\"string\\", nullable: true },\\n productUrl: { type: \\"string\\", nullable: true },\\n },\\n required: [\\"productName\\", \\"reason\\"],\\n },\\n },\\n },\\n required: [\\"picks\\"],\\n },\\n `The shopper is viewing \\"${productName}\\". From the store\'s real catalog, ` +\\n `pick 2-4 products that genuinely pair with it and say why each one ` +\\n `completes the look or use-case. Include image and product URLs only ` +\\n `if known.`\\n);\\n\\nfor (const pick of data.picks) {\\n const card = document.createElement(\\"a\\");\\n if (pick.productUrl) card.href = pick.productUrl;\\n if (pick.imageUrl) {\\n const img = document.createElement(\\"img\\");\\n img.src = pick.imageUrl;\\n img.alt = pick.productName;\\n img.loading = \\"lazy\\";\\n card.append(img);\\n }\\n const name = document.createElement(\\"strong\\");\\n name.textContent = pick.productName;\\n const why = document.createElement(\\"p\\");\\n why.textContent = pick.reason;\\n card.append(name, why);\\n strip.append(card);\\n}\\nstrip.hidden = false;\\n```\\n\\n`imageUrl`/`productUrl` are `nullable` and optional in the render \u2014 the\\ngrounding contract means they\'re only present when the catalog actually has\\nthem. Never `require` URLs.\\n","recipes/custom-chat-button.md":"# Custom chat button synced to panel state\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nReplace the default launcher with your own UI using the lifecycle surface:\\n\\n```js\\nconst gs = window.GreatStore;\\nconst btn = document.getElementById(\\"my-chat-button\\");\\n\\ngs.ready.then(() => { btn.hidden = false; });\\nbtn.addEventListener(\\"click\\", () => gs.toggle());\\n\\ngs.on(\\"open\\", () => btn.setAttribute(\\"aria-expanded\\", \\"true\\"));\\ngs.on(\\"close\\", () => btn.setAttribute(\\"aria-expanded\\", \\"false\\"));\\n```\\n\\n`ready` resolves even when `.then()` is attached after mount, so script\\nordering doesn\'t matter. The `open`/`close` events also fire for opens the\\nSDK triggers itself (`sendMessage`, `?gs_chat=open`), keeping your button\\nstate honest.\\n","recipes/gift-finder-funnel.md":"# Gift finder funnel (teaser \u2192 conversation \u2192 action)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nThe flagship pattern: an AI-generated quiz card in your design, whose answers\\ndrop the shopper into a chat that can act on the page. Uses the\\n`onGreatStoreReady` helper from the\\n[embed API reference](../references/embed-api.md#availability) \u2014 define\\nthat once and it works for every use on the page.\\n\\n```html\\n<section id=\\"gift-finder\\" hidden>\\n <h3 id=\\"gf-question\\"></h3>\\n <div id=\\"gf-options\\"></div>\\n</section>\\n\\n<script>\\n // Depending on where this script tag sits relative to the embed script\\n // tag, this inline script could run either before embed.js has executed\\n // at all, or after it\'s already mounted \u2014 checking `window.GreatStore`\\n // synchronously and bailing if it\'s not there yet would silently skip\\n // the whole funnel in the first case, and a bare\\n // `addEventListener(\\"greatstore:ready\\", ...)` would silently miss the\\n // (one-shot, already-fired) event in the second.\\n // onGreatStoreReady (see the embed API reference) handles both.\\n onGreatStoreReady(async (gs) => {\\n // Tools the resulting conversation can use \u2014 discovered on its next\\n // turn automatically.\\n document.modelContext.registerTool({\\n name: \\"go_to_product\\",\\n description:\\n \\"Navigate the shopper to a product page on this site. Use when \\" +\\n \\"the shopper picks a product they want to see.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: { url: { type: \\"string\\" } },\\n required: [\\"url\\"],\\n },\\n execute({ url }) {\\n const u = new URL(String(url), location.origin);\\n if (u.origin !== location.origin) throw new Error(\\"Only same-site URLs allowed\\");\\n location.assign(u.href);\\n return { content: [{ type: \\"text\\", text: \\"Navigating.\\" }] };\\n },\\n });\\n\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 80 },\\n options: {\\n type: \\"array\\",\\n minItems: 3,\\n maxItems: 4,\\n items: {\\n type: \\"object\\",\\n properties: {\\n label: { type: \\"string\\", maxLength: 30 },\\n chatMessage: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"label\\", \\"chatMessage\\"],\\n },\\n },\\n },\\n required: [\\"question\\", \\"options\\"],\\n },\\n \\"Create one engaging gift-finder opening question for this store, \\" +\\n \\"with 3-4 answer options that map to real areas of the catalog. \\" +\\n \\"For each option also write chatMessage: the message a shopper \\" +\\n \\"would send to a shopping assistant after picking it, phrased in \\" +\\n \\"first person (e.g. \\\\\\"I\'m shopping for my dad who loves hiking\\\\\\").\\"\\n );\\n\\n document.getElementById(\\"gf-question\\").textContent = data.question;\\n const wrap = document.getElementById(\\"gf-options\\");\\n for (const opt of data.options) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = opt.label;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(opt.chatMessage));\\n wrap.append(btn);\\n }\\n document.getElementById(\\"gift-finder\\").hidden = false;\\n } catch {}\\n });\\n</script>\\n```\\n\\nWhy it works: the teaser costs one cached generation per page, each\\nclick opens a conversation that already has direction, and `go_to_product`\\nlets the conversation end on a product page instead of in a dead end.\\n","recipes/launchers.md":"# GreatStore launchers \u2014 AI question chips\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nA horizontally scrollable row of chips, each a highly engaging first-person\\nquestion about the current page. Tapping a chip sends that question to the\\nassistant \u2014 `generateStructuredContent` writes the questions, `sendMessage`\\nfires them. Simple yet effective: it works on every page type, costs one\\ncached generation per page, and every tap starts a conversation that already\\nhas a great opening line.\\n\\n```html\\n<div id=\\"gs-launchers\\" hidden></div>\\n\\n<style>\\n #gs-launchers {\\n display: flex;\\n gap: 0.5em;\\n overflow-x: auto;\\n -webkit-overflow-scrolling: touch;\\n scrollbar-width: none;\\n padding: 0.5em 1em;\\n }\\n #gs-launchers::-webkit-scrollbar { display: none; }\\n #gs-launchers button {\\n flex: 0 0 auto;\\n white-space: nowrap;\\n border: 1px solid #ddd;\\n border-radius: 999px;\\n padding: 0.5em 0.9em;\\n background: #fff;\\n cursor: pointer;\\n }\\n</style>\\n\\n<script>\\n (async () => {\\n const gs = window.GreatStore;\\n if (!gs?.generateStructuredContent) return;\\n try {\\n const data = await gs.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n launchers: {\\n type: \\"array\\", minItems: 4, maxItems: 6,\\n items: {\\n type: \\"object\\",\\n properties: {\\n chip: { type: \\"string\\", maxLength: 32 },\\n question: { type: \\"string\\", maxLength: 140 },\\n },\\n required: [\\"chip\\", \\"question\\"],\\n },\\n },\\n },\\n required: [\\"launchers\\"],\\n },\\n `The shopper is on the page \\"${document.title}\\". Write 4-6 launcher ` +\\n `chips for a shopping assistant. For each, \\"question\\" is a highly ` +\\n `engaging first-person question this shopper would genuinely want ` +\\n `answered on this page \u2014 specific to its product, category, or ` +\\n `content, never generic \u2014 and \\"chip\\" is a 2-4 word teaser of it. ` +\\n `Vary the angles: fit and use, comparisons, gifting, care, what\'s ` +\\n `popular.`\\n );\\n\\n const row = document.getElementById(\\"gs-launchers\\");\\n for (const { chip, question } of data.launchers) {\\n const btn = document.createElement(\\"button\\");\\n btn.type = \\"button\\";\\n btn.textContent = chip;\\n btn.title = question;\\n btn.addEventListener(\\"click\\", () => gs.sendMessage(question));\\n row.append(btn);\\n }\\n row.hidden = false;\\n } catch {}\\n })();\\n</script>\\n```\\n\\nWhy it works:\\n\\n- **The chip is the teaser, the question is the payload.** A 2-4 word chip\\n scans instantly; the full first-person question lands in the transcript\\n reading like something the shopper typed, and gives the assistant a\\n well-formed prompt. The `title` attribute previews the full question on\\n hover.\\n- **Per-page for free.** The page URL is part of the generation context and\\n the cache key, so one site-wide snippet yields different chips on every\\n page \u2014 each served from cache on repeat renders.\\n- **Placement is the lever.** Under the product title, above the grid on\\n collections, at the end of a blog post \u2014 wherever a shopper pauses to\\n wonder, the chips name the question for them.\\n\\nTips:\\n\\n- Inline the product or collection name into the prompt when the platform\\n exposes it \u2014 it beats relying on `document.title`.\\n- Restyle the chips to the site\'s design system; the CSS above is just the\\n scroll mechanics (flex row, `overflow-x: auto`, hidden scrollbars,\\n `white-space: nowrap`).\\n- Resist adding more than ~6 chips \u2014 a launcher row is an invitation, not a\\n sitemap.\\n","recipes/page-action-suite.md":"# Page-action suite (WebMCP)\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\nGive every conversation on the site real capabilities. Register once in a\\nshared snippet, after the SDK is ready (which guarantees\\n`document.modelContext` exists) \u2014 using the `onGreatStoreReady` helper from\\nthe [embed API reference](../references/embed-api.md#availability), which\\nhandles both load orderings (embed script not run yet vs. already mounted)\\nthat a plain `window.GreatStore?.ready.then()` or a bare\\n`addEventListener(\\"greatstore:ready\\", ...)` each only cover one side of:\\n\\n```js\\nonGreatStoreReady(() => {\\n const text = (value) => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(value) }],\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_cart\\",\\n description:\\n \\"Read the shopper\'s current cart on this site: items, quantities, \\" +\\n \\"and totals. Use before answering any cart question.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n async execute() {\\n return text(await (await fetch(\\"/cart.js\\")).json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the cart on this site. Use when the \\" +\\n \\"shopper asks to add or buy something. Confirm the variant with \\" +\\n \\"the shopper first if ambiguous.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1, maximum: 10 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n if (!variantId) throw new Error(\\"variantId is required\\");\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Could not add to cart (${res.status})`);\\n document.dispatchEvent(new CustomEvent(\\"cart:refresh\\"));\\n return text(await res.json());\\n },\\n });\\n\\n document.modelContext.registerTool({\\n name: \\"get_current_page\\",\\n description:\\n \\"Read what page the shopper is currently on, including structured \\" +\\n \\"product data when on a product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute() {\\n return text({\\n url: location.href,\\n title: document.title,\\n productJson: document.querySelector(\\"#product-json\\")?.textContent ?? null,\\n });\\n },\\n });\\n});\\n```\\n\\nPrinciples at work: throw on failure (the assistant explains and recovers),\\nreturn fresh state after mutations (the assistant confirms accurately), cap\\nquantities in the schema, and notify your own UI (`cart:refresh`) so the\\npage reflects what the AI did.\\n\\nFor a product-page-only tool, register with an `AbortSignal` and abort on\\nSPA navigation:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(reviewsTool, { signal: ac.signal });\\nrouter.onLeave(\\"/products/:handle\\", () => ac.abort());\\n```\\n","recipes/product-faq.md":"# Product FAQ accordion\\n\\n> **Treat this code as a reference, not a drop-in.** Samples are\\n> framework-free vanilla JS so they stay portable \u2014 re-express the same\\n> logic in the conventions of the repo you\'re working in (React, Vue,\\n> Shopify Liquid sections, Svelte, \u2026) instead of retrofitting the sample\\n> as-is.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(\\n {\\n type: \\"object\\",\\n properties: {\\n faqs: {\\n type: \\"array\\", minItems: 3, maxItems: 5,\\n items: {\\n type: \\"object\\",\\n properties: {\\n question: { type: \\"string\\", maxLength: 90 },\\n answer: { type: \\"string\\", maxLength: 300 },\\n },\\n required: [\\"question\\", \\"answer\\"],\\n },\\n },\\n },\\n required: [\\"faqs\\"],\\n },\\n `Generate the questions shoppers most plausibly have before buying the ` +\\n `product \\"${productName}\\", with accurate answers grounded in the real ` +\\n `product details and store policies. Skip any question the store data ` +\\n `can\'t answer confidently.`\\n);\\n\\nconst wrap = document.getElementById(\\"gs-faq\\");\\nfor (const { question, answer } of data.faqs) {\\n const details = document.createElement(\\"details\\");\\n const summary = document.createElement(\\"summary\\");\\n summary.textContent = question;\\n const p = document.createElement(\\"p\\");\\n p.textContent = answer;\\n details.append(summary, p);\\n wrap.append(details);\\n}\\nwrap.hidden = false;\\n```\\n\\nEngagement bonus \u2014 append a hand-off row so unanswered questions become\\nconversations:\\n\\n```js\\nconst ask = document.createElement(\\"button\\");\\nask.type = \\"button\\";\\nask.textContent = \\"Have a different question? Ask us\\";\\nask.addEventListener(\\"click\\", () =>\\n window.GreatStore.sendMessage(`I have a question about \\"${productName}\\".`)\\n);\\nwrap.append(ask);\\n```\\n","references/agents-and-mcp.md":"# AI agents and the store\'s MCP endpoint\\n\\nEvery GreatStore store publishes a standard MCP server card, and the\\nplatform runs one MCP endpoint for coding agents \u2014 no authentication\\nrequired for either:\\n\\n| URL | What it is |\\n|---|---|\\n| `https://<slug>.greatstore.ai/.well-known/mcp/server-card.json` | Standard MCP server card \u2014 machine-readable discovery document for the store. |\\n| `https://admin.greatstore.ai/mcp` | A **documentation server for coding agents**: its tools return usage docs for the `gs` CLI. Same URL for every store. |\\n\\n## CLI docs for coding agents \u2014 `/mcp`\\n\\nA stateless HTTP MCP whose tools hand back markdown documentation for `gs`\\nCLI commands.\\n\\n```\\nclaude mcp add --transport http greatstore-admin https://admin.greatstore.ai/mcp\\n```\\n\\nUseful when a coding agent is shipping chat components\\n([chat-components.md](chat-components.md)) and needs the exact command for\\nthe next step. If this skill is installed, the agent already has the\\nworkflow \u2014 the MCP is the self-serve alternative for agents that don\'t.\\n\\n## What to use when\\n\\n- **Building the merchant\'s site** \u2192 this skill\'s other references (the SDK,\\n structured content, page tools).\\n- **A coding agent shipping chat components** \u2192 the `gs` CLI, with `/mcp`\\n as its built-in documentation.\\n- **Reading or changing store settings** (origin allowlists, CSP hosts, MCP\\n connectors, brand config) \u2192 the `gs` CLI\'s admin commands \u2014\\n [store-admin.md](store-admin.md), including which changes are safe to make\\n without asking the merchant.\\n","references/chat-components.md":"# Custom chat components \u2014 authoring with the `gs` CLI\\n\\nRemote components are React components the assistant renders **inside the\\nconversation** \u2014 product configurators, quizzes, size guides, booking forms,\\nanything richer than text. Each component is an AI-callable tool: the\\nmanifest\'s `description` tells the assistant *when* to show it, its\\n`inputSchema` declares the props the assistant fills in, and a `displayMode`\\npicks where it appears.\\n\\nAuthoring requires store-owner access (`gs login` signs in with the store\\nowner\'s account).\\n\\n## Workflow\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # browser sign-in\\ngs apps init --store my-store # scaffold a project root\\ncd <project> && npm install\\ngs apps init size_guide # scaffold components/size_guide/\\n# \u2026 edit components/size_guide/{component.tsx,manifest.json} \u2026\\ngs apps build # bundle every component\\ngs apps push # upload changed components as drafts\\ngs apps publish size_guide # promote to live\\n```\\n\\n`gs apps list` shows what\'s deployed (with dashboard links); `gs apps pull`\\nround-trips remote components back to disk. `gs apps push` hashes components\\nand only uploads what changed. (The subcommands also work at the top level \u2014\\n`gs push` == `gs apps push`.)\\n\\nThe scaffold writes an `AGENTS.md` into the project (with `CLAUDE.md` /\\n`GEMINI.md` symlinked) containing the complete design rules and brand\\nvariable table \u2014 your coding agent picks it up automatically when working in\\nthe project. `https://admin.greatstore.ai/mcp` serves the same CLI docs to\\nagents over MCP (see [agents-and-mcp.md](agents-and-mcp.md)).\\n\\n## `manifest.json`\\n\\n```json\\n{\\n \\"name\\": \\"size_guide\\",\\n \\"displayName\\": \\"Size guide\\",\\n \\"description\\": \\"Interactive size guide. Show when the shopper asks about sizing or fit for apparel.\\",\\n \\"displayMode\\": \\"inline\\",\\n \\"inputSchema\\": {\\n \\"type\\": \\"object\\",\\n \\"properties\\": {\\n \\"productName\\": { \\"type\\": \\"string\\" },\\n \\"category\\": { \\"type\\": \\"string\\" }\\n },\\n \\"required\\": [\\"productName\\"]\\n }\\n}\\n```\\n\\n| Field | Meaning |\\n|---|---|\\n| `name` | Tool name, snake_case (`^[a-z][a-z0-9_]*$`), matches the folder under `components/`. |\\n| `displayName` | Friendly label shown in chat UI. |\\n| `description` | **Load-bearing** \u2014 how the assistant decides when to render the component. Say what it shows *and* when to use it, like any good tool description. |\\n| `displayMode` | Where it renders \u2014 see below. |\\n| `inputSchema` | JSON Schema for the props the assistant fills. Keep it tight; required fields the AI can\'t infer cause bad calls. |\\n| `async` | Set `true` for backend-backed components (see Async below). |\\n\\n### Display modes\\n\\n- `inline` \u2014 a bubble inside the chat transcript; persists with the message\\n log.\\n- `over-input` \u2014 floats above the chat input (like a question overlay);\\n cleared by the next user turn or an explicit close.\\n- `fullscreen` \u2014 takes over the full preview surface; persists until the\\n next widget-emitting tool call or an explicit close.\\n\\n## The component contract\\n\\n`component.tsx` default-exports a React component. Its props are the\\n`inputSchema` fields the assistant filled, plus five GreatStore-injected\\nlifecycle props (always present):\\n\\n| Prop | What it does |\\n|---|---|\\n| `onSendMessage(text)` | Send text into the chat as if the shopper typed it \u2014 lets the component drive the conversation (\\"Selected size M, what\'s the return policy?\\"). |\\n| `onCallTool(name, args)` | Chain into another remote-component tool by name. |\\n| `onUpdateModelContext(context)` | Inject **invisible** background context for the model \u2014 the variant the shopper selected, the options they configured, the step they\'re on. Replaces the prior value (never appends); pass `\\"\\"` to clear. Read on the next chat turn. Use this instead of `onSendMessage` when the assistant should *know* state without a message appearing in the transcript. |\\n| `onShowLightbox({ src, originRect? })` | Expand an image in the chat\'s shared full-screen lightbox \u2014 an on-brand zoom overlay you can\'t render yourself (your component is boxed inside its own bounds and shadow root). Pass the image `src`; for a smooth zoom, also pass the clicked element\'s `getBoundingClientRect()` as `originRect` (omit it and the image grows from the viewport centre). |\\n| `onClose()` | Dismiss the host slot. `over-input` clears the overlay, `fullscreen` reverts the pane, `inline` is a no-op. |\\n\\nReach for `onUpdateModelContext` when the shopper changes something inside\\nthe component (picks a size, configures a build, advances a quiz) and you\\nwant the assistant to factor it into the *next* thing they ask \u2014 without\\nspamming the chat with a visible \\"I selected M\\" message. Use `onSendMessage`\\nwhen you actually want a turn to happen now.\\n\\nReach for `onShowLightbox` whenever your component shows imagery the shopper\\nmight want to inspect closely \u2014 product photos, swatches, a size chart. A\\nthumbnail\'s `onClick` handler is the natural place to call it. Don\'t build\\nyour own full-screen modal: a remote component is sandboxed inside its own\\nbounds and shadow root, so a self-rendered overlay can\'t cover the chat. The\\nshared lightbox escapes those bounds and themes itself from the store\'s CSS\\nvariables.\\n\\n```tsx\\nimport React from \\"react\\";\\n\\ninterface Props {\\n productName: string;\\n category?: string;\\n onSendMessage: (text: string) => void;\\n onCallTool: (name: string, args: Record<string, unknown>) => void;\\n onUpdateModelContext: (context: string) => void;\\n onShowLightbox: (options: { src: string; originRect?: DOMRect }) => void;\\n onClose: () => void;\\n}\\n\\nexport default function SizeGuide({\\n productName,\\n onSendMessage,\\n onUpdateModelContext,\\n onShowLightbox,\\n}: Props) {\\n return (\\n <div\\n style={{\\n padding: \\"1em\\",\\n border: \\"1px solid var(--color-border-default)\\",\\n borderRadius: \\"var(--radius-lg)\\",\\n background: \\"var(--color-surface)\\",\\n color: \\"var(--color-foreground)\\",\\n fontFamily: \\"var(--font-primary)\\",\\n }}\\n >\\n {/* \u2026 sizes for {productName} \u2026 */}\\n <img\\n src={`/size-charts/${productName}.png`}\\n alt={`${productName} size chart`}\\n style={{ cursor: \\"zoom-in\\", width: \\"100%\\" }}\\n onClick={(e) =>\\n onShowLightbox({\\n src: `/size-charts/${productName}.png`,\\n originRect: e.currentTarget.getBoundingClientRect(),\\n })\\n }\\n />\\n <button\\n onClick={() =>\\n // Silent: the assistant now knows the pick for the shopper\'s\\n // next question, with nothing added to the transcript.\\n onUpdateModelContext(`Shopper selected size M of \\"${productName}\\".`)\\n }\\n >\\n Select size M\\n </button>\\n <button onClick={() => onSendMessage(`Size M of \\"${productName}\\" \u2014 is it in stock?`)}>\\n Ask about size M\\n </button>\\n </div>\\n );\\n}\\n```\\n\\n## Design rules (non-negotiable)\\n\\nComponents render inside arbitrary publisher pages *and* the GreatStore\\nstorefront; you control neither the host\'s root font size nor its colors.\\n\\n1. **Size in `em`, never `rem`** \u2014 `rem` resolves against the host page\'s\\n root font size, which is arbitrary (`html { font-size: 8px }` breaks every\\n `rem` dimension). `em` stays self-consistent anywhere. Borders may stay\\n `px`.\\n2. **Never hardcode colors, fonts, or radii** \u2014 read the brand CSS variables\\n GreatStore injects (`--color-primary`, `--color-surface`,\\n `--color-foreground`, `--color-border-default`, `--font-primary`,\\n `--radius-lg`, \u2026) so the component restyles itself with the store\'s\\n theme. The scaffolded `AGENTS.md` has the full variable table.\\n\\n## Async components (backend-backed data)\\n\\nIf a component must load data before it can render correctly, don\'t render a\\nshell and fetch in `useEffect` \u2014 set `\\"async\\": true` in the manifest and\\nexport an **async** default. GreatStore shows its own loading state, awaits\\nyour promise, and renders what it resolves to.\\n\\nA thrown error is a **retry signal**: the assistant sees it and usually\\nre-calls the tool. So only throw when a *different* call could help:\\n\\n1. Validate the AI-passed props first and throw on bad input \u2014 the AI can\\n fix the args and retry. (Don\'t validate the API\'s *output* and throw: the\\n AI can\'t fix your backend, it\'ll just loop.)\\n2. Throw on failures where retrying differently could succeed, and say what\\n to change (e.g. empty search \u2192 `\\"no results for X \u2014 try a broader keyword\\"`).\\n3. For idempotent failures (500, timeout, missing record) render a graceful\\n fallback instead of throwing \u2014 re-running the same call changes nothing.\\n\\n```tsx\\nexport default async function Results(props: Props) {\\n if (!props.query?.trim()) throw new Error(\\"missing required prop: query\\");\\n const res = await fetch(`/api/search?q=${encodeURIComponent(props.query)}`);\\n if (res.ok) {\\n const { results } = await res.json();\\n if (results.length === 0)\\n throw new Error(`no results for \\"${props.query}\\" \u2014 try a broader keyword`);\\n return <ul>{/* render results */}</ul>;\\n }\\n return <p>Couldn\'t load results right now.</p>; // idempotent: don\'t throw\\n}\\n```\\n\\n## When to build a component vs. the other facets\\n\\n- Content for the **merchant\'s page** \u2192 `generateStructuredContent`\\n ([structured-content.md](structured-content.md)).\\n- Letting the assistant **act on the page** \u2192 WebMCP page tools\\n ([embed-api.md](embed-api.md)).\\n- Rich, interactive UI **inside the conversation itself**, available on the\\n storefront and every embed without page changes \u2192 a chat component.\\n","references/embed-api.md":"# `window.GreatStore` API reference\\n\\n## Setup\\n\\n```html\\n<script src=\\"https://my-store.greatstore.ai/embed.js\\"></script>\\n```\\n\\nOne script tag, anywhere on the page (end of `<body>` preferred), with the\\nstore\'s slug in the host. The `window.GreatStore` object exists synchronously\\nonce the script executes; every method below is safe to call before the chat\\nUI has finished loading \u2014 pre-mount calls are queued and replayed in order\\nonce it mounts. The SDK pre-warms its chat bundle in the background\\nautomatically; the panel stays closed until `open()` / `toggle()` /\\n`sendMessage()` is called or the shopper clicks the launcher.\\n\\nRequirements:\\n\\n- The page\'s domain must be in the store\'s **allowed domains** (GreatStore\\n store settings) \u2014 see Troubleshooting below for the failure signature.\\n- If the store also wants push notifications, host `gs.js` at the site root\\n and load that instead of `embed.js` \u2014 it injects the embed for you. See\\n [push-notifications.md](push-notifications.md).\\n\\nCode samples throughout are framework-free reference implementations \u2014\\nre-express them in the host repo\'s framework (React, Vue, Shopify Liquid,\\n\u2026) rather than retrofitting them as-is. **Using React?** `@greatstore/react`\\nwraps all of this in a `<GreatStore>` component with typed props (no\\n`data-*` attribute quoting) and clean mount/unmount for SPA routing \u2014\\n`npm install @greatstore/react`.\\n\\n## Appearance overrides\\n\\nThe store\'s configured appearance (theme colors, fonts, roundedness, panel\\nposition, mobile bar, AI disclaimer) is the default everywhere. To make the\\nembed look or behave differently on a specific page \u2014 e.g. matching a\\ncampaign landing page\'s palette \u2014 set `data-*` attributes on the embed\\nscript tag. Every attribute is optional; anything you don\'t set falls back\\nto the store\'s configured value.\\n\\n```html\\n<script\\n src=\\"https://my-store.greatstore.ai/embed.js\\"\\n data-theme-mode=\\"dark\\"\\n data-theme-radius=\\"rounded\\"\\n data-theme-panel-position=\\"right\\"\\n data-theme-mobile-bar=\\"false\\"\\n data-z-index=\\"9999\\"\\n data-theme-brand-color=\\"#1a1a2e\\"\\n data-theme-surface-color=\\"#ffffff\\"\\n data-theme-text-color=\\"#111111\\"\\n data-theme-font=\\"Inter, sans-serif\\"\\n data-ai-disclaimer=\\"false\\"\\n></script>\\n```\\n\\n| Attribute | Values | Overrides |\\n|---|---|---|\\n| `data-theme-mode` | `auto` \\\\| `light` \\\\| `dark` \\\\| `custom` | Color scheme. |\\n| `data-theme-radius` | `sharp` \\\\| `default` \\\\| `rounded` | Corner roundedness. |\\n| `data-theme-panel-position` | `left` \\\\| `right` \\\\| `middle` | Desktop panel placement. |\\n| `data-theme-mobile-bar` | `true` \\\\| `false` | Whether the collapsed mobile bar shows. |\\n| `data-z-index` | integer, `0`\u2013`2147483647` | Stacking order of the chat overlay on your page. Lower it if something on the page must stay above the chat. |\\n| `data-theme-font` | CSS font-family string | Primary font. |\\n| `data-theme-font-secondary` | CSS font-family string | Secondary font. |\\n| `data-theme-brand-color` | any CSS color | Brand/primary color (only used in `custom` mode). |\\n| `data-theme-surface-color` | any CSS color | Surface/background color (only used in `custom` mode). |\\n| `data-theme-text-color` | any CSS color | Text color (only used in `custom` mode). |\\n| `data-ai-disclaimer` | `true` \\\\| `false` | Shows or hides the AI disclaimer line. |\\n| `data-ai-disclaimer-text` | string, up to 200 chars | Custom disclaimer message (implies shown, unless `data-ai-disclaimer=\\"false\\"` is also set). |\\n\\nNot overridable this way: display name, assistant name, logo, and icons \u2014\\nthose stay whatever\'s configured in the store\'s admin.\\n\\nInvalid values (unrecognized enum, malformed color, disallowed font\\ncharacters) are silently ignored and fall back to the configured default.\\n\\n## Properties\\n\\n| Property | Type | Description |\\n|---|---|---|\\n| `slug` | `string` | The store identifier the script was loaded for. |\\n| `host` | `string` | `\\"greatstore.ai\\"`. |\\n| `embedHost` | `string` | Origin the embed assets load from, e.g. `https://<slug>.greatstore.ai`. |\\n| `ready` | `Promise<void>` | Resolves when the chat UI has mounted and `open()` would render instantly. Resolved promises replay, so `.then()` works no matter when it\'s attached. The readiness signal to gate on. |\\n\\nA `greatstore:ready` `CustomEvent` (with the SDK object as `detail`) is also\\ndispatched on `window` at the moment `ready` resolves, for declarative\\ntooling. Unlike the promise, the listener must be attached before mount\\ncompletes \u2014 attach it before (or immediately after) the embed script tag.\\n\\n## Methods\\n\\n### `load(): void`\\n\\nPre-warms the chat bundle and identity in the background without opening the\\npanel. Called automatically when `embed.js` runs, so you rarely need it.\\nIdempotent.\\n\\n### `open(): void` / `close(): void` / `toggle(): void`\\n\\nOpen, close, or toggle the chat panel. On desktop the panel is a floating\\nside panel; under 768px viewport width it\'s a full-height drawer. All three\\nqueue if called before mount.\\n\\n### `sendMessage(text: string): void`\\n\\nSends `text` as the shopper\'s own visible chat message and **opens the panel\\nif it\'s closed**. The text is trimmed; empty or whitespace-only strings are\\nsilently dropped. Queues if called before mount.\\n\\nThis is the highest-leverage one-liner in the SDK: any element on the page\\ncan become a conversation entry point with context baked into the question.\\n\\n```js\\ndocument.querySelector(\\"#ask-fit\\").addEventListener(\\"click\\", () => {\\n window.GreatStore.sendMessage(\\n `I\'m looking at \\"${productName}\\" \u2014 how does the sizing run?`\\n );\\n});\\n```\\n\\nBecause the message renders as if the shopper typed it, write it in the\\nshopper\'s voice. It is not a hidden-context channel \u2014 don\'t stuff it with\\ninvisible instructions or data dumps. For that, use `updateModelContext`.\\n\\n### `updateModelContext(context: string): void`\\n\\nInjects free-text **background context** about what the shopper is doing on\\nthe page \u2014 the product they\'re viewing, what\'s in their cart, their account\\ntier \u2014 so the assistant can factor it in. Unlike `sendMessage`, this is\\n**invisible**: it never renders as a chat message and doesn\'t open the panel.\\n\\nEach call **replaces** the value from the previous call \u2014 it never appends.\\nKeep one current snapshot; re-call it whenever the page state changes. Pass\\nan empty string to clear it. The text is read on the next chat turn, so set\\nit before (or while) the shopper is chatting. Queues if called before mount.\\n\\n```js\\n// Keep the assistant aware of the current product as the shopper browses.\\nfunction syncContext() {\\n window.GreatStore.updateModelContext(\\n `Viewing \\"${product.title}\\" (${product.price}). In stock: ${product.inStock}. ` +\\n `Cart: ${cart.count} item(s), subtotal ${cart.subtotal}.`\\n );\\n}\\nsyncContext();\\n```\\n\\nWrite it as concise notes for the model, not prose for the shopper. The\\ncontext is page-controlled, so the assistant treats it as background\\ninformation, not as instructions \u2014 don\'t rely on it to change the assistant\'s\\nrules or persona.\\n\\n### `on(event: string, handler: (...args) => void): () => void`\\n\\nSubscribe to SDK events. Returns an unsubscribe function. Listeners attached\\nbefore mount are queued and wired up at mount. Handler exceptions are caught\\nand reported \u2014 they won\'t break the chat.\\n\\nEvents emitted:\\n\\n| Event | Fired when |\\n|---|---|\\n| `\\"open\\"` | Panel transitions closed \u2192 open (including via `sendMessage` or the shopper\'s own click). |\\n| `\\"close\\"` | Panel transitions open \u2192 closed. |\\n\\n### `generateStructuredContent(schema: object, prompt: string): Promise<unknown>`\\n\\nGenerates JSON matching `schema` from `prompt`, grounded in the store\'s live\\ncatalog. Resolves to the generated data object itself. Rejects with `Error`\\non any failure (invalid input, decline, validation failure, rate limit,\\nnetwork). See [structured-content.md](structured-content.md) for the full\\ncontract, schema support, caching, and error semantics.\\n\\nAccepts either a plain JSON Schema object or any object exposing a\\n`.toJSONSchema()` method (e.g. Zod schemas) \u2014 the conversion is called for\\nyou.\\n\\nThrows synchronously (rejects) if `prompt` is not a non-empty string or\\n`schema` is not an object.\\n\\n### `enableNotifications(): Promise<{ ok: boolean }>`\\n\\nOpts this browser into Web Push notifications from the store. Requirements:\\n\\n- Must be called from a user gesture (e.g. a click handler).\\n- The site must host GreatStore\'s `gs.js` service-worker file. By default the\\n SDK looks for it at `/gs.js`; if it\'s hosted elsewhere, point to it via an\\n attribute on the embed script tag:\\n `<script src=\\"\u2026/embed.js\\" data-push-sw-path=\\"/path/to/gs.js\\"></script>`.\\n\\nResolves `{ ok: true }` on success and `{ ok: false }` on any failure\\n(unsupported browser, no service worker hosted, permission denied). It never\\nrejects.\\n\\n### `destroy(): void`\\n\\nFully tears down a mounted panel: unmounts the chat UI, closes any open voice\\nconnection, and removes the embed\'s DOM/listeners from the page. Use this\\nwhen your page is done with the embed for good \u2014 e.g. a single-page app\\nnavigating away from the only route that should show it.\\n\\nNo-ops (with a console warning) if nothing is mounted. The cached identity\\nand downloaded chat bundle are kept, so a later `load()` (or any call that\\ntriggers a mount, like `open()`) mounts a fresh panel without a network\\nround-trip for either. `ready` becomes a new pending promise at the moment\\n`destroy()` is called, resolving again once the next mount completes:\\n\\n```js\\nwindow.GreatStore.destroy();\\n// ...later, on the page/route where the embed should come back:\\nwindow.GreatStore.load();\\nawait window.GreatStore.ready; // resolves once the fresh mount is done\\n```\\n\\n## Page tools \u2014 WebMCP (`document.modelContext`)\\n\\nThe recommended way to expose page capabilities to the assistant is the\\nWebMCP standard. The GreatStore assistant discovers every tool registered on\\n`document.modelContext`, re-reading the list on each conversational turn \u2014\\nso tools registered mid-session appear on the next message without a reload.\\n\\n### Availability\\n\\nIf the browser implements WebMCP natively, `document.modelContext` is just\\nthere. Otherwise the SDK installs a minimal fallback the instant `embed.js`\\nstarts running \u2014 synchronously, no async gap \u2014 so `document.modelContext`\\nis normally available immediately. The embed script tag deliberately has no\\n`async`/`defer` \u2014 a script with neither blocks parsing and runs at its own\\nposition, which matters if the page also has another script (a nav widget,\\nan analytics tag) registering its own WebMCP tools: whichever executes\\nfirst wins that registration, and only a synchronous, unconditionally-first\\nscript gives GreatStore\'s fallback shim a real chance of installing before\\none of those calls happens. That said, your own code can still end up on\\neither side of two different races relative to `embed.js`, depending on\\nwhere your script tag sits and whether it uses `async`/`defer` itself:\\n\\n- **Your script runs before `embed.js` has executed at all** \u2014 `window.GreatStore`\\n doesn\'t exist yet. `window.GreatStore?.ready` silently evaluates to\\n `undefined` here (optional chaining swallows it), so accessing `.then()`\\n on it throws or (written more defensively) just does nothing.\\n- **Your script runs after `embed.js` has already mounted** \u2014 the\\n `greatstore:ready` event already fired once, in the past.\\n `window.addEventListener(\\"greatstore:ready\\", ...)` attached now will\\n never see it: the event isn\'t replayed for late listeners, unlike a\\n resolved Promise (`.then()` on an already-resolved Promise still fires).\\n\\nNeither `.ready.then(...)` alone nor `addEventListener(\\"greatstore:ready\\", ...)`\\nalone is safe against both orderings. Use both, picking whichever is valid\\nat the moment your code runs:\\n\\n```js\\nfunction onGreatStoreReady(callback) {\\n if (window.GreatStore?.ready) {\\n // embed.js has already run \u2014 .then() on its ready Promise fires\\n // immediately if it already resolved, or once it does.\\n window.GreatStore.ready.then(() => callback(window.GreatStore));\\n } else {\\n // embed.js hasn\'t run yet \u2014 wait for the one-shot event it\'ll dispatch\\n // once it has. Safe to attach now: nothing can fire it between this\\n // check and the listener attaching, since JS execution isn\'t preemptible.\\n window.addEventListener(\\n \\"greatstore:ready\\",\\n (event) => callback(event.detail),\\n { once: true },\\n );\\n }\\n}\\n\\nonGreatStoreReady(() => {\\n document.modelContext.registerTool(/* \u2026 */);\\n});\\n```\\n\\n(`navigator.modelContext` is a deprecated alias for the same object; use\\n`document.modelContext` in new code.)\\n\\n### `registerTool(tool, options?)`\\n\\n```ts\\ndocument.modelContext.registerTool(\\n {\\n name: string, // required, non-empty, unique on the page\\n description: string, // required \u2014 how the AI decides when to call it\\n inputSchema?: object, // JSON Schema for execute\'s args;\\n // defaults to { type: \\"object\\", properties: {} }\\n execute(args): Result | Promise<Result>,\\n },\\n options?: { signal?: AbortSignal }, // abort to unregister\\n);\\n```\\n\\n- **Result shape**: `execute` returns MCP content blocks \u2014\\n `{ content: [{ type: \\"text\\", text: \\"\u2026\\" }] }`. For structured data,\\n `JSON.stringify` it into `text`. Add `isError: true` to mark a handled\\n failure.\\n- **Errors**: a thrown error or rejected promise is delivered to the\\n assistant as a *failed* tool call carrying the error message \u2014 the\\n assistant can explain or adapt. Errors never escape into your page.\\n- **Duplicate names throw.** To replace a tool, abort its registration first.\\n- **Unregistration is `AbortSignal`-driven**: pass `{ signal }` and call\\n `abort()` when the tool\'s context goes away (SPA navigation, modal close).\\n A pre-aborted signal skips registration. (A legacy\\n `unregisterTool(name)` exists but is deprecated in the spec.)\\n- **Treat `args` as untrusted input**: values are AI-generated. Validate\\n before passing to your own APIs, and never `eval` anything from them.\\n\\nA complete tool, registered once GreatStore is ready (using the\\n`onGreatStoreReady` helper defined above):\\n\\n```js\\nonGreatStoreReady(() => {\\n document.modelContext.registerTool({\\n name: \\"add_to_cart\\",\\n description:\\n \\"Add a product variant to the shopper\'s cart on this site. \\" +\\n \\"Use when the shopper asks to add, buy, or get a product.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n variantId: { type: \\"string\\" },\\n quantity: { type: \\"integer\\", minimum: 1 },\\n },\\n required: [\\"variantId\\"],\\n },\\n async execute({ variantId, quantity }) {\\n const res = await fetch(\\"/cart/add.js\\", {\\n method: \\"POST\\",\\n headers: { \\"Content-Type\\": \\"application/json\\" },\\n body: JSON.stringify({ id: variantId, quantity: quantity ?? 1 }),\\n });\\n if (!res.ok) throw new Error(`Cart add failed (${res.status})`);\\n const cart = await res.json();\\n return { content: [{ type: \\"text\\", text: JSON.stringify(cart) }] };\\n },\\n });\\n});\\n```\\n\\nReturning the fresh cart state after the mutation lets the assistant confirm\\naccurately. Good tool families: cart (`get_cart`, `add_to_cart`), navigation\\n(`go_to_page`), page state (`get_current_product`, `apply_filters`), UI\\n(`highlight_section`, `scroll_to_reviews`).\\n\\nAnd a context-scoped tool, unregistered via `AbortSignal`:\\n\\n```js\\nconst ac = new AbortController();\\ndocument.modelContext.registerTool(\\n {\\n name: \\"get_product_reviews\\",\\n description: \\"Read the reviews shown on the current product page.\\",\\n inputSchema: { type: \\"object\\", properties: {} },\\n execute: () => ({\\n content: [{ type: \\"text\\", text: JSON.stringify(collectReviews()) }],\\n }),\\n },\\n { signal: ac.signal },\\n);\\n\\n// On SPA route change away from the product page:\\nac.abort();\\n```\\n\\n## URL parameter: `?gs_chat=open`\\n\\nWhen the page URL carries `gs_chat=open`, the panel opens automatically once\\nthe embed mounts. The param is consumed and stripped from the URL via\\n`history.replaceState`, so a manual reload doesn\'t re-open the panel. Use it\\nin campaign links, emails, and post-login redirects.\\n\\n## Troubleshooting\\n\\n| Symptom | Likely cause |\\n|---|---|\\n| Console: `[GreatStore] Chat is unavailable on <origin>\u2026 this domain isn\'t in the store\'s allowed domains` | The page\'s origin isn\'t in the store\'s allowed domains. Add it in store settings. Until then every SDK network call fails. |\\n| Console: `[GreatStore] Embed script must be loaded from <slug>.greatstore.ai/embed.js` | The script was copied/self-hosted instead of loaded from the store\'s embed URL. Always load it from `https://<slug>.greatstore.ai/embed.js`. |\\n| `generateStructuredContent` rejects with a rate-limit message | More than ~20 requests/minute from one visitor. Consolidate calls into fewer, richer schemas. |\\n| Panel won\'t auto-open on mobile after returning to the page | Intentional: the mobile drawer never auto-opens on resume \u2014 it would cover the content the shopper is reading. The transcript is preserved; they\'ll see it when they tap the launcher. |\\n| Tools registered but the assistant doesn\'t use them | Check the `description` \u2014 it\'s the only signal for *when* to call. Also confirm the registration ran (`document.modelContext` exists after `ready`) on the same page the conversation is on. |\\n","references/push-notifications.md":"# Web push re-engagement\\n\\nShoppers who opt in receive browser notifications from the store \u2014 under the\\nmerchant\'s own domain and branding, with the permission prompt shown inline\\non the merchant\'s page. Setup is two pieces: a single file hosted at the site\\nroot, and an opt-in button.\\n\\n## 1. Host `gs.js` at the site root\\n\\nDownload the store\'s loader and serve it at `/gs.js` on the merchant\'s\\ndomain:\\n\\n```\\nhttps://<slug>.greatstore.ai/gs.js \u2192 https://www.merchant-site.com/gs.js\\n```\\n\\nAlways download it from the **store\'s own subdomain** \u2014 the file is built for\\nthat store; don\'t copy one from elsewhere.\\n\\nThen load it with one tag (replacing the `embed.js` tag \u2014 `gs.js` injects the\\nembed for you and registers itself as the service worker):\\n\\n```html\\n<script src=\\"/gs.js\\"></script>\\n```\\n\\nHosting this file is what enables push. Without it, push is simply off \u2014\\n`enableNotifications()` returns `{ ok: false }` and nothing else changes.\\n\\n### Non-root hosting\\n\\nIf the platform can\'t serve files at the site root (e.g. Shopify themes\\nserve assets under a path), keep the regular `embed.js` tag and point it at\\nwhere the file lives \u2014 the path must be on the merchant\'s own origin:\\n\\n```html\\n<script\\n src=\\"https://my-store.greatstore.ai/embed.js\\"\\n data-push-sw-path=\\"/cdn/shop/files/gs.js\\"\\n></script>\\n```\\n\\n## 2. Offer the opt-in from a user gesture\\n\\n```js\\noptInButton.addEventListener(\\"click\\", async () => {\\n const { ok } = await window.GreatStore.enableNotifications();\\n optInButton.hidden = ok; // done \u2014 or quietly keep the button\\n});\\n```\\n\\nRules that make this work well:\\n\\n- **Always call it from a click** \u2014 browsers ignore or penalize permission\\n prompts that aren\'t user-initiated, and the call is designed for gesture\\n context.\\n- **Never prompt on page load.** Tie the button to a moment where\\n notifications have obvious value (\\"Notify me when this is back in stock\\",\\n post-purchase, after a chat conversation).\\n- `{ ok: false }` covers every failure the same way \u2014 unsupported browser, no\\n `gs.js` hosted, permission denied. It never rejects, and there\'s no popup\\n fallback. Design the button so a decline just leaves the page as it was;\\n don\'t show an error.\\n- The promise resolving `{ ok: true }` means this browser is subscribed.\\n There\'s nothing else to wire \u2014 notification delivery is handled by\\n GreatStore.\\n","references/store-admin.md":"# Store administration from the CLI\\n\\nThe `gs` CLI is a full admin surface for a GreatStore store, mirroring the\\nmerchant\'s admin dashboard one-to-one: `gs configure` \u2194 the Configure panel,\\n`gs connectors` \u2194 the Connectors panel, `gs apps` \u2194 the Apps panel. Same\\nfields, same behaviour \u2014 anything you change is what the merchant sees in\\ntheir dashboard.\\n\\nThat makes the CLI the way a coding agent grounds and unblocks its own work:\\nread the store\'s configuration to make better decisions, and make the narrow\\nclass of additive, integration-enabling changes yourself instead of telling\\nthe user to go click through a dashboard.\\n\\n## Setup\\n\\n```\\nnpm install -g @greatstore/cli # or npx @greatstore/cli <command>\\ngs login # one-time browser sign-in (needs a human)\\n```\\n\\nCredentials persist across runs. Admin commands take `--store <slug>`\\ndirectly, or read the nearest `.gsrc` (`{\\"store\\":\\"my-store\\"}`) \u2014 so they work\\nfrom any repo, not just a scaffolded component project. Every read supports\\n`--json` for machine-readable output.\\n\\n## Read freely \u2014 always safe\\n\\nReading store state is never destructive. Do it whenever it would improve a\\ndecision:\\n\\n```\\ngs configure --store my-store --json # brand config\\ngs connectors --store my-store --json # MCP connectors feeding the assistant\\ngs connectors health --store my-store # live-probe them (reports tool counts)\\ngs apps list --store my-store --json # deployed custom chat components\\n```\\n\\nWhat each read is good for:\\n\\n- **`gs configure`** \u2014 the store\'s `extraOrigins` (is the site you\'re\\n integrating actually allowlisted? \u2014 see the embed troubleshooting note in\\n [embed-api.md](embed-api.md)), `theme` (match generated on-page UI to the\\n brand\'s colors/radius), `storeLink`, and CSP host lists.\\n- **`gs connectors`** \u2014 which MCP connectors the assistant has, and whether\\n the Maker MCP is on. This tells you what the assistant can actually *do*:\\n check it before building features that lean on catalog data (e.g.\\n [structured content](structured-content.md) grounding), and before\\n registering WebMCP page tools that would duplicate a capability a\\n server-side connector already provides.\\n- **`gs connectors health`** \u2014 a connector being listed doesn\'t mean it\'s\\n reachable; this probes each one live. Run it when assistant behaviour\\n suggests a tool isn\'t responding.\\n\\n## Safe to change without asking\\n\\nWhen the task the user gave you requires it, these changes are yours to\\nmake: they\'re additive, easy to undo, and invisible to shoppers.\\n\\n- **`extraOrigins`** \u2014 the origin allowlist for the embed. Installing the\\n widget on a new site/staging domain? Add that origin yourself instead of\\n reporting \\"ask the merchant to allowlist it\\".\\n- **`cspScriptHosts` / `cspConnectHosts`** \u2014 hosts the storefront accepts\\n for scripts/connections. Add a host when a custom chat component\\n legitimately needs a third-party SDK or API.\\n\\n**List flags replace the stored list \u2014 always read-merge-write.** Fetch the\\ncurrent value, append yours, write the union:\\n\\n```\\ngs configure --store my-store --json\\n# extraOrigins is [\\"https://shop.example.com\\"] \u2192 write both, comma-separated:\\ngs configure set --store my-store \\\\\\n --extraOrigins \\"https://shop.example.com,https://staging.example.com\\"\\n```\\n\\nNever drop an entry you didn\'t add, and say what you changed (and why) when\\nyou report back to the user.\\n\\n## Ask the merchant first\\n\\nEverything below is shopper-visible or changes what the live assistant can\\ndo for every shopper. Propose it, don\'t do it unprompted:\\n\\n- **Brand-visible config** \u2014 `displayName`, `assistantName`, `storeLink`,\\n `theme`, and asset uploads\\n (`gs configure upload|clear icon|logoLight|logoDark`).\\n- **Connector mutations** \u2014 `gs connectors add|remove|enable|disable` and\\n `gs connectors maker on|off`. (`add` probes the connector for tool\\n discovery before saving and refuses if it doesn\'t answer; `--force`\\n overrides. Still: adding capabilities to the merchant\'s assistant is the\\n merchant\'s call.)\\n- **Anything that clears** \u2014 passing `\\"\\"` to empty a field, removing list\\n entries, `clear`ing assets.\\n\\nWhen the user has *explicitly asked* for one of these (\\"set the assistant\'s\\nname to Voyager\\", \\"connect this MCP server\\"), that\'s the go-ahead \u2014 do it\\nand confirm the result with a read.\\n\\n## Command reference\\n\\n```\\ngs configure [--json] show configuration\\ngs configure set --<field> <value> displayName, assistantName,\\n storeLink, extraOrigins a,b,\\n theme \'<json>\' (--themeFile <path>),\\n cspScriptHosts a,b, cspConnectHosts a,b\\n (\\"\\" clears a field)\\ngs configure upload <kind> <file> icon | logoLight | logoDark (.png/.jpg/.webp)\\ngs configure clear <kind> remove an uploaded asset\\n\\ngs connectors [--json] list Maker toggle + custom connectors\\ngs connectors add <name> --url <url> [--token <t>] [--profileUrl <u>]\\n [--disabled] [--force]\\ngs connectors remove <name|id>\\ngs connectors enable|disable <name|id>\\ngs connectors maker on|off\\ngs connectors health [<name|id>] [--json]\\n\\ngs apps <init|build|list|pull|push|publish|unpublish|delete>\\n see chat-components.md for the workflow\\n```\\n","references/structured-content.md":"# `generateStructuredContent` deep dive\\n\\nAI-generated, catalog-grounded JSON for UI you render yourself \u2014 the chat\\npanel is not involved.\\n\\n```js\\nconst data = await window.GreatStore.generateStructuredContent(schema, prompt);\\n```\\n\\n- `schema` \u2014 a JSON Schema describing the output (or a Zod schema exposing\\n `.toJSONSchema()`).\\n- `prompt` \u2014 what to generate.\\n- Resolves to **the generated JSON object itself**, matching the schema.\\n Rejects with an `Error` on any failure.\\n\\nCode samples here are framework-free reference implementations \u2014 re-express\\nthem in the host repo\'s framework (React, Vue, Shopify Liquid, \u2026) rather\\nthan retrofitting them as-is.\\n\\n## The rules that make it work well\\n\\nEach is unpacked in the sections below; this is the checklist.\\n\\n1. **Top level must be an object.** Want a list? Wrap it:\\n `{ type: \\"object\\", properties: { items: { type: \\"array\\", \u2026 } }, required: [\\"items\\"] }`.\\n2. **Steer with the prompt, not schema `description`s** \u2014 free-text schema\\n fields are stripped before the AI sees them. Use self-explanatory\\n property names (`benefitHeadline`, not `text1`).\\n3. **Only `require` what\'s guaranteed.** `required` is strictly enforced; if\\n the catalog can\'t ground a required field the whole call can fail. Require\\n structural fields, keep per-product details (image URLs, prices) optional,\\n and make rendering tolerate missing values.\\n4. **Point, don\'t paste.** GreatStore researches the store\'s live catalog on\\n its own \u2014 name the entity (`` `\u2026the product \\"${productName}\\" (SKU ${sku})` ``)\\n and let it look the facts up. Don\'t fetch specs/prices/descriptions\\n yourself and paste them into the prompt. The one thing it *can\'t* see is\\n your page, so page-only context (which page the shopper is on, what the\\n section is for) does belong in the prompt.\\n5. **Research is bounded by the store\'s connectors \u2014 validate before you\\n build.** The AI can only look up what the store\'s MCP connectors actually\\n provide. Ask for something outside them \u2014 currency conversion, live\\n shipping rates, review data the store never wired up \u2014 and it has nothing\\n to ground on, so it will decline, omit\u2026 or hallucinate. Before writing a\\n prompt or schema that depends on a data capability, run `gs connectors`\\n (and `gs connectors health`, [store-admin.md](store-admin.md)) and confirm\\n a connector for it exists; if it doesn\'t, don\'t ask for it.\\n6. **Responses are cached** for up to ~24h per page + prompt + schema \u2014\\n shared across anonymous visitors, per-shopper for identified ones. Keep\\n prompts deterministic per page \u2014 no timestamps, random values, or\\n per-visitor data (GreatStore already knows who the shopper is; see below).\\n7. **Progressive enhancement, always.** Generate after the page renders into\\n a hidden container, reveal on success, leave the fallback on error. Render\\n generated strings via `textContent`, never `innerHTML`.\\n8. **One rich call beats many small ones** \u2014 there\'s a per-visitor rate limit\\n (~20 requests/minute); fetch multiple surfaces with one combined schema.\\n\\n## What actually happens\\n\\n1. The SDK posts your schema + prompt to the store\'s GreatStore endpoint,\\n along with the current **page URL and page title** (sent automatically \u2014\\n you don\'t pass them, and you can\'t override them).\\n2. GreatStore first **researches**: it looks up real data from the store\'s\\n live catalog (products, prices, availability, store info) using read-only\\n lookups. The page URL/title serve as hints about which product or category\\n to look up \u2014 they are *not* treated as a source of product data, and the\\n page\'s DOM is never read. Research happens on GreatStore\'s side \u2014 your\\n prompt only needs to *point* it at the right SKU, product, or collection,\\n not carry the material. Its reach is exactly the store\'s MCP connectors:\\n validate with `gs connectors` ([store-admin.md](store-admin.md)) that a\\n connector for the data you want actually exists before you build on it \u2014\\n research can\'t exceed the wired-up connectors, and prompts that assume\\n otherwise invite hallucinated filler.\\n3. The AI then fills your schema from the researched data, under a strict\\n grounding contract: it must not invent product names, prices, images, IDs,\\n or descriptions. Fields it can\'t ground are omitted or `null`. For an\\n identified shopper this step also sees their shopper profile \u2014 the same\\n identity the chat assistant has \u2014 so the output can be subtly personalized\\n without you passing anything about the visitor.\\n4. The output is validated against your schema (with internal retries) before\\n being returned and cached.\\n\\nGreatStore already knows who\'s reading: identity rides the request the same\\nway it does for chat, and responses for identified shoppers are cached just\\nfor them (anonymous visitors share one entry). The practical consequence:\\nnever put shopper data in the prompt \u2014 it\'s redundant, and it poisons the\\ncache key.\\n\\n## Schema support\\n\\nTop level **must describe an object**: `type: \\"object\\"` (or a bare\\n`properties` / `anyOf`). To get a list, wrap it in an object property.\\n\\nSupported keywords (anything else is tolerated but ignored):\\n\\n- Types: `object`, `array`, `string`, `number`, `integer`, `boolean`, `null`\\n- Structure: `properties`, `required`, `items`, `additionalProperties`\\n- Choice: `enum`, `const`, `anyOf`, `nullable`\\n- Constraints: `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`,\\n `maxItems`, `pattern`, `format`, `default`\\n\\nValidation of the output is real: `required` is enforced, `enum`/`const`\\nmust match, numeric and length bounds are checked, and\\n`additionalProperties: false` rejects extra keys. Constraints are therefore a\\n*tool* \u2014 `maxItems: 4` reliably caps a list, `enum` reliably restricts a\\nfield \u2014 but every constraint is also a way for generation to fail, so apply\\nthem only where you\'d rather have no content than non-conforming content.\\n\\nZod schemas (or anything with a `.toJSONSchema()` method) are accepted and\\nconverted automatically.\\n\\n### Free-text schema fields are stripped\\n\\n`description`, `title`, and `example` are removed from the schema before the\\nAI sees it (they\'re a prompt-injection surface, so they\'re filtered\\nserver-side). Consequences:\\n\\n- Schema descriptions **cannot** steer generation. All steering lives in the\\n prompt string.\\n- Property *names* are the only in-schema signal of intent \u2014 make them\\n self-documenting: `ctaLabel`, `warmthRating`, `priceJustification`.\\n\\n## Prompting guide\\n\\nThe prompt is the entire instruction channel. A good prompt states, in order:\\n\\n1. **Context** \u2014 what page/situation the shopper is in, naming the entity so\\n research targets the right thing (the AI can\'t see your DOM, so identify\\n it explicitly):\\n `The shopper is viewing the product \\"Aurora Down Parka\\" (SKU AUR-021) on its product page.`\\n2. **Task** \u2014 what to generate, mapped loosely onto your schema\'s fields:\\n `Write a heading and 3 reasons to love it; each reason has a short title and one supporting sentence.`\\n3. **Grounding expectations** \u2014 what store data to draw on:\\n `Base every claim on the product\'s real materials, features, and price.`\\n4. **Voice** \u2014 tone and constraints:\\n `Warm and concrete. No exclamation marks, no generic marketing filler.`\\n\\nNote what\'s *not* in that prompt: no pasted specs, prices, or descriptions.\\nPointing at the SKU is enough \u2014 GreatStore researches the rest itself, from\\ndata that\'s live rather than whatever was true when you wrote the prompt.\\n\\nAnti-patterns:\\n\\n- **Pasting researched material into the prompt** (specs, prices,\\n descriptions you fetched from your platform\'s API). GreatStore researches\\n the live catalog itself \u2014 point it at the SKU/product/collection and let\\n it look things up. Pasted facts go stale, bloat the cache key, and compete\\n with the fresher data research returns.\\n- **Per-visitor or per-moment data in the prompt** (names, cart contents,\\n timestamps, `Math.random()`): destroys caching, so every visitor pays full\\n generation latency and the store pays for every call \u2014 and it\'s redundant,\\n because GreatStore already knows the shopper and personalizes for\\n identified ones server-side. If you need per-shopper *interaction*, that\'s\\n what `sendMessage` and the chat panel are for.\\n- **Asking for data no connector provides** (\\"convert the price to EUR\\",\\n \\"estimate delivery to the shopper\'s city\\"): research can\'t exceed the\\n store\'s MCP connectors, and the AI may hallucinate plausible-looking\\n values rather than leave the field empty. Validate first \u2014 `gs connectors`\\n shows what\'s wired up ([store-admin.md](store-admin.md)); if there\'s no\\n connector for it, don\'t ask for it.\\n- **Asking it to read the page** (\\"summarize the reviews shown below\\") \u2014 it\\n can\'t. Page-only data (review snippets, UGC, things that exist nowhere but\\n the DOM) is the one kind worth inlining \u2014 keep it stable per page so\\n caching still works.\\n- **Asking for minute-fresh operational data** (exact live stock counts,\\n delivery countdowns). Even when a connector could answer, responses cache\\n for up to ~24h \u2014 display operational data from your own platform APIs and\\n use GreatStore for *editorial intelligence over the catalog*.\\n- **Burying instructions in schema descriptions** \u2014 stripped, see above.\\n\\n## Caching: design for it\\n\\nResponses are cached server-side for up to **24 hours**, keyed by the\\ncombination of page URL + page title + prompt + schema. Anonymous visitors\\nshare one entry per key; identified shoppers each get their own (their\\noutput may be personalized, so it\'s only ever served back to them).\\n(Tracking query params like `utm_*`/`gclid` and the URL fragment are\\nignored, so ad-tagged visits share the campaign-free page\'s cache entry.\\nMeaningful params like `?product=123` are part of the key.)\\n\\nPractical consequences:\\n\\n- **The first render pays, repeats fly.** Expect a few seconds on a cache\\n miss and near-instant responses after \u2014 shared across all anonymous\\n traffic, per-shopper for identified traffic (the research underneath is\\n cached briefly and shared, so even those misses are cheaper than cold).\\n Design loading states for the miss case.\\n- **Same call on different pages = different content**, automatically \u2014 the\\n page URL is in the key and in the AI\'s hints. A single site-wide snippet\\n with a constant prompt yields per-page content for free.\\n- **Content refreshes roughly daily.** Don\'t build experiences that assume\\n minute-level freshness.\\n- **To force different content, change the prompt or schema** (e.g. a\\n campaign variant string that changes weekly \u2014 deliberate, low-cardinality\\n variation is fine; per-visitor cardinality is not).\\n\\nThe catalog research underneath is also cached briefly, so several distinct\\nsurfaces on the same page (different prompts/schemas) stay cheap even on\\ncold cache.\\n\\n## Errors and how to handle them\\n\\nThe promise rejects with `new Error(message)`. The message is\\ndeveloper-facing \u2014 never render it to shoppers. Cases:\\n\\n| Case | Message you\'ll see | Retry? |\\n|---|---|---|\\n| Bad input (empty prompt, non-object schema) | thrown immediately by the SDK | Fix the call |\\n| Invalid schema shape | `Invalid schema: \u2026` | Fix the schema |\\n| AI declined the request | `The assistant declined to generate content for this request.` | No \u2014 permanent for that prompt/schema. Rework the prompt. |\\n| Output couldn\'t satisfy the schema | `Failed to produce valid structured content` | No \u2014 usually `required`/constraints demand data the catalog lacks. Loosen the schema. |\\n| Rate limit (~20/min per visitor) | rate-limit message | Later \u2014 and consolidate calls |\\n| Network / server | varies | Next page load |\\n\\nThe uniform shopper-facing strategy: render into a hidden-by-default\\ncontainer, reveal on success, leave hidden (or show your static fallback) on\\nany rejection. One `try/catch`, no case analysis needed unless you\'re\\nlogging.\\n\\n## Performance pattern\\n\\nFire generation as early as possible without blocking render \u2014 top of your\\ndeferred script, before other work:\\n\\n```js\\nconst highlightsPromise = window.GreatStore?.generateStructuredContent\\n ? window.GreatStore.generateStructuredContent(schema, prompt).catch(() => null)\\n : Promise.resolve(null);\\n\\n// \u2026rest of page setup\u2026\\n\\nconst data = await highlightsPromise;\\nif (data) renderHighlights(data);\\n```\\n\\nThe `.catch(() => null)` attached immediately avoids unhandled-rejection\\nnoise while keeping a single render path.\\n\\nFor multiple surfaces on one page, prefer **one call with a combined\\nschema** over parallel calls \u2014 it\'s one research pass, one cache entry, and\\nno rate-limit pressure:\\n\\n```js\\nconst schema = {\\n type: \\"object\\",\\n properties: {\\n highlights: { /* \u2026 */ },\\n faq: { /* \u2026 */ },\\n crossSell: { /* \u2026 */ },\\n },\\n required: [\\"highlights\\"],\\n};\\n```\\n"}') : readTreeFromDisk(new URL("../../../gs-skill/", import.meta.url));
697
700
  if (!cache2["SKILL.md"]) {
698
701
  throw new Error("internal: agent skill tree is missing SKILL.md");
699
702
  }
@@ -1807,18 +1810,12 @@ function printBrand(slug, brand) {
1807
1810
  lines.push(` theme ${brand.theme ? JSON.stringify(brand.theme) : "\u2014"}`);
1808
1811
  lines.push(` cspScriptHosts ${formatList(brand.cspScriptHosts)}`);
1809
1812
  lines.push(` cspConnectHosts ${formatList(brand.cspConnectHosts)}`);
1810
- lines.push(` salesGuide ${formatSalesGuide(brand.salesGuide)}`);
1811
1813
  process.stdout.write(lines.join("\n") + "\n");
1812
1814
  }
1813
1815
  function formatList(list) {
1814
1816
  if (!list || list.length === 0) return "\u2014";
1815
1817
  return list.join(", ");
1816
1818
  }
1817
- function formatSalesGuide(guide) {
1818
- if (!guide) return "\u2014";
1819
- const flat = guide.replace(/\s+/g, " ").trim();
1820
- return flat.length > 60 ? `${flat.slice(0, 57)}\u2026 (${guide.length} chars)` : flat;
1821
- }
1822
1819
 
1823
1820
  // src/commands/configure/set.ts
1824
1821
  import * as fs9 from "fs";
@@ -1826,7 +1823,6 @@ import * as path9 from "path";
1826
1823
  var TEXT_FIELDS = [
1827
1824
  "displayName",
1828
1825
  "assistantName",
1829
- "salesGuide",
1830
1826
  "storeLink"
1831
1827
  ];
1832
1828
  var LIST_FIELDS = ["extraOrigins", "cspScriptHosts", "cspConnectHosts"];
@@ -1841,13 +1837,6 @@ async function setConfig(args) {
1841
1837
  }
1842
1838
  body[field] = v === "" ? null : v;
1843
1839
  }
1844
- const salesGuideFile = flagString(args.flags, "salesGuideFile");
1845
- if (salesGuideFile !== void 0) {
1846
- if ("salesGuide" in body) {
1847
- throw new Error("Pass either --salesGuide or --salesGuideFile, not both.");
1848
- }
1849
- body["salesGuide"] = readTextFile(salesGuideFile, "--salesGuideFile");
1850
- }
1851
1840
  for (const field of LIST_FIELDS) {
1852
1841
  if (!(field in args.flags)) continue;
1853
1842
  const v = args.flags[field];
@@ -2369,8 +2358,8 @@ function parseVer(v) {
2369
2358
  }
2370
2359
 
2371
2360
  // src/index.ts
2372
- var VERSION = true ? "0.0.38" : "0.0.0-dev";
2373
- var CHANGELOG = true ? "# Changelog\n\nAll notable changes to `@greatstore/cli` are recorded here. The format\nfollows [Keep a Changelog](https://keepachangelog.com/).\n\n## 0.0.38 \u2014 2026-07-13\n\n### Added\n- `gs login --store <slug>` skips the store picker and signs in\n directly to that store \u2014 fails immediately if your account doesn't\n have access to it, instead of falling back to the picker.\n\n## 0.0.37 \u2014 2026-07-10\n\n### Added\n- Signing in now ends by choosing which store to work on \u2014 skipped\n automatically when your account has exactly one. Commands default to\n that store, so `--store` is rarely needed anymore.\n- `gs switch [<slug>]` changes the working store without signing in\n again.\n- `gs apps init` no longer requires `--store` when your sign-in already\n selected a store.\n\n### Changed\n- A `--store` flag or project `.gsrc` that names a different store than\n the one you signed in to is now an error, so work can't accidentally\n target the wrong store. Run `gs switch` to change stores.\n\n## 0.0.33 \u2014 2026-07-10\n\n### Changed\n- `gs apps push` now rejects a component whose `inputSchema` is too\n complex for the assistant to call reliably: union keywords\n (`anyOf`/`oneOf`/`allOf`/`$ref`/`not`), more than 8 KB serialized, or\n more than 50 declared fields. Keep schemas to a small set of flat,\n single-type fields \u2014 one canonical name per concept \u2014 and handle\n aliases or edge cases in component code instead.\n\n## 0.0.32 \u2014 2026-06-18\n\n### Added\n- Chat components can expand an image into a full-screen, on-brand\n lightbox via a new `onShowLightbox({ src, originRect })` prop. Wire it\n to an image's `onClick` \u2014 pass the image URL and, for a smooth zoom,\n the clicked element's `getBoundingClientRect()`. Use it for product\n photos, swatches, or size charts the shopper may want to inspect up\n close, instead of building your own overlay.\n\n## 0.0.31 \u2014 2026-06-18\n\n### Added\n- Components can read a secondary brand font from `--font-secondary`,\n for a second layer of typography. Falls back to the primary font.\n\n### Changed\n- The brand font variable is now `--font-primary` (was `--font-sans`).\n\n## 0.0.30 \u2014 2026-06-17\n\n### Added\n- The agent skill documents a new way to give the assistant background\n context without sending a visible message:\n `window.GreatStore.updateModelContext(text)` on the page, and the\n matching `onUpdateModelContext(text)` prop inside a chat component. Use\n it to keep the assistant aware of what the shopper is doing \u2014 the\n product they're viewing, what's in their cart, the variant they just\n selected \u2014 so its replies stay on point. Each call replaces the previous\n value, and nothing renders in the chat.\n\n## 0.0.29 \u2014 2026-06-12\n\n### Fixed\n- The \"update available\" notice actually fires now. It previously raced\n a 1-second timeout against the npm registry and usually lost, so most\n installs never saw it. The notice is now served instantly from a local\n cache, refreshed in the background after each day's first invocation \u2014\n it can lag one run behind a release, but it no longer adds latency or\n goes silent on slow networks.\n\n## 0.0.28 \u2014 2026-06-12\n\n### Added\n- `gs configure` \u2014 view and edit the store configuration from the CLI:\n display name, assistant name, sales guide, store link, extra origins,\n theme, CSP host lists, and icon/logo uploads. Same fields and\n behaviour as the dashboard's Configure panel.\n- `gs connectors` \u2014 manage the store's MCP connectors: list, add (with\n a discovery probe before saving), remove, enable/disable, toggle the\n Maker MCP, and health-check. Same behaviour as the dashboard's\n Connectors panel.\n- `--store <slug>` on the new admin commands, so they work outside a\n scaffolded component project (a `.gsrc` is still used when present).\n- The agent skill gains a store-administration reference: coding agents\n can read the store's configuration and connectors to ground their\n work, self-serve additive changes like origin allowlists and CSP\n hosts (read-merge-write), and are told which changes need the\n merchant's go-ahead first.\n\n### Changed\n- Component commands now live under `gs apps` (`gs apps push`,\n `gs apps build`, \u2026), matching the dashboard's Apps panel. The old\n top-level forms keep working as aliases, so existing scripts and\n scaffolded projects are unaffected.\n- The skill's structured-content guide now teaches \"point, don't\n paste\": name the SKU/product/collection and let GreatStore research\n the catalog itself instead of inlining fetched specs; validate with\n `gs connectors` that a connector exists for the data a prompt or\n schema depends on (research can't exceed the wired-up connectors);\n and never put shopper data in prompts \u2014 GreatStore already knows the\n shopper, and identified shoppers get per-shopper cached responses.\n\n## 0.0.27 \u2014 2026-06-11\n\n### Changed\n- Skill code samples now carry an explicit reference-only disclaimer:\n coding agents are told to re-express the logic in the host repo's\n framework (React, Vue, Shopify Liquid, Svelte, \u2026) instead of\n retrofitting the framework-free samples as-is.\n\n## 0.0.26 \u2014 2026-06-11\n\n### Added\n- The agent skill gains a \"GreatStore launchers\" recipe: a horizontally\n scrollable row of AI-generated chips, each an engaging first-person\n question about the current page that's sent to the assistant on tap.\n\n### Changed\n- Skill recipes are now one file each under `recipes/`, indexed from\n SKILL.md by a table with description and use-case columns.\n\n## 0.0.25 \u2014 2026-06-11\n\n### Added\n- `gs skill` installs the GreatStore agent skill \u2014 a guide AI coding\n agents use to build with GreatStore: AI content for your own UI, chat\n entry points, page tools, custom in-chat components, push\n notifications, and the store's MCP endpoints. Installs into\n `./.claude/skills/`; use `--global` for `~/.claude/skills/`, or\n `--dir <path>` for agents that read skills from somewhere else. Run\n it again any time to update an installed copy.\n\n## 0.0.24 \u2014 2026-06-03\n\n### Changed\n- `gs init` in an existing project now fills in any scaffold files that\n are missing (for example, the `AGENTS.md` design guide added in\n 0.0.23) and leaves your own files alone. Pass `--force` to refresh\n every scaffold file to the latest version. Your pinned store\n (`.gsrc`) is never rewritten either way.\n\n## 0.0.23 \u2014 2026-06-03\n\n### Added\n- `gs init` now scaffolds an `AGENTS.md` (with `CLAUDE.md` and\n `GEMINI.md` symlinked to it) documenting the design rules every\n component should follow \u2014 use `em` rather than `rem` for sizing, and\n style from the provided brand CSS variables so components match the\n store's theme. It doubles as guidance for AI coding agents.\n\n## 0.0.22 \u2014 2026-06-02\n\n### Added\n- `gs list` now shows a link to each component's page in the dashboard,\n so you can jump straight to a component to preview or publish it. The\n link is also included in `gs list --json`.\n\n## 0.0.21 \u2014 2026-05-31\n\n### Changed\n- The `gs init` component scaffold now shows how to write **async**\n components that load data before they render \u2014 including validating\n inputs up front and signalling a failure by throwing. The scaffolded\n component no longer includes an `onError` prop; throw from an async\n component to report a failure instead.\n\n## 0.0.20 \u2014 2026-05-29\n\n### Added\n- `gs pull`, `gs push`, and `gs publish` now accept several component\n names at once (e.g. `gs publish header footer cart`). Each component\n is reported on its own line and one failure no longer stops the rest.\n\n## 0.0.19 \u2014 2026-05-28\n\n### Fixed\n- `gs login` on Windows no longer opens a sign-in URL with parameters\n stripped, which surfaced as a \"Missing redirect_uri or state\n parameter\" page in the browser.\n\n## 0.0.18 \u2014 2026-05-28\n\n### Changed\n- Push and publish errors now name the specific reason \u2014 including\n every failing field in `manifest.json` \u2014 instead of the previous\n generic message.\n\n## 0.0.17 \u2014 2026-05-28\n\n### Added\n- Each command now prints a one-line upgrade notice when a newer\n `@greatstore/cli` is available on npm.\n\n## 0.0.16 \u2014 2026-05-28\n\n### Changed\n- Simplified error messages.\n\n## 0.0.15 \u2014 2026-05-24\n\n### Added\n- Scaffolded `component.tsx` now declares the four injected lifecycle\n props (`onSendMessage`, `onCallTool`, `onClose`, `onError`) on\n `Props`. Use `onError(message)` to report expected failures (failed\n fetch, host action rejected, invalid host state) so the AI can\n recover on its next turn. Render-time crashes are reported for you.\n\n## 0.0.14 \u2014 2026-05-24\n\n### Changed\n- `gs build` output is now whitespace-minified \u2014 typically ~50% smaller.\n\n## 0.0.13 \u2014 2026-05-24\n\n### Changed\n- `gs build` prints the next-step hint (`gs push`, then `gs publish`).\n\n## 0.0.12 \u2014 2026-05-24\n\n### Fixed\n- `gs build` failing to load Vite in some setups.\n\n## 0.0.11 \u2014 2026-05-24\n\n### Added\n- Multi-component projects. `gs init` (no args) scaffolds the project\n root; `gs init <name>` adds a component under `components/<name>/`.\n- `gs build [<name>]` \u2014 compiles every `components/<name>/bundle.js`.\n- `gs push` (no args) uploads only the components that changed.\n- `gs pull` (no args, or `*`) downloads every component. Locally\n edited components are skipped; pass `--force` to overwrite.\n- Public `CHANGELOG.md`; `gs --version` prints recent entries.\n\n### Changed\n- A project folder ships to exactly one store. Only `gs init` accepts\n `--store`; every other command reads the slug from `.gsrc`. The old\n single-component layout is rejected with a migration hint.\n- `gs init` requires `--store <slug>` for a fresh root, and rejects\n `--store` on an existing root.\n- `gs init` no longer writes `build.mjs` \u2014 scripts call `gs build`.\n\n## 0.0.10 \u2014 2026-05-23\n\n### Changed\n- The sign-in browser tab auto-closes once `gs login` finishes.\n\n## 0.0.9 \u2014 2026-05-23\n\n### Changed\n- Scaffolded manifests include a `displayName` so the admin UI has a\n friendlier label.\n\n## 0.0.8 \u2014 2026-05-23\n\n### Changed\n- `gs push` and `gs publish` print a link to view the component.\n\n## 0.0.6 \u2014 2026-05-23\n\n### Changed\n- `gs --version` reads from the published package version.\n\n## 0.0.4 \u2014 2026-05-23\n\n### Changed\n- Scaffolded projects produce browser-ready bundles out of the box.\n\n## 0.0.3 \u2014 2026-05-23\n\n### Changed\n- Trimmed public README to the essentials.\n\n## 0.0.2 \u2014 2026-05-23\n\n### Fixed\n- Sign-in callback parameter handling.\n" : "";
2361
+ var VERSION = true ? "0.0.40" : "0.0.0-dev";
2362
+ var CHANGELOG = true ? "# Changelog\n\nAll notable changes to `@greatstore/cli` are recorded here. The format\nfollows [Keep a Changelog](https://keepachangelog.com/).\n\n## 0.0.40 \u2014 2026-07-24\n\n### Removed\n- `gs configure set` no longer accepts `--salesGuide` or\n `--salesGuideFile`, and `gs configure show` no longer lists the field.\n\n## 0.0.39 \u2014 2026-07-21\n\n### Added\n- Components now receive an `Image` prop \u2014 a drop-in for `<img>` that\n serves images at the size they're displayed. Render `<Image src=\u2026 />`\n instead of `<img>`; pass `Image={\"img\"}` to preview a component\n outside a store. Scaffolded into `gs apps init` and documented in\n `AGENTS.md`.\n\n## 0.0.38 \u2014 2026-07-13\n\n### Added\n- `gs login --store <slug>` skips the store picker and signs in\n directly to that store \u2014 fails immediately if your account doesn't\n have access to it, instead of falling back to the picker.\n\n## 0.0.37 \u2014 2026-07-10\n\n### Added\n- Signing in now ends by choosing which store to work on \u2014 skipped\n automatically when your account has exactly one. Commands default to\n that store, so `--store` is rarely needed anymore.\n- `gs switch [<slug>]` changes the working store without signing in\n again.\n- `gs apps init` no longer requires `--store` when your sign-in already\n selected a store.\n\n### Changed\n- A `--store` flag or project `.gsrc` that names a different store than\n the one you signed in to is now an error, so work can't accidentally\n target the wrong store. Run `gs switch` to change stores.\n\n## 0.0.33 \u2014 2026-07-10\n\n### Changed\n- `gs apps push` now rejects a component whose `inputSchema` is too\n complex for the assistant to call reliably: union keywords\n (`anyOf`/`oneOf`/`allOf`/`$ref`/`not`), more than 8 KB serialized, or\n more than 50 declared fields. Keep schemas to a small set of flat,\n single-type fields \u2014 one canonical name per concept \u2014 and handle\n aliases or edge cases in component code instead.\n\n## 0.0.32 \u2014 2026-06-18\n\n### Added\n- Chat components can expand an image into a full-screen, on-brand\n lightbox via a new `onShowLightbox({ src, originRect })` prop. Wire it\n to an image's `onClick` \u2014 pass the image URL and, for a smooth zoom,\n the clicked element's `getBoundingClientRect()`. Use it for product\n photos, swatches, or size charts the shopper may want to inspect up\n close, instead of building your own overlay.\n\n## 0.0.31 \u2014 2026-06-18\n\n### Added\n- Components can read a secondary brand font from `--font-secondary`,\n for a second layer of typography. Falls back to the primary font.\n\n### Changed\n- The brand font variable is now `--font-primary` (was `--font-sans`).\n\n## 0.0.30 \u2014 2026-06-17\n\n### Added\n- The agent skill documents a new way to give the assistant background\n context without sending a visible message:\n `window.GreatStore.updateModelContext(text)` on the page, and the\n matching `onUpdateModelContext(text)` prop inside a chat component. Use\n it to keep the assistant aware of what the shopper is doing \u2014 the\n product they're viewing, what's in their cart, the variant they just\n selected \u2014 so its replies stay on point. Each call replaces the previous\n value, and nothing renders in the chat.\n\n## 0.0.29 \u2014 2026-06-12\n\n### Fixed\n- The \"update available\" notice actually fires now. It previously raced\n a 1-second timeout against the npm registry and usually lost, so most\n installs never saw it. The notice is now served instantly from a local\n cache, refreshed in the background after each day's first invocation \u2014\n it can lag one run behind a release, but it no longer adds latency or\n goes silent on slow networks.\n\n## 0.0.28 \u2014 2026-06-12\n\n### Added\n- `gs configure` \u2014 view and edit the store configuration from the CLI:\n display name, assistant name, sales guide, store link, extra origins,\n theme, CSP host lists, and icon/logo uploads. Same fields and\n behaviour as the dashboard's Configure panel.\n- `gs connectors` \u2014 manage the store's MCP connectors: list, add (with\n a discovery probe before saving), remove, enable/disable, toggle the\n Maker MCP, and health-check. Same behaviour as the dashboard's\n Connectors panel.\n- `--store <slug>` on the new admin commands, so they work outside a\n scaffolded component project (a `.gsrc` is still used when present).\n- The agent skill gains a store-administration reference: coding agents\n can read the store's configuration and connectors to ground their\n work, self-serve additive changes like origin allowlists and CSP\n hosts (read-merge-write), and are told which changes need the\n merchant's go-ahead first.\n\n### Changed\n- Component commands now live under `gs apps` (`gs apps push`,\n `gs apps build`, \u2026), matching the dashboard's Apps panel. The old\n top-level forms keep working as aliases, so existing scripts and\n scaffolded projects are unaffected.\n- The skill's structured-content guide now teaches \"point, don't\n paste\": name the SKU/product/collection and let GreatStore research\n the catalog itself instead of inlining fetched specs; validate with\n `gs connectors` that a connector exists for the data a prompt or\n schema depends on (research can't exceed the wired-up connectors);\n and never put shopper data in prompts \u2014 GreatStore already knows the\n shopper, and identified shoppers get per-shopper cached responses.\n\n## 0.0.27 \u2014 2026-06-11\n\n### Changed\n- Skill code samples now carry an explicit reference-only disclaimer:\n coding agents are told to re-express the logic in the host repo's\n framework (React, Vue, Shopify Liquid, Svelte, \u2026) instead of\n retrofitting the framework-free samples as-is.\n\n## 0.0.26 \u2014 2026-06-11\n\n### Added\n- The agent skill gains a \"GreatStore launchers\" recipe: a horizontally\n scrollable row of AI-generated chips, each an engaging first-person\n question about the current page that's sent to the assistant on tap.\n\n### Changed\n- Skill recipes are now one file each under `recipes/`, indexed from\n SKILL.md by a table with description and use-case columns.\n\n## 0.0.25 \u2014 2026-06-11\n\n### Added\n- `gs skill` installs the GreatStore agent skill \u2014 a guide AI coding\n agents use to build with GreatStore: AI content for your own UI, chat\n entry points, page tools, custom in-chat components, push\n notifications, and the store's MCP endpoints. Installs into\n `./.claude/skills/`; use `--global` for `~/.claude/skills/`, or\n `--dir <path>` for agents that read skills from somewhere else. Run\n it again any time to update an installed copy.\n\n## 0.0.24 \u2014 2026-06-03\n\n### Changed\n- `gs init` in an existing project now fills in any scaffold files that\n are missing (for example, the `AGENTS.md` design guide added in\n 0.0.23) and leaves your own files alone. Pass `--force` to refresh\n every scaffold file to the latest version. Your pinned store\n (`.gsrc`) is never rewritten either way.\n\n## 0.0.23 \u2014 2026-06-03\n\n### Added\n- `gs init` now scaffolds an `AGENTS.md` (with `CLAUDE.md` and\n `GEMINI.md` symlinked to it) documenting the design rules every\n component should follow \u2014 use `em` rather than `rem` for sizing, and\n style from the provided brand CSS variables so components match the\n store's theme. It doubles as guidance for AI coding agents.\n\n## 0.0.22 \u2014 2026-06-02\n\n### Added\n- `gs list` now shows a link to each component's page in the dashboard,\n so you can jump straight to a component to preview or publish it. The\n link is also included in `gs list --json`.\n\n## 0.0.21 \u2014 2026-05-31\n\n### Changed\n- The `gs init` component scaffold now shows how to write **async**\n components that load data before they render \u2014 including validating\n inputs up front and signalling a failure by throwing. The scaffolded\n component no longer includes an `onError` prop; throw from an async\n component to report a failure instead.\n\n## 0.0.20 \u2014 2026-05-29\n\n### Added\n- `gs pull`, `gs push`, and `gs publish` now accept several component\n names at once (e.g. `gs publish header footer cart`). Each component\n is reported on its own line and one failure no longer stops the rest.\n\n## 0.0.19 \u2014 2026-05-28\n\n### Fixed\n- `gs login` on Windows no longer opens a sign-in URL with parameters\n stripped, which surfaced as a \"Missing redirect_uri or state\n parameter\" page in the browser.\n\n## 0.0.18 \u2014 2026-05-28\n\n### Changed\n- Push and publish errors now name the specific reason \u2014 including\n every failing field in `manifest.json` \u2014 instead of the previous\n generic message.\n\n## 0.0.17 \u2014 2026-05-28\n\n### Added\n- Each command now prints a one-line upgrade notice when a newer\n `@greatstore/cli` is available on npm.\n\n## 0.0.16 \u2014 2026-05-28\n\n### Changed\n- Simplified error messages.\n\n## 0.0.15 \u2014 2026-05-24\n\n### Added\n- Scaffolded `component.tsx` now declares the four injected lifecycle\n props (`onSendMessage`, `onCallTool`, `onClose`, `onError`) on\n `Props`. Use `onError(message)` to report expected failures (failed\n fetch, host action rejected, invalid host state) so the AI can\n recover on its next turn. Render-time crashes are reported for you.\n\n## 0.0.14 \u2014 2026-05-24\n\n### Changed\n- `gs build` output is now whitespace-minified \u2014 typically ~50% smaller.\n\n## 0.0.13 \u2014 2026-05-24\n\n### Changed\n- `gs build` prints the next-step hint (`gs push`, then `gs publish`).\n\n## 0.0.12 \u2014 2026-05-24\n\n### Fixed\n- `gs build` failing to load Vite in some setups.\n\n## 0.0.11 \u2014 2026-05-24\n\n### Added\n- Multi-component projects. `gs init` (no args) scaffolds the project\n root; `gs init <name>` adds a component under `components/<name>/`.\n- `gs build [<name>]` \u2014 compiles every `components/<name>/bundle.js`.\n- `gs push` (no args) uploads only the components that changed.\n- `gs pull` (no args, or `*`) downloads every component. Locally\n edited components are skipped; pass `--force` to overwrite.\n- Public `CHANGELOG.md`; `gs --version` prints recent entries.\n\n### Changed\n- A project folder ships to exactly one store. Only `gs init` accepts\n `--store`; every other command reads the slug from `.gsrc`. The old\n single-component layout is rejected with a migration hint.\n- `gs init` requires `--store <slug>` for a fresh root, and rejects\n `--store` on an existing root.\n- `gs init` no longer writes `build.mjs` \u2014 scripts call `gs build`.\n\n## 0.0.10 \u2014 2026-05-23\n\n### Changed\n- The sign-in browser tab auto-closes once `gs login` finishes.\n\n## 0.0.9 \u2014 2026-05-23\n\n### Changed\n- Scaffolded manifests include a `displayName` so the admin UI has a\n friendlier label.\n\n## 0.0.8 \u2014 2026-05-23\n\n### Changed\n- `gs push` and `gs publish` print a link to view the component.\n\n## 0.0.6 \u2014 2026-05-23\n\n### Changed\n- `gs --version` reads from the published package version.\n\n## 0.0.4 \u2014 2026-05-23\n\n### Changed\n- Scaffolded projects produce browser-ready bundles out of the box.\n\n## 0.0.3 \u2014 2026-05-23\n\n### Changed\n- Trimmed public README to the essentials.\n\n## 0.0.2 \u2014 2026-05-23\n\n### Fixed\n- Sign-in callback parameter handling.\n" : "";
2374
2363
  var HELP = `gs \u2014 GreatStore CLI (v${VERSION})
2375
2364
 
2376
2365
  Usage:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatstore/cli",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "CLI for administering GreatStore stores and authoring custom components.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",