OPS ► ECOM — Shopify Store Command Center skill
OPS on-demand: This skill should be used when the user asks to "shopify", "orders inventory", or…
by Lifecycle-Innovations-Limited·MIT license·★ 543 Stars on the repo·GitHub ↗
npx degit Lifecycle-Innovations-Limited/claude-ops/claude-ops/skills/ops-ecom#main ~/.claude/skills/ops-ecomChecked ·commit main
Inside this skill
SKILL.md · the author's own words# OPS ► ECOM — Shopify Store Command Center Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Work… ## Runtime Context Before executing, load available context: 1. **Preferences**: Read `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json` - `timezone` — display all timestamps correctly - `shopify_store_url`, `shopify_admin_token` — check userConfig keys before env vars - `ecom.shopify.sales_channels` — optional last probe cache (publications + agentic/Shop gaps) - `ecom.shopify.agentic_storefronts` — optional operator-declared enablement map (channel → status) 2. **Daemon health**: Read `${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json` - If `action_needed` is not null → surface it before running any store operations
Word-for-word excerpt, not a result. Read the full SKILL.md ↓
Files of OPS ► ECOM — Shopify Store Command Center
Show the full text751 lines
OPS ► ECOM — Shopify Store Command Center
Load ops-rules before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If AskUserQuestion / Workflow are missing, follow Rule 10 in ops-rules (Hermes: numbered options / two-turn Telegram card; delegate_task).
Runtime Context
Before executing, load available context:
Preferences: Read
${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.jsontimezone— display all timestamps correctlyshopify_store_url,shopify_admin_token— check userConfig keys before env varsecom.shopify.sales_channels— optional last probe cache (publications + agentic/Shop gaps)ecom.shopify.agentic_storefronts— optional operator-declared enablement map (channel → status)
Daemon health: Read
${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json- If
action_neededis not null → surface it before running any store operations
- If
Secrets: Resolve Shopify credentials via userConfig → env vars → Doppler (see Phase 1 below)
Agent Teams support
If CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 is set, use Agent Teams when probing store data in parallel. This enables:
- Agents share context and can coordinate mid-flight
- You can steer priorities in real-time
- Agents report progress as they complete
Team setup (only when flag is enabled):
TeamCreate("ecom-team")
Agent(team_name="ecom-team", name="orders-scanner", prompt="Fetch recent orders, compute revenue for today/7d/30d")
Agent(team_name="ecom-team", name="inventory-scanner", prompt="Fetch all products, flag low stock and out-of-stock items")
Agent(team_name="ecom-team", name="fulfillment-scanner", prompt="Fetch unfulfilled orders and ShipBob shipment status")
Agent(team_name="ecom-team", name="analytics-scanner", prompt="Compute revenue analytics, AOV, and top products for 30d")
If the flag is NOT set, use standard fire-and-forget subagents.
Phase 1 — Resolve credentials
Resolve Shopify credentials in this order:
# 1. Plugin userConfig
SHOPIFY_STORE="${user_config.shopify_store_url}"
SHOPIFY_TOKEN="${user_config.shopify_admin_token}"
SHIPBOB_TOKEN="${user_config.shipbob_access_token}"
# 2. Environment variables (override userConfig if set)
[ -n "$SHOPIFY_STORE_URL" ] && SHOPIFY_STORE="$SHOPIFY_STORE_URL"
[ -n "$SHOPIFY_ACCESS_TOKEN" ] && SHOPIFY_TOKEN="$SHOPIFY_ACCESS_TOKEN"
[ -n "$SHIPBOB_ACCESS_TOKEN" ] && SHIPBOB_TOKEN="$SHIPBOB_ACCESS_TOKEN"
# 3. Doppler fallback
if [ -z "$SHOPIFY_TOKEN" ] && command -v doppler &>/dev/null; then
SHOPIFY_TOKEN=$(doppler secrets get SHOPIFY_ACCESS_TOKEN --plain 2>/dev/null)
fi
if [ -z "$SHOPIFY_STORE" ] && command -v doppler &>/dev/null; then
SHOPIFY_STORE=$(doppler secrets get SHOPIFY_STORE_URL --plain 2>/dev/null)
fi
if [ -z "$SHIPBOB_TOKEN" ] && command -v doppler &>/dev/null; then
SHIPBOB_TOKEN=$(doppler secrets get SHIPBOB_ACCESS_TOKEN --plain 2>/dev/null)
fi
If $SHOPIFY_STORE or $SHOPIFY_TOKEN is still empty after all resolution steps, route to setup flow below.
Set base URLs:
SHOPIFY_BASE="https://${SHOPIFY_STORE}/admin/api/2024-10"
SHOPIFY_GQL="https://${SHOPIFY_STORE}/admin/api/2024-10/graphql.json"
SHOPIFY_AUTH="X-Shopify-Access-Token: ${SHOPIFY_TOKEN}"
Phase 2 — Route by $ARGUMENTS
| Input | Action |
|---|---|
| (empty) | Show store summary |
| orders, order | Orders dashboard |
| inventory, stock, inv | Inventory levels |
| fulfillment, fulfill, shipbob, shipping | Fulfillment status |
| health, check, status | Store health check |
| products, product, catalog | Products manager |
| customers, customer, crm | Customer stats |
| analytics, revenue, stats, metrics | Analytics dashboard |
| channels, publications, sales-channels | Sales channel inventory (publications + gaps) |
| agentic, agentic-storefronts, ai-channels | Agentic storefront health |
| shop, shop-channel, shop-campaigns | Shop channel + Shop Campaigns readiness |
| setup, configure, init, token | Setup flow |
ORDERS
Fetch recent orders and compute revenue:
TODAY=$(date -u +"%Y-%m-%dT00:00:00Z")
WEEK_AGO=$(date -u -v-7d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "7 days ago" +"%Y-%m-%dT00:00:00Z")
MONTH_AGO=$(date -u -v-30d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "30 days ago" +"%Y-%m-%dT00:00:00Z")
# Recent orders (last 50)
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/orders.json?status=any&limit=50&order=created_at+desc" | \
jq '{
total: .orders | length,
today: [.orders[] | select(.created_at >= "'"$TODAY"'")],
orders: [.orders[:10] | .[] | {
id: .order_number,
name: .name,
status: .financial_status,
fulfillment: .fulfillment_status,
total: .total_price,
currency: .currency,
customer: (.customer.first_name + " " + .customer.last_name),
created: .created_at
}]
}'
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► ORDERS — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REVENUE
Today [N orders] $[amount]
7 days [N orders] $[amount]
30 days [N orders] $[amount]
RECENT ORDERS
#[id] [customer] $[total] [status] / [fulfillment] [age]
...
──────────────────────────────────────────────────────
Actions:
a) View order details for #[id]
b) Mark order as fulfilled
c) Export orders CSV
d) Filter by status (unfulfilled/refunded/paid)
──────────────────────────────────────────────────────
Use AskUserQuestion for action selection.
INVENTORY
Fetch all products and variant inventory:
# Get all products with variants
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/products.json?limit=250&fields=id,title,status,variants" | \
jq '[.products[] | {
id: .id,
title: .title,
status: .status,
variants: [.variants[] | {
id: .id,
title: .title,
sku: .sku,
inventory_quantity: .inventory_quantity,
inventory_policy: .inventory_policy
}]
}]'
Flag low stock (inventory_quantity < 10) and out-of-stock (inventory_quantity <= 0).
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► INVENTORY — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OUT OF STOCK
[product] — [variant] — SKU: [sku]
LOW STOCK (< 10 units)
[product] — [variant] — [N] units — SKU: [sku]
ALL PRODUCTS
[product]
[variant] [N] units SKU: [sku]
...
──────────────────────────────────────────────────────
Actions:
a) Update inventory for [product]
b) Export inventory CSV
c) Set reorder alerts
──────────────────────────────────────────────────────
Use AskUserQuestion for action selection.
FULFILLMENT
Fetch unfulfilled orders and ShipBob status (if token available):
# Unfulfilled orders
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/orders.json?fulfillment_status=unfulfilled&status=open&limit=50" | \
jq '[.orders[] | {
id: .order_number,
name: .name,
customer: (.customer.first_name + " " + .customer.last_name),
total: .total_price,
created: .created_at,
items: [.line_items[] | {title: .title, qty: .quantity}]
}]'
# Shipments with tracking (fulfilled)
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/orders.json?fulfillment_status=fulfilled&status=any&limit=20&order=updated_at+desc" | \
jq '[.orders[] | .fulfillments[] | {
order: .order_id,
tracking_number: .tracking_number,
tracking_url: .tracking_url,
shipment_status: .shipment_status,
carrier: .tracking_company,
updated: .updated_at
}]'
If $SHIPBOB_TOKEN is set, also query ShipBob:
# ShipBob pending shipments
curl -s -H "Authorization: Bearer ${SHIPBOB_TOKEN}" \
"https://api.shipbob.com/1.0/shipment?Status=Processing&PageSize=20" | \
jq '[.[] | {
id: .id,
status: .status,
order_id: .reference_id,
tracking: .tracking_number,
created: .created_date
}]'
Render fulfillment dashboard with pending/in-transit/delivered counts.
Use AskUserQuestion for action selection (mark fulfilled, update tracking, etc.).
HEALTH
Run the health check script, then augment with API checks:
${CLAUDE_PLUGIN_ROOT}/bin/ops-ecom-health 2>/dev/null || echo '{"error":"health script unavailable"}'
Also check:
# Active theme
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/themes.json" | \
jq '[.themes[] | select(.role == "main") | {id: .id, name: .name, updated: .updated_at}]'
# Store info
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/shop.json" | \
jq '.shop | {name: .name, domain: .domain, country: .country_name, plan: .plan_display_name, currency: .currency, timezone: .timezone}'
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► HEALTH — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STORE
Name: [name]
Plan: [plan]
Currency: [currency]
Timezone: [tz]
API CONNECTIVITY [OK / FAIL]
ACTIVE THEME [theme name]
PRODUCT COUNT [N] active
ORDERS (24h) [N] orders
ISSUES
[any warnings from health check]
──────────────────────────────────────────────────────
Actions:
a) Check theme assets for errors
b) Run full SEO audit
c) View API rate limit status
──────────────────────────────────────────────────────
Use AskUserQuestion for action selection.
PRODUCTS
List, search, and manage products:
# All products
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/products.json?limit=250&order=updated_at+desc" | \
jq '[.products[] | {
id: .id,
title: .title,
status: .status,
handle: .handle,
price: (.variants[0].price // "N/A"),
inventory: ([.variants[].inventory_quantity] | add // 0),
variants: (.variants | length),
updated: .updated_at
}]'
If $ARGUMENTS contains a search term (e.g., products shoes), filter by title.
For price updates, use:
# Update variant price
curl -s -X PUT -H "$SHOPIFY_AUTH" -H "Content-Type: application/json" \
"${SHOPIFY_BASE}/variants/${VARIANT_ID}.json" \
-d '{"variant":{"id":'${VARIANT_ID}',"price":"'${NEW_PRICE}'"}}'
Use AskUserQuestion before making any product updates. Show before/after prices.
CUSTOMERS
Fetch customer stats:
# Customer count and recent
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/customers.json?limit=50&order=created_at+desc" | \
jq '{
total_shown: (.customers | length),
recent: [.customers[:10] | .[] | {
id: .id,
name: (.first_name + " " + .last_name),
email: .email,
orders: .orders_count,
total_spent: .total_spent,
currency: .currency,
created: .created_at
}]
}'
# Top customers by spend
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/customers.json?limit=10&order=total_spent+desc" | \
jq '[.customers[] | {name: (.first_name + " " + .last_name), orders: .orders_count, spent: .total_spent}]'
Render customer overview with LTV stats and top spenders.
ANALYTICS
Pull revenue and order data for dashboard:
TODAY=$(date -u +"%Y-%m-%dT00:00:00Z")
WEEK_AGO=$(date -u -v-7d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "7 days ago" +"%Y-%m-%dT00:00:00Z")
MONTH_AGO=$(date -u -v-30d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "30 days ago" +"%Y-%m-%dT00:00:00Z")
# Orders for revenue calculation
curl -s -H "$SHOPIFY_AUTH" \
"${SHOPIFY_BASE}/orders.json?status=any&financial_status=paid&created_at_min=${MONTH_AGO}&limit=250" | \
jq '{
month_orders: (.orders | length),
month_revenue: ([.orders[].total_price | tonumber] | add // 0),
week_orders: ([.orders[] | select(.created_at >= "'"$WEEK_AGO"'")] | length),
week_revenue: ([.orders[] | select(.created_at >= "'"$WEEK_AGO"'") | .total_price | tonumber] | add // 0),
today_orders: ([.orders[] | select(.created_at >= "'"$TODAY"'")] | length),
today_revenue: ([.orders[] | select(.created_at >= "'"$TODAY"'") | .total_price | tonumber] | add // 0),
avg_order_value: ([.orders[].total_price | tonumber] | (add // 0) / (length // 1)),
top_products: ([.orders[].line_items[] | {title: .title, qty: .quantity}] | group_by(.title) | map({title: .[0].title, total_qty: map(.qty) | add}) | sort_by(-.total_qty)[:5])
}'
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► ANALYTICS — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REVENUE
Today $[amount] ([N] orders)
7 days $[amount] ([N] orders)
30 days $[amount] ([N] orders)
AVERAGES
AOV (30d) $[amount]
TOP PRODUCTS (30d)
1. [product] [N] sold
2. [product] [N] sold
...
──────────────────────────────────────────────────────
Actions:
a) Export revenue report (CSV)
b) View by product breakdown
c) Compare to previous period
──────────────────────────────────────────────────────
Use AskUserQuestion for action selection.
CHANNELS
List publications / sales channels from Shopify Admin GraphQL. Prefer live API; fall back to ecom.shopify.sales_channels cache if API fails.
# GraphQL publications (use store's configured API version when set; default 2024-10+)
curl -s -X POST -H "$SHOPIFY_AUTH" -H "Content-Type: application/json" \
"$SHOPIFY_GQL" \
-d '{"query":"{ publications(first: 50) { nodes { id name autoPublish app { title handle } } } channels(first: 50) { nodes { name handle app { title handle } } } }"}' | \
jq '{
publications: [.data.publications.nodes[]? | {id, name, autoPublish, app: .app.title}],
channels: [.data.channels.nodes[]? | {name, handle, app: .app.title}]
}'
Also merge operator prefs when present:
jq -r '.ecom.shopify.sales_channels // empty' "$PREFS_PATH" 2>/dev/null
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► CHANNELS — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PUBLICATIONS / CHANNELS (live)
[name] handle=[…] app=[…] autoPublish=[true|false]
...
KNOWN GAPS (prefs / docs — not always in API list)
Agentic Storefronts app [present|missing|unknown]
ChatGPT / OpenAI channel [present|missing|unknown]
Google AI Mode / Gemini [present|missing|unknown]
Shop channel [present|missing|unknown]
Microsoft Copilot [present|missing|unknown]
──────────────────────────────────────────────────────
Actions:
a) Re-probe GraphQL publications
b) Open agentic health (/ops:ecom agentic)
c) Open Shop readiness (/ops:ecom shop)
──────────────────────────────────────────────────────
Guardrails: publication names vary by locale; match case-insensitively. Do not invent channels. Unknown → unknown. Write-back of probe cache only with operator OK.
AGENTIC
Agentic storefront health: which AI commerce surfaces are enabled and whether products are published to them.
# Reuse CHANNELS probe; optional prefs:
jq -r '.ecom.shopify.agentic_storefronts // empty' "$PREFS_PATH" 2>/dev/null
jq -r '.marketing.projects // {} | to_entries[]? | select(.value.agentic_storefronts != null) | {project: .key, agentic: .value.agentic_storefronts}' "$PREFS_PATH" 2>/dev/null
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► AGENTIC — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AI / AGENTIC SURFACES
Microsoft Copilot [on|off|unknown] channel=[name|—]
ChatGPT / OpenAI [on|off|unknown]
Google AI Mode [on|off|unknown]
(other detected) ...
PRODUCT COVERAGE
Sampled: [N] products published_to_agentic: [M] unknown: [K]
DOCS
https://help.shopify.com/en/manual/online-sales-channels/agentic-storefronts
──────────────────────────────────────────────────────
Actions:
a) Re-probe publications
b) List channels (/ops:ecom channels)
c) Shop readiness (/ops:ecom shop)
──────────────────────────────────────────────────────
Guardrails: Read-only. Never install apps or publish products without explicit operator confirmation (Rule 5). If API cannot list agentic apps, say so — do not claim disabled.
SHOP
Shop sales channel + Shop Campaigns readiness (pay-per-sale on Shop and expanded surfaces).
# Detect Shop from publications/channels (name/handle contains shop; avoid false positives)
jq -r '
.ecom.shopify.shop // empty,
(.marketing.projects // {} | to_entries[]? | select(.value.shop_campaigns != null) | {project: .key, shop_campaigns: .value.shop_campaigns})
' "$PREFS_PATH" 2>/dev/null
Render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM ► SHOP — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SHOP CHANNEL
Publication: [present|missing|unknown]
Products: [sample published count or unknown]
SHOP CAMPAIGNS (prefs + admin)
Configured in prefs: [yes|no]
Status: [not_configured|stage_only|active|unknown]
Budget approval: [required|granted|n/a]
DOCS
https://help.shopify.com/en/manual/online-sales-channels/shop/shop-campaigns
https://www.shopify.com/shop-campaigns
──────────────────────────────────────────────────────
NEVER LEAK MONEY: never create or raise Shop Campaigns
budgets without explicit owner approval (Rule 5).
──────────────────────────────────────────────────────
SETUP FLOW
Before asking the user for anything, auto-discover store URLs and tokens. Run ALL of these scans in a single batch:
# 1. Env vars
printenv SHOPIFY_ACCESS_TOKEN SHOPIFY_ADMIN_TOKEN SHOPIFY_STORE_URL SHOPIFY_ADMIN_API_ACCESS_TOKEN 2>/dev/null
# 2. Shell profiles
grep -h 'SHOPIFY\|myshopify' ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'
# 3. Doppler — ALL projects, not just default
for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
doppler secrets --project "$proj" --config prd --json 2>/dev/null | \
jq -r --arg proj "$proj" 'to_entries[] | select(.key | test("SHOPIFY|STORE"; "i")) | "\(.key)=\(.value.computed) (doppler:\($proj)/prd)"'
done
# 4. Dashlane — URLs reveal store identity
dcli password shopify --output json 2>/dev/null | jq -r '.[].url // empty' | grep -oE '[a-z0-9-]+\.myshopify\.com' | sort -u
# 5. Keychain
security find-generic-password -s "shopify-admin-token" -w 2>/dev/null
security find-generic-password -s "shopify-access-token" -w 2>/dev/null
# 6. Chrome history — reveals store URLs from admin sessions
sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History \
"SELECT DISTINCT url FROM urls WHERE url LIKE '%myshopify.com/admin%' OR url LIKE '%admin.shopify.com/store/%' ORDER BY last_visit_time DESC LIMIT 10" 2>/dev/null | \
grep -oE '[a-z0-9-]+\.myshopify\.com|admin\.shopify\.com/store/[a-z0-9-]+' | sort -u
# 7. Project .env files
grep -rhE 'myshopify\.com|SHOPIFY_STORE|SHOPIFY.*TOKEN' ~/Projects/*/.env* 2>/dev/null | grep -v '^#' | head -5
# 8. Existing prefs + userConfig
jq -r '.ecom.shopify // empty' "$PREFS_PATH" 2>/dev/null
Token acquisition — automate before asking. If store URL found but no token:
- Doppler deep scan — check ALL projects/configs (dev, staging, prd)
- Shopify CLI — if
command -v shopifysucceeds:shopify auth login --store <store>.myshopify.com(opens browser OAuth), then generate token - Browser automation — if Kapture/Playwright available, navigate to
admin.shopify.com/store/<slug>/settings/apps/developmentand automate app creation with scopes:read_orders,write_orders,read_products,write_products,read_customers,read_inventory,write_inventory,read_fulfillments,write_fulfillments,read_analytics - Manual fallback — only if all automated approaches fail:
No automated path for <store>.myshopify.com.
1. Go to https://admin.shopify.com/store/<slug>/settings/apps/development
2. Create an app → Configure → grant scopes → Install → copy token
Token starts with "shpat_"
Multi-store: If multiple stores discovered, process each independently. Present all found stores with their token status before asking for input.
Store credentials via: userConfig (preferred) → Doppler → env vars. ShipBob optional — check SHIPBOB_ACCESS_TOKEN in same scan.
Verify connectivity after acquisition:
curl -s -H "X-Shopify-Access-Token: ${PROVIDED_TOKEN}" \
"https://${PROVIDED_STORE}/admin/api/2024-10/shop.json" | \
jq '.shop | {name, domain, plan: .plan_display_name}'
If the connectivity check returns valid shop data, confirm success. If it fails with 401/403, explain the token is invalid and re-prompt.
STORE SUMMARY (empty $ARGUMENTS)
When called with no arguments, show a compact store overview:
Competitor activity (gather before rendering)
# Resolve PLUGIN_ROOT: directory containing the ops-ops-marketplace plugin scripts
_COMP_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}"
_COMP_LIB="$_COMP_PLUGIN_ROOT/scripts/lib/competitor/context.sh"
_COMP_ECOM_SIGNALS="[]"
if [[ -f "$_COMP_LIB" ]]; then
# shellcheck source=/dev/null
. "$_COMP_LIB"
_COMP_ECOM_SIGNALS=$(competitor_vertical_slice ecom --window-days 7 2>/dev/null || echo "[]")
fi
Parse _COMP_ECOM_SIGNALS JSON array. If it is non-empty, append the COMPETITOR ACTIVITY section after FULFILLMENT in the store summary render. If it is [], skip the section entirely.
Render using this shape (mobile mode: plain text, no banners; desktop: with ━━━ rules):
━━━ COMPETITOR ACTIVITY (last 7d) ━━━
APP RELEASES
• [competitor] — v[version] released this week (rating: [X.X], [N]k reviews)
PRODUCT / PRICING CHANGES
• [competitor] — pricing page changed ([low/med/high] severity)
• [competitor] — features page changed ([brief description from snippet])
Mapping rules (apply per event in the JSON array):
source == "appstore"→ APP RELEASES entry. Extract version, rating, and review count fromsnippetif present.source == "page-diff"andkindmatchesfeatures→ PRODUCT / PRICING CHANGES entry, labelled as features change.source == "page-diff"andkind == "pricing"→ PRODUCT / PRICING CHANGES entry, labelled as pricing change with severity fromseverityfield.
Run orders, inventory, and health checks in parallel (separate Bash calls), then render:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OPS ► ECOM — [store] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STORE [name] ([plan])
CURRENCY [currency]
TODAY $[revenue] [N] orders
7 DAYS $[revenue] [N] orders
30 DAYS $[revenue] [N] orders
INVENTORY [N] products | [N] low stock | [N] out of stock
FULFILLMENT [N] unfulfilled orders pending
[COMPETITOR ACTIVITY section here if non-empty — see above]
──────────────────────────────────────────────────────
/ops:ops-ecom orders — order management
/ops:ops-ecom inventory — stock levels
/ops:ops-ecom products — product catalog
/ops:ops-ecom customers — customer stats
/ops:ops-ecom analytics — revenue dashboard
/ops:ops-ecom health — store health check
/ops:ops-ecom channels — sales channel inventory
/ops:ops-ecom agentic — agentic storefront health
/ops:ops-ecom shop — Shop channel + Shop Campaigns readiness
/ops:ops-ecom setup — configure credentials
──────────────────────────────────────────────────────
Windsor.ai (optional live data)
If Windsor.ai is connected (mcp__*Windsor*__* or a windsor_api_key), use it as the
live cross-channel source for the GA4 acquisition funnel (sessions → checkout → purchase) and ad-driven traffic layered over store orders.
Map accounts per project via registry.json → .projects[].windsor. Prefer blended
ROAS (store/analytics revenue ÷ total ad spend) over platform-reported ROAS. See
docs/integrations/windsor-ai.md for the full playbook (REST + MCP modes,
registry mapping, analysis mandate, and caveats).
Data sanity: if Windsor returns only zeros across sources (Meta + Google spend/impressions and
Instagram reach all exactly 0 over 30d while accounts are connected), treat the data as unavailable —
the plan may be expired (check get_current_user → is_paid) — warn the user, and never present
zeros as real metrics. scripts/windsor-data-sanity.sh automates the check.
Windsor is optional. If Windsor is not connected, returns errors, or returns the
all-zero pattern, fall back to the free direct libraries:
scripts/lib/ad-spend-aggregator.sh (paid), scripts/lib/ga4-data-api.sh
(analytics), and scripts/lib/organic-metrics-aggregator.sh (organic + merchant).
See docs/integrations/direct-channel-wiring.md.
Never present zeros from a dead source as real metrics.
Additional resources
CLI detail: references/cli.md.
| 1 | |
| 2 | name ops-ecom |
| 3 | description "OPS on-demand: This skill should be used when the user asks to \"shopify\", \"orders inventory\", or…" |
| 4 | argument-hint '[orders|inventory|fulfillment|health|products|customers|analytics|channels|agentic|shop|setup]' |
| 5 | allowed-tools |
| 6 | - Bash |
| 7 | - Read |
| 8 | - Write |
| 9 | - Grep |
| 10 | - Glob |
| 11 | - Agent |
| 12 | - TeamCreate |
| 13 | - SendMessage |
| 14 | - AskUserQuestion |
| 15 | - WebFetch |
| 16 | - WebSearch |
| 17 | effort medium |
| 18 | maxTurns 40 |
| 19 | context fork |
| 20 | |
| 21 | |
| 22 | # OPS ► ECOM — Shopify Store Command Center |
| 23 | |
| 24 | Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Workflow` are missing, follow Rule 10 in `ops-rules` (Hermes: numbered options / two-turn Telegram card; `delegate_task`). |
| 25 | |
| 26 | ## Runtime Context |
| 27 | |
| 28 | Before executing, load available context: |
| 29 | |
| 30 | **Preferences**: Read `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json` |
| 31 | `timezone` — display all timestamps correctly |
| 32 | `shopify_store_url`, `shopify_admin_token` — check userConfig keys before env vars |
| 33 | `ecom.shopify.sales_channels` — optional last probe cache (publications + agentic/Shop gaps) |
| 34 | `ecom.shopify.agentic_storefronts` — optional operator-declared enablement map (channel → status) |
| 35 | |
| 36 | **Daemon health**: Read `${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json` |
| 37 | If `action_needed` is not null → surface it before running any store operations |
| 38 | |
| 39 | **Secrets**: Resolve Shopify credentials via userConfig → env vars → Doppler (see Phase 1 below) |
| 40 | |
| 41 | ## Agent Teams support |
| 42 | |
| 43 | If `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` is set, use **Agent Teams** when probing store data in parallel. This enables: |
| 44 | |
| 45 | Agents share context and can coordinate mid-flight |
| 46 | You can steer priorities in real-time |
| 47 | Agents report progress as they complete |
| 48 | |
| 49 | **Team setup** (only when flag is enabled): |
| 50 | |
| 51 | |
| 52 | TeamCreate("ecom-team") |
| 53 | Agent(team_name="ecom-team", name="orders-scanner", prompt="Fetch recent orders, compute revenue for today/7d/30d") |
| 54 | Agent(team_name="ecom-team", name="inventory-scanner", prompt="Fetch all products, flag low stock and out-of-stock items") |
| 55 | Agent(team_name="ecom-team", name="fulfillment-scanner", prompt="Fetch unfulfilled orders and ShipBob shipment status") |
| 56 | Agent(team_name="ecom-team", name="analytics-scanner", prompt="Compute revenue analytics, AOV, and top products for 30d") |
| 57 | |
| 58 | |
| 59 | If the flag is NOT set, use standard fire-and-forget subagents. |
| 60 | |
| 61 | ## Phase 1 — Resolve credentials |
| 62 | |
| 63 | Resolve Shopify credentials in this order: |
| 64 | |
| 65 | |
| 66 | # 1. Plugin userConfig |
| 67 | SHOPIFY_STORE="${user_config.shopify_store_url}" |
| 68 | SHOPIFY_TOKEN="${user_config.shopify_admin_token}" |
| 69 | SHIPBOB_TOKEN="${user_config.shipbob_access_token}" |
| 70 | |
| 71 | # 2. Environment variables (override userConfig if set) |
| 72 | [ -n "$SHOPIFY_STORE_URL" ] && SHOPIFY_STORE="$SHOPIFY_STORE_URL" |
| 73 | [ -n "$SHOPIFY_ACCESS_TOKEN" ] && SHOPIFY_TOKEN="$SHOPIFY_ACCESS_TOKEN" |
| 74 | [ -n "$SHIPBOB_ACCESS_TOKEN" ] && SHIPBOB_TOKEN="$SHIPBOB_ACCESS_TOKEN" |
| 75 | |
| 76 | # 3. Doppler fallback |
| 77 | if [ -z "$SHOPIFY_TOKEN" ] && command -v doppler &>/dev/null; then |
| 78 | SHOPIFY_TOKEN=$(doppler secrets get SHOPIFY_ACCESS_TOKEN --plain 2>/dev/null) |
| 79 | fi |
| 80 | if [ -z "$SHOPIFY_STORE" ] && command -v doppler &>/dev/null; then |
| 81 | SHOPIFY_STORE=$(doppler secrets get SHOPIFY_STORE_URL --plain 2>/dev/null) |
| 82 | fi |
| 83 | if [ -z "$SHIPBOB_TOKEN" ] && command -v doppler &>/dev/null; then |
| 84 | SHIPBOB_TOKEN=$(doppler secrets get SHIPBOB_ACCESS_TOKEN --plain 2>/dev/null) |
| 85 | fi |
| 86 | |
| 87 | |
| 88 | If `$SHOPIFY_STORE` or `$SHOPIFY_TOKEN` is still empty after all resolution steps, route to **setup flow** below. |
| 89 | |
| 90 | Set base URLs: |
| 91 | |
| 92 | |
| 93 | SHOPIFY_BASE="https://${SHOPIFY_STORE}/admin/api/2024-10" |
| 94 | SHOPIFY_GQL="https://${SHOPIFY_STORE}/admin/api/2024-10/graphql.json" |
| 95 | SHOPIFY_AUTH="X-Shopify-Access-Token: ${SHOPIFY_TOKEN}" |
| 96 | |
| 97 | |
| 98 | |
| 99 | |
| 100 | ## Phase 2 — Route by $ARGUMENTS |
| 101 | |
| 102 | | Input | Action | |
| 103 | | --------------------------------------- | ------------------- | |
| 104 | | (empty) | Show store summary | |
| 105 | | orders, order | Orders dashboard | |
| 106 | | inventory, stock, inv | Inventory levels | |
| 107 | | fulfillment, fulfill, shipbob, shipping | Fulfillment status | |
| 108 | | health, check, status | Store health check | |
| 109 | | products, product, catalog | Products manager | |
| 110 | | customers, customer, crm | Customer stats | |
| 111 | | analytics, revenue, stats, metrics | Analytics dashboard | |
| 112 | | channels, publications, sales-channels | Sales channel inventory (publications + gaps) | |
| 113 | | agentic, agentic-storefronts, ai-channels | Agentic storefront health | |
| 114 | | shop, shop-channel, shop-campaigns | Shop channel + Shop Campaigns readiness | |
| 115 | | setup, configure, init, token | Setup flow | |
| 116 | |
| 117 | |
| 118 | |
| 119 | ## ORDERS |
| 120 | |
| 121 | Fetch recent orders and compute revenue: |
| 122 | |
| 123 | |
| 124 | TODAY=$(date -u +"%Y-%m-%dT00:00:00Z") |
| 125 | WEEK_AGO=$(date -u -v-7d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "7 days ago" +"%Y-%m-%dT00:00:00Z") |
| 126 | MONTH_AGO=$(date -u -v-30d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "30 days ago" +"%Y-%m-%dT00:00:00Z") |
| 127 | |
| 128 | # Recent orders (last 50) |
| 129 | curl -s -H "$SHOPIFY_AUTH" \ |
| 130 | "${SHOPIFY_BASE}/orders.json?status=any&limit=50&order=created_at+desc" | \ |
| 131 | jq '{ |
| 132 | total: .orders | length, |
| 133 | today: [.orders[] | select(.created_at >= "'"$TODAY"'")], |
| 134 | orders: [.orders[:10] | .[] | { |
| 135 | id: .order_number, |
| 136 | name: .name, |
| 137 | status: .financial_status, |
| 138 | fulfillment: .fulfillment_status, |
| 139 | total: .total_price, |
| 140 | currency: .currency, |
| 141 | customer: (.customer.first_name + " " + .customer.last_name), |
| 142 | created: .created_at |
| 143 | }] |
| 144 | }' |
| 145 | |
| 146 | |
| 147 | Render: |
| 148 | |
| 149 | |
| 150 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 151 | OPS ► ECOM ► ORDERS — [store] — [timestamp] |
| 152 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 153 | |
| 154 | REVENUE |
| 155 | Today [N orders] $[amount] |
| 156 | 7 days [N orders] $[amount] |
| 157 | 30 days [N orders] $[amount] |
| 158 | |
| 159 | RECENT ORDERS |
| 160 | #[id] [customer] $[total] [status] / [fulfillment] [age] |
| 161 | ... |
| 162 | |
| 163 | ────────────────────────────────────────────────────── |
| 164 | Actions: |
| 165 | a) View order details for #[id] |
| 166 | b) Mark order as fulfilled |
| 167 | c) Export orders CSV |
| 168 | d) Filter by status (unfulfilled/refunded/paid) |
| 169 | ────────────────────────────────────────────────────── |
| 170 | |
| 171 | |
| 172 | Use `AskUserQuestion` for action selection. |
| 173 | |
| 174 | |
| 175 | |
| 176 | ## INVENTORY |
| 177 | |
| 178 | Fetch all products and variant inventory: |
| 179 | |
| 180 | |
| 181 | # Get all products with variants |
| 182 | curl -s -H "$SHOPIFY_AUTH" \ |
| 183 | "${SHOPIFY_BASE}/products.json?limit=250&fields=id,title,status,variants" | \ |
| 184 | jq '[.products[] | { |
| 185 | id: .id, |
| 186 | title: .title, |
| 187 | status: .status, |
| 188 | variants: [.variants[] | { |
| 189 | id: .id, |
| 190 | title: .title, |
| 191 | sku: .sku, |
| 192 | inventory_quantity: .inventory_quantity, |
| 193 | inventory_policy: .inventory_policy |
| 194 | }] |
| 195 | }]' |
| 196 | |
| 197 | |
| 198 | Flag low stock (inventory_quantity < 10) and out-of-stock (inventory_quantity <= 0). |
| 199 | |
| 200 | Render: |
| 201 | |
| 202 | |
| 203 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 204 | OPS ► ECOM ► INVENTORY — [store] — [timestamp] |
| 205 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 206 | |
| 207 | OUT OF STOCK |
| 208 | [product] — [variant] — SKU: [sku] |
| 209 | |
| 210 | LOW STOCK (< 10 units) |
| 211 | [product] — [variant] — [N] units — SKU: [sku] |
| 212 | |
| 213 | ALL PRODUCTS |
| 214 | [product] |
| 215 | [variant] [N] units SKU: [sku] |
| 216 | ... |
| 217 | |
| 218 | ────────────────────────────────────────────────────── |
| 219 | Actions: |
| 220 | a) Update inventory for [product] |
| 221 | b) Export inventory CSV |
| 222 | c) Set reorder alerts |
| 223 | ────────────────────────────────────────────────────── |
| 224 | |
| 225 | |
| 226 | Use `AskUserQuestion` for action selection. |
| 227 | |
| 228 | |
| 229 | |
| 230 | ## FULFILLMENT |
| 231 | |
| 232 | Fetch unfulfilled orders and ShipBob status (if token available): |
| 233 | |
| 234 | |
| 235 | # Unfulfilled orders |
| 236 | curl -s -H "$SHOPIFY_AUTH" \ |
| 237 | "${SHOPIFY_BASE}/orders.json?fulfillment_status=unfulfilled&status=open&limit=50" | \ |
| 238 | jq '[.orders[] | { |
| 239 | id: .order_number, |
| 240 | name: .name, |
| 241 | customer: (.customer.first_name + " " + .customer.last_name), |
| 242 | total: .total_price, |
| 243 | created: .created_at, |
| 244 | items: [.line_items[] | {title: .title, qty: .quantity}] |
| 245 | }]' |
| 246 | |
| 247 | # Shipments with tracking (fulfilled) |
| 248 | curl -s -H "$SHOPIFY_AUTH" \ |
| 249 | "${SHOPIFY_BASE}/orders.json?fulfillment_status=fulfilled&status=any&limit=20&order=updated_at+desc" | \ |
| 250 | jq '[.orders[] | .fulfillments[] | { |
| 251 | order: .order_id, |
| 252 | tracking_number: .tracking_number, |
| 253 | tracking_url: .tracking_url, |
| 254 | shipment_status: .shipment_status, |
| 255 | carrier: .tracking_company, |
| 256 | updated: .updated_at |
| 257 | }]' |
| 258 | |
| 259 | |
| 260 | If `$SHIPBOB_TOKEN` is set, also query ShipBob: |
| 261 | |
| 262 | |
| 263 | # ShipBob pending shipments |
| 264 | curl -s -H "Authorization: Bearer ${SHIPBOB_TOKEN}" \ |
| 265 | "https://api.shipbob.com/1.0/shipment?Status=Processing&PageSize=20" | \ |
| 266 | jq '[.[] | { |
| 267 | id: .id, |
| 268 | status: .status, |
| 269 | order_id: .reference_id, |
| 270 | tracking: .tracking_number, |
| 271 | created: .created_date |
| 272 | }]' |
| 273 | |
| 274 | |
| 275 | Render fulfillment dashboard with pending/in-transit/delivered counts. |
| 276 | |
| 277 | Use `AskUserQuestion` for action selection (mark fulfilled, update tracking, etc.). |
| 278 | |
| 279 | |
| 280 | |
| 281 | ## HEALTH |
| 282 | |
| 283 | Run the health check script, then augment with API checks: |
| 284 | |
| 285 | |
| 286 | ${CLAUDE_PLUGIN_ROOT}/bin/ops-ecom-health 2>/dev/null || echo '{"error":"health script unavailable"}' |
| 287 | |
| 288 | |
| 289 | Also check: |
| 290 | |
| 291 | |
| 292 | # Active theme |
| 293 | curl -s -H "$SHOPIFY_AUTH" \ |
| 294 | "${SHOPIFY_BASE}/themes.json" | \ |
| 295 | jq '[.themes[] | select(.role == "main") | {id: .id, name: .name, updated: .updated_at}]' |
| 296 | |
| 297 | # Store info |
| 298 | curl -s -H "$SHOPIFY_AUTH" \ |
| 299 | "${SHOPIFY_BASE}/shop.json" | \ |
| 300 | jq '.shop | {name: .name, domain: .domain, country: .country_name, plan: .plan_display_name, currency: .currency, timezone: .timezone}' |
| 301 | |
| 302 | |
| 303 | Render: |
| 304 | |
| 305 | |
| 306 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 307 | OPS ► ECOM ► HEALTH — [store] — [timestamp] |
| 308 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 309 | |
| 310 | STORE |
| 311 | Name: [name] |
| 312 | Plan: [plan] |
| 313 | Currency: [currency] |
| 314 | Timezone: [tz] |
| 315 | |
| 316 | API CONNECTIVITY [OK / FAIL] |
| 317 | ACTIVE THEME [theme name] |
| 318 | PRODUCT COUNT [N] active |
| 319 | ORDERS (24h) [N] orders |
| 320 | |
| 321 | ISSUES |
| 322 | [any warnings from health check] |
| 323 | |
| 324 | ────────────────────────────────────────────────────── |
| 325 | Actions: |
| 326 | a) Check theme assets for errors |
| 327 | b) Run full SEO audit |
| 328 | c) View API rate limit status |
| 329 | ────────────────────────────────────────────────────── |
| 330 | |
| 331 | |
| 332 | Use `AskUserQuestion` for action selection. |
| 333 | |
| 334 | |
| 335 | |
| 336 | ## PRODUCTS |
| 337 | |
| 338 | List, search, and manage products: |
| 339 | |
| 340 | |
| 341 | # All products |
| 342 | curl -s -H "$SHOPIFY_AUTH" \ |
| 343 | "${SHOPIFY_BASE}/products.json?limit=250&order=updated_at+desc" | \ |
| 344 | jq '[.products[] | { |
| 345 | id: .id, |
| 346 | title: .title, |
| 347 | status: .status, |
| 348 | handle: .handle, |
| 349 | price: (.variants[0].price // "N/A"), |
| 350 | inventory: ([.variants[].inventory_quantity] | add // 0), |
| 351 | variants: (.variants | length), |
| 352 | updated: .updated_at |
| 353 | }]' |
| 354 | |
| 355 | |
| 356 | If `$ARGUMENTS` contains a search term (e.g., `products shoes`), filter by title. |
| 357 | |
| 358 | For price updates, use: |
| 359 | |
| 360 | |
| 361 | # Update variant price |
| 362 | curl -s -X PUT -H "$SHOPIFY_AUTH" -H "Content-Type: application/json" \ |
| 363 | "${SHOPIFY_BASE}/variants/${VARIANT_ID}.json" \ |
| 364 | -d '{"variant":{"id":'${VARIANT_ID}',"price":"'${NEW_PRICE}'"}}' |
| 365 | |
| 366 | |
| 367 | Use `AskUserQuestion` before making any product updates. Show before/after prices. |
| 368 | |
| 369 | |
| 370 | |
| 371 | ## CUSTOMERS |
| 372 | |
| 373 | Fetch customer stats: |
| 374 | |
| 375 | |
| 376 | # Customer count and recent |
| 377 | curl -s -H "$SHOPIFY_AUTH" \ |
| 378 | "${SHOPIFY_BASE}/customers.json?limit=50&order=created_at+desc" | \ |
| 379 | jq '{ |
| 380 | total_shown: (.customers | length), |
| 381 | recent: [.customers[:10] | .[] | { |
| 382 | id: .id, |
| 383 | name: (.first_name + " " + .last_name), |
| 384 | email: .email, |
| 385 | orders: .orders_count, |
| 386 | total_spent: .total_spent, |
| 387 | currency: .currency, |
| 388 | created: .created_at |
| 389 | }] |
| 390 | }' |
| 391 | |
| 392 | # Top customers by spend |
| 393 | curl -s -H "$SHOPIFY_AUTH" \ |
| 394 | "${SHOPIFY_BASE}/customers.json?limit=10&order=total_spent+desc" | \ |
| 395 | jq '[.customers[] | {name: (.first_name + " " + .last_name), orders: .orders_count, spent: .total_spent}]' |
| 396 | |
| 397 | |
| 398 | Render customer overview with LTV stats and top spenders. |
| 399 | |
| 400 | |
| 401 | |
| 402 | ## ANALYTICS |
| 403 | |
| 404 | Pull revenue and order data for dashboard: |
| 405 | |
| 406 | |
| 407 | TODAY=$(date -u +"%Y-%m-%dT00:00:00Z") |
| 408 | WEEK_AGO=$(date -u -v-7d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "7 days ago" +"%Y-%m-%dT00:00:00Z") |
| 409 | MONTH_AGO=$(date -u -v-30d +"%Y-%m-%dT00:00:00Z" 2>/dev/null || date -u -d "30 days ago" +"%Y-%m-%dT00:00:00Z") |
| 410 | |
| 411 | # Orders for revenue calculation |
| 412 | curl -s -H "$SHOPIFY_AUTH" \ |
| 413 | "${SHOPIFY_BASE}/orders.json?status=any&financial_status=paid&created_at_min=${MONTH_AGO}&limit=250" | \ |
| 414 | jq '{ |
| 415 | month_orders: (.orders | length), |
| 416 | month_revenue: ([.orders[].total_price | tonumber] | add // 0), |
| 417 | week_orders: ([.orders[] | select(.created_at >= "'"$WEEK_AGO"'")] | length), |
| 418 | week_revenue: ([.orders[] | select(.created_at >= "'"$WEEK_AGO"'") | .total_price | tonumber] | add // 0), |
| 419 | today_orders: ([.orders[] | select(.created_at >= "'"$TODAY"'")] | length), |
| 420 | today_revenue: ([.orders[] | select(.created_at >= "'"$TODAY"'") | .total_price | tonumber] | add // 0), |
| 421 | avg_order_value: ([.orders[].total_price | tonumber] | (add // 0) / (length // 1)), |
| 422 | top_products: ([.orders[].line_items[] | {title: .title, qty: .quantity}] | group_by(.title) | map({title: .[0].title, total_qty: map(.qty) | add}) | sort_by(-.total_qty)[:5]) |
| 423 | }' |
| 424 | |
| 425 | |
| 426 | Render: |
| 427 | |
| 428 | |
| 429 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 430 | OPS ► ECOM ► ANALYTICS — [store] — [timestamp] |
| 431 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 432 | |
| 433 | REVENUE |
| 434 | Today $[amount] ([N] orders) |
| 435 | 7 days $[amount] ([N] orders) |
| 436 | 30 days $[amount] ([N] orders) |
| 437 | |
| 438 | AVERAGES |
| 439 | AOV (30d) $[amount] |
| 440 | |
| 441 | TOP PRODUCTS (30d) |
| 442 | 1. [product] [N] sold |
| 443 | 2. [product] [N] sold |
| 444 | ... |
| 445 | |
| 446 | ────────────────────────────────────────────────────── |
| 447 | Actions: |
| 448 | a) Export revenue report (CSV) |
| 449 | b) View by product breakdown |
| 450 | c) Compare to previous period |
| 451 | ────────────────────────────────────────────────────── |
| 452 | |
| 453 | |
| 454 | Use `AskUserQuestion` for action selection. |
| 455 | |
| 456 | |
| 457 | |
| 458 | |
| 459 | |
| 460 | |
| 461 | ## CHANNELS |
| 462 | |
| 463 | List publications / sales channels from Shopify Admin GraphQL. Prefer live API; fall back to `ecom.shopify.sales_channels` cache if API fails. |
| 464 | |
| 465 | |
| 466 | # GraphQL publications (use store's configured API version when set; default 2024-10+) |
| 467 | curl -s -X POST -H "$SHOPIFY_AUTH" -H "Content-Type: application/json" \ |
| 468 | "$SHOPIFY_GQL" \ |
| 469 | -d '{"query":"{ publications(first: 50) { nodes { id name autoPublish app { title handle } } } channels(first: 50) { nodes { name handle app { title handle } } } }"}' | \ |
| 470 | jq '{ |
| 471 | publications: [.data.publications.nodes[]? | {id, name, autoPublish, app: .app.title}], |
| 472 | channels: [.data.channels.nodes[]? | {name, handle, app: .app.title}] |
| 473 | }' |
| 474 | |
| 475 | |
| 476 | Also merge operator prefs when present: |
| 477 | |
| 478 | |
| 479 | jq -r '.ecom.shopify.sales_channels // empty' "$PREFS_PATH" 2>/dev/null |
| 480 | |
| 481 | |
| 482 | **Render:** |
| 483 | |
| 484 | |
| 485 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 486 | OPS ► ECOM ► CHANNELS — [store] — [timestamp] |
| 487 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 488 | |
| 489 | PUBLICATIONS / CHANNELS (live) |
| 490 | [name] handle=[…] app=[…] autoPublish=[true|false] |
| 491 | ... |
| 492 | |
| 493 | KNOWN GAPS (prefs / docs — not always in API list) |
| 494 | Agentic Storefronts app [present|missing|unknown] |
| 495 | ChatGPT / OpenAI channel [present|missing|unknown] |
| 496 | Google AI Mode / Gemini [present|missing|unknown] |
| 497 | Shop channel [present|missing|unknown] |
| 498 | Microsoft Copilot [present|missing|unknown] |
| 499 | |
| 500 | ────────────────────────────────────────────────────── |
| 501 | Actions: |
| 502 | a) Re-probe GraphQL publications |
| 503 | b) Open agentic health (/ops:ecom agentic) |
| 504 | c) Open Shop readiness (/ops:ecom shop) |
| 505 | ────────────────────────────────────────────────────── |
| 506 | |
| 507 | |
| 508 | **Guardrails:** publication names vary by locale; match case-insensitively. Do not invent channels. Unknown → `unknown`. Write-back of probe cache only with operator OK. |
| 509 | |
| 510 | |
| 511 | |
| 512 | ## AGENTIC |
| 513 | |
| 514 | Agentic storefront health: which AI commerce surfaces are enabled and whether products are published to them. |
| 515 | |
| 516 | |
| 517 | # Reuse CHANNELS probe; optional prefs: |
| 518 | jq -r '.ecom.shopify.agentic_storefronts // empty' "$PREFS_PATH" 2>/dev/null |
| 519 | jq -r '.marketing.projects // {} | to_entries[]? | select(.value.agentic_storefronts != null) | {project: .key, agentic: .value.agentic_storefronts}' "$PREFS_PATH" 2>/dev/null |
| 520 | |
| 521 | |
| 522 | **Render:** |
| 523 | |
| 524 | |
| 525 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 526 | OPS ► ECOM ► AGENTIC — [store] — [timestamp] |
| 527 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 528 | |
| 529 | AI / AGENTIC SURFACES |
| 530 | Microsoft Copilot [on|off|unknown] channel=[name|—] |
| 531 | ChatGPT / OpenAI [on|off|unknown] |
| 532 | Google AI Mode [on|off|unknown] |
| 533 | (other detected) ... |
| 534 | |
| 535 | PRODUCT COVERAGE |
| 536 | Sampled: [N] products published_to_agentic: [M] unknown: [K] |
| 537 | |
| 538 | DOCS |
| 539 | https://help.shopify.com/en/manual/online-sales-channels/agentic-storefronts |
| 540 | |
| 541 | ────────────────────────────────────────────────────── |
| 542 | Actions: |
| 543 | a) Re-probe publications |
| 544 | b) List channels (/ops:ecom channels) |
| 545 | c) Shop readiness (/ops:ecom shop) |
| 546 | ────────────────────────────────────────────────────── |
| 547 | |
| 548 | |
| 549 | **Guardrails:** Read-only. Never install apps or publish products without explicit operator confirmation (Rule 5). If API cannot list agentic apps, say so — do not claim disabled. |
| 550 | |
| 551 | |
| 552 | |
| 553 | ## SHOP |
| 554 | |
| 555 | Shop sales channel + Shop Campaigns readiness (pay-per-sale on Shop and expanded surfaces). |
| 556 | |
| 557 | |
| 558 | # Detect Shop from publications/channels (name/handle contains shop; avoid false positives) |
| 559 | jq -r ' |
| 560 | .ecom.shopify.shop // empty, |
| 561 | (.marketing.projects // {} | to_entries[]? | select(.value.shop_campaigns != null) | {project: .key, shop_campaigns: .value.shop_campaigns}) |
| 562 | ' "$PREFS_PATH" 2>/dev/null |
| 563 | |
| 564 | |
| 565 | **Render:** |
| 566 | |
| 567 | |
| 568 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 569 | OPS ► ECOM ► SHOP — [store] — [timestamp] |
| 570 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 571 | |
| 572 | SHOP CHANNEL |
| 573 | Publication: [present|missing|unknown] |
| 574 | Products: [sample published count or unknown] |
| 575 | |
| 576 | SHOP CAMPAIGNS (prefs + admin) |
| 577 | Configured in prefs: [yes|no] |
| 578 | Status: [not_configured|stage_only|active|unknown] |
| 579 | Budget approval: [required|granted|n/a] |
| 580 | |
| 581 | DOCS |
| 582 | https://help.shopify.com/en/manual/online-sales-channels/shop/shop-campaigns |
| 583 | https://www.shopify.com/shop-campaigns |
| 584 | |
| 585 | ────────────────────────────────────────────────────── |
| 586 | NEVER LEAK MONEY: never create or raise Shop Campaigns |
| 587 | budgets without explicit owner approval (Rule 5). |
| 588 | ────────────────────────────────────────────────────── |
| 589 | |
| 590 | |
| 591 | |
| 592 | ## SETUP FLOW |
| 593 | |
| 594 | **Before asking the user for anything**, auto-discover store URLs and tokens. Run ALL of these scans in a single batch: |
| 595 | |
| 596 | |
| 597 | # 1. Env vars |
| 598 | printenv SHOPIFY_ACCESS_TOKEN SHOPIFY_ADMIN_TOKEN SHOPIFY_STORE_URL SHOPIFY_ADMIN_API_ACCESS_TOKEN 2>/dev/null |
| 599 | |
| 600 | # 2. Shell profiles |
| 601 | grep -h 'SHOPIFY\|myshopify' ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#' |
| 602 | |
| 603 | # 3. Doppler — ALL projects, not just default |
| 604 | for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do |
| 605 | doppler secrets --project "$proj" --config prd --json 2>/dev/null | \ |
| 606 | jq -r --arg proj "$proj" 'to_entries[] | select(.key | test("SHOPIFY|STORE"; "i")) | "\(.key)=\(.value.computed) (doppler:\($proj)/prd)"' |
| 607 | done |
| 608 | |
| 609 | # 4. Dashlane — URLs reveal store identity |
| 610 | dcli password shopify --output json 2>/dev/null | jq -r '.[].url // empty' | grep -oE '[a-z0-9-]+\.myshopify\.com' | sort -u |
| 611 | |
| 612 | # 5. Keychain |
| 613 | security find-generic-password -s "shopify-admin-token" -w 2>/dev/null |
| 614 | security find-generic-password -s "shopify-access-token" -w 2>/dev/null |
| 615 | |
| 616 | # 6. Chrome history — reveals store URLs from admin sessions |
| 617 | sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History \ |
| 618 | "SELECT DISTINCT url FROM urls WHERE url LIKE '%myshopify.com/admin%' OR url LIKE '%admin.shopify.com/store/%' ORDER BY last_visit_time DESC LIMIT 10" 2>/dev/null | \ |
| 619 | grep -oE '[a-z0-9-]+\.myshopify\.com|admin\.shopify\.com/store/[a-z0-9-]+' | sort -u |
| 620 | |
| 621 | # 7. Project .env files |
| 622 | grep -rhE 'myshopify\.com|SHOPIFY_STORE|SHOPIFY.*TOKEN' ~/Projects/*/.env* 2>/dev/null | grep -v '^#' | head -5 |
| 623 | |
| 624 | # 8. Existing prefs + userConfig |
| 625 | jq -r '.ecom.shopify // empty' "$PREFS_PATH" 2>/dev/null |
| 626 | |
| 627 | |
| 628 | **Token acquisition — automate before asking.** If store URL found but no token: |
| 629 | |
| 630 | **Doppler deep scan** — check ALL projects/configs (dev, staging, prd) |
| 631 | **Shopify CLI** — if `command -v shopify` succeeds: `shopify auth login --store <store>.myshopify.com` (opens browser OAuth), then generate token |
| 632 | **Browser automation** — if Kapture/Playwright available, navigate to `admin.shopify.com/store/<slug>/settings/apps/development` and automate app creation with scopes: `read_orders,write_orders,read_products,write_products,read_customers,read_inventory,write_inventory,read_fulfillments,write_fulfillments,read_analytics` |
| 633 | **Manual fallback** — only if all automated approaches fail: |
| 634 | |
| 635 | |
| 636 | No automated path for <store>.myshopify.com. |
| 637 | 1. Go to https://admin.shopify.com/store/<slug>/settings/apps/development |
| 638 | 2. Create an app → Configure → grant scopes → Install → copy token |
| 639 | Token starts with "shpat_" |
| 640 | |
| 641 | |
| 642 | **Multi-store**: If multiple stores discovered, process each independently. Present all found stores with their token status before asking for input. |
| 643 | |
| 644 | Store credentials via: userConfig (preferred) → Doppler → env vars. ShipBob optional — check `SHIPBOB_ACCESS_TOKEN` in same scan. |
| 645 | |
| 646 | Verify connectivity after acquisition: |
| 647 | |
| 648 | |
| 649 | curl -s -H "X-Shopify-Access-Token: ${PROVIDED_TOKEN}" \ |
| 650 | "https://${PROVIDED_STORE}/admin/api/2024-10/shop.json" | \ |
| 651 | jq '.shop | {name, domain, plan: .plan_display_name}' |
| 652 | |
| 653 | |
| 654 | If the connectivity check returns valid shop data, confirm success. If it fails with 401/403, explain the token is invalid and re-prompt. |
| 655 | |
| 656 | |
| 657 | |
| 658 | ## STORE SUMMARY (empty $ARGUMENTS) |
| 659 | |
| 660 | When called with no arguments, show a compact store overview: |
| 661 | |
| 662 | ### Competitor activity (gather before rendering) |
| 663 | |
| 664 | |
| 665 | # Resolve PLUGIN_ROOT: directory containing the ops-ops-marketplace plugin scripts |
| 666 | _COMP_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}" 2>/dev/null || echo "$0")")")}" |
| 667 | _COMP_LIB="$_COMP_PLUGIN_ROOT/scripts/lib/competitor/context.sh" |
| 668 | _COMP_ECOM_SIGNALS="[]" |
| 669 | if [[ -f "$_COMP_LIB" ]]; then |
| 670 | # shellcheck source=/dev/null |
| 671 | . "$_COMP_LIB" |
| 672 | _COMP_ECOM_SIGNALS=$(competitor_vertical_slice ecom --window-days 7 2>/dev/null || echo "[]") |
| 673 | fi |
| 674 | |
| 675 | |
| 676 | Parse `_COMP_ECOM_SIGNALS` JSON array. If it is non-empty, append the **COMPETITOR ACTIVITY** section after FULFILLMENT in the store summary render. If it is `[]`, skip the section entirely. |
| 677 | |
| 678 | Render using this shape (mobile mode: plain text, no banners; desktop: with `━━━` rules): |
| 679 | |
| 680 | |
| 681 | ━━━ COMPETITOR ACTIVITY (last 7d) ━━━ |
| 682 | APP RELEASES |
| 683 | • [competitor] — v[version] released this week (rating: [X.X], [N]k reviews) |
| 684 | PRODUCT / PRICING CHANGES |
| 685 | • [competitor] — pricing page changed ([low/med/high] severity) |
| 686 | • [competitor] — features page changed ([brief description from snippet]) |
| 687 | |
| 688 | |
| 689 | Mapping rules (apply per event in the JSON array): |
| 690 | |
| 691 | `source == "appstore"` → APP RELEASES entry. Extract version, rating, and review count from `snippet` if present. |
| 692 | `source == "page-diff"` and `kind` matches `features` → PRODUCT / PRICING CHANGES entry, labelled as features change. |
| 693 | `source == "page-diff"` and `kind == "pricing"` → PRODUCT / PRICING CHANGES entry, labelled as pricing change with severity from `severity` field. |
| 694 | |
| 695 | Run orders, inventory, and health checks in parallel (separate Bash calls), then render: |
| 696 | |
| 697 | |
| 698 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 699 | OPS ► ECOM — [store] — [timestamp] |
| 700 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 701 | |
| 702 | STORE [name] ([plan]) |
| 703 | CURRENCY [currency] |
| 704 | |
| 705 | TODAY $[revenue] [N] orders |
| 706 | 7 DAYS $[revenue] [N] orders |
| 707 | 30 DAYS $[revenue] [N] orders |
| 708 | |
| 709 | INVENTORY [N] products | [N] low stock | [N] out of stock |
| 710 | FULFILLMENT [N] unfulfilled orders pending |
| 711 | |
| 712 | [COMPETITOR ACTIVITY section here if non-empty — see above] |
| 713 | |
| 714 | ────────────────────────────────────────────────────── |
| 715 | /ops:ops-ecom orders — order management |
| 716 | /ops:ops-ecom inventory — stock levels |
| 717 | /ops:ops-ecom products — product catalog |
| 718 | /ops:ops-ecom customers — customer stats |
| 719 | /ops:ops-ecom analytics — revenue dashboard |
| 720 | /ops:ops-ecom health — store health check |
| 721 | /ops:ops-ecom channels — sales channel inventory |
| 722 | /ops:ops-ecom agentic — agentic storefront health |
| 723 | /ops:ops-ecom shop — Shop channel + Shop Campaigns readiness |
| 724 | /ops:ops-ecom setup — configure credentials |
| 725 | ────────────────────────────────────────────────────── |
| 726 | |
| 727 | |
| 728 | ## Windsor.ai (optional live data) |
| 729 | |
| 730 | If Windsor.ai is connected (`mcp__*Windsor*__*` or a `windsor_api_key`), use it as the |
| 731 | live cross-channel source for the GA4 acquisition funnel (sessions → checkout → purchase) and ad-driven traffic layered over store orders. |
| 732 | Map accounts per project via `registry.json` → `.projects[].windsor`. Prefer **blended |
| 733 | ROAS** (store/analytics revenue ÷ total ad spend) over platform-reported ROAS. See |
| 734 | [docs/integrations/windsor-ai.md] for the full playbook (REST + MCP modes, |
| 735 | registry mapping, analysis mandate, and caveats). |
| 736 | Data sanity: if Windsor returns only zeros across sources (Meta + Google spend/impressions and |
| 737 | Instagram reach all exactly 0 over 30d while accounts are connected), treat the data as unavailable — |
| 738 | the plan may be expired (check `get_current_user` → `is_paid`) — warn the user, and never present |
| 739 | zeros as real metrics. `scripts/windsor-data-sanity.sh` automates the check. |
| 740 | |
| 741 | Windsor is optional. If Windsor is not connected, returns errors, or returns the |
| 742 | all-zero pattern, fall back to the free direct libraries: |
| 743 | `scripts/lib/ad-spend-aggregator.sh` (paid), `scripts/lib/ga4-data-api.sh` |
| 744 | (analytics), and `scripts/lib/organic-metrics-aggregator.sh` (organic + merchant). |
| 745 | See [docs/integrations/direct-channel-wiring.md]. |
| 746 | Never present zeros from a dead source as real metrics. |
| 747 | |
| 748 | ## Additional resources |
| 749 | |
| 750 | CLI detail: `references/cli.md`. |
| 751 |
Discussion
Alternatives
Browse more free Claude skills or everything in Sales.