Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions connectors/shopify/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
SHOPIFY_SHOP=drinkvaritea.myshopify.com
SHOPIFY_ADMIN_TOKEN=shpat_xxx
SHOPIFY_API_VERSION=2025-07
SHOPIFY_WEBHOOK_SECRET=xxx
SHOPLOOP_MCP_TOKEN=xxx
TEMPORAL_ADDRESS=localhost:7233
TEMPORAL_NAMESPACE=shoploop
OPEN_IMAGINE_ROUTER_URL=https://router.open-imagine.local
BRAND_RULES_PATH=/etc/shoploop/brand_rules.json
USD_OUT_DIR=/var/shoploop/twins
SHOPLOOP_MCP_PORT=8787
5 changes: 5 additions & 0 deletions connectors/shopify/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
*.tsbuildinfo
.env
.env.local
63 changes: 63 additions & 0 deletions connectors/shopify/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# DESIGN — connectors/shopify

Companion to the root [`design.md`](../../design.md) and [`AGENTS.md`](../../AGENTS.md). This
doc covers only what is specific to the Shopify → USD connector.

## Position in the architecture loop

```
Shopify Catalog → [THIS] Product Ingest (→ product_twin.usda)
→ [THIS] Scene Composer hook (brand_rules.json + USD stage)
→ open-imagine Render Pipeline (Flux Pro default) ← contract boundary
→ Creative Pack → Distribution → Performance Ingest → brand_rules.json (loop closes)
```

The moat is the **USD scene graph + brand-constraint enforcement**, not image generation.
This connector owns the left half: turning a commerce catalog into a constrained scene graph
**before any pixels exist**. Render is `open-imagine`'s job.

## Why MCP-shaped, not a REST wrapper

A REST wrapper is commodity (decision gate #2 → cut). The first-class surface is the value:
products are addressable as USD primitives that an agent (Claude/Cursor, Open Employee,
Agentframe) can **read, inspect, and manipulate** — `inspect_product_twin` returns a prim tree
+ variant sets + a textual summary; `manipulate_variant_set` edits the stage. Same shape as
`daslabhq/openusd-mcp`, commerce-typed.

## Single internal core

`client.ts` (Admin GraphQL, 2025-07) + `usd/*` (emit / inspect / manipulate) are the only
place catalog→USD logic lives. The CLI, the MCP tools, and the Temporal activities are thin
adapters over that core — a product update via webhook and a manual `ingest` produce byte-for-byte
identical `.usda` (modulo the `commerce:generated_at` line). Tests assert this against the golden.

## Brand-constraint layer (Primitive 3)

`composeScene` emits a composed stage that sublayers `[brand_rules, product_twin, scene_preset]`
and clamps preset params (depth-of-field, copy density) into the brand's allowed band. Defaults
are the Kinfolk profile: soft window/overcast light, matte/raw materials, sparse copy.

### Kinfolk test

> **If a composed scene wouldn't fit a Kinfolk spread, `brand_rules` failed.**

This is the gate. `rulesApplied` always includes `kinfolk_gate`; the brand-rules sublayer pins
palette, allowed materials, allowed lighting rigs, the DoF band, and the copy-density cap. A
scene that violates these never reaches the renderer.

## Idempotency

Workflow id = `shopify:<shop>:<topic>:<productId>:<updated_at>` with
`WorkflowIdReusePolicy=REJECT_DUPLICATE`. Duplicate webhook deliveries (Shopify retries) collapse
to one execution per product revision.

## Contracts (referenced, not vendored)

- `open-imagine/contracts/product_twin.v1.json` — the `.usda` we emit validates against this.
- `open-imagine/contracts/render_job.v1.json` — the body `RecomposeCreativePackWorkflow` POSTs
to `OPEN_IMAGINE_ROUTER_URL`.

## Cost discipline (decision gate #5)

CPU-only. ~$13/mo at Varitea dogfood scale (1× t4g.small co-locating MCP + worker, Temporal Cloud
dev tier, S3 for `.usda`). No GPU here — that cost lives in `open-imagine`. Never Vercel.
102 changes: 102 additions & 0 deletions connectors/shopify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# @shoploop/connector-shopify

First-class **Shopify → `product_twin.usda` → Scene Composer** primitives. Not a thin REST
wrapper. Modeled on [`daslabhq/openusd-mcp`](https://github.com/daslabhq/openusd-mcp) (read /
inspect / manipulate over MCP) and Griptape Nodes' agent-callable USD pattern.

Shopify products become OpenUSD primitives the [`open-imagine`](../../../open-imagine) render
path consumes. This connector is **CPU-only** — it emits `.usda` text; it does **not** render.

> Contracts referenced (not vendored): `open-imagine/contracts/product_twin.v1.json`,
> `open-imagine/contracts/render_job.v1.json`.

## The four primitives

| # | Primitive | Entry |
|---|-----------|-------|
| 1 | **Catalog ingest** → `<handle>.usda` + `_manifest.json` | `ingestCatalog()` / `shoploop-shopify ingest` |
| 2 | **MCP** read/inspect/manipulate (7 tools, HTTP/SSE) | `shoploop-shopify mcp` → `:8787/sse` |
| 3 | **Scene composition** (brand-rule enforcement at USD layer) | `composeScene()` |
| 4 | **Webhook → Temporal** (4 workflows, idempotent) | `POST /webhooks/shopify` + `shoploop-shopify worker` |

All four call the **same internal core** (`client.ts`, `usd/*`). Importable from
`@shoploop/connector-shopify`, callable from MCP, triggerable from webhooks. No duplication.

## Quickstart — Varitea dogfood path

```bash
cd connectors/shopify
pnpm install # or: npm install
cp .env.example .env # fill in SHOPIFY_ADMIN_TOKEN at minimum
```

```bash
# .env essentials
SHOPIFY_SHOP=drinkvaritea.myshopify.com
SHOPIFY_ADMIN_TOKEN=shpat_...
SHOPIFY_API_VERSION=2025-07
BRAND_RULES_PATH=/etc/shoploop/brand_rules.json
USD_OUT_DIR=/var/shoploop/twins
```

### 1. Ingest the catalog

```bash
pnpm shoploop-shopify ingest --shop drinkvaritea.myshopify.com --out ./twins
# incremental:
pnpm shoploop-shopify ingest --shop drinkvaritea.myshopify.com --since 2026-06-01T00:00:00Z --out ./twins
```

**Expected output:** one `<handle>.usda` per product plus `twins/_manifest.json`
(`shopify_id ↔ usda_path`). For `varitea-loose-leaf-original` you get a `def Xform` root prim,
a `variantSet "size"` with `_50g / _100g / _250g`, a placeholder `def Mesh` (no Model3d media on
that SKU), a `PreviewMat` bound from the product image, and commerce/brand attrs
(`commerce:price`, `commerce:sku`, `commerce:inventory`, `commerce:vendor`, `brand:tags`,
`brand:rules_ref`). See `test/fixtures/varitea_product_twin.usda` for the golden shape.

### 2. Boot the MCP server (read/inspect/manipulate)

```bash
pnpm shoploop-shopify mcp # Express + SSE on :8787 (/sse, /messages, /webhooks/shopify)
```

Bearer auth via `SHOPLOOP_MCP_TOKEN` (unset ⇒ open, dev only). CORS allowlist for Open Employee
+ Agentframe origins. Tools: `list_products`, `get_product`, `get_variants`, `get_metafields`,
`update_metafield`, `inspect_product_twin`, `manipulate_variant_set`. `inspect_product_twin`
also returns a textual prim summary for LLM consumption.

### 3. Run the Temporal worker

```bash
pnpm shoploop-shopify worker # task queue: shoploop-shopify
```

Webhook topics → workflows (workflow id `shopify:<shop>:<topic>:<productId>:<updated_at>`):

| Topic | Workflow(s) |
|-------|-------------|
| `products/create` | `ReingestProductWorkflow` |
| `products/update` | `ReingestProductWorkflow` + `RecomposeCreativePackWorkflow` |
| `products/delete` | `PurgeProductTwinWorkflow` |
| `inventory_levels/update` | `UpdateCommerceAttrWorkflow` (patches `commerce:inventory`, no re-render) |

## Develop

```bash
pnpm typecheck # tsc --noEmit (src + test)
pnpm test # vitest (primitives, mcp, webhook)
pnpm build # emit dist/ (CLI bin → dist/cli.js)
```

## Cost (dogfood scale)

Varitea ~30 SKUs, ~100 webhook events/day: **~$13/mo** — 1× t4g.small (MCP + worker co-located)
≈ $12, Temporal Cloud dev tier $0, S3 for `.usda` (~50 MB) <$1. At 100 brands ≈ $60/mo
(Fargate + Temporal starter).

## Boundaries

- No render/image code → `open-imagine`.
- No Polaris/Remix embed app → `shoploop-shopify`.
- No ad-buying / agency replacement → `shoploop-growth`.
- AWS-only for the GPU-adjacent path. This connector is CPU-only.
2 changes: 2 additions & 0 deletions connectors/shopify/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import "../dist/cli.js";
Loading
Loading