@koda-sl/baker-cli 0.265.2-dev.1f1c09c80 → 0.266.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +89 -76
  2. package/canvas/tiktok-captions-composition/index.html +2 -23
  3. package/dist/{chunk-2IUN3S3E.js → chunk-2IB3IL4K.js} +2 -2
  4. package/dist/{chunk-SZJWEOW2.js → chunk-36KVUYVL.js} +38 -156
  5. package/dist/chunk-36KVUYVL.js.map +1 -0
  6. package/dist/{chunk-XAUTT2Q6.js → chunk-6F52WYB7.js} +3 -3
  7. package/dist/{chunk-WFWU3CHS.js → chunk-DZUVUGEP.js} +1 -9
  8. package/dist/{chunk-WFWU3CHS.js.map → chunk-DZUVUGEP.js.map} +1 -1
  9. package/dist/{chunk-P2PEABMQ.js → chunk-GU7IWEBC.js} +12 -5
  10. package/dist/chunk-GU7IWEBC.js.map +1 -0
  11. package/dist/{chunk-RHEQTHT7.js → chunk-KSOGK7SJ.js} +4 -4
  12. package/dist/cli.js +6809 -6051
  13. package/dist/cli.js.map +1 -1
  14. package/dist/client-VDCCDEHE.js +17 -0
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-C7YXL6C6.js → env-FWMZXMQK.js} +2 -6
  17. package/dist/{output-ALQGBDPQ.js → output-JBYI5IVX.js} +5 -5
  18. package/dist/{shared-UKJH3PS7.js → shared-GWFXJXKJ.js} +6 -6
  19. package/package.json +1 -1
  20. package/dist/chunk-P2PEABMQ.js.map +0 -1
  21. package/dist/chunk-SZJWEOW2.js.map +0 -1
  22. package/dist/client-KOUPT7HU.js +0 -15
  23. /package/dist/{chunk-2IUN3S3E.js.map → chunk-2IB3IL4K.js.map} +0 -0
  24. /package/dist/{chunk-XAUTT2Q6.js.map → chunk-6F52WYB7.js.map} +0 -0
  25. /package/dist/{chunk-RHEQTHT7.js.map → chunk-KSOGK7SJ.js.map} +0 -0
  26. /package/dist/{client-KOUPT7HU.js.map → client-VDCCDEHE.js.map} +0 -0
  27. /package/dist/{env-C7YXL6C6.js.map → env-FWMZXMQK.js.map} +0 -0
  28. /package/dist/{output-ALQGBDPQ.js.map → output-JBYI5IVX.js.map} +0 -0
  29. /package/dist/{shared-UKJH3PS7.js.map → shared-GWFXJXKJ.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
 
@@ -653,17 +658,21 @@ baker tag-manager draft show --chat <chat-id> gtm_temp_ab12
653
658
  baker actions draft --chat <chat-id>
654
659
  ```
655
660
 
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 <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
661
+ 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
662
 
658
663
  **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
664
 
665
+ **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.
666
+
660
667
  **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
668
 
662
669
  **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
670
 
671
+ **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.
672
+
664
673
  **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
674
 
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.
675
+ **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
676
 
668
677
  **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
678
 
@@ -1063,6 +1072,8 @@ audit --format md # deliverable-ready markdown table
1063
1072
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1064
1073
  campaigns create|update|pause|resume|archive|duplicate
1065
1074
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1075
+ account url-params # UTMs for every ad in the whole account
1076
+ url-params [--ad-sets] # read what is tagged now, and what lands twice
1066
1077
  creatives create|update|pause|resume|duplicate
1067
1078
  audiences create|upload
1068
1079
  conversions create|update
@@ -1090,6 +1101,10 @@ baker ads linkedin campaigns url-params 123456 \
1090
1101
  --param "utm_source=linkedin&utm_medium=paid-social" \
1091
1102
  --dynamic utm_campaign=CAMPAIGN_NAME --dynamic utm_content=CREATIVE_ID
1092
1103
  baker ads linkedin campaigns url-params 123456 --clear # remove them all
1104
+
1105
+ # what never varies across the account goes one level up — LinkedIn appends BOTH,
1106
+ # so a key set at the account AND the ad set lands in the URL twice
1107
+ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=paid_social"
1093
1108
  # a boolean flag written with a space (`--clear false`) is rejected: citty sets it to TRUE and drops
1094
1109
  # the word. Attach it — `--clear=false` — or leave the flag off.
1095
1110
 
@@ -1977,7 +1992,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
1977
1992
  |---|---|---|
1978
1993
  | `baker images library <q>` | Hybrid library search (replaces `search`) | n/a |
1979
1994
  | `baker images find <q>` | Fanout: library + opted-in providers | off |
1980
- | `baker images stock <q> [--type photo\|vector\|psd]` | Magnific (Freepik's dev API)photos, vectors, illustrations, PSDs (~250M assets) | off |
1995
+ | `baker images stock <q> [--type photo\|illustration\|vector]` | Free stock across two libraries Pexels photographs, Pixabay illustrations and vectors | off |
1981
1996
  | `baker images google <q>` | Google Images via the official Custom Search JSON API | off |
1982
1997
  | `baker images pinterest <q>` | Pinterest reference imagery via ScrapeCreators (photo-real mood boards) | off |
1983
1998
  | `baker images logo <domain>` | Brand logo via Brandfetch CDN | **on** (top 1) |
@@ -2002,7 +2017,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
2002
2017
 
2003
2018
  **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
2019
 
2005
- **Provider context biases the description.** When a hit carries human-readable text (Giphy `alt_text` / `title`, Magnific `title`, 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.
2020
+ **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
2021
 
2007
2022
  **`--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
2023
 
@@ -2017,6 +2032,8 @@ baker images screenshot https://competitor.com/pricing --context "Competitor pri
2017
2032
 
2018
2033
  **`baker images search` is a deprecated alias** for `library` (stderr deprecation log, removed in next minor).
2019
2034
 
2035
+ **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.
2036
+
2020
2037
  **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
2038
 
2022
2039
  ### `baker images library <query>`
@@ -2026,7 +2043,7 @@ Search the company library (hybrid BM25 + vector + Cohere rerank). Use this FIRS
2026
2043
  ```bash
2027
2044
  baker images library "hero banner"
2028
2045
  baker images library "logo" --aspect-ratio 1:1 --tags logo
2029
- baker images library "office" --source magnific
2046
+ baker images library "office" --source pexels
2030
2047
  baker images library "pricing" --external-url-host competitor.com
2031
2048
  ```
2032
2049
 
@@ -2038,7 +2055,7 @@ baker images library "pricing" --external-url-host competitor.com
2038
2055
  | `--min-score` | Minimum relevance score, 0-1 |
2039
2056
  | `--aspect-ratio` | Filter by aspect ratio |
2040
2057
  | `--tags` | Comma-separated tag names |
2041
- | `--source` | Filter by source (uploaded, magnific, brandfetch, google_images, firecrawl, screenshotone, iconify, giphy, …) |
2058
+ | `--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
2059
  | `--external-url-host` | Filter by host substring of `externalUrl` (e.g. competitor.com) |
2043
2060
  | `--output` | Output format: `json` \| `files` \| `md` |
2044
2061
  | `--fields` | Comma-separated field names to include |
@@ -2051,12 +2068,12 @@ baker images library "pricing" --external-url-host competitor.com
2051
2068
  Fanout image search: library first, then opted-in providers in parallel.
2052
2069
 
2053
2070
  ```bash
2054
- baker images find "office" --sources library,magnific --limit 20
2055
- baker images find "office" --sources library,magnific --fallback --threshold 0.4
2071
+ baker images find "office" --sources library,pexels,pixabay --limit 20
2072
+ baker images find "office" --sources library,pexels --fallback --threshold 0.4
2056
2073
  baker images find "celebration" --sources library,giphy --auto-ingest 3
2057
2074
  ```
2058
2075
 
2059
- Providers: `library`, `magnific`, `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.
2076
+ 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
2077
 
2061
2078
  **Flags:**
2062
2079
 
@@ -2071,17 +2088,28 @@ Providers: `library`, `magnific`, `google`, `iconify`, `giphy`, `pinterest`. Bra
2071
2088
 
2072
2089
  ### `baker images stock <query>`
2073
2090
 
2074
- Stock search via **Magnific** Freepik's developer API (post-acquisition rebrand `api.magnific.com`, header `x-magnific-api-key`). One library, ~250M assets covering photos, vectors, illustrations, icon sets, mockups, PSDs.
2091
+ `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.
2092
+
2093
+ 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
2094
 
2076
2095
  ```bash
2077
2096
  baker images stock "minimalist office"
2078
- baker images stock "flat office workers" --type vector
2079
- baker images stock "hero photo of a kitchen" --type photo --orientation landscape --ai exclude
2080
- baker images stock "brand pattern" --color "#0a0a0a" --license freemium --auto-ingest 2
2081
- baker images stock "office reaction" --people only
2097
+ baker images stock "hero photo of a kitchen" --orientation landscape --size large
2098
+ baker images stock "flat office workers" --type illustration
2099
+ baker images stock "leaf outline mark" --type vector
2100
+ baker images stock "brand pattern" --color "#0a0a0a" --auto-ingest 2
2101
+ baker images stock "oficina luminosa" --locale es-ES
2082
2102
  ```
2083
2103
 
2084
- Free tier exists but watermarks previewspass `--license freemium` to filter to clean free assets explicitly.
2104
+ **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.
2105
+
2106
+ **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".
2107
+
2108
+ 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.
2109
+
2110
+ **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.
2111
+
2112
+ 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
2113
 
2086
2114
  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
2115
 
@@ -2089,14 +2117,12 @@ An empty result comes back with `hints` naming the filters that narrowed the sea
2089
2117
 
2090
2118
  | Flag | Description |
2091
2119
  |------------------|--------------------------------------------------------------------------------------------|
2092
- | `--type` | Content type: `photo \| vector \| psd` |
2093
- | `--orientation` | `landscape \| portrait \| square \| panoramic` |
2094
- | `--license` | `freemium` (free with attribution) or `premium` (paid, watermark-free) |
2095
- | `--color` | Hex color filter (`#0a0a0a` or `0a0a0a`) |
2096
- | `--ai` | AI-generated filter: `exclude` or `only` (default: no filter) |
2097
- | `--people` | People-in-image filter: `include` \| `exclude` \| `only` |
2098
- | `--order` | `relevance` (default) or `recent` |
2099
- | `--limit` | Max results (1–50, default 10) |
2120
+ | `--type` | `photo` (default, both libraries) \| `illustration` \| `vector` (Pixabay only) |
2121
+ | `--orientation` | `landscape \| portrait \| square` (`square` is Pexels only) |
2122
+ | `--size` | Minimum resolution: `large` (24MP) \| `medium` (12MP) \| `small` (4MP) |
2123
+ | `--color` | Colour name (`red`, `turquoise`, `black`, …; both libraries) or hex (`#0a0a0a`; Pexels only) |
2124
+ | `--locale` | Locale of the search terms, e.g. `es-ES`, `de-DE` Pexels matches translated tags |
2125
+ | `--limit` | Max results (1–80, default 10) |
2100
2126
  | `--page` | Page number for pagination |
2101
2127
  | `--auto-ingest` | Ingest top N hits (0–20, default 0) and return Baker-owned URLs on ingested hits |
2102
2128
  | `--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 +2352,11 @@ If the target page is unreachable or returns a non-2xx status (e.g. a 404 path o
2326
2352
  Download a remote URL and store it in the library. Byte-exact deduped via sha256 + externalId, then queued for describe + embed.
2327
2353
 
2328
2354
  ```bash
2329
- baker images ingest https://img.freepik.com/free-photo/xyz.jpg --source magnific --external-id 12345
2355
+ baker images ingest https://images.pexels.com/photos/13219418/pexels-photo-13219418.jpeg --source pexels --external-id 13219418
2330
2356
  baker images ingest https://acme.com/hero.png --source firecrawl --external-url https://acme.com/pricing --context "competitor pricing hero"
2331
2357
  ```
2332
2358
 
2333
- `--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `magnific`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
2359
+ `--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
2360
 
2335
2361
  Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing library row is returned — no new bytes are stored. Max ingest size 25MB.
2336
2362
 
@@ -2338,7 +2364,7 @@ Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing l
2338
2364
 
2339
2365
  | Flag | Description |
2340
2366
  |------------------|--------------------------------------------------------------------------------------------|
2341
- | `--source` | **Required.** Source enum (`uploaded`, `magnific`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `instagram`, …) |
2367
+ | `--source` | **Required.** Source enum (`uploaded`, `pexels`, `pixabay`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `instagram`, …) |
2342
2368
  | `--external-id` | Provider asset id — enables per-source dedup before bytes are fetched |
2343
2369
  | `--external-url` | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
2344
2370
  | `--context` | Free-text hint passed to Gemini describe to bias the generated description and tags |
@@ -2402,7 +2428,7 @@ Local-mode supported extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg
2402
2428
 
2403
2429
  | Flag | Mode | Description |
2404
2430
  |------------------|--------|--------------------------------------------------------------------------------------------|
2405
- | `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `magnific`, etc. to match the origin |
2431
+ | `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `pexels`, etc. to match the origin |
2406
2432
  | `--content-type` | local | MIME type override (default: inferred from file extension). Ignored for URLs |
2407
2433
  | `--external-id` | remote | Provider asset id — enables per-source dedup before bytes are fetched |
2408
2434
  | `--external-url` | remote | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
@@ -2729,6 +2755,15 @@ When `list` or `search` returns nothing, the envelope carries `meta.empty` expla
2729
2755
 
2730
2756
  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
2757
 
2758
+ `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`:
2759
+
2760
+ ```bash
2761
+ baker testimonials list --sentiment positive --limit 50
2762
+ baker testimonials list --sentiment positive --limit 50 --cursor <meta.next_cursor>
2763
+ ```
2764
+
2765
+ No `meta.next_cursor` means the library ended there.
2766
+
2732
2767
  ### `baker testimonials tags`
2733
2768
 
2734
2769
  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 +3039,7 @@ baker scheduled-actions create --template baker-ads --name "Weekly ad review" --
3004
3039
  Rules:
3005
3040
 
3006
3041
  - `--cron` and `--run-at` are mutually exclusive.
3007
- - `--run-at` must be an ISO UTC timestamp ending in `Z`.
3042
+ - `--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
3043
  - Draft-created ids use `temp_sched_*`; use them with `get`, `update`, or `delete` before publish.
3009
3044
  - Staged commands require `BAKER_CHAT_ID` to point to an `in_progress` chat.
3010
3045
  - `trigger` rejects `temp_sched_*` and only accepts published scheduled-action ids.
@@ -3480,7 +3515,6 @@ Reach for it when you are relaying the **client's** own words. When you are writ
3480
3515
 
3481
3516
  ```bash
3482
3517
  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
3518
  baker studio animate "handheld drift right, steam rising from the cup" \
3485
3519
  --image ./out/hero.png --duration 6 --quality 1080p
3486
3520
  baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
@@ -3630,40 +3664,6 @@ baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and br
3630
3664
  baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
3631
3665
  ```
3632
3666
 
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
- - **`cast`** — one description of the person the ad is about. The engine builds a reference
3645
- sheet from it and grounds every beat they appear in on that one identity. A beat sets
3646
- `cast: false` to opt out (a fitter on a roof, a product close-up).
3647
-
3648
- Each of these also removes a generation: the closing beat costs no image and no clip.
3649
-
3650
- #### The finished video is reviewed
3651
-
3652
- When a run's output is a video, `baker canvas run` samples twelve frames across it and
3653
- looks at them before reporting success. It reports, never blocks — the clips are already
3654
- paid for by the time it runs — and the findings come back as `review` and `hints`:
3655
-
3656
- - **Per frame:** anything that could not physically happen, or shows a job being done
3657
- incorrectly, plus objects floating unattached in shot. Malformed hands and limbs are
3658
- rolled up into one finding across the whole render rather than repeated per frame.
3659
- - **Across the reel:** whether the cast stays the same people, whether a brand mark is
3660
- visible anywhere, and whether the last frames close the ad or simply stop.
3661
-
3662
- It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
3663
- says the video was **not** reviewed rather than reporting it clean, and a frame that
3664
- could not be read is reported as unchecked for the same reason.
3665
-
3666
-
3667
3667
  > Scaffolded canvases use **workspace-relative** composition/asset paths and are validated
3668
3668
  > against the engine's hyperframe pre-render gate at `validate` time, so a broken overlay
3669
3669
  > (or a moved/renamed workspace) is caught before any billed clip. The scaffold also adds
@@ -4266,7 +4266,7 @@ There is no `prompt` param — the extraction schema is the node. Use `focus` to
4266
4266
 
4267
4267
  ##### `image_search`
4268
4268
 
4269
- Search the web for real images instead of generating them. A backend LLM agent picks among Google Images, stock photography (Freepik), 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.
4269
+ 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.
4270
4270
 
4271
4271
  **Inputs** — none.
4272
4272
 
@@ -5825,18 +5825,7 @@ This CLI is designed for AI agent consumption. Key patterns:
5825
5825
 
5826
5826
  - **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.
5827
5827
 
5828
- - **0.250.1**: `--avatar <handle>` on `baker studio generate` and `baker studio animate` never worked. Both were declared in `registerSchema` and read at the call site, but neither was in the command's own citty `args` — and citty does not reject an undeclared flag, it keeps it and sets it to `true`. So `--avatar elena` reached the route as `avatar: true` with `elena` dropped as a stray positional, and the body schema (`z.string()`) refused it. Documented since 0.217.0, broken since 0.217.0: casting an avatar into a render was not possible from the CLI at all, which is why a clip of a named presenter came back with neither the identity sheet nor the voice. Both flags are now declared. A new test in `command-reference-integrity` compares every registered schema against the args of the command that parses it, so the two cannot drift again — it found a third case on the way, `actions status`, whose schema advertised a `--refs` flag for what the command actually takes as positionals.
5829
- - **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.
5830
- - **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`.
5831
- - **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`.
5832
- - **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.
5833
- - **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.
5834
- - **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.
5835
- - **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%.
5836
- - **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.
5837
- - **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.
5838
- - **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.
5839
- - **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.
5828
+ ## Publishing
5840
5829
 
5841
5830
  ### Auto-publish (CI)
5842
5831
 
@@ -5849,6 +5838,30 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
5849
5838
  ./scripts/publish-package.sh cli next # Publish as @next (pre-release)
5850
5839
  ```
5851
5840
 
5841
+ - **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).
5842
+
5843
+ - **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.
5844
+
5845
+ - **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.
5846
+
5847
+ - **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.
5848
+
5849
+ - **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.
5850
+
5851
+ - **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.
5852
+
5853
+ - **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`.
5854
+
5855
+ - **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.
5856
+
5857
+ 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.
5858
+
5859
+ 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.
5860
+
5861
+ - **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.
5862
+
5863
+ - **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.
5864
+
5852
5865
  ### Testing a pre-release in sandboxes
5853
5866
 
5854
5867
  ```bash
@@ -35,10 +35,6 @@
35
35
 
36
36
  .caption-group {
37
37
  position: absolute;
38
- /* Absolutely positioned with no width, a card sizes to its content and
39
- runs off the 1080px frame rather than wrapping. Matches the container's
40
- 60px gutters, so a long clause becomes two lines instead of an overflow. */
41
- max-width: 960px;
42
38
  font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
43
39
  font-size: {{font_size}}px;
44
40
  font-weight: 900;
@@ -84,27 +80,10 @@
84
80
  const container = document.getElementById('captions');
85
81
  const tl = gsap.timeline({ paused: true });
86
82
 
87
- // A card closes where the SCRIPT closes, not on a word count. Slicing every
88
- // N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
89
- // preposition. The count stays as a ceiling (a long clause still has to be
90
- // split), and a linking word never ends a card: it belongs to what follows.
91
- const CLOSES = /[.,;:!?\u2026]$/;
92
- const CARRIES_ON = new Set([
93
- 'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
94
- 'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
95
- ]);
96
- const carriesOn = (w) => CARRIES_ON.has(String(w.text || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
97
-
98
83
  const groups = [];
99
- let current = [];
100
- for (const word of transcript) {
101
- current.push(word);
102
- const last = current[current.length - 1];
103
- const closes = CLOSES.test(String(last.text || '')) ||
104
- (current.length >= WORDS_PER_GROUP && !carriesOn(last));
105
- if (closes) { groups.push(current); current = []; }
84
+ for (let i = 0; i < transcript.length; i += WORDS_PER_GROUP) {
85
+ groups.push(transcript.slice(i, i + WORDS_PER_GROUP));
106
86
  }
107
- if (current.length > 0) groups.push(current);
108
87
 
109
88
  groups.forEach((group, gi) => {
110
89
  const el = document.createElement('div');
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debugLogSetting
3
- } from "./chunk-WFWU3CHS.js";
3
+ } from "./chunk-DZUVUGEP.js";
4
4
 
5
5
  // src/debugLog.ts
6
6
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
@@ -150,4 +150,4 @@ export {
150
150
  readBodyForLog,
151
151
  installStreamTaps
152
152
  };
153
- //# sourceMappingURL=chunk-2IUN3S3E.js.map
153
+ //# sourceMappingURL=chunk-2IB3IL4K.js.map