OPS ► MARKETING COMMAND CENTER skill
OPS on-demand: This skill should be used when the user asks to "klaviyo", "ads spend", or…
by Lifecycle-Innovations-Limited·MIT license·★ 543 Stars on the repo·GitHub ↗
npx degit Lifecycle-Innovations-Limited/claude-ops/claude-ops/skills/ops-marketing#main ~/.claude/skills/ops-marketingChecked ·commit main
Inside this skill
SKILL.md · the author's own words# OPS ► MARKETING COMMAND CENTER Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Work… ## DNS provisioning `bin/ops-dns-provision` is the canonical DNS surface for any marketing project — it covers every record an end-to-end SaaS launch typically… | Subcommand | What it does… | -------------------------------------------- | ------------------------------------------------------------------------------------------… | `gsc <project> <domain>` | Google Search Console site verification — fetches token via `siteVerification/v1/token`, u… | `meta-aem <project> <domain>` | Meta Aggregated Event Measurement — reads `verification_string` from `/me/owned_domains`,… | `apple-pay <project> <domain>` | Two modes via `apple_pay.mode`: `static-file` (default — surfaces the `.well-known/apple-d… | `spf <project> <domain>` | Builds `v=spf1 include:... <policy>` from `.esp.spf_includes`, upserts merge-safely at ape…
Word-for-word excerpt, not a result. Read the full SKILL.md ↓
Files of OPS ► MARKETING COMMAND CENTER
Show the full text208 lines
OPS ► MARKETING 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).
DNS provisioning
bin/ops-dns-provision is the canonical DNS surface for any marketing project — it covers every record an end-to-end SaaS launch typically needs, all routed through scripts/lib/cloudflare-dns.sh for GET-first idempotency. Re-running is safe; OPS_DRY_RUN=1 prints planned API calls without firing.
| Subcommand | What it does |
|---|---|
gsc <project> <domain> |
Google Search Console site verification — fetches token via siteVerification/v1/token, upserts TXT at apex, calls webResource?verificationMethod=DNS_TXT to verify. |
meta-aem <project> <domain> |
Meta Aggregated Event Measurement — reads verification_string from /me/owned_domains, upserts facebook-domain-verification=<token> TXT at apex. |
apple-pay <project> <domain> |
Two modes via apple_pay.mode: static-file (default — surfaces the .well-known/apple-developer-merchantid-domain-association deploy-hook path) or stripe-dns (Stripe POST /v1/payment_method_domains). |
spf <project> <domain> |
Builds v=spf1 include:... <policy> from .esp.spf_includes, upserts merge-safely at apex (refuses to overwrite a foreign TXT lacking the v=spf1 marker). |
dkim <project> <domain> |
ESP-keyed off .esp.provider. Resend implemented (parses records[] from POST /domains, CNAME upserts). Postmark/SES stubbed. |
dmarc <project> <domain> |
_dmarc.<apex> TXT with v=DMARC1; p=<policy>; rua=<rua>. Defaults: policy=quarantine, rua=mailto:dmarc@<apex>. |
mx <project> <domain> |
Provider template keyed off .inbound.provider: google-workspace (smtp.google.com pri 1) / resend-inbound / ses (region from .inbound.region). |
klaviyo-sending <project> <domain> |
Klaviyo dedicated sending domain — POST /api/dedicated-sending-domains/, CNAME upserts. |
audit <project> [--json] |
Read-only — for each row, queries CF and reports present / absent / conflicting. |
provision-all <project> [--skip <row,row>] |
Idempotent full sweep. |
Auth: CLOUDFLARE_API_TOKEN (Bearer, preferred) or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL (Global key, fallback). Zone lookup: GET /zones?name=<apex> finds the zone ID. Record writes: GET-first by name+type, then PUT existing ID or POST new — never duplicate.
Apex resolver (cf_apex_for) handles co.uk-style 2nd-level TLDs (foo.co.uk → foo.co.uk) and strips proto/path/port.
Preferences schema additions
The new bin reads project-level config from $PREFS_PATH under marketing.projects.<key>:
{
"marketing": {
"projects": {
"myapp": {
"domain": "example.com",
// Outbound email service provider (DKIM + SPF includes)
"esp": {
"provider": "resend", // "resend" | "postmark" | "ses"
"credentials": "doppler:prd:RESEND_API_KEY", // cred-ref: "env:VAR" | "doppler:CFG:KEY"
"spf_includes": ["_spf.resend.com", "_spf.klaviyo.com"], // optional; sensible defaults applied
"spf_policy": "-all", // "-all" (strict) | "~all" (soft-fail)
},
// Inbound mail (MX)
"inbound": {
"provider": "google-workspace", // "google-workspace" | "resend-inbound" | "ses"
"region": "us-east-1", // only used by resend-inbound / ses
},
// DMARC policy
"dmarc": {
"policy": "quarantine", // "none" | "quarantine" | "reject"
"rua": "mailto:[email protected]", // defaults to mailto:dmarc@<apex>
},
// Optional Cloudflare account override (for accounts that own multiple zones)
"dns": {
"cloudflare_account_id": "<your-cf-account-id>",
},
// Apple Pay domain registration
"apple_pay": {
"enabled": true,
"mode": "static-file", // default; "stripe-dns" registers via Stripe
},
// Cred-refs for individual rows
"stripe": {
"secret_key": "env:STRIPE_SECRET_KEY", // legacy DNS provisioner key
"api_key": "env:STRIPE_API_KEY", // P3: used by ops-marketing-autopilot stripe ROAS gate
"account_id": "acct_<id>", // optional, only when calling on behalf of a connected acct
},
"meta": { "access_token": "env:META_ACCESS_TOKEN" },
"klaviyo": {
"private_key": "doppler:prd:KLAVIYO_API_KEY", // legacy DNS row
"api_key": "doppler:prd:KLAVIYO_API_KEY", // P3: used by gather_klaviyo_metrics
"account_id": "<klaviyo-account-id>", // P3: optional
"sending_subdomain": "em.example.com",
},
},
},
},
}
Defaults are applied bash-side via ${var:-default}, so all values are optional except domain (required by audit and provision-all).
P3 — perf-data wiring (autopilot)
bin/ops-marketing-autopilot reads four perf-data sources per pass and persists them to ${OPS_DATA_DIR}/state/autopilot/<project>-{ga4-conversions,gsc-signal,klaviyo,stripe}.json:
| Source | Prefs path | Helper | Purpose |
|---|---|---|---|
| GA4 conversions | marketing.projects.<key>.ga4.{property_id, sa_key_file_ref} |
gather_ga4_conversions |
source/medium/campaign rows for the blended bandit reward |
| GSC search | marketing.projects.<key>.gsc.site_url |
gather_gsc_signal |
rescue + ad-copy-hook candidate buckets |
| Klaviyo | marketing.projects.<key>.klaviyo.{api_key, account_id} |
gather_klaviyo_metrics |
Placed Order revenue + flow inventory |
| Stripe | marketing.projects.<key>.stripe.{api_key, account_id} |
gather_stripe_revenue |
UTM-attributed revenue per source/medium/campaign and per ad_id — ground-truth ROAS denominator + pause-rescue gate |
Env knobs:
| Env | Default | Effect |
|---|---|---|
OPS_BANDIT_SOURCE |
blended |
meta → meta-only reward (legacy), ga4 → GA4 attribution only, blended → (meta + ga4)/2 |
OPS_PAUSE_ROAS_FLOOR |
1.0 |
An ad with stripe_revenue ≥ floor × meta_spend is kept despite Meta CPL/CTR pause criteria |
OPS_KLAVIYO_REVENUE_RATIO_FLAG |
0.5 |
When klaviyo_revenue / paid_spend > flag, surface "email channel underweighted" in the daily report (no auto-shift) |
UTM enforcement: every create_object campaign … call runs utm_validate (from scripts/lib/utm-validate.sh) on the derived (utm_source, utm_medium, utm_campaign) triple before any API mutation. Non-conforming names escalate + stage-only.
Quick examples
# Dry-run every row for a project
OPS_DRY_RUN=1 ops-dns-provision provision-all myapp
# Single row, ad-hoc domain (no prefs needed)
ops-dns-provision dmarc myapp example.com
# JSON audit for CI healthcheck
ops-dns-provision audit myapp --json
# → {"project":"myapp","domain":"example.com","zone":"...","rows":{"gsc":"present","spf":"present","dmarc":"present","mx":"present","meta_aem":"absent","dkim":"unknown"}}
# Skip rows that need provider-specific manual setup first
ops-dns-provision provision-all myapp --skip dkim,klaviyo-sending
Quick start — autonomous mode
Run /ops:marketing <project> to point-and-go:
ops-marketing-provision status --project <project>— what's missing- For each missing channel, run
ops-marketing-provision provision-<channel> --project <project>(interactive only if OAuth/keys missing; otherwise idempotent) - Verify with
ops-marketing-dash --project <project> - If autopilot not yet enabled, enable:
ops-marketing-autopilot --project <project> --first-run-dry
Provision a brand-new project end-to-end:
# One-shot: GA4 + GSC + Instagram + Google Ads — sequential, idempotent
ops-marketing-provision provision-all --project <project>
# Iterate every project in prefs
ops-marketing-provision provision-all --all-projects
# Or individually:
ops-marketing-provision provision-ga4 --project <project> \
--domain <domain> --account-id <YOUR_GA4_ACCOUNT_ID>
ops-marketing-provision provision-gsc --project <project> --site https://<domain>/
ops-marketing-provision provision-instagram --project <project> # auto-resolves via Meta token
ops-marketing-provision provision-google-ads --project <project> # 4-step OAuth flow
# Check results
ops-marketing-provision status --project <project> --json
ops-marketing-dash --project <project>
provision-instagram requires marketing.projects.<key>.meta.access_token (and optional meta.app_secret for appsecret_proof signing — required when the app's "Require App Secret" setting is on, which is the default for all system-user tokens). The verb is fully idempotent: smoke-tests an existing instagram.account_id before making any API calls; pass --force to re-resolve.
provision-google-ads is a 4-step flow (each step is a no-op if the credential already exists):
- Developer token — scans env + Doppler. If missing, writes a pending-state JSON at
${OPS_DATA_DIR}/state/marketing-provision/<project>-google-ads-pending.jsonand exits 1. Apply at https://ads.google.com/aw/apicenter (24–48h approval) then re-run. - OAuth client — scans env + Doppler. If missing, prints Cloud Console URL for creating a Desktop OAuth client.
- Refresh token — launches a localhost HTTP server on
:8080(120s timeout), opens the Google consent URL, captures the auth code, exchanges forrefresh_token, writes to Doppler asGOOGLE_ADS_<PROJECT_UPPER>_REFRESH_TOKEN. - Customer ID — calls
v24/customers:listAccessibleCustomers, auto-detects MCC manager accounts (setslogin_customer_id), writescustomer_idto prefs.
Pass --skip-if-pending to skip when a dev-token application is in-flight (used by provision-all to keep the chain unblocked).
Set OPS_MARKETING_DRY_RUN=1 to print planned API calls without executing.
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 correctlyklaviyo_private_key,meta_ads_token,meta_ad_account_id,ga4_property_id,google_search_console_site— check userConfig keys before env varsgoogle_ads_developer_token,google_ads_client_id,google_ads_client_secret,google_ads_refresh_token,google_ads_customer_id,google_ads_login_customer_id— Google Ads credentials
Daemon health: Read
${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json- If
action_neededis not null → surface it before running any channel queries
- If
Secrets: Resolve API keys via userConfig → env vars → Doppler MCP (
mcp__doppler__*) → Doppler CLI fallback (see Credential Resolution section below)
Additional resources
Channel, CLI, and edge-case detail lives in references/ next to this skill. Read those files before acting on a matching channel or sub-command. Do not skip them.
| 1 | |
| 2 | name ops-marketing |
| 3 | description "OPS on-demand: This skill should be used when the user asks to \"klaviyo\", \"ads spend\", or…" |
| 4 | argument-hint '<project> [email|ads|analytics|seo|social|campaigns|setup|autopilot ...]' |
| 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 ► MARKETING 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 | ## DNS provisioning |
| 27 | |
| 28 | `bin/ops-dns-provision` is the canonical DNS surface for any marketing project — it covers every record an end-to-end SaaS launch typically needs, all routed through `scripts/lib/cloudflare-dns.sh` for GET-first idempotency. Re-running is safe; `OPS_DRY_RUN=1` prints planned API calls without firing. |
| 29 | |
| 30 | | Subcommand | What it does | |
| 31 | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 32 | | `gsc <project> <domain>` | Google Search Console site verification — fetches token via `siteVerification/v1/token`, upserts TXT at apex, calls `webResource?verificationMethod=DNS_TXT` to verify. | |
| 33 | | `meta-aem <project> <domain>` | Meta Aggregated Event Measurement — reads `verification_string` from `/me/owned_domains`, upserts `facebook-domain-verification=<token>` TXT at apex. | |
| 34 | | `apple-pay <project> <domain>` | Two modes via `apple_pay.mode`: `static-file` (default — surfaces the `.well-known/apple-developer-merchantid-domain-association` deploy-hook path) or `stripe-dns` (Stripe `POST /v1/payment_method_domains`). | |
| 35 | | `spf <project> <domain>` | Builds `v=spf1 include:... <policy>` from `.esp.spf_includes`, upserts merge-safely at apex (refuses to overwrite a foreign TXT lacking the `v=spf1` marker). | |
| 36 | | `dkim <project> <domain>` | ESP-keyed off `.esp.provider`. Resend implemented (parses `records[]` from `POST /domains`, CNAME upserts). Postmark/SES stubbed. | |
| 37 | | `dmarc <project> <domain>` | `_dmarc.<apex>` TXT with `v=DMARC1; p=<policy>; rua=<rua>`. Defaults: `policy=quarantine`, `rua=mailto:dmarc@<apex>`. | |
| 38 | | `mx <project> <domain>` | Provider template keyed off `.inbound.provider`: `google-workspace` (smtp.google.com pri 1) / `resend-inbound` / `ses` (region from `.inbound.region`). | |
| 39 | | `klaviyo-sending <project> <domain>` | Klaviyo dedicated sending domain — `POST /api/dedicated-sending-domains/`, CNAME upserts. | |
| 40 | | `audit <project> [--json]` | Read-only — for each row, queries CF and reports `present` / `absent` / `conflicting`. | |
| 41 | | `provision-all <project> [--skip <row,row>]` | Idempotent full sweep. | |
| 42 | |
| 43 | **Auth**: `CLOUDFLARE_API_TOKEN` (Bearer, preferred) or `CLOUDFLARE_API_KEY` + `CLOUDFLARE_EMAIL` (Global key, fallback). Zone lookup: `GET /zones?name=<apex>` finds the zone ID. Record writes: GET-first by `name+type`, then PUT existing ID or POST new — never duplicate. |
| 44 | |
| 45 | **Apex resolver** (`cf_apex_for`) handles co.uk-style 2nd-level TLDs (foo.co.uk → foo.co.uk) and strips proto/path/port. |
| 46 | |
| 47 | ### Preferences schema additions |
| 48 | |
| 49 | The new bin reads project-level config from `$PREFS_PATH` under `marketing.projects.<key>`: |
| 50 | |
| 51 | |
| 52 | { |
| 53 | "marketing": { |
| 54 | "projects": { |
| 55 | "myapp": { |
| 56 | "domain": "example.com", |
| 57 | |
| 58 | // Outbound email service provider (DKIM + SPF includes) |
| 59 | "esp": { |
| 60 | "provider": "resend", // "resend" | "postmark" | "ses" |
| 61 | "credentials": "doppler:prd:RESEND_API_KEY", // cred-ref: "env:VAR" | "doppler:CFG:KEY" |
| 62 | "spf_includes": ["_spf.resend.com", "_spf.klaviyo.com"], // optional; sensible defaults applied |
| 63 | "spf_policy": "-all", // "-all" (strict) | "~all" (soft-fail) |
| 64 | }, |
| 65 | |
| 66 | // Inbound mail (MX) |
| 67 | "inbound": { |
| 68 | "provider": "google-workspace", // "google-workspace" | "resend-inbound" | "ses" |
| 69 | "region": "us-east-1", // only used by resend-inbound / ses |
| 70 | }, |
| 71 | |
| 72 | // DMARC policy |
| 73 | "dmarc": { |
| 74 | "policy": "quarantine", // "none" | "quarantine" | "reject" |
| 75 | "rua": "mailto:[email protected]", // defaults to mailto:dmarc@<apex> |
| 76 | }, |
| 77 | |
| 78 | // Optional Cloudflare account override (for accounts that own multiple zones) |
| 79 | "dns": { |
| 80 | "cloudflare_account_id": "<your-cf-account-id>", |
| 81 | }, |
| 82 | |
| 83 | // Apple Pay domain registration |
| 84 | "apple_pay": { |
| 85 | "enabled": true, |
| 86 | "mode": "static-file", // default; "stripe-dns" registers via Stripe |
| 87 | }, |
| 88 | |
| 89 | // Cred-refs for individual rows |
| 90 | "stripe": { |
| 91 | "secret_key": "env:STRIPE_SECRET_KEY", // legacy DNS provisioner key |
| 92 | "api_key": "env:STRIPE_API_KEY", // P3: used by ops-marketing-autopilot stripe ROAS gate |
| 93 | "account_id": "acct_<id>", // optional, only when calling on behalf of a connected acct |
| 94 | }, |
| 95 | "meta": { "access_token": "env:META_ACCESS_TOKEN" }, |
| 96 | "klaviyo": { |
| 97 | "private_key": "doppler:prd:KLAVIYO_API_KEY", // legacy DNS row |
| 98 | "api_key": "doppler:prd:KLAVIYO_API_KEY", // P3: used by gather_klaviyo_metrics |
| 99 | "account_id": "<klaviyo-account-id>", // P3: optional |
| 100 | "sending_subdomain": "em.example.com", |
| 101 | }, |
| 102 | }, |
| 103 | }, |
| 104 | }, |
| 105 | } |
| 106 | |
| 107 | |
| 108 | Defaults are applied bash-side via `${var:-default}`, so all values are optional except `domain` (required by `audit` and `provision-all`). |
| 109 | |
| 110 | ### P3 — perf-data wiring (autopilot) |
| 111 | |
| 112 | `bin/ops-marketing-autopilot` reads four perf-data sources per pass and persists them to `${OPS_DATA_DIR}/state/autopilot/<project>-{ga4-conversions,gsc-signal,klaviyo,stripe}.json`: |
| 113 | |
| 114 | | Source | Prefs path | Helper | Purpose | |
| 115 | | --------------- | ------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | |
| 116 | | GA4 conversions | `marketing.projects.<key>.ga4.{property_id, sa_key_file_ref}` | `gather_ga4_conversions` | source/medium/campaign rows for the blended bandit reward | |
| 117 | | GSC search | `marketing.projects.<key>.gsc.site_url` | `gather_gsc_signal` | rescue + ad-copy-hook candidate buckets | |
| 118 | | Klaviyo | `marketing.projects.<key>.klaviyo.{api_key, account_id}` | `gather_klaviyo_metrics` | Placed Order revenue + flow inventory | |
| 119 | | Stripe | `marketing.projects.<key>.stripe.{api_key, account_id}` | `gather_stripe_revenue` | UTM-attributed revenue per `source/medium/campaign` and per `ad_id` — ground-truth ROAS denominator + pause-rescue gate | |
| 120 | |
| 121 | Env knobs: |
| 122 | |
| 123 | | Env | Default | Effect | |
| 124 | | -------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | |
| 125 | | `OPS_BANDIT_SOURCE` | `blended` | `meta` → meta-only reward (legacy), `ga4` → GA4 attribution only, `blended` → `(meta + ga4)/2` | |
| 126 | | `OPS_PAUSE_ROAS_FLOOR` | `1.0` | An ad with `stripe_revenue ≥ floor × meta_spend` is kept despite Meta CPL/CTR pause criteria | |
| 127 | | `OPS_KLAVIYO_REVENUE_RATIO_FLAG` | `0.5` | When `klaviyo_revenue / paid_spend > flag`, surface "email channel underweighted" in the daily report (no auto-shift) | |
| 128 | |
| 129 | UTM enforcement: every `create_object campaign …` call runs `utm_validate` (from `scripts/lib/utm-validate.sh`) on the derived `(utm_source, utm_medium, utm_campaign)` triple before any API mutation. Non-conforming names escalate + stage-only. |
| 130 | |
| 131 | ### Quick examples |
| 132 | |
| 133 | |
| 134 | # Dry-run every row for a project |
| 135 | OPS_DRY_RUN=1 ops-dns-provision provision-all myapp |
| 136 | |
| 137 | # Single row, ad-hoc domain (no prefs needed) |
| 138 | ops-dns-provision dmarc myapp example.com |
| 139 | |
| 140 | # JSON audit for CI healthcheck |
| 141 | ops-dns-provision audit myapp --json |
| 142 | # → {"project":"myapp","domain":"example.com","zone":"...","rows":{"gsc":"present","spf":"present","dmarc":"present","mx":"present","meta_aem":"absent","dkim":"unknown"}} |
| 143 | |
| 144 | # Skip rows that need provider-specific manual setup first |
| 145 | ops-dns-provision provision-all myapp --skip dkim,klaviyo-sending |
| 146 | |
| 147 | |
| 148 | ## Quick start — autonomous mode |
| 149 | |
| 150 | Run `/ops:marketing <project>` to point-and-go: |
| 151 | |
| 152 | `ops-marketing-provision status --project <project>` — what's missing |
| 153 | For each missing channel, run `ops-marketing-provision provision-<channel> --project <project>` (interactive only if OAuth/keys missing; otherwise idempotent) |
| 154 | Verify with `ops-marketing-dash --project <project>` |
| 155 | If autopilot not yet enabled, enable: `ops-marketing-autopilot --project <project> --first-run-dry` |
| 156 | |
| 157 | Provision a brand-new project end-to-end: |
| 158 | |
| 159 | |
| 160 | # One-shot: GA4 + GSC + Instagram + Google Ads — sequential, idempotent |
| 161 | ops-marketing-provision provision-all --project <project> |
| 162 | |
| 163 | # Iterate every project in prefs |
| 164 | ops-marketing-provision provision-all --all-projects |
| 165 | |
| 166 | # Or individually: |
| 167 | ops-marketing-provision provision-ga4 --project <project> \ |
| 168 | --domain <domain> --account-id <YOUR_GA4_ACCOUNT_ID> |
| 169 | ops-marketing-provision provision-gsc --project <project> --site https://<domain>/ |
| 170 | ops-marketing-provision provision-instagram --project <project> # auto-resolves via Meta token |
| 171 | ops-marketing-provision provision-google-ads --project <project> # 4-step OAuth flow |
| 172 | |
| 173 | # Check results |
| 174 | ops-marketing-provision status --project <project> --json |
| 175 | ops-marketing-dash --project <project> |
| 176 | |
| 177 | |
| 178 | `provision-instagram` requires `marketing.projects.<key>.meta.access_token` (and optional `meta.app_secret` for `appsecret_proof` signing — required when the app's "Require App Secret" setting is on, which is the default for all system-user tokens). The verb is fully idempotent: smoke-tests an existing `instagram.account_id` before making any API calls; pass `--force` to re-resolve. |
| 179 | |
| 180 | `provision-google-ads` is a 4-step flow (each step is a no-op if the credential already exists): |
| 181 | |
| 182 | **Developer token** — scans env + Doppler. If missing, writes a pending-state JSON at `${OPS_DATA_DIR}/state/marketing-provision/<project>-google-ads-pending.json` and exits 1. Apply at <https://ads.google.com/aw/apicenter> (24–48h approval) then re-run. |
| 183 | **OAuth client** — scans env + Doppler. If missing, prints Cloud Console URL for creating a Desktop OAuth client. |
| 184 | **Refresh token** — launches a localhost HTTP server on `:8080` (120s timeout), opens the Google consent URL, captures the auth code, exchanges for `refresh_token`, writes to Doppler as `GOOGLE_ADS_<PROJECT_UPPER>_REFRESH_TOKEN`. |
| 185 | **Customer ID** — calls `v24/customers:listAccessibleCustomers`, auto-detects MCC manager accounts (sets `login_customer_id`), writes `customer_id` to prefs. |
| 186 | |
| 187 | Pass `--skip-if-pending` to skip when a dev-token application is in-flight (used by `provision-all` to keep the chain unblocked). |
| 188 | |
| 189 | Set `OPS_MARKETING_DRY_RUN=1` to print planned API calls without executing. |
| 190 | |
| 191 | ## Runtime Context |
| 192 | |
| 193 | Before executing, load available context: |
| 194 | |
| 195 | **Preferences**: Read `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json` |
| 196 | `timezone` — display all timestamps correctly |
| 197 | `klaviyo_private_key`, `meta_ads_token`, `meta_ad_account_id`, `ga4_property_id`, `google_search_console_site` — check userConfig keys before env vars |
| 198 | `google_ads_developer_token`, `google_ads_client_id`, `google_ads_client_secret`, `google_ads_refresh_token`, `google_ads_customer_id`, `google_ads_login_customer_id` — Google Ads credentials |
| 199 | |
| 200 | **Daemon health**: Read `${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json` |
| 201 | If `action_needed` is not null → surface it before running any channel queries |
| 202 | |
| 203 | **Secrets**: Resolve API keys via userConfig → env vars → Doppler MCP (`mcp__doppler__*`) → Doppler CLI fallback (see Credential Resolution section below) |
| 204 | |
| 205 | ## Additional resources |
| 206 | |
| 207 | Channel, CLI, and edge-case detail lives in `references/` next to this skill. Read those files before acting on a matching channel or sub-command. Do not skip them. |
| 208 |
Discussion
Browse more free Claude skills.