@koda-sl/baker-cli 0.281.10-dev.7a0f94402 → 0.286.0-dev.93be96120
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 +246 -109
- package/canvas/tiktok-captions-composition/index.html +2 -23
- package/dist/{chunk-2IUN3S3E.js → chunk-2IB3IL4K.js} +2 -2
- package/dist/{chunk-RHEQTHT7.js → chunk-D3HHUBIL.js} +4 -4
- package/dist/{chunk-WFWU3CHS.js → chunk-DZUVUGEP.js} +1 -9
- package/dist/{chunk-WFWU3CHS.js.map → chunk-DZUVUGEP.js.map} +1 -1
- package/dist/{chunk-XAUTT2Q6.js → chunk-EX2OIOVL.js} +7 -8
- package/dist/chunk-EX2OIOVL.js.map +1 -0
- package/dist/{chunk-P2PEABMQ.js → chunk-GU7IWEBC.js} +12 -5
- package/dist/chunk-GU7IWEBC.js.map +1 -0
- package/dist/{chunk-XSGYVQXF.js → chunk-LN5O53BG.js} +39 -171
- package/dist/chunk-LN5O53BG.js.map +1 -0
- package/dist/cli.js +12422 -7356
- package/dist/cli.js.map +1 -1
- package/dist/client-VDCCDEHE.js +17 -0
- package/dist/engine/index.js +3 -3
- package/dist/{env-C7YXL6C6.js → env-FWMZXMQK.js} +2 -6
- package/dist/{output-ALQGBDPQ.js → output-4H2RNBXL.js} +5 -5
- package/dist/{shared-UKJH3PS7.js → shared-UKL36AXT.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-P2PEABMQ.js.map +0 -1
- package/dist/chunk-XAUTT2Q6.js.map +0 -1
- package/dist/chunk-XSGYVQXF.js.map +0 -1
- package/dist/client-KOUPT7HU.js +0 -15
- /package/dist/{chunk-2IUN3S3E.js.map → chunk-2IB3IL4K.js.map} +0 -0
- /package/dist/{chunk-RHEQTHT7.js.map → chunk-D3HHUBIL.js.map} +0 -0
- /package/dist/{client-KOUPT7HU.js.map → client-VDCCDEHE.js.map} +0 -0
- /package/dist/{env-C7YXL6C6.js.map → env-FWMZXMQK.js.map} +0 -0
- /package/dist/{output-ALQGBDPQ.js.map → output-4H2RNBXL.js.map} +0 -0
- /package/dist/{shared-UKJH3PS7.js.map → shared-UKL36AXT.js.map} +0 -0
package/README.md
CHANGED
|
@@ -226,6 +226,11 @@ baker ads google query "SELECT campaign.name, metrics.clicks, metrics.cost_micro
|
|
|
226
226
|
# Use a preset (saves tokens) — returns only actually-serving entities by default
|
|
227
227
|
baker ads google query --preset campaign-performance --customer-id 1234567890
|
|
228
228
|
|
|
229
|
+
# Campaigns Google reports as limited by budget, with each one's bid target next to its
|
|
230
|
+
# actual cost per conversion. Since 17 Aug 2026 a budget-limited campaign delivers toward
|
|
231
|
+
# its target instead of holding its recent actual cost, so a target above actual raises cost.
|
|
232
|
+
baker ads google query --preset budget-limited --customer-id 1234567890
|
|
233
|
+
|
|
229
234
|
# Include paused entities too (default is serving-only)
|
|
230
235
|
baker ads google query --preset ad-copy-performance --customer-id 1234567890 --include-paused
|
|
231
236
|
|
|
@@ -612,6 +617,13 @@ baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgr
|
|
|
612
617
|
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format demandGen \
|
|
613
618
|
--headlines "Discover the app" --descriptions "Try it free today" --business-name "Acme" --final-url https://example.com \
|
|
614
619
|
--image-assets g_temp_<img> --square-image-assets g_temp_<img_square> --logo-image-assets g_temp_<img_logo>
|
|
620
|
+
# Demand Gen VIDEO ad — the one format with long headlines (DEMAND_GEN_VIDEO_RESPONSIVE_AD)
|
|
621
|
+
# A create needs --ad-name, a video ref and a logo ref; the ad group under a Demand Gen campaign takes no --type
|
|
622
|
+
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format demandGenVideo \
|
|
623
|
+
--ad-name "IA — video prospecting" \
|
|
624
|
+
--headlines "Discover the app" --long-headlines "The programme is updated every term" \
|
|
625
|
+
--descriptions "Try it free today" --business-name "Acme" --final-url https://example.com \
|
|
626
|
+
--video-assets g_temp_<video> --logo-image-assets g_temp_<img_logo>
|
|
615
627
|
# App campaign ad — images are fields on the ad itself, same asset-ref flow
|
|
616
628
|
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format app \
|
|
617
629
|
--headlines "Insure your dog" --descriptions "Refunds in 2 hours" --image-assets g_temp_<img1>,g_temp_<img2>
|
|
@@ -653,17 +665,21 @@ baker tag-manager draft show --chat <chat-id> gtm_temp_ab12
|
|
|
653
665
|
baker actions draft --chat <chat-id>
|
|
654
666
|
```
|
|
655
667
|
|
|
656
|
-
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), 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
|
|
668
|
+
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal/upload), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--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 — a path to a JSON file, or the JSON object written inline (flags override its 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**.
|
|
657
669
|
|
|
658
670
|
**A device criterion can be added, but never removed** — `campaign-criteria add` with a device criterion works, but Google then rejects `campaign-criteria remove` on it forever, including on a criterion you added yourself (and Baker refuses the removal before publish). Adjust it instead with `campaign-criteria update customers/<cid>/campaignCriteria/<campaignId>~<criterionId> --bid-modifier <n>`: `1` for no adjustment, `1.25` to bid 25% more, `0.9` to bid 10% less, `0` to stop serving on that device. Locations, languages and ad schedules remove normally.
|
|
659
671
|
|
|
672
|
+
**Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach` rejects it at stage time and names what to do instead. Staging an image therefore also stages a **Task** — "Add N images to <campaign> in Google Ads" — which applies with the rest of the chat, so the one step Google keeps inside its own interface is tracked rather than mentioned. Attach the image to a Performance Max asset group (`asset-groups attach`) and the Task withdraws itself. The one image a Search campaign does take through a link is the advertiser's logo: `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
|
|
673
|
+
|
|
660
674
|
**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.
|
|
661
675
|
|
|
662
676
|
**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.
|
|
663
677
|
|
|
678
|
+
**Uploading conversions Google never received** — `conversions upload --customer-id 1234567890 --conversion-action 987654321 --file missing.csv` files conversions the account has no record of, for a period when the tracking was broken. The file is a CSV, JSON or one-object-per-line export whose header names the columns `orderId`, `conversionDateTime`, `gclid` / `gbraid` / `wbraid`, `email`, `phone`, `value`, `currency`; column names are matched loosely (`Order ID`, `order_id`, `transaction_id` are one column) and any column an upload has no use for is ignored and listed back in the hints. A single conversion can be given with `--order-id --date --gclid --email --value --currency` instead. Each conversion needs an order id, a date **with its timezone** (a bare `2026-08-01T10:00:00` is refused, since it means one instant to the person who typed it and another to everything else), and something to match on — a click id or the customer's email / phone. Reuse each conversion's **original** order id: that is what makes a re-send a correction rather than a second conversion. A value always carries its currency, because Google reads one without in the account's own. Emails and phone numbers are hashed at stage time, so nothing identifying is stored, sent in the clear, or kept afterwards. Staging checks the conversion action exists and is not removed (either refuses), and warns about conversions older than that action's click-through window — Google accepts those and attributes them to nothing. Like every other Google write it applies on publish; more than 200 conversions stage as several uploads that publish independently.
|
|
679
|
+
|
|
664
680
|
**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.
|
|
665
681
|
|
|
666
|
-
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with 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.
|
|
682
|
+
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Once the chat has published it reports the outcome instead — `published · N applied, M failed`, with a `✗` line naming each op that did not land and why. 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.
|
|
667
683
|
|
|
668
684
|
**Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
|
|
669
685
|
|
|
@@ -725,7 +741,7 @@ All errors include a `fix` object with `action`, `correctedCommand` (when applic
|
|
|
725
741
|
|
|
726
742
|
### X (Twitter) Ads (`baker ads x`)
|
|
727
743
|
|
|
728
|
-
Read X Ads campaigns, line items, promoted tweets, creatives, audiences, and analytics. Powered by the X Ads API v12.
|
|
744
|
+
Read X Ads campaigns, line items, promoted tweets, creatives, audiences, and analytics — and stage changes that apply when the chat is published. Powered by the X Ads API v12.
|
|
729
745
|
|
|
730
746
|
**Environment:**
|
|
731
747
|
- `BAKER_X_ADS_ACCOUNT_ID` — default account ID (base36). Used when `--account-id` is not passed. If neither is set and exactly one X Ads account is connected, it's auto-selected.
|
|
@@ -743,12 +759,17 @@ Read X Ads campaigns, line items, promoted tweets, creatives, audiences, and ana
|
|
|
743
759
|
| `media` | Media library (images / GIFs / videos) — `--media-type IMAGE\|GIF\|VIDEO` |
|
|
744
760
|
| `audiences` | Custom audiences (size, targetable status) |
|
|
745
761
|
| `targeting-criteria` | Targeting attached to line items |
|
|
746
|
-
| `targeting-constants` | Lookup locations / interests / events / devices etc. — `--constant <name> --q "Madrid"` |
|
|
762
|
+
| `targeting-constants` | Lookup locations / interests / events / devices etc. — `--constant <name> --q "Madrid"`. `--location-type` is plural (`COUNTRIES`, `REGIONS`, `CITIES`, `METROS`, `POSTAL_CODES`); `tv_shows` needs `--locale` (take one from `tv_markets`) |
|
|
747
763
|
| `active-entities` | Entities with metric activity in a time range |
|
|
748
|
-
| `stats sync` | Synchronous analytics (≤7 days, no segmentation) |
|
|
764
|
+
| `stats sync` | Synchronous analytics (≤7 days, no segmentation). A preset asking for a longer window is shortened to 7 days and says so in `hints` |
|
|
749
765
|
| `stats job` | Async stats end-to-end (creates + polls + downloads + decompresses). Must run in the background (the harness enforces this). Use for ranges >7 days, segmented stats, or when sync limits are hit. |
|
|
750
766
|
| `stats job-create` | Low-level: create async stats job, return ID immediately |
|
|
751
767
|
| `stats job-status` | Low-level: poll job status / get download URL |
|
|
768
|
+
| `campaigns create\|update\|pause\|resume` | **Staged write** — a new or changed campaign |
|
|
769
|
+
| `line-items create\|update\|pause\|resume` | **Staged write** — a new or changed ad group |
|
|
770
|
+
| `promoted-tweets create\|remove` | **Staged write** — promote posts, or stop promoting them |
|
|
771
|
+
| `targeting-criteria create\|remove` | **Staged write** — targeting on an ad group |
|
|
772
|
+
| `draft` | Review the staged Campaign ▸ Ad group ▸ Ad tree; `show`, `amend`, `remove`, `clear` |
|
|
752
773
|
|
|
753
774
|
**Examples:**
|
|
754
775
|
|
|
@@ -761,9 +782,10 @@ baker ads x promoted-tweets --account-id 18ce53xyz
|
|
|
761
782
|
# Sync analytics with a preset (saves tokens)
|
|
762
783
|
baker ads x stats sync --preset campaign-engagement-7d --entity-ids abc,def
|
|
763
784
|
|
|
764
|
-
# Free-form sync stats
|
|
785
|
+
# Free-form sync stats. At DAY granularity X requires both times to be midnight in the ad
|
|
786
|
+
# account's own timezone (`baker ads x accounts` reports it) — below, midnight in a UTC-7 account.
|
|
765
787
|
baker ads x stats sync --account-id 18ce53xyz --entity LINE_ITEM \
|
|
766
|
-
--entity-ids abc,def --start-time 2026-05-
|
|
788
|
+
--entity-ids abc,def --start-time 2026-05-01T07:00:00Z --end-time 2026-05-07T07:00:00Z \
|
|
767
789
|
--metric-groups ENGAGEMENT,BILLING --granularity DAY
|
|
768
790
|
|
|
769
791
|
# Async job, sync from the CLI's perspective (creates → polls → downloads → returns).
|
|
@@ -778,14 +800,25 @@ baker ads x stats job-create --account-id 18ce53xyz --entity CAMPAIGN \
|
|
|
778
800
|
--metric-groups ENGAGEMENT,BILLING --segmentation-type LOCATIONS
|
|
779
801
|
baker ads x stats job-status --account-id 18ce53xyz --job-id <jobId>
|
|
780
802
|
|
|
803
|
+
# Staged writes — nothing reaches X until the chat is published
|
|
804
|
+
baker ads x campaigns create --name "Spring" --funding-instrument-id vtbot --daily-budget 5
|
|
805
|
+
baker ads x line-items create --campaign-id x_temp_abc --name "Spain" --objective WEBSITE_CLICKS
|
|
806
|
+
baker ads x targeting-criteria create --line-item-id x_temp_def --type LOCATION --value ecdce75d48b13b64
|
|
807
|
+
baker ads x promoted-tweets create --line-item-id x_temp_def --tweet-ids 2042198488033661285
|
|
808
|
+
baker ads x draft # review; after publishing, the authoritative record
|
|
809
|
+
baker ads x campaigns pause oilf0,omyq3 # every id in one call
|
|
810
|
+
|
|
781
811
|
# List sync presets
|
|
782
812
|
baker ads x stats sync --list-presets
|
|
783
813
|
|
|
784
|
-
# Targeting lookups
|
|
785
|
-
baker ads x targeting-constants --constant locations --q "Madrid" --country-code ES
|
|
814
|
+
# Targeting lookups (--location-type is plural; tv_shows needs a --locale from tv_markets)
|
|
815
|
+
baker ads x targeting-constants --constant locations --q "Madrid" --country-code ES --location-type CITIES
|
|
786
816
|
baker ads x targeting-constants --constant interests
|
|
817
|
+
baker ads x targeting-constants --constant tv_shows --locale en-US
|
|
787
818
|
```
|
|
788
819
|
|
|
820
|
+
**Writes are staged.** Every `create`/`update`/`pause`/`resume`/`remove` appends an op to the current chat's draft (`BAKER_CHAT_ID`); X is not called. The draft applies when the chat is published and discarding the chat reverses it. Creates default to `PAUSED`. Budgets and bids are given in account currency (`--daily-budget 12.50`). Chain creates with the `x_temp_*` ref a create returns.
|
|
821
|
+
|
|
789
822
|
**Caching:** account list 1h · campaigns/line items/promoted tweets 1h (date-keyed) · cards/media/audiences 6h · stats sync 1h · targeting constants 7 days. Pass `--no-cache` to bypass.
|
|
790
823
|
|
|
791
824
|
**Rate limits:** server-side buckets (`xAds:read`, `xAds:write`, `xAds:analyticsSync`, `xAds:analyticsAsync`, `xAds:audiences`) sit well under X's published quotas; 429s honor `x-account-rate-limit-reset` / `x-rate-limit-reset` headers.
|
|
@@ -1063,6 +1096,8 @@ audit --format md # deliverable-ready markdown table
|
|
|
1063
1096
|
campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
|
|
1064
1097
|
campaigns create|update|pause|resume|archive|duplicate
|
|
1065
1098
|
campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
|
|
1099
|
+
account url-params # UTMs for every ad in the whole account
|
|
1100
|
+
url-params [--ad-sets] # read what is tagged now, and what lands twice
|
|
1066
1101
|
creatives create|update|pause|resume|duplicate
|
|
1067
1102
|
audiences create|upload
|
|
1068
1103
|
conversions create|update
|
|
@@ -1090,6 +1125,10 @@ baker ads linkedin campaigns url-params 123456 \
|
|
|
1090
1125
|
--param "utm_source=linkedin&utm_medium=paid-social" \
|
|
1091
1126
|
--dynamic utm_campaign=CAMPAIGN_NAME --dynamic utm_content=CREATIVE_ID
|
|
1092
1127
|
baker ads linkedin campaigns url-params 123456 --clear # remove them all
|
|
1128
|
+
|
|
1129
|
+
# what never varies across the account goes one level up — LinkedIn appends BOTH,
|
|
1130
|
+
# so a key set at the account AND the ad set lands in the URL twice
|
|
1131
|
+
baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=paid_social"
|
|
1093
1132
|
# a boolean flag written with a space (`--clear false`) is rejected: citty sets it to TRUE and drops
|
|
1094
1133
|
# the word. Attach it — `--clear=false` — or leave the flag off.
|
|
1095
1134
|
|
|
@@ -1217,6 +1256,10 @@ baker analytics tracking # do the ad URLs carry the campaign, a
|
|
|
1217
1256
|
baker analytics tracking --platform meta # the same for one platform, read from their own ad account
|
|
1218
1257
|
baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
|
|
1219
1258
|
baker analytics map --platform google --remove kw # take that answer back
|
|
1259
|
+
baker analytics conversions # what counts as a conversion, and what each one produced
|
|
1260
|
+
baker analytics conversions --candidates # everything these pages do, and which of it is counted
|
|
1261
|
+
baker analytics conversions --event page:request_demo --name "Demo requested"
|
|
1262
|
+
baker analytics conversions --remove page:request_demo
|
|
1220
1263
|
baker analytics delivery --page 2 # the next page of a long list
|
|
1221
1264
|
baker analytics submissions --flow contact # every attempt at a Form, delivered or not
|
|
1222
1265
|
```
|
|
@@ -1229,7 +1272,9 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1229
1272
|
- The `sparkline` on `landings` is dropped unless `--full`, and resampled to 30 points when present. It is bucketed by `--granularity` upstream, so ninety days at `hour` is 2,160 numbers per page.
|
|
1230
1273
|
- Past ~25 rows the response carries a hint naming the row count and pointing at `--output md`.
|
|
1231
1274
|
|
|
1232
|
-
**Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the dashboard's
|
|
1275
|
+
**Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
|
|
1276
|
+
|
|
1277
|
+
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit. One outcome per call — repeating `--event` is refused rather than silently keeping the last one.
|
|
1233
1278
|
|
|
1234
1279
|
**Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
|
|
1235
1280
|
|
|
@@ -1264,6 +1309,48 @@ Data is retained for 400 days; a window reaching further back comes back with a
|
|
|
1264
1309
|
|
|
1265
1310
|
---
|
|
1266
1311
|
|
|
1312
|
+
### A/B tests (`baker experiment`)
|
|
1313
|
+
|
|
1314
|
+
Two versions of one landing page, split 50/50 at the edge, decided by a verdict rather than by two rates somebody reads.
|
|
1315
|
+
|
|
1316
|
+
```bash
|
|
1317
|
+
baker experiment plan --landing oferta --variant oferta--1 # FIRST — can this page settle the question at all?
|
|
1318
|
+
baker experiment plan --landing oferta --variant oferta--1 --goal event:request_demo
|
|
1319
|
+
baker experiment plan --landing oferta --variant oferta--1 --days 21 # what could 21 days settle?
|
|
1320
|
+
baker experiment start --landing oferta --variant oferta--1 --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
|
|
1321
|
+
baker experiment status # the verdict on every test
|
|
1322
|
+
baker experiment status --id a3f91c2b --full # one test, with the posteriors behind it
|
|
1323
|
+
baker experiment finish --id a3f91c2b # send all the traffic to whichever won
|
|
1324
|
+
baker experiment finish --id a3f91c2b --abandon # stop it, keep the page as it is, record no result
|
|
1325
|
+
baker experiment fold # make the page's own content the version that won
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
**A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap, carries `noindex`, and `/oferta--1/` returns **404** at the edge. Both versions answer at `/oferta/`. Assignment happens while the HTML is served, so there is no flicker, no client JavaScript and nothing for an ad blocker to suppress, and the split is a path rewrite and never a redirect. To look at one side deliberately, `/oferta/?_bkr_preview=a` and `?_bkr_preview=b` serve it exactly, with no assignment and no cookie.
|
|
1329
|
+
|
|
1330
|
+
**Finishing takes two beats, and `fold` is the second.** `finish` records which version won, and the edge serves it to everyone within the hour — that is all a decision made with nobody present can do, because it has no workspace and cannot move a file. `baker experiment fold` moves the winning version into the page's own folder, archives the one that lost as a self-contained snapshot under `src/_variants/<page id>/<n>/`, and deletes the variant folder. Until it runs, the page is served by a rewrite while its own source renders the version that lost, and it cannot be tested again. Run `fold` with no arguments after any test ends, then publish; Baker drops the rewrite by itself once the new pages are live.
|
|
1331
|
+
|
|
1332
|
+
**`--days` answers the other question.** You cannot set how long a test runs — the finish line is a visitor count fixed before it starts, and a horizon that moves is not a horizon. But `--days 21` says what a window that long *could* settle on this page's traffic: *"in 21 days this page could only settle a change of +31% or more"*. That turns a refusal into a brief, because the lever is the size of the change, not the calendar.
|
|
1333
|
+
|
|
1334
|
+
**`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*; most landing pages will never get there, and `plan` refuses rather than letting a test run for three weeks and report a conclusion made on noise. `--lift` sets the smallest lift worth detecting (default `0.2`), and it is the most consequential flag here — halving it roughly quadruples the traffic needed.
|
|
1335
|
+
|
|
1336
|
+
**`--goal` decides what "better" means, and it is fixed for the life of the test.**
|
|
1337
|
+
|
|
1338
|
+
| `--goal` | Counts |
|
|
1339
|
+
|---|---|
|
|
1340
|
+
| `leads` (default) | Whatever this company already marks as a conversion |
|
|
1341
|
+
| `leads:<form>` | The same, one Form only |
|
|
1342
|
+
| `event:<name>` | An event the page declares for itself (`data-baker-click="request_demo"`) |
|
|
1343
|
+
| `event:<name>/<key>=<value>` | The same, narrowed by one of its own properties |
|
|
1344
|
+
| `click` / `click:<host>` | Leaving the site, optionally for one destination |
|
|
1345
|
+
|
|
1346
|
+
`baker analytics events` lists what a page already reports. A goal that has never fired on the page is **refused**, not run — it would return zero on both sides forever, and that reads as a tie rather than as "not measured".
|
|
1347
|
+
|
|
1348
|
+
**Read `verdict` and nothing else** to decide: `keep_running` | `winner` | `no_difference` | `invalid` | `stopped_early_harmful`. `summary` is one sentence fit to show a client. The numbers under `--full` are for printing — a test that says `keep_running` has not finished however good they look, and only `finish` on a concluded test can record a result.
|
|
1349
|
+
|
|
1350
|
+
**`--abandon` overrides the verdict.** It stops the test, keeps the original page and records no result — the only way to end one that has not concluded, and it applies even when the test *has*. That is deliberate: the only outcome it can disagree with is a variant win, so deferring to the verdict would mean a flag asking to keep the original page handed all the traffic to the alternative instead. The response reports `discardedVerdict` when a real finding was thrown away.
|
|
1351
|
+
|
|
1352
|
+
Nothing goes live until the session is published: starting stages onto the session, and the person publishing is the review of the variant page.
|
|
1353
|
+
|
|
1267
1354
|
### Google Analytics 4 (`baker ga4`)
|
|
1268
1355
|
|
|
1269
1356
|
GA4 commands for multi-channel audits **and** for configuring what the property measures. Playbook-aligned report presets, property health audits, free-form Data API queries, and staged Admin API writes.
|
|
@@ -1759,6 +1846,54 @@ baker research intent "buy running shoes,best running shoes 2026,how to tie shoe
|
|
|
1759
1846
|
|
|
1760
1847
|
---
|
|
1761
1848
|
|
|
1849
|
+
### `baker research keyword-metrics "kw1,kw2,kw3"`
|
|
1850
|
+
|
|
1851
|
+
Search volume, CPC, top-of-page bids and competition for a list of keywords you name — no domain involved. This is the command for sizing a market you already have keywords for; `keywords-for-site` and `keyword-gap` return volume only for the terms a domain happens to rank for.
|
|
1852
|
+
|
|
1853
|
+
```bash
|
|
1854
|
+
baker research keyword-metrics "chatbot whatsapp,crm whatsapp" --location mx --language es
|
|
1855
|
+
baker research keyword-metrics "chatbot whatsapp" --location br --language pt --clickstream
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1858
|
+
**Response:**
|
|
1859
|
+
|
|
1860
|
+
```json
|
|
1861
|
+
{
|
|
1862
|
+
"ok": true,
|
|
1863
|
+
"data": [
|
|
1864
|
+
{
|
|
1865
|
+
"source": "keyword_planner_estimate_via_dataforseo",
|
|
1866
|
+
"keyword": "chatbot whatsapp",
|
|
1867
|
+
"planner_search_volume": 1000,
|
|
1868
|
+
"planner_cpc": 6.27,
|
|
1869
|
+
"planner_low_top_of_page_bid": 1.57,
|
|
1870
|
+
"planner_high_top_of_page_bid": 5.32,
|
|
1871
|
+
"planner_competition": "MEDIUM",
|
|
1872
|
+
"planner_competition_index": 66
|
|
1873
|
+
}
|
|
1874
|
+
]
|
|
1875
|
+
}
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
Every keyword you ask for comes back, in the order asked. A row whose metrics are all `null` means DataForSEO holds no data on that term — not that nobody searches it.
|
|
1879
|
+
|
|
1880
|
+
**The volume is Google's own number.** DataForSEO resells the Keyword Planner figure, so `planner_search_volume` is the same value `baker ads google keywords metrics` returns; the two agreeing is not corroboration. `--clickstream` adds `clickstream_search_volume`, DataForSEO's own panel-derived estimate, which is independent — and costs twice as much.
|
|
1881
|
+
|
|
1882
|
+
**Flags:**
|
|
1883
|
+
|
|
1884
|
+
| Flag | Description |
|
|
1885
|
+
|----------------|--------------------------------------|
|
|
1886
|
+
| `--location` | Country code (us, mx, br...) or DataForSEO location code. **Defaults to us** |
|
|
1887
|
+
| `--language` | Language code or name. **Defaults to en** |
|
|
1888
|
+
| `--clickstream`| Add DataForSEO's own volume estimate (costs 2x) |
|
|
1889
|
+
| `--full` | Add keyword difficulty, search intent and the 12-month trend |
|
|
1890
|
+
| `--no-cache` | Skip cache (7d TTL) |
|
|
1891
|
+
| `--output` | Format: json\|csv\|md\|jsonl |
|
|
1892
|
+
|
|
1893
|
+
Max 700 keywords per call, 80 characters each.
|
|
1894
|
+
|
|
1895
|
+
---
|
|
1896
|
+
|
|
1762
1897
|
### `baker research keywords-for-site "domain.com"`
|
|
1763
1898
|
|
|
1764
1899
|
Get keywords a competitor targets. Use `--type paid` to see only paid keywords, `--type organic` for organic only.
|
|
@@ -1977,7 +2112,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
|
|
|
1977
2112
|
|---|---|---|
|
|
1978
2113
|
| `baker images library <q>` | Hybrid library search (replaces `search`) | n/a |
|
|
1979
2114
|
| `baker images find <q>` | Fanout: library + opted-in providers | off |
|
|
1980
|
-
| `baker images stock <q> [--type photo\|vector
|
|
2115
|
+
| `baker images stock <q> [--type photo\|illustration\|vector]` | Free stock across two libraries — Pexels photographs, Pixabay illustrations and vectors | off |
|
|
1981
2116
|
| `baker images google <q>` | Google Images via the official Custom Search JSON API | off |
|
|
1982
2117
|
| `baker images pinterest <q>` | Pinterest reference imagery via ScrapeCreators (photo-real mood boards) | off |
|
|
1983
2118
|
| `baker images logo <domain>` | Brand logo via Brandfetch CDN | **on** (top 1) |
|
|
@@ -2002,7 +2137,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
|
|
|
2002
2137
|
|
|
2003
2138
|
**Auto-ingest** runs the full `processImage` pipeline (Gemini describe + Voyage multimodal embed + OpenRouter text embed) on every hit. Override with `--auto-ingest N` (turn on) or `--no-auto-ingest` (turn off where default is on). When auto-ingest succeeds, the matching returned hit uses the Baker-owned URL and keeps the original provider URL as `sourceUrl`. After auto-ingest the next `baker images library` query for the same concept hits the local row.
|
|
2004
2139
|
|
|
2005
|
-
**Provider context biases the description.** When a hit carries human-readable text (Giphy `alt_text` / `title`,
|
|
2140
|
+
**Provider context biases the description.** When a hit carries human-readable text (Giphy `alt_text` / `title`, Pexels `alt`, Pixabay `tags`, Brandfetch type+theme, etc.) it's surfaced on the hit as `descriptionContext` and forwarded to Gemini during auto-ingest as a hint — Gemini still trusts the pixels (e.g. it'll correct a wrong botanical name) but uses the hint for cultural/brand/scene awareness it can't infer from bytes alone.
|
|
2006
2141
|
|
|
2007
2142
|
**`--context` is available on every command that adds an image to the library** — `ingest`, `use`, `upload`, `find`, `extract`, `screenshot`, `logo`, `stock`, `google`, `icon`, `gif`, `sticker`. On auto-ingest paths it **overrides** the provider-derived `descriptionContext` for every hit that gets ingested. Especially valuable for brand-vs-competitor disambiguation — Gemini cannot tell from pixels alone whether an icon belongs to "Stripe (integration partner)" vs "Stripe (competitor)". Concrete cases:
|
|
2008
2143
|
|
|
@@ -2017,6 +2152,8 @@ baker images screenshot https://competitor.com/pricing --context "Competitor pri
|
|
|
2017
2152
|
|
|
2018
2153
|
**`baker images search` is a deprecated alias** for `library` (stderr deprecation log, removed in next minor).
|
|
2019
2154
|
|
|
2155
|
+
**Migrating from Magnific:** `baker images stock` used to run on Magnific, Freepik's developer API. That provider is gone and the command now searches Pexels and Pixabay together. Consequences: it is free (no `image-magnific-search` credit line any more); `--type` survives as `photo|illustration|vector` (PSDs and mockups are the one thing genuinely lost); `--license`, `--ai`, `--people` and `--order` now fail with a fix naming the replacement rather than being silently dropped; and `--orientation` loses `panoramic` while `--size` and `--locale` are new. Library rows already ingested from Magnific keep `source: "magnific"`, stay searchable, and remain a valid `--source` filter; nothing writes that value any more.
|
|
2156
|
+
|
|
2020
2157
|
**Migrating from Tenor:** the old `baker images meme` command and Tenor provider have been removed. Use `baker images gif` (Giphy) instead. Existing library rows ingested via Tenor remain stored with `source: "tenor"` but are no longer returned by `baker images find` and `--source tenor` is no longer a valid filter on `library`. Re-ingest via `baker images gif --auto-ingest N` if you need fresh equivalents.
|
|
2021
2158
|
|
|
2022
2159
|
### `baker images library <query>`
|
|
@@ -2026,7 +2163,7 @@ Search the company library (hybrid BM25 + vector + Cohere rerank). Use this FIRS
|
|
|
2026
2163
|
```bash
|
|
2027
2164
|
baker images library "hero banner"
|
|
2028
2165
|
baker images library "logo" --aspect-ratio 1:1 --tags logo
|
|
2029
|
-
baker images library "office" --source
|
|
2166
|
+
baker images library "office" --source pexels
|
|
2030
2167
|
baker images library "pricing" --external-url-host competitor.com
|
|
2031
2168
|
```
|
|
2032
2169
|
|
|
@@ -2038,7 +2175,7 @@ baker images library "pricing" --external-url-host competitor.com
|
|
|
2038
2175
|
| `--min-score` | Minimum relevance score, 0-1 |
|
|
2039
2176
|
| `--aspect-ratio` | Filter by aspect ratio |
|
|
2040
2177
|
| `--tags` | Comma-separated tag names |
|
|
2041
|
-
| `--source` | Filter by source (uploaded,
|
|
2178
|
+
| `--source` | Filter by source (uploaded, pexels, pixabay, brandfetch, google_images, firecrawl, screenshotone, iconify, giphy, …). `magnific` still filters the rows the retired stock provider left behind |
|
|
2042
2179
|
| `--external-url-host` | Filter by host substring of `externalUrl` (e.g. competitor.com) |
|
|
2043
2180
|
| `--output` | Output format: `json` \| `files` \| `md` |
|
|
2044
2181
|
| `--fields` | Comma-separated field names to include |
|
|
@@ -2051,12 +2188,12 @@ baker images library "pricing" --external-url-host competitor.com
|
|
|
2051
2188
|
Fanout image search: library first, then opted-in providers in parallel.
|
|
2052
2189
|
|
|
2053
2190
|
```bash
|
|
2054
|
-
baker images find "office" --sources library,
|
|
2055
|
-
baker images find "office" --sources library,
|
|
2191
|
+
baker images find "office" --sources library,pexels,pixabay --limit 20
|
|
2192
|
+
baker images find "office" --sources library,pexels --fallback --threshold 0.4
|
|
2056
2193
|
baker images find "celebration" --sources library,giphy --auto-ingest 3
|
|
2057
2194
|
```
|
|
2058
2195
|
|
|
2059
|
-
Providers: `library`, `
|
|
2196
|
+
Providers: `library`, `pexels`, `pixabay`, `google`, `iconify`, `giphy`, `pinterest`. Brandfetch is not part of the fanout — it takes a domain, not a query, so it lives at `baker images logo <domain>`. (`pinterest` is photo-real/candid imagery — best for sourcing fresh people and sets, e.g. a recast creator or a pinned location in a video reproduction.) Response shape: `{ groups: { library, external }, ingested, meta: { counts, errors } }`. When `--auto-ingest` is used, `ingested[]` preserves successful external ingest order and matching external hits are enriched with the Baker-owned URL plus source provenance. Partial failures (one provider throws) return the successful providers plus a `meta.errors` array; the whole call never fails on a single provider error.
|
|
2060
2197
|
|
|
2061
2198
|
**Flags:**
|
|
2062
2199
|
|
|
@@ -2071,17 +2208,28 @@ Providers: `library`, `magnific`, `google`, `iconify`, `giphy`, `pinterest`. Bra
|
|
|
2071
2208
|
|
|
2072
2209
|
### `baker images stock <query>`
|
|
2073
2210
|
|
|
2074
|
-
|
|
2211
|
+
`stock` is **one verb over the free-stock class**, not one provider. It searches **Pexels** (`api.pexels.com`, raw key in `Authorization`, `PEXELS_API_KEY`) for photographs and **Pixabay** (`pixabay.com/api`, `key` query param, `PIXABAY_API_KEY`) for photographs plus illustrations and vector artwork, then interleaves and dedupes the two. Both APIs are free — the command costs nothing and reports no credit consumption.
|
|
2212
|
+
|
|
2213
|
+
They are one verb because they carry the same licence shape: everything either holds is free for commercial use, including a client's landing page and paid social, with no attribution required. They differ only in what they hold, and an agent should not have to know which library a picture lives in.
|
|
2075
2214
|
|
|
2076
2215
|
```bash
|
|
2077
2216
|
baker images stock "minimalist office"
|
|
2078
|
-
baker images stock "
|
|
2079
|
-
baker images stock "
|
|
2080
|
-
baker images stock "
|
|
2081
|
-
baker images stock "
|
|
2217
|
+
baker images stock "hero photo of a kitchen" --orientation landscape --size large
|
|
2218
|
+
baker images stock "flat office workers" --type illustration
|
|
2219
|
+
baker images stock "leaf outline mark" --type vector
|
|
2220
|
+
baker images stock "brand pattern" --color "#0a0a0a" --auto-ingest 2
|
|
2221
|
+
baker images stock "oficina luminosa" --locale es-ES
|
|
2082
2222
|
```
|
|
2083
2223
|
|
|
2084
|
-
|
|
2224
|
+
**A filter only one library understands narrows the search to that library** rather than being dropped — `convex/imageProviders/stock/plan.ts` resolves this once. `--type illustration|vector` and a *named* `--color` are Pixabay-only; `--orientation square` and a *hex* `--color` are Pexels-only. Querying a library without a filter it cannot honour would return an unfiltered result against a filtered request, which reads as a right answer.
|
|
2225
|
+
|
|
2226
|
+
**A partial result says so.** If one library fails the other still answers, and the response carries `errors[]` plus a `hints` entry naming the library that dropped out — so a thin result reads as unproven rather than as "stock does not have this".
|
|
2227
|
+
|
|
2228
|
+
Credits ride on each hit's `providerMeta`: Pexels `photographer` / `photographerUrl`, Pixabay `contributor`. Neither licence requires attribution, but both ask that the source be shown where results are displayed, and `externalUrl` is that link.
|
|
2229
|
+
|
|
2230
|
+
**Pixabay's tier decides what you actually get.** The full-resolution raster (`imageURL`) and the source SVG (`vectorURL`) need "full API access", requested free from Pixabay. Without it every Pixabay hit is the 1280px `largeImageURL`, so `--type vector` returns a raster of vector artwork rather than an SVG, and `--size` returns Pexels alone — the floor is applied to the file you receive, not to the original the library holds, because a 1.1MP download satisfying a 24MP request is how an agent ends up putting a soft image in a full-bleed hero. `width`/`height` on a hit describe the delivered file for the same reason. Granting full access fixes all three with no code change.
|
|
2231
|
+
|
|
2232
|
+
Pixabay's terms also require results to be cached 24h — the provider's `DAY` action cache is what satisfies that, so don't shorten it.
|
|
2085
2233
|
|
|
2086
2234
|
An empty result comes back with `hints` naming the filters that narrowed the search plus the fallbacks (`baker images find --sources …`, `baker studio generate`); a provider failure returns an `error.fix` pointing at the same exits. `baker images google` does the same, minus the cross-provider fallback (it is the last-resort provider).
|
|
2087
2235
|
|
|
@@ -2089,14 +2237,12 @@ An empty result comes back with `hints` naming the filters that narrowed the sea
|
|
|
2089
2237
|
|
|
2090
2238
|
| Flag | Description |
|
|
2091
2239
|
|------------------|--------------------------------------------------------------------------------------------|
|
|
2092
|
-
| `--type` |
|
|
2093
|
-
| `--orientation` | `landscape \| portrait \| square
|
|
2094
|
-
| `--
|
|
2095
|
-
| `--color` |
|
|
2096
|
-
| `--
|
|
2097
|
-
| `--
|
|
2098
|
-
| `--order` | `relevance` (default) or `recent` |
|
|
2099
|
-
| `--limit` | Max results (1–50, default 10) |
|
|
2240
|
+
| `--type` | `photo` (default, both libraries) \| `illustration` \| `vector` (Pixabay only) |
|
|
2241
|
+
| `--orientation` | `landscape \| portrait \| square` (`square` is Pexels only) |
|
|
2242
|
+
| `--size` | Minimum resolution: `large` (24MP) \| `medium` (12MP) \| `small` (4MP) |
|
|
2243
|
+
| `--color` | Colour name (`red`, `turquoise`, `black`, …; both libraries) or hex (`#0a0a0a`; Pexels only) |
|
|
2244
|
+
| `--locale` | Locale of the search terms, e.g. `es-ES`, `de-DE` — Pexels matches translated tags |
|
|
2245
|
+
| `--limit` | Max results (1–80, default 10) |
|
|
2100
2246
|
| `--page` | Page number for pagination |
|
|
2101
2247
|
| `--auto-ingest` | Ingest top N hits (0–20, default 0) and return Baker-owned URLs on ingested hits |
|
|
2102
2248
|
| `--context` | Free-text hint passed to Gemini describe to bias the generated description and tags (overrides provider-derived context on auto-ingest paths) |
|
|
@@ -2326,11 +2472,11 @@ If the target page is unreachable or returns a non-2xx status (e.g. a 404 path o
|
|
|
2326
2472
|
Download a remote URL and store it in the library. Byte-exact deduped via sha256 + externalId, then queued for describe + embed.
|
|
2327
2473
|
|
|
2328
2474
|
```bash
|
|
2329
|
-
baker images ingest https://
|
|
2475
|
+
baker images ingest https://images.pexels.com/photos/13219418/pexels-photo-13219418.jpeg --source pexels --external-id 13219418
|
|
2330
2476
|
baker images ingest https://acme.com/hero.png --source firecrawl --external-url https://acme.com/pricing --context "competitor pricing hero"
|
|
2331
2477
|
```
|
|
2332
2478
|
|
|
2333
|
-
`--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `
|
|
2479
|
+
`--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `pexels`, `pixabay`, `magnific` (retired provider, kept for existing rows), `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
|
|
2334
2480
|
|
|
2335
2481
|
Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing library row is returned — no new bytes are stored. Max ingest size 25MB.
|
|
2336
2482
|
|
|
@@ -2338,7 +2484,7 @@ Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing l
|
|
|
2338
2484
|
|
|
2339
2485
|
| Flag | Description |
|
|
2340
2486
|
|------------------|--------------------------------------------------------------------------------------------|
|
|
2341
|
-
| `--source` | **Required.** Source enum (`uploaded`, `
|
|
2487
|
+
| `--source` | **Required.** Source enum (`uploaded`, `pexels`, `pixabay`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `instagram`, …) |
|
|
2342
2488
|
| `--external-id` | Provider asset id — enables per-source dedup before bytes are fetched |
|
|
2343
2489
|
| `--external-url` | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
|
|
2344
2490
|
| `--context` | Free-text hint passed to Gemini describe to bias the generated description and tags |
|
|
@@ -2402,7 +2548,7 @@ Local-mode supported extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg
|
|
|
2402
2548
|
|
|
2403
2549
|
| Flag | Mode | Description |
|
|
2404
2550
|
|------------------|--------|--------------------------------------------------------------------------------------------|
|
|
2405
|
-
| `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `
|
|
2551
|
+
| `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `pexels`, etc. to match the origin |
|
|
2406
2552
|
| `--content-type` | local | MIME type override (default: inferred from file extension). Ignored for URLs |
|
|
2407
2553
|
| `--external-id` | remote | Provider asset id — enables per-source dedup before bytes are fetched |
|
|
2408
2554
|
| `--external-url` | remote | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
|
|
@@ -2729,6 +2875,15 @@ When `list` or `search` returns nothing, the envelope carries `meta.empty` expla
|
|
|
2729
2875
|
|
|
2730
2876
|
If the source check itself fails, `meta.empty` is omitted entirely and the hint says so — an unverified absence is never reported as one.
|
|
2731
2877
|
|
|
2878
|
+
`list` is paged. `--limit` is the page size (default 50); when the library continues past the page you got, the envelope carries `meta.next_cursor` and a hint, and `meta.empty` is omitted — a page that is not the last page establishes nothing about the corpus. Read on with the same filters plus `--cursor`:
|
|
2879
|
+
|
|
2880
|
+
```bash
|
|
2881
|
+
baker testimonials list --sentiment positive --limit 50
|
|
2882
|
+
baker testimonials list --sentiment positive --limit 50 --cursor <meta.next_cursor>
|
|
2883
|
+
```
|
|
2884
|
+
|
|
2885
|
+
No `meta.next_cursor` means the library ended there.
|
|
2886
|
+
|
|
2732
2887
|
### `baker testimonials tags`
|
|
2733
2888
|
|
|
2734
2889
|
List the available testimonial tag names — built-in defaults plus the company's custom tags. Use it before filtering with `--tags`. Defaults to a markdown list (`--output json` for the `{ ok, data }` envelope).
|
|
@@ -3004,7 +3159,7 @@ baker scheduled-actions create --template baker-ads --name "Weekly ad review" --
|
|
|
3004
3159
|
Rules:
|
|
3005
3160
|
|
|
3006
3161
|
- `--cron` and `--run-at` are mutually exclusive.
|
|
3007
|
-
- `--run-at` must be an ISO UTC timestamp ending in `Z
|
|
3162
|
+
- `--run-at` must be an ISO UTC timestamp ending in `Z`, and still in the future — a time that has already passed is refused when the op is staged. If a one-off's time passes while the chat is still open, publishing creates the task **switched off** with the reason on it rather than dropping it, so it is never lost.
|
|
3008
3163
|
- Draft-created ids use `temp_sched_*`; use them with `get`, `update`, or `delete` before publish.
|
|
3009
3164
|
- Staged commands require `BAKER_CHAT_ID` to point to an `in_progress` chat.
|
|
3010
3165
|
- `trigger` rejects `temp_sched_*` and only accepts published scheduled-action ids.
|
|
@@ -3480,7 +3635,6 @@ Reach for it when you are relaying the **client's** own words. When you are writ
|
|
|
3480
3635
|
|
|
3481
3636
|
```bash
|
|
3482
3637
|
baker studio animate "slow push in, model turns to camera and smiles" --image j57abc123def456ghi789
|
|
3483
|
-
baker studio animate "she looks to camera and says: \"Hola, soy Elena\"" --avatar elena --quality 720p --aspect-ratio 9:16
|
|
3484
3638
|
baker studio animate "handheld drift right, steam rising from the cup" \
|
|
3485
3639
|
--image ./out/hero.png --duration 6 --quality 1080p
|
|
3486
3640
|
baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
|
|
@@ -3630,65 +3784,6 @@ baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and br
|
|
|
3630
3784
|
baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
|
|
3631
3785
|
```
|
|
3632
3786
|
|
|
3633
|
-
#### The brand, the ending and the cast are drawn, not generated
|
|
3634
|
-
|
|
3635
|
-
`baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
|
|
3636
|
-
because a video model garbles a wordmark every time:
|
|
3637
|
-
|
|
3638
|
-
- **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
|
|
3639
|
-
and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
|
|
3640
|
-
- **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
|
|
3641
|
-
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
3642
|
-
the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
|
|
3643
|
-
the words on the button, which otherwise default to the last line of the script.
|
|
3644
|
-
- **The brand fills itself.** When the spec leaves `brand.logo` or `brand.palette` empty,
|
|
3645
|
-
they are read from the workspace — `src/brand/BRAND.md` for the colours (in document
|
|
3646
|
-
order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
|
|
3647
|
-
(vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
|
|
3648
|
-
the ad needs something other than the client's default.
|
|
3649
|
-
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
3650
|
-
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
3651
|
-
- **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
|
|
3652
|
-
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
3653
|
-
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
3654
|
-
or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
|
|
3655
|
-
- **A real brand mark is required.** `scaffold-ad` refuses to build without one in
|
|
3656
|
-
`src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
|
|
3657
|
-
card, so a stand-in ships an ad wearing the wrong brand.
|
|
3658
|
-
- **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
|
|
3659
|
-
a separately cast voice, which is dubbing and looks like it.
|
|
3660
|
-
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
3661
|
-
become on-screen text and are captioned straight from the script, so the words are
|
|
3662
|
-
exact. Give it `music` too.
|
|
3663
|
-
- **`--avatar <handle>`** — the same flag as on `studio generate` / `studio animate`, and the way to cast a person into an ad. Overrides `cast.avatar` in the spec.
|
|
3664
|
-
- **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
|
|
3665
|
-
every beat they appear in on that avatar's identity sheet and copies its subject
|
|
3666
|
-
description verbatim, so the ad shows the same face as the rest of the company's work.
|
|
3667
|
-
This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
|
|
3668
|
-
- **`cast.description`** — a person described in words, when there is no avatar.
|
|
3669
|
-
|
|
3670
|
-
Each of these also removes a generation: the closing beat costs no image and no clip.
|
|
3671
|
-
|
|
3672
|
-
#### The finished video is reviewed
|
|
3673
|
-
|
|
3674
|
-
When a run's output is a video, `baker canvas run` samples twelve frames across it and
|
|
3675
|
-
looks at them before reporting success. It reports, never blocks — the clips are already
|
|
3676
|
-
paid for by the time it runs — and the findings come back as `review` and `hints`:
|
|
3677
|
-
|
|
3678
|
-
- **Per frame:** anything that could not physically happen, or shows a job being done
|
|
3679
|
-
incorrectly, plus objects floating unattached in shot. Malformed hands and limbs are
|
|
3680
|
-
rolled up into one finding across the whole render rather than repeated per frame.
|
|
3681
|
-
- **Across the reel:** whether the cast stays the same people, whether a brand mark is
|
|
3682
|
-
visible anywhere, and whether the last frames close the ad or simply stop.
|
|
3683
|
-
|
|
3684
|
-
Each per-frame finding names the **scene file** that produced that second, so a defect is
|
|
3685
|
-
an edit rather than a hunt — and re-running re-renders only the scenes you changed.
|
|
3686
|
-
|
|
3687
|
-
It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
|
|
3688
|
-
says the video was **not** reviewed rather than reporting it clean, and a frame that
|
|
3689
|
-
could not be read is reported as unchecked for the same reason.
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
3787
|
> Scaffolded canvases use **workspace-relative** composition/asset paths and are validated
|
|
3693
3788
|
> against the engine's hyperframe pre-render gate at `validate` time, so a broken overlay
|
|
3694
3789
|
> (or a moved/renamed workspace) is caught before any billed clip. The scaffold also adds
|
|
@@ -4291,7 +4386,7 @@ There is no `prompt` param — the extraction schema is the node. Use `focus` to
|
|
|
4291
4386
|
|
|
4292
4387
|
##### `image_search`
|
|
4293
4388
|
|
|
4294
|
-
Search the web for real images instead of generating them. A backend LLM agent picks among Google Images, stock photography (
|
|
4389
|
+
Search the web for real images instead of generating them. A backend LLM agent picks among Google Images, free stock photography (Pexels and Pixabay), and Pinterest, refines queries, selects the best matches, and the chosen images are downloaded into canvas assets. Use it to gather references or candidates (e.g. five photos of an australian shepherd) for a later pick-the-best step.
|
|
4295
4390
|
|
|
4296
4391
|
**Inputs** — none.
|
|
4297
4392
|
|
|
@@ -5733,6 +5828,23 @@ baker landing inspiration scrape <url> --out .baker/<dir> # capture any page n
|
|
|
5733
5828
|
- **Inspiration, never a clipboard.** A reference's structure and mechanism are the lesson; write the client's own words.
|
|
5734
5829
|
- **A capture has to land in `.baker/`, and `scrape` refuses anywhere else in a workspace.** `--out` is free-form and the directory is the agent's choice, but a capture is tens of MB of lossless PNG whose individual files clear every size gate — written to a tracked directory it becomes a permanent part of the client's repo, force-cloned into every later session. `.baker/` is gitignored precisely so a capture costs nothing. The refusal is scoped to the workspace: outside a git work tree — a local run against `/tmp` — any path is allowed, because there is no history to bloat.
|
|
5735
5830
|
|
|
5831
|
+
### `baker landing variant`
|
|
5832
|
+
|
|
5833
|
+
Creates a new **version of a page** for an A/B test. It **imports the page's sections** and owns its own copy of only what you fork, so the two versions differ by exactly the hypothesis.
|
|
5834
|
+
|
|
5835
|
+
```bash
|
|
5836
|
+
baker landing variant oferta --fork Hero.astro
|
|
5837
|
+
baker landing variant oferta --fork Hero.astro,Proof.astro --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
|
|
5838
|
+
```
|
|
5839
|
+
|
|
5840
|
+
- **You do not choose the slug.** It is `<page>--<n>`, numbered per page and never reused — so it cannot collide with an archived version, and it cannot merge with an older variant's analytics history. `--` is also what every tool decides by: which analytics id the page stamps, whether the sitemap lists it, and whether the edge answers the URL. It replaced a `<page>-b` convention that nine live pages across the fleet would have been misread by.
|
|
5841
|
+
|
|
5842
|
+
- **Sharing, not copying, is what keeps the test honest.** A duplicated page drifts from its control on every later edit — a price, a typo, a new testimonial — and the test then reports that drift plus the hypothesis as one result. A shared section is one file, so an edit reaches both arms at once. Baker follows the sharing too: both pages redeploy together, and both are re-critiqued when a shared section changes.
|
|
5843
|
+
- **Relative paths are re-aimed, not string-patched.** Every specifier in the copied files is resolved and re-relativised, so `index.astro` and a forked `_components/Hero.astro` — which sit at different depths — both keep pointing at the right thing, including the control's `_images/` and shared wrappers like `src/components/Video.astro`.
|
|
5844
|
+
- **`_definition.md` is the page's**, with a fresh quoted `internalId`, a `(variant)` suffix on `internalTitle`, and a note recording what differs, the observation behind it and the change. `metaTitle`/`metaDescription` stay identical on purpose: the variant is served under the control's URL.
|
|
5845
|
+
- **Errors carry the retry.** `NOT_FOUND` returns `availableSlugs` (wrong page) or `availableComponents` (wrong `--fork`); `INVALID_SLUG` fires when the slug is not a page, or is already a variant of one. Omitting `--fork` succeeds with a hint saying the page currently renders exactly the control, so a test between them cannot conclude.
|
|
5846
|
+
- **Run `baker experiment plan` first.** Most pages never get the traffic to settle most questions.
|
|
5847
|
+
|
|
5736
5848
|
### `baker landing critique`
|
|
5737
5849
|
|
|
5738
5850
|
Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.
|
|
@@ -5752,6 +5864,10 @@ baker landing critique spring-offer summer-offer claude # score three landings
|
|
|
5752
5864
|
|
|
5753
5865
|
- **Agent washing is a copy tell.** A page that claims autonomy ("fully autonomous", "while you sleep", "no human intervention") and never says anywhere what the buyer approves, overrides, or audits scores an `agent-washing` warn. One signal about oversight of the agent's actions — approval, review, override, undo/rollback, audit log, escalation — anywhere on the page clears it; the rule is page-scope precisely because the hero can defer the trust story to a section below it. Generic privacy boilerplate ("GDPR", "encrypted", "your data") deliberately does **not** clear it, since it appears in most footers and says nothing about who is in charge of the agent.
|
|
5754
5866
|
|
|
5867
|
+
- **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a comparative sentence outside a `<table>` — a comparison table is a section the landing skill teaches, but naming a rival anywhere else on paid traffic is the client's call, not the build's. Spanish, Portuguese and English.
|
|
5868
|
+
|
|
5869
|
+
- **Copy held in component frontmatter is read like markup copy.** Astro landings keep FAQ pairs, testimonial quotes, feature cards and pricing rows as a `const items = [{ q, a }]` array above the `---` fence and render them in a `.map()`, so a critic that blanked the whole block was blind to a large share of every page's actual words. Prose string literals in that block now feed every copy rule; imports, class strings, URLs and config do not, and the bar is deliberately high in that direction — a Tailwind class list in front of the copy rules is worse than one missed tell.
|
|
5870
|
+
|
|
5755
5871
|
Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, slug, overall, counts, dimensions, findings }`. `dimensions` scores seven design families (typography, color, borders_depth, motion, spacing, copy, integrity) 0–1 (higher is better); `counts` is the block/warn/advisory tally.
|
|
5756
5872
|
|
|
5757
5873
|
## Brand Fonts (`baker brand fonts`)
|
|
@@ -5850,18 +5966,7 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
5850
5966
|
|
|
5851
5967
|
- **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
|
|
5852
5968
|
|
|
5853
|
-
|
|
5854
|
-
- **0.251.0**: `--accent` on `baker avatars create|update` — the region an avatar's voice comes from, e.g. `"Spanish from Spain, Madrid accent"`. Its own field rather than a sentence inside `--speech`, because that is where it went to die: asked for "pace, register, accent", the agent wrote *"clear Spanish accent"* for a Madrid presenter, the video model read that as no accent, and the clip came out sounding like nowhere. A place can be checked; an adjective cannot. It reaches the render through `buildDeliveryDirection` as its own `ACCENT:` instruction, and `thinProfileHint` now names it first among the missing fields since it is the only one that fails silently — a clip with no accent still looks finished.
|
|
5855
|
-
- **0.252.0**: avatar voices removed. `--voice-id` / `--voice-description` are gone from `baker avatars create|update`, and an avatar no longer holds a voice at all — every clip is voiced by the video model as it renders, directed by the profile's `--accent`. The picker they replaced offered a voice, played its preview, and then shipped a clip that did not sound like it: the re-voice was speech-to-speech, which replaces timbre and keeps pronunciation, so the accent the user picked could never survive. Three other routes were built and judged on video before removing it — TTS + lip-sync (mouth), audio-driven avatar models (picture), and Seedance 2.5 driven by audio (refuses AI faces). Reasoning and the re-open condition: `docs/adr/0005-an-avatar-is-a-face-not-a-voice.md`.
|
|
5856
|
-
- **0.253.0**: the pre-render gate runs `hyperframes check` — lint, runtime, layout, motion and contrast in one browser session, sampled at transition seams — instead of the static `lint` + `inspect` pair, falling back to them when the installed binary predates `check`. The browser half is signal the parser could never produce: measured on HyperFrames' own `warm-grain` example, lint/runtime/layout/motion all reported `ok: true` while the composition rendered an almost entirely blank ten-second video, the only trace being six `GSAP target #a-roll not found` warnings — a tween pointed at an element that does not exist, so nothing it animates ever appears. Baker promotes that warning to blocking, alongside `sweep_static` ("the timeline never advanced", i.e. a still image billed as a video). Warnings are grouped by code with a count (one run returned 62, 53 of them identical) and the highest-value findings carry advice on what to do rather than only what was seen. Requires `hyperframes@0.8.17` in the sandbox, bumped from 0.7.5 in the same change. Reasoning: `docs/adr/0007-the-render-gate-runs-in-a-browser.md`.
|
|
5857
|
-
- **0.254.0**: the nested-composition smoke test now looks at the frames instead of trusting the exit code — exiting 0 is what a blank render does too. `hyperframes snapshot --describe` sends the captured frames to Gemini and writes back what is actually in them, and the credential Studio already uses for Gemini and Omni (`GOOGLE_GENERATIVE_AI_API_KEY`) is the `GEMINI_API_KEY` it reads; the backend now ships it to the sandbox by omission, so a deployment without one keeps the plain smoke test. Verified against a real render: the pass reported "the image is completely empty" and named the single visible element without being told what to look for. Frame verdicts are condensed into the node log, one line per frame — a file the agent would have to know to open is a file it does not read.
|
|
5858
|
-
- **0.263.0**: shot briefs refuse the frame that contradicts its own caption, and the vision pass reads the numbers back. Observed verbatim: a phone screen reading "$215.40 SAVED THIS MONTH" under a caption reading "Save over £900 a year" — wrong currency, and a figure that contradicts the claim it sits beneath, since 215.40 a month is 2,584 a year. The model invents a number whenever a screen or document is in shot, so the frames no longer carry legible text at all: the caption track is where an ad's numbers live. Same brief also pins each object's real-world FORM after a phone rendered with a second screen on its back, and names the market's currency for anything unavoidably visible. The frame-vision question now reads out every number and word inside the picture and says whether it contradicts the caption — the defect only exists between the two, so neither alone can see it.
|
|
5859
|
-
- **0.262.0**: `scaffold-ad` carries the brand. The spec takes `brand.palette` (hex tokens) and `brand.logo` (repo path to the mark), and emits them as the `style.palette` and `branding` the engine already reads to colour its brand plate and CTA. The palette dresses the plate and the type and is deliberately kept OUT of the shot briefs — hex in a frame prompt makes the model paint the whole scene that colour, which reads as a filter. Also takes `market`, since an ad that does not say where it is set is an ad the model sets wherever it likes.
|
|
5860
|
-
- **0.261.0**: four defects that reached a finished ad, fixed in `scaffold-ad` where they originate. Frames render on **`openai/gpt-image-2`**, pinned rather than inheriting the catalogue default (Gemini Flash — right for cheap high-volume work, wrong for the opening still a five-second clip is built from). Every shot brief now states the MARKET, inferred from the voice language when unstated: an ad for Spain came back full of British houses because nothing said where it was. Every shot brief also rules out the impossible — paper and screens are opaque, objects are at real-world scale — after a bill rendered with the text from its reverse showing through, and a solar panel five metres tall. And beat durations snap to lengths the video model can actually render: free-form durations were rounded down per clip, so 11 beats summed to 27.0s of picture against a 31.4s timeline and the 4.4s difference rendered as a black tail. The read rate was re-measured off a real Spanish ad (81 words in 26.48s = 3.06 w/s); the old 2.6 over-predicted every beat by 18%.
|
|
5861
|
-
- **0.260.0**: burned-in captions break where the script punctuates instead of every N words. The karaoke composition sliced the transcript into fixed groups, and a word count knows nothing about the sentence it is cutting — an ad the engine rendered showed cards reading "OTRA VEZ, EN" and "TU CASA GENERA", half-thoughts ending on a preposition. A card now closes when the clause does; the word count stays as a ceiling so a long clause is still split, and a linking word ("de", "al", "y") never ends a card because it belongs to what follows. Same rule the pre-render gate already checked for, now enforced where the cards are actually built.
|
|
5862
|
-
- **0.259.0**: `baker canvas scaffold-ad` is listed in `baker canvas --help` with a description, and the three video routes are separated in the skill. It had a usage line and no description row, so an agent reading help saw two scaffolders explained and a bare name — and three runs from the same prompt took three different wrong paths: hand-directing the flow, reproducing a competitor ad found via `winning-ads`, and hand-authoring a canvas. The routes now state their own boundary: `scaffold-ad` is the default for a new ad, `scaffold-video` only reproduces a video the USER supplied, and `video-flow.md` opens by saying so rather than calling itself the default eleven times.
|
|
5863
|
-
- **0.257.0**: the frame-vision pass now asks whether what is on screen could physically happen, not just whether the frame is empty. The defect that motivated it was a generated shot of a solar panel roughly five metres tall being lifted onto a roof by one person — well lit, on brief, perfectly legible, and impossible. That is what reads loudest as "AI-generated", and the previous question could not see it: nothing was missing, something present was the wrong size. Asked the new question, the pass called the rejected shot *"physically implausible due to the immense weight and surface area"* and the replacement *"consistent with what two people could reasonably maneuver"* — the same verdict a person reached, unprompted.
|
|
5864
|
-
- **0.256.0**: the pre-render gate reports caption cards that end mid-clause. The first cut of this check asked for consistency and got it the wrong way round — stripping terminal punctuation makes the cards agree and leaves them wrong, because "En solo nueve días tu casa" is not a line anyone wrote, it is a sentence halved by a word count. Captions split where the script punctuates; each card is then a clause carrying its own mark, grammatical, and consistent as a by-product. The last card is exempt, since a CTA legitimately ends bare. Reported, never rewritten — the copy belongs to whoever wrote it.
|
|
5969
|
+
## Publishing
|
|
5865
5970
|
|
|
5866
5971
|
### Auto-publish (CI)
|
|
5867
5972
|
|
|
@@ -5874,6 +5979,38 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
|
|
|
5874
5979
|
./scripts/publish-package.sh cli next # Publish as @next (pre-release)
|
|
5875
5980
|
```
|
|
5876
5981
|
|
|
5982
|
+
- **0.243.0**: `baker ads linkedin account url-params` stages the URL tracking parameters on the whole **ad account** — the "Account tracking parameters" box in Campaign Manager, which the CLI (and this README, and the playbook) said was UI-only and unreachable through the API. It was, until LinkedIn's 202608 added `sponsoredAccount` to the `adEntity` key of `/adTrackingParameters`; Baker's LinkedIn version pin moves 202606 → 202608 with it. Same flags as the ad-set form (`--param`, `--dynamic`, `--clear`, `--file`) and no positional: the target is built from `--account-id`, because a bare numeric id has always meant an ad set and reading one as an account would silently stage the wrong write. **LinkedIn concatenates the two levels rather than overriding**, so a key set at both appears twice in the served URL — the stage warnings now name the other level for exactly that check, and the staged change says which level it applies to. `baker ads linkedin account` keeps reading account detail unchanged (it is now `account detail`, kept as the default subcommand).
|
|
5983
|
+
|
|
5984
|
+
- **0.244.0**: `baker ads linkedin url-params` **reads** the tracking parameters in effect — the ad account's, optionally every ad set's, and the keys set at BOTH levels. There was no read at all before: the only way to see what an entity carried was to stage a change and look at the before-state, which meant the concatenation trap (a key at the account AND an ad set lands in the served URL twice) was invisible until a report came back split across two values of the same parameter. Account-only by default because `adTrackingParameters` has no batch read, so each ad set is its own call; `--ad-sets` sweeps them, capped at 50, and the response carries `adSetCoverage: {examined, total, capped}` so a partial sweep can't read as a clean account. `--collisions-only` for just the finding.
|
|
5985
|
+
|
|
5986
|
+
- **0.245.0**: **a repeated flag no longer loses everything but its last value.** citty declares no array arg type — it calls `util.parseArgs` without `multiple: true` — so `--param utm_source=linkedin --param utm_medium=paid` reached the command as the single string `utm_medium=paid`. The arg descriptions said "repeatable", the unit tests passed arrays citty never produces, and `url-params` REPLACES an entity's whole parameter set: staging three UTMs set one and deleted the rest at publish, reporting success throughout. Found by staging against the real ad account and reading the payload back, not by any test. Every repeatable flag now reads its values off the command line: LinkedIn `--param`/`--dynamic`, Google Ads `--custom-param` (Google replaces the set wholesale too) and `--text`, `baker mcp add --header`, and `baker flows map --map`, which had already solved this locally and is now the shared helper.
|
|
5987
|
+
|
|
5988
|
+
- **0.245.0**: `baker ads linkedin url-params --campaign-ids` says when an id names no ad set. LinkedIn answers NOT_FOUND identically for "this ad set carries no parameters" and "there is no such ad set", so a mistyped id came back as an ad set that exists and is empty — and the next move on that is a write that only fails at publish. Such ids are now reported under `unreadable` with the reason, and the "nothing is tagged" hint is suppressed, exactly as for a level that could not be read.
|
|
5989
|
+
|
|
5990
|
+
- **0.245.0**: a backend condition the caller can act on reaches the caller. "Analytics reporting is not switched on for this workspace yet" was thrown as an `INTERNAL`, and the HTTP layer withholds the message of every 5xx so a real fault cannot leak its internals — so the CLI printed `INTERNAL_ERROR: Internal server error` and, worse, LinkedIn and Meta commands marked it retryable. It is now a `CONFLICT`, which the CLI maps to a new non-retryable `CONFLICT` code and prints verbatim.
|
|
5991
|
+
|
|
5992
|
+
- **0.246.0**: `baker ads linkedin draft` reports what a publish DID. It used to render a published draft exactly like a staged one — same "1 staged change", same tree, no mention of the outcome — even though the per-op `result` was in the response the whole time. So the one surface an agent reaches for to check a publish could not answer, and a publish that changed LinkedIn and then failed to finish looked identical to one that had not run yet. It now reads `published · 1 applied` / `2 applied, 1 failed`, names each failed or skipped op with its error, and says `publishing` while one is still in flight. Found because a tracking-parameter publish did exactly that: applied at LinkedIn, then left the draft stuck, with the only trace an `Error calling onComplete` in the server log.
|
|
5993
|
+
|
|
5994
|
+
- **0.247.0**: a tracking-parameter write with no `--dynamic` no longer fails at publish. LinkedIn's `/adTrackingParameters` record requires BOTH maps and rejects a body missing either — `ERROR :: /dynamicValueParameters :: field is required but not found and has no default value` — so the most ordinary write there is, a couple of fixed UTMs and no placeholder, was a hard failure, while every write that happened to carry a `--dynamic` succeeded. Both maps are now always sent, empty when unused. Found by the 0.246.0 outcome line, on the first real publish that did not pass `--dynamic`.
|
|
5995
|
+
|
|
5996
|
+
- **0.248.0**: a tracking-parameter read that fails no longer reads as "there is nothing there". The idempotency token is LinkedIn's concurrency guard AND the only look at the before-state, and `url-params` REPLACES the entity's whole set — so writing without it is not a degraded write, it is an unguarded one that overwrites live parameters having never seen them. Both readers collapsed every failure into "none set": the executor logged `not readable; writing as a fresh set` and did exactly that, and the stage-time snapshot logged `previewing as none set`, which silently removed the drop warning that exists to catch this. Only LinkedIn's NOT_FOUND now means "none yet" (a first write — and no longer logged, since it is the normal case that was filling the log). Any other failure fails the publish, and at stage time produces a warning saying the current parameters could not be read rather than none being there. It matters most on the 409 retry: a 409 means somebody changed the parameters between our read and our write, and a swallowed failure on the re-read turned that detected conflict into a blind overwrite of their change.
|
|
5997
|
+
|
|
5998
|
+
Reading it back changes three things beyond the command. **`campaigns url-params` / `account url-params` now warn by name when a write would delete a live key** — LinkedIn replaces an entity's whole parameter set rather than merging into it, so staging one pair on an ad set that already carries three published as "one pair, and the other three are gone". Pairs accumulate inside a chat, which is exactly what made a merge at publish the natural expectation; the stage warning names what would be dropped, and the guidance now says to read first and re-send everything that should survive. **`baker analytics map` can check a proposed LinkedIn mapping against the account** — LinkedIn was on the list of platforms with no configuration read, so its mappings shipped with no evidence behind them; it now reads the account level and up to ten ad sets. And **the `tracking.campaign_parameters` audit check answers before a campaign runs**: with no traffic it used to be `n_a`, honest but useless at the one moment the fix is free, and it now judges the account's own parameters — with wording that says so rather than claiming clicks arrived carrying nothing when no click arrived.
|
|
5999
|
+
|
|
6000
|
+
Three more things the 202608 pin brought with it. **The conversion-type enum went from 8 values to all 34 LinkedIn accepts** — `QUALIFIED_LEAD`, `BOOK_APPOINTMENT`, `SUBMIT_APPLICATION` and 23 others were rejected by our own schema before the request ever left, and 202608 adds `MARKETING_QUALIFIED_LEAD` / `SALES_QUALIFIED_LEAD` on top. **Attribution windows gained 365** (and view-through gained 90), with the type-conditional rule enforced on `conversions create`: 365 is only legal for `SUBMIT_APPLICATION`, `PURCHASE`, `ADD_TO_CART`, `QUALIFIED_LEAD`, `LEAD`. And **a 429 carrying `SEGMENT_LIMIT_EXCEEDED` is no longer reported as a throttle** — 202608 caps an ad account at 1,000 DMP segments and reuses the rate-limit status for it, so an agent that read it as "retry in a second" would have retried forever against a cap only a human can clear in Campaign Manager.
|
|
6001
|
+
|
|
6002
|
+
- **0.256.0**: **`assets attach` no longer offers an image link, because Google accepts none.** A production publish came back with six `AssetLinkError.FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE` rows (trigger `MARKETING_IMAGE,SEARCH`) on an otherwise built and live Search campaign — staged exactly as this CLI's own advisory and the scaffold's ads-google doc instructed, at `"fieldType": "MARKETING_IMAGE"`. The obvious fix was `AD_IMAGE`, which is what Google's documentation and its own forum answers name for a Search image extension. **It does not work either.** Sent to a real account on v23, all four combinations are refused: `MARKETING_IMAGE` on a Search `CampaignAsset` → `FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE`; `MARKETING_IMAGE` on a Search `AdGroupAsset` → `UNSUPPORTED_FIELD_TYPE`; `AD_IMAGE` on a `CampaignAsset` → `UNSUPPORTED_FIELD_TYPE`; `AD_IMAGE` on an `AdGroupAsset` → `UNSUPPORTED_FIELD_TYPE`. Image extensions never moved to asset-based extensions, so there is no request the API takes. Every image field type is now refused at the CLI boundary with the interface named instead of another field type to try, and the **"recommend at least 3 image assets" advisory is removed** — it had been firing on every Search campaign since 0.191.0 and could not be satisfied by any draft, because the write it named has never once succeeded. Performance Max asset-group images are a different mechanism and are untouched (`asset-groups attach`). Four force-synced scaffold surfaces were saying the same wrong thing and now agree with the API: the `ads-google.md` tool doc, the `assets` command group description, the account-audit playbook's Dynamic Images row, and `staged-writes.md`. An audit should still raise a missing-images gap — it is a real SERP disadvantage — it just has to name the Google Ads interface as the fix rather than a command.
|
|
6003
|
+
|
|
6004
|
+
- **0.256.0**: a structured snippet value over 25 characters is refused before it is staged. Google caps each VALUE at 25 and answers a longer one with a bare `Too long.` at `structured_snippet_asset.values[i]` — no header, no value, nothing to act on — and it takes the asset's campaign link down with it as `RESOURCE_NOT_FOUND`. `assets create --values` and `assets update --values` now cap each entry; the per-asset 3-10 count check was already there, and only the per-value length was missing. Found on a live publish where four of five values were fine and "Bases de datos vectoriales" (26) was not, and confirmed fixed by re-publishing the same snippet at 25 characters against a real account, where it applied and linked cleanly.
|
|
6005
|
+
|
|
6006
|
+
- **0.274.0**: `baker landing critique` gains a `positioning integrity` pair in the `integrity` family, and starts reading the copy Astro components keep in their frontmatter. `competitor-concession` is **block-tier and fires at a single sentence** — unlike every other prose rule, which needs a cluster — because a page that ranks a competitor above the client, or says the client does not lead at what it sells, spends the client's ad budget arguing the visitor should leave. A concession about **price** in either direction never fires it ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and neither does a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim FOR the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a comparative sentence outside a `<table>` — naming a rival on paid traffic is the client's call. Both read Spanish, Portuguese and English. Alongside them, prose string literals in a component's `---` block are now scored like markup copy (imports, class strings, URLs and config still are not), so an FAQ held as `const faqs = [{ q, a }]` — the standard Astro shape — is no longer invisible to the critic and to `BRAND.md` copy rules. `CRITIC_VERSION` bumps to `3`.
|
|
6007
|
+
|
|
6008
|
+
- **0.273.0**: **an ad's name can no longer take a whole publish down.** `Ad.name` is IMMUTABLE, and `--ad-name` is shared by `ads create` and `ads update` (every content flag is), so an update restating the ad's name put `name` in the field mask and Google refused the operation — and, being one op inside an atomic batch, everything staged with it. The name is now dropped from an update's mutate, and an update that would actually RENAME the ad is refused at stage time instead, so a rename is answered rather than silently ignored. Two related corrections: `--ad-name` is required on `demandGenVideo` **only** — a multi-asset `demandGen` ad published with no name at all and Google created it, so the docs that called it a Demand-Gen-family requirement were wrong — and a content flag on `--format call` is now refused like every other format's, instead of being dropped because a call ad reads its content from `--file` alone. The Demand Gen video ad's live videos and logos are also read into the review card now, so an update that replaces them shows what it is replacing.
|
|
6009
|
+
|
|
6010
|
+
- **0.272.0**: what one live publish of a Demand Gen video ad found, all four fixed and each of them a whole-batch failure (a campaign, its ad group, its ad and its assets are one atomic mutate). `--type DEMAND_GEN_AD` and `--type VIDEO_OUTSTREAM` are **gone from `ad-groups create`** — v23's ad group types have never had either, and Google answers with a request-level `Invalid value` that fails every sibling operation. `--type` is now **optional and best omitted**: Google derives an ad group's type from its campaign's channel, and on a Demand Gen campaign that is the only way to create one (its `ad_group.type` comes back empty). `--bidding-strategy MANUAL_CPC` on a **Demand Gen** campaign is refused before staging — Google rejects it outright (`OPERATION_NOT_PERMITTED_FOR_CONTEXT`), and the Demand Gen playbook shipped in every sandbox was teaching it; `MAXIMIZE_CONVERSIONS` and `TARGET_CPC` work. And a new Demand Gen video ad now needs `--ad-name` (Google's "Ad name", required on Demand Gen ads and no other format) and at least one `--logo-image-assets` ref alongside its video — Google only says so at publish, once the assets in the same batch have already been created.
|
|
6011
|
+
|
|
6012
|
+
- **0.271.0**: **`--format demandGenVideo` — the Demand Gen ad most Demand Gen spend actually runs through can be edited.** A `DEMAND_GEN_VIDEO_RESPONSIVE_AD` keeps its copy in `demandGenVideoResponsiveAd`, which has a `longHeadlines` list no other ad format has and takes its business name as a text asset rather than a string. Baker modelled only the multi-asset Demand Gen ad and labelled all four Demand Gen types as one format, so every served video ad was uneditable: the strict content schema refused `longHeadlines` and named a field instead of a missing format, and the only route left was the Google Ads interface. On the account that surfaced it, Demand Gen was 76% of the spend. New flags on `ads create|update`: `--long-headlines` (up to 5 × 90 chars, this format only), `--video-assets`, `--logo-image-assets`, `--call-to-action-assets`; a create with no video is refused with the `assets create` call to make first, and an omitted media list on an update leaves that media on the ad. Two more gates ship with it. **A content flag a format does not read is now refused instead of dropped** — `--long-headlines` on `--format demandGen` staged the copy without them and reported success, the same silent-drop the backend's strict schemas exist to stop. And **the format is checked against the live `ad_group_ad.ad.type` when it stages**: a mismatch is refused with the right format named, and the two Demand Gen types Baker writes no format for (`DEMAND_GEN_CAROUSEL_AD`, `DEMAND_GEN_PRODUCT_AD`) are refused by name, so "Baker cannot edit this ad" arrives at stage time rather than as a publish failure a Session too late.
|
|
6013
|
+
|
|
5877
6014
|
### Testing a pre-release in sandboxes
|
|
5878
6015
|
|
|
5879
6016
|
```bash
|