@koda-sl/baker-cli 0.165.0 → 0.169.0

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/README.md CHANGED
@@ -296,7 +296,7 @@ Read it before interpreting any null. It does not fire on the `--out` path (rows
296
296
  | Preset | Description | Default date range |
297
297
  |------------------------|------------------------------------------|--------------------|
298
298
  | `campaign-performance` | Campaign metrics overview | LAST_30_DAYS |
299
- | `keyword-analysis` | Keyword performance with match type | LAST_30_DAYS |
299
+ | `keyword-analysis` | Keyword performance per ad group | LAST_30_DAYS |
300
300
  | `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
301
301
  | `negative-keywords` | Negative (blocking) keywords only | ALL_TIME |
302
302
  | `negative-keyword-lists` | Shared negative lists + their terms | ALL_TIME |
@@ -572,7 +572,13 @@ baker ads google draft remove g_temp_ab12 # cascades to dependents
572
572
  baker ads google draft clear
573
573
  ```
574
574
 
575
- Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update — Performance Max), `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--max-cpc` take major units; `--target-roas` a ratio. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
575
+ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--max-cpc` take major units; `--target-roas` a ratio. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
576
+
577
+ **Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
578
+
579
+ **Account-level conversion goals** — `conversions goal --customer-id … --category PURCHASE --origin WEBSITE --biddable` sets Google's `CustomerConversionGoal` for one (category, origin) pair; `--no-biddable` (or `--biddable=false`) excludes the category from bidding while it keeps being measured. Neither flag defaults, and the spaced form `--biddable false` is rejected. A conversion drives automated bidding only when both halves are on — the action is primary AND its category's account-level goal is biddable — so this is where "my conversion is Primary but bidding ignores it" is fixed. One staged op per pair.
580
+
581
+ **Retiring a conversion action** — `status` accepts `ENABLED` and `REMOVED` only. Google denies `HIDDEN` on every conversion-action write, so to stop bidding toward an action while it keeps measuring use `--no-primary`, and to stop it counting at all use `"status": "REMOVED"`. Conversion actions **Google owns** cannot be changed at all — Smart campaign call/directions actions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions. Staging any edit to one, a rename included, is refused up front; there is no manual route either, since the Google Ads interface will not change them.
576
582
 
577
583
  **Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with the simulated/live mode banner and non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
578
584
 
@@ -582,6 +588,8 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
582
588
 
583
589
  **Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
584
590
 
591
+ **Pausing or removing a keyword returns a per-ad-group hint** — `keywords update --status PAUSED|REMOVED` and `keywords remove` come back with hints telling you to base the decision on `keyword_view` rows carrying `ad_group.id` + `ad_group_criterion.criterion_id`, never on campaign totals. A keyword's criterion id is derived from its text + match type, so every copy of the same keyword across ad groups shares one id and any aggregate blends the copy holding the history with the copies that never served. The `keyword-analysis` and `positive-keywords` presets select both ids so one query identifies each copy. A bid or URL edit gets no hint — only a pause/removal can strand history.
592
+
585
593
  ---
586
594
 
587
595
  ### Caching
@@ -775,7 +783,7 @@ baker ads meta draft remove meta_temp_cr # removing a create cascades to depen
775
783
  baker ads meta draft clear
776
784
  ```
777
785
 
778
- Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`ads`. `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
786
+ Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`ads`. `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes — write it attached, `--replace=false` or `--no-replace`, since the spaced form `--replace false` is rejected: a boolean flag written with a space is set to true and the word dropped). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
779
787
 
780
788
  #### Smart defaults (so agents don't enumerate the API)
781
789
 
@@ -969,6 +977,8 @@ baker ads linkedin campaigns url-params 123456 \
969
977
  --param "utm_source=linkedin&utm_medium=paid-social" \
970
978
  --dynamic utm_campaign=CAMPAIGN_NAME --dynamic utm_content=CREATIVE_ID
971
979
  baker ads linkedin campaigns url-params 123456 --clear # remove them all
980
+ # a boolean flag written with a space (`--clear false`) is rejected: citty sets it to TRUE and drops
981
+ # the word. Attach it — `--clear=false` — or leave the flag off.
972
982
 
973
983
  # Fix the classic audit findings
974
984
  baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
@@ -4883,6 +4893,13 @@ This CLI is designed for AI agent consumption. Key patterns:
4883
4893
  - **0.152.0**: `baker ads linkedin campaigns url-params <ad-set-id>` stages an ad set's URL tracking parameters (LinkedIn's `adTrackingParameters`) — `--param key=value` for fixed values (repeatable or `&`-joined), `--dynamic key=PLACEHOLDER` for values LinkedIn fills in per ad (`CAMPAIGN_NAME`, `CREATIVE_ID`, …), `--clear` to remove them. LinkedIn appends these to the landing URL of every ad in the ad set, including ads already running, so this replaces stamping the same UTM onto each ad's `--landing-url` — which missed later ads and double-appended keys the ad set already set. Account-level parameters remain UI-only (LinkedIn exposes no API for them).
4884
4894
  - **0.153.0**: `baker landing critique` accepts several slugs in one call (`baker landing critique a b c`). `data.landings[]` always carries one entry per slug; a single slug additionally keeps the old top-level `data.slug`/`counts`/`dimensions`/`findings` shape, so existing readers are unaffected. An invalid or missing slug anywhere in the batch still fails the whole call up front. The scaffold's Stop-hook critique gate now emits one batched command instead of one line per stale landing.
4885
4895
  - **0.159.0**: `baker ads google ads update` takes the same content flags as `ads create` (`--format` + `--headlines`/`--descriptions`/image refs/…), and `--format app` becomes a flag-buildable format on both, with `--image-assets` wiring an App campaign ad's images (`AppAdInfo.images`) the same way Responsive Display does. Content flags without `--format` now fail instead of being silently dropped. An ad content update replaces **every field it sends** — a list has to include the entries you want to keep, while an optional asset list you omit is left untouched on the ad — and a partial payload (only the images, or Google's read-shaped `{"appAd":{…}}`) is rejected at stage time with the fields it's missing, instead of staging cleanly and blocking publish later. Content updates also record the staged image assets they consume as dependencies, so discarding an asset no longer leaves the ad pointing at nothing.
4896
+ - **0.167.0**: `baker ads google conversions update` stops staging two conversion-action changes Google always rejects. `status: "HIDDEN"` leaves the write contract — Google denies it on every conversion action ("the field's value is on a deny-list for this field"), and offering it made it the obvious pick for retiring an action gently, so a whole cleanup published nothing; the refusal now names the two changes that do work (`--no-primary` to stop bidding toward it while it keeps measuring, `status: "REMOVED"` to stop it counting). `HIDDEN` is unchanged everywhere it is *read* — an action hidden long ago still reads back. And an update against a conversion action **Google owns** — Smart campaign calls/directions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions — is refused at stage time, renames included, since Google rejects every write against one and its own interface will not change them either. Both refusals name what to do instead and tell the agent to carry on with the rest of the job.
4897
+ - **0.166.0**: `baker ads google conversions` can set a conversion action's primary/secondary role (`ConversionAction.primary_for_goal`) — `update <id> --primary` to have automated bidding optimize toward it, `--no-primary` to demote it to reported-only. Demoting an action was previously manual work in the Google Ads UI. `conversions create` now states the role instead of leaving it to Google, defaulting to **secondary** so a new action can't silently join what a Maximize Conversions / Target CPA campaign bids toward. The staged card gains an "Action optimization" row, and re-staging the role an action already has is reported as a no-op.
4898
+ - **0.167.1**: the `--flag false` trap is closed on the Meta and LinkedIn staged writes, as it already was on Google. A boolean flag written with a space parses as the flag set to **TRUE** plus a stray positional nobody consumes, so `campaigns duplicate 6123 --replace false` staged the copy *and* a pause of the live original, and reported "staged" either way — an inverted boolean is a valid op, so nothing downstream could catch it. On LinkedIn (`campaigns url-params 123456 --clear false`) the one-id guard did stop it, but blamed a second id and sent the agent back with `--clear` alone, which is TRUE again and wipes an ad set's tracking parameters. Both now refuse the word up front and name the raised flag with the form to type instead (`--replace=false`, or `--no-replace`); when several flags are raised at once the message lists all of them rather than naming a `--flag` nobody typed. Tag Manager is unaffected: no target-taking command there declares a boolean.
4899
+
4900
+ - **0.168.0**: `baker ads google conversions goal` sets the **account-level** conversion goal (`CustomerConversionGoal`) — Google Ads' "Included in account-level goals" column, keyed by `(category, origin)`. `goal --category PURCHASE --origin WEBSITE --biddable` includes a conversion category in what the account bids toward; `--no-biddable` measures it only. This is the half the surface was missing: a conversion drives automated bidding only when BOTH its action is primary AND its category's account-level goal is biddable, which is why an account could show lead-form conversions as Primary and still bid toward nothing. Promoting an action whose goal is off now stages with a warning saying the promotion alone changes no spend, and names the command for the other half. One staged op per pair — a second op for the same `(category, origin)` is refused rather than silently overwriting the first at publish. Switching off the last goal that still has a primary action behind it is refused like a last demotion, and the "no primary left" guard now discounts actions whose goal is already off (they were never steering bidding), while still treating an unread goal as bidding. A goal staged against an account that does not own conversion tracking is refused up front with the id that does, instead of failing the whole atomic batch at publish. The staged card gains "Included in account-level goals", "Conversion category" and "Where it happens" rows.
4901
+
4902
+ - **0.169.0**: `baker capabilities` — one call, before any platform work, that answers what Baker **cannot** do. Everything else (`baker schema`, the family docs, `--help`) describes what it can, so the only way to learn a capability was absent was to try it, usually after telling the user it would happen. Per surface (`google-ads`, `meta-ads`, `linkedin-ads`, `x-ads`, `tag-manager`, `ga4`, `gsc`) it reports: what is connected and which accounts/properties/containers were picked; whether writes are **live** or **simulated** for this company, and that Tag Manager has neither mode — completing the chat changes the real container; and a `limits` list separating *not built yet* from *the platform forbids it* from *we hold no permission for it*, because each leads to something different to tell the user. Naming one surface (`baker capabilities google-ads`) also returns `ops`: every change with its required and optional fields and the exact values each accepts — so "is there a field to demote a conversion action, and how is it spelled?" is one call instead of four guesses. Almost everything is derived rather than written down: the accounts and permissions come from the connection, the write mode from the company's gate, the writable entities and field contracts from the same schemas the backend parses with, and read-only vs read-write from the OAuth scopes the app actually requests (which is why "GA4 is read-only" is a fact, not a sentence). The handful of hand-written entries each carry a machine-checkable claim about the code behind them, and a coverage test fails when one stops holding. A rejected staged write on Google, Meta or LinkedIn now points at this command instead of leaving the agent to guess another field name.
4886
4903
 
4887
4904
  ## Publishing
4888
4905