@koda-sl/baker-cli 0.270.1-dev.1f1c09c80 → 0.270.3-dev.e350bbc93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
 
@@ -1217,6 +1232,10 @@ baker analytics tracking # do the ad URLs carry the campaign, a
1217
1232
  baker analytics tracking --platform meta # the same for one platform, read from their own ad account
1218
1233
  baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
1219
1234
  baker analytics map --platform google --remove kw # take that answer back
1235
+ baker analytics conversions # what counts as a conversion, and what each one produced
1236
+ baker analytics conversions --candidates # everything these pages do, and which of it is counted
1237
+ baker analytics conversions --event page:request_demo --name "Demo requested"
1238
+ baker analytics conversions --remove page:request_demo
1220
1239
  baker analytics delivery --page 2 # the next page of a long list
1221
1240
  baker analytics submissions --flow contact # every attempt at a Form, delivered or not
1222
1241
  ```
@@ -1229,7 +1248,9 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1229
1248
  - 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
1249
  - Past ~25 rows the response carries a hint naming the row count and pointing at `--output md`.
1231
1250
 
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 People 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.
1251
+ **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.
1252
+
1253
+ **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
1254
 
1234
1255
  **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
1256
 
@@ -1977,7 +1998,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
1977
1998
  |---|---|---|
1978
1999
  | `baker images library <q>` | Hybrid library search (replaces `search`) | n/a |
1979
2000
  | `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 |
2001
+ | `baker images stock <q> [--type photo\|illustration\|vector]` | Free stock across two libraries Pexels photographs, Pixabay illustrations and vectors | off |
1981
2002
  | `baker images google <q>` | Google Images via the official Custom Search JSON API | off |
1982
2003
  | `baker images pinterest <q>` | Pinterest reference imagery via ScrapeCreators (photo-real mood boards) | off |
1983
2004
  | `baker images logo <domain>` | Brand logo via Brandfetch CDN | **on** (top 1) |
@@ -2002,7 +2023,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
2002
2023
 
2003
2024
  **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
2025
 
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.
2026
+ **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
2027
 
2007
2028
  **`--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
2029
 
@@ -2017,6 +2038,8 @@ baker images screenshot https://competitor.com/pricing --context "Competitor pri
2017
2038
 
2018
2039
  **`baker images search` is a deprecated alias** for `library` (stderr deprecation log, removed in next minor).
2019
2040
 
2041
+ **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.
2042
+
2020
2043
  **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
2044
 
2022
2045
  ### `baker images library <query>`
@@ -2026,7 +2049,7 @@ Search the company library (hybrid BM25 + vector + Cohere rerank). Use this FIRS
2026
2049
  ```bash
2027
2050
  baker images library "hero banner"
2028
2051
  baker images library "logo" --aspect-ratio 1:1 --tags logo
2029
- baker images library "office" --source magnific
2052
+ baker images library "office" --source pexels
2030
2053
  baker images library "pricing" --external-url-host competitor.com
2031
2054
  ```
2032
2055
 
@@ -2038,7 +2061,7 @@ baker images library "pricing" --external-url-host competitor.com
2038
2061
  | `--min-score` | Minimum relevance score, 0-1 |
2039
2062
  | `--aspect-ratio` | Filter by aspect ratio |
2040
2063
  | `--tags` | Comma-separated tag names |
2041
- | `--source` | Filter by source (uploaded, magnific, brandfetch, google_images, firecrawl, screenshotone, iconify, giphy, …) |
2064
+ | `--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
2065
  | `--external-url-host` | Filter by host substring of `externalUrl` (e.g. competitor.com) |
2043
2066
  | `--output` | Output format: `json` \| `files` \| `md` |
2044
2067
  | `--fields` | Comma-separated field names to include |
@@ -2051,12 +2074,12 @@ baker images library "pricing" --external-url-host competitor.com
2051
2074
  Fanout image search: library first, then opted-in providers in parallel.
2052
2075
 
2053
2076
  ```bash
2054
- baker images find "office" --sources library,magnific --limit 20
2055
- baker images find "office" --sources library,magnific --fallback --threshold 0.4
2077
+ baker images find "office" --sources library,pexels,pixabay --limit 20
2078
+ baker images find "office" --sources library,pexels --fallback --threshold 0.4
2056
2079
  baker images find "celebration" --sources library,giphy --auto-ingest 3
2057
2080
  ```
2058
2081
 
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.
2082
+ 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
2083
 
2061
2084
  **Flags:**
2062
2085
 
@@ -2071,17 +2094,28 @@ Providers: `library`, `magnific`, `google`, `iconify`, `giphy`, `pinterest`. Bra
2071
2094
 
2072
2095
  ### `baker images stock <query>`
2073
2096
 
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.
2097
+ `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.
2098
+
2099
+ 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
2100
 
2076
2101
  ```bash
2077
2102
  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
2103
+ baker images stock "hero photo of a kitchen" --orientation landscape --size large
2104
+ baker images stock "flat office workers" --type illustration
2105
+ baker images stock "leaf outline mark" --type vector
2106
+ baker images stock "brand pattern" --color "#0a0a0a" --auto-ingest 2
2107
+ baker images stock "oficina luminosa" --locale es-ES
2082
2108
  ```
2083
2109
 
2084
- Free tier exists but watermarks previewspass `--license freemium` to filter to clean free assets explicitly.
2110
+ **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.
2111
+
2112
+ **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".
2113
+
2114
+ 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.
2115
+
2116
+ **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.
2117
+
2118
+ 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
2119
 
2086
2120
  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
2121
 
@@ -2089,14 +2123,12 @@ An empty result comes back with `hints` naming the filters that narrowed the sea
2089
2123
 
2090
2124
  | Flag | Description |
2091
2125
  |------------------|--------------------------------------------------------------------------------------------|
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) |
2126
+ | `--type` | `photo` (default, both libraries) \| `illustration` \| `vector` (Pixabay only) |
2127
+ | `--orientation` | `landscape \| portrait \| square` (`square` is Pexels only) |
2128
+ | `--size` | Minimum resolution: `large` (24MP) \| `medium` (12MP) \| `small` (4MP) |
2129
+ | `--color` | Colour name (`red`, `turquoise`, `black`, …; both libraries) or hex (`#0a0a0a`; Pexels only) |
2130
+ | `--locale` | Locale of the search terms, e.g. `es-ES`, `de-DE` Pexels matches translated tags |
2131
+ | `--limit` | Max results (1–80, default 10) |
2100
2132
  | `--page` | Page number for pagination |
2101
2133
  | `--auto-ingest` | Ingest top N hits (0–20, default 0) and return Baker-owned URLs on ingested hits |
2102
2134
  | `--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 +2358,11 @@ If the target page is unreachable or returns a non-2xx status (e.g. a 404 path o
2326
2358
  Download a remote URL and store it in the library. Byte-exact deduped via sha256 + externalId, then queued for describe + embed.
2327
2359
 
2328
2360
  ```bash
2329
- baker images ingest https://img.freepik.com/free-photo/xyz.jpg --source magnific --external-id 12345
2361
+ baker images ingest https://images.pexels.com/photos/13219418/pexels-photo-13219418.jpeg --source pexels --external-id 13219418
2330
2362
  baker images ingest https://acme.com/hero.png --source firecrawl --external-url https://acme.com/pricing --context "competitor pricing hero"
2331
2363
  ```
2332
2364
 
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.
2365
+ `--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
2366
 
2335
2367
  Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing library row is returned — no new bytes are stored. Max ingest size 25MB.
2336
2368
 
@@ -2338,7 +2370,7 @@ Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing l
2338
2370
 
2339
2371
  | Flag | Description |
2340
2372
  |------------------|--------------------------------------------------------------------------------------------|
2341
- | `--source` | **Required.** Source enum (`uploaded`, `magnific`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `instagram`, …) |
2373
+ | `--source` | **Required.** Source enum (`uploaded`, `pexels`, `pixabay`, `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `instagram`, …) |
2342
2374
  | `--external-id` | Provider asset id — enables per-source dedup before bytes are fetched |
2343
2375
  | `--external-url` | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
2344
2376
  | `--context` | Free-text hint passed to Gemini describe to bias the generated description and tags |
@@ -2402,7 +2434,7 @@ Local-mode supported extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg
2402
2434
 
2403
2435
  | Flag | Mode | Description |
2404
2436
  |------------------|--------|--------------------------------------------------------------------------------------------|
2405
- | `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `magnific`, etc. to match the origin |
2437
+ | `--source` | both | Source enum (default `uploaded`). For remote URLs prefer `firecrawl`, `pexels`, etc. to match the origin |
2406
2438
  | `--content-type` | local | MIME type override (default: inferred from file extension). Ignored for URLs |
2407
2439
  | `--external-id` | remote | Provider asset id — enables per-source dedup before bytes are fetched |
2408
2440
  | `--external-url` | remote | Canonical page URL (parent page for scraped/Google hits, provider page for stock) |
@@ -2729,6 +2761,15 @@ When `list` or `search` returns nothing, the envelope carries `meta.empty` expla
2729
2761
 
2730
2762
  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
2763
 
2764
+ `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`:
2765
+
2766
+ ```bash
2767
+ baker testimonials list --sentiment positive --limit 50
2768
+ baker testimonials list --sentiment positive --limit 50 --cursor <meta.next_cursor>
2769
+ ```
2770
+
2771
+ No `meta.next_cursor` means the library ended there.
2772
+
2732
2773
  ### `baker testimonials tags`
2733
2774
 
2734
2775
  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).
@@ -2779,7 +2820,7 @@ baker avatars create --name "Marta" --subject "…" \
2779
2820
  --source-image "<imageId1>,<imageId2>" --likeness-confirmed
2780
2821
  ```
2781
2822
 
2782
- Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--voice-id`, `--voice-description`. Returns `{ avatarId, handle, likeness, status }`.
2823
+ Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--accent`. Returns `{ avatarId, handle, likeness, status }`.
2783
2824
 
2784
2825
  #### `baker avatars update <handle>`
2785
2826
 
@@ -2788,7 +2829,7 @@ Edit an avatar. Profile flags are merged over the stored profile, so `--persona`
2788
2829
  ```bash
2789
2830
  baker avatars update marta --wardrobe "charcoal blazer over a white tee"
2790
2831
  baker avatars update marta --subject "woman in her early 50s, silver bob, …" --regenerate-sheet
2791
- baker avatars update marta --new-handle marta-founder --voice-id <voiceId>
2832
+ baker avatars update marta --new-handle marta-founder --accent "Spanish from Spain, Madrid accent"
2792
2833
  ```
2793
2834
 
2794
2835
  #### `baker avatars delete <handle>`
@@ -3004,7 +3045,7 @@ baker scheduled-actions create --template baker-ads --name "Weekly ad review" --
3004
3045
  Rules:
3005
3046
 
3006
3047
  - `--cron` and `--run-at` are mutually exclusive.
3007
- - `--run-at` must be an ISO UTC timestamp ending in `Z`.
3048
+ - `--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
3049
  - Draft-created ids use `temp_sched_*`; use them with `get`, `update`, or `delete` before publish.
3009
3050
  - Staged commands require `BAKER_CHAT_ID` to point to an `in_progress` chat.
3010
3051
  - `trigger` rejects `temp_sched_*` and only accepts published scheduled-action ids.
@@ -3648,6 +3689,18 @@ because a video model garbles a wordmark every time:
3648
3689
  the ad needs something other than the client's default.
3649
3690
  - **The spoken language is read off the script** when the spec does not declare it, so a
3650
3691
  Spanish ad is cast from Spanish voices without anyone having to say so.
3692
+ - **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
3693
+ alongside the music bed. Most beats should not have one: it belongs where the picture
3694
+ shows a specific physical event (a drill, a door, rain on glass), not on a talking head
3695
+ or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
3696
+ - **A real brand mark is required.** `scaffold-ad` refuses to build without one in
3697
+ `src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
3698
+ card, so a stand-in ships an ad wearing the wrong brand.
3699
+ - **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
3700
+ a separately cast voice, which is dubbing and looks like it.
3701
+ - **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
3702
+ become on-screen text and are captioned straight from the script, so the words are
3703
+ exact. Give it `music` too.
3651
3704
  - **`--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.
3652
3705
  - **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
3653
3706
  every beat they appear in on that avatar's identity sheet and copies its subject
@@ -3669,6 +3722,9 @@ paid for by the time it runs — and the findings come back as `review` and `hin
3669
3722
  - **Across the reel:** whether the cast stays the same people, whether a brand mark is
3670
3723
  visible anywhere, and whether the last frames close the ad or simply stop.
3671
3724
 
3725
+ Each per-frame finding names the **scene file** that produced that second, so a defect is
3726
+ an edit rather than a hunt — and re-running re-renders only the scenes you changed.
3727
+
3672
3728
  It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
3673
3729
  says the video was **not** reviewed rather than reporting it clean, and a frame that
3674
3730
  could not be read is reported as unchecked for the same reason.
@@ -4276,7 +4332,7 @@ There is no `prompt` param — the extraction schema is the node. Use `focus` to
4276
4332
 
4277
4333
  ##### `image_search`
4278
4334
 
4279
- 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.
4335
+ 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.
4280
4336
 
4281
4337
  **Inputs** — none.
4282
4338
 
@@ -5859,6 +5915,30 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
5859
5915
  ./scripts/publish-package.sh cli next # Publish as @next (pre-release)
5860
5916
  ```
5861
5917
 
5918
+ - **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).
5919
+
5920
+ - **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.
5921
+
5922
+ - **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.
5923
+
5924
+ - **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.
5925
+
5926
+ - **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.
5927
+
5928
+ - **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.
5929
+
5930
+ - **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`.
5931
+
5932
+ - **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.
5933
+
5934
+ 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.
5935
+
5936
+ 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.
5937
+
5938
+ - **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.
5939
+
5940
+ - **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.
5941
+
5862
5942
  ### Testing a pre-release in sandboxes
5863
5943
 
5864
5944
  ```bash
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-P2PEABMQ.js";
4
+ } from "./chunk-KDTHRRAC.js";
5
5
  import {
6
6
  getEnv
7
7
  } from "./chunk-WFWU3CHS.js";
@@ -295,4 +295,4 @@ export {
295
295
  writeAdsOutput,
296
296
  resolveCustomerId
297
297
  };
298
- //# sourceMappingURL=chunk-XAUTT2Q6.js.map
298
+ //# sourceMappingURL=chunk-ARUM5SYE.js.map
@@ -2,10 +2,10 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-XAUTT2Q6.js";
5
+ } from "./chunk-ARUM5SYE.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-P2PEABMQ.js";
8
+ } from "./chunk-KDTHRRAC.js";
9
9
  import {
10
10
  getEnv
11
11
  } from "./chunk-WFWU3CHS.js";
@@ -108,4 +108,4 @@ export {
108
108
  csvOrJson,
109
109
  resolveEffectiveStatus
110
110
  };
111
- //# sourceMappingURL=chunk-RHEQTHT7.js.map
111
+ //# sourceMappingURL=chunk-IK3GODKH.js.map
@@ -79,6 +79,9 @@ function mapHttpError(status) {
79
79
  if (status === 422 || status === 400) {
80
80
  return "VALIDATION_ERROR";
81
81
  }
82
+ if (status === 409) {
83
+ return "CONFLICT";
84
+ }
82
85
  if (status === 429) {
83
86
  return "RATE_LIMITED";
84
87
  }
@@ -107,7 +110,7 @@ async function handleResponse(response) {
107
110
  throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
108
111
  }
109
112
  }
110
- async function apiGet(path, params) {
113
+ async function apiGetWithHeaders(path, params) {
111
114
  const env = getEnv();
112
115
  const url = new URL(path, env.BAKER_API_URL);
113
116
  if (params) {
@@ -145,7 +148,10 @@ async function apiGet(path, params) {
145
148
  responseBody: await readBodyForLog(response),
146
149
  durationMs: Date.now() - startedAt
147
150
  });
148
- return handleResponse(response);
151
+ return { data: await handleResponse(response), headers: response.headers };
152
+ }
153
+ async function apiGet(path, params) {
154
+ return (await apiGetWithHeaders(path, params)).data;
149
155
  }
150
156
  async function apiPost(path, body, opts) {
151
157
  const env = getEnv();
@@ -195,7 +201,8 @@ async function apiPost(path, body, opts) {
195
201
  export {
196
202
  ApiError,
197
203
  validateConvexId,
204
+ apiGetWithHeaders,
198
205
  apiGet,
199
206
  apiPost
200
207
  };
201
- //# sourceMappingURL=chunk-P2PEABMQ.js.map
208
+ //# sourceMappingURL=chunk-KDTHRRAC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { debugLogHttp, readBodyForLog } from \"./debugLog.ts\";\nimport { getEnv } from \"./env.ts\";\n\nconst MAX_RATE_LIMIT_RETRIES = 3;\nconst MAX_TOTAL_WAIT_MS = 2 * 60 * 1000;\n\nasync function fetchWithRateLimitRetry(url: string, init: RequestInit): Promise<Response> {\n let totalWaited = 0;\n\n for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {\n const response = await fetch(url, init);\n\n if (response.status !== 429 || attempt >= MAX_RATE_LIMIT_RETRIES) {\n return response;\n }\n\n const retryAfterHeader = response.headers.get(\"Retry-After\");\n const waitMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : 2000 * 2 ** attempt;\n\n if (totalWaited + waitMs > MAX_TOTAL_WAIT_MS) {\n return response;\n }\n\n totalWaited += waitMs;\n await new Promise((resolve) => setTimeout(resolve, waitMs));\n }\n\n return fetch(url, init);\n}\n\ntype ErrorCode =\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"VALIDATION_ERROR\"\n | \"RATE_LIMITED\"\n | \"INTERNAL_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"IMAGE_PROCESSING_ERROR\";\n\nexport class ApiError extends Error {\n code: ErrorCode;\n\n constructor(code: ErrorCode, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.code = code;\n }\n}\n\nconst CONVEX_ID_RE = /^[a-zA-Z0-9_]+$/;\n\nfunction hasControlCharacters(value: string): boolean {\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n // Allow tab (9), newline (10), carriage return (13)\n if (code < 32 && code !== 9 && code !== 10 && code !== 13) {\n return true;\n }\n }\n return false;\n}\n\nfunction validateStringValue(value: string): void {\n if (hasControlCharacters(value)) {\n throw new ApiError(\"VALIDATION_ERROR\", \"String value contains invalid control characters\");\n }\n}\n\nexport function validateConvexId(id: string): void {\n if (!CONVEX_ID_RE.test(id)) {\n throw new ApiError(\"VALIDATION_ERROR\", `Invalid ID format: \"${id}\". Expected alphanumeric string.`);\n }\n}\n\nfunction sanitizeParams(params: Record<string, string>): Record<string, string> {\n const sanitized: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n validateStringValue(value);\n sanitized[key] = value;\n }\n return sanitized;\n}\n\n/**\n * Sent on every call so the backend can attribute immediately-applied writes\n * (library images and videos, followed advertisers) to the Session they were\n * made from. Absent outside a chat-attached environment.\n */\nfunction chatHeader(): Record<string, string> {\n const chatId = getEnv().BAKER_CHAT_ID;\n return chatId ? { \"x-baker-chat-id\": chatId } : {};\n}\n\nfunction mapHttpError(status: number): ErrorCode {\n if (status === 401) {\n return \"UNAUTHORIZED\";\n }\n // 403 is never \"not connected\" — every backend `FORBIDDEN` means connected but\n // not allowed (an asset outside the granted scope, a capability the account\n // lacks). Folding it into UNAUTHORIZED sent agents down the reconnect path,\n // which cannot fix a permission gap and wastes the user's time.\n if (status === 403) {\n return \"FORBIDDEN\";\n }\n if (status === 404) {\n return \"NOT_FOUND\";\n }\n if (status === 422 || status === 400) {\n return \"VALIDATION_ERROR\";\n }\n // 409 is a state the caller has to change, never a fault to sit out. It is\n // what the backend returns for \"this workspace has not switched analytics on\n // yet\" and its siblings — folded into INTERNAL_ERROR, that reaches an agent\n // as a transient failure worth retrying, which it never is.\n if (status === 409) {\n return \"CONFLICT\";\n }\n if (status === 429) {\n return \"RATE_LIMITED\";\n }\n return \"INTERNAL_ERROR\";\n}\n\nasync function handleResponse<T>(response: Response): Promise<T> {\n const body = await response.text();\n\n if (!response.ok) {\n let message = `HTTP ${response.status}: ${response.statusText}`;\n try {\n const parsed = JSON.parse(body) as { error?: string | { message?: string }; message?: string };\n if (typeof parsed.error === \"string\") {\n message = parsed.error;\n } else if (parsed.error?.message) {\n message = parsed.error.message;\n } else if (parsed.message) {\n message = parsed.message;\n }\n } catch {\n // Use default message\n }\n throw new ApiError(mapHttpError(response.status), message);\n }\n\n try {\n return JSON.parse(body) as T;\n } catch {\n throw new ApiError(\"INTERNAL_ERROR\", \"Failed to parse API response as JSON\");\n }\n}\n\n/**\n * A GET that also hands back the response headers.\n *\n * Some endpoints have to say something *about* the answer that the answer has\n * no room for — `GET /api/testimonials` returns a bare array and sets\n * `X-Baker-Next-Cursor` when rows past this page remain unexamined, so the rows\n * alone cannot tell one page of a library from the whole of it.\n */\nexport async function apiGetWithHeaders<T>(\n path: string,\n params?: Record<string, string>,\n): Promise<{ data: T; headers: Headers }> {\n const env = getEnv();\n const url = new URL(path, env.BAKER_API_URL);\n if (params) {\n const clean = sanitizeParams(params);\n for (const [key, value] of Object.entries(clean)) {\n url.searchParams.set(key, value);\n }\n }\n\n const urlStr = url.toString();\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(urlStr, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n Accept: \"application/json\",\n ...chatHeader(),\n },\n signal: AbortSignal.timeout(60_000),\n });\n } catch (err) {\n debugLogHttp({ source: \"cli\", method: \"GET\", url: urlStr, durationMs: Date.now() - startedAt, error: err });\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", \"Request timed out after 60 seconds\");\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n debugLogHttp({\n source: \"cli\",\n method: \"GET\",\n url: urlStr,\n status: response.status,\n ok: response.ok,\n responseBody: await readBodyForLog(response),\n durationMs: Date.now() - startedAt,\n });\n return { data: await handleResponse<T>(response), headers: response.headers };\n}\n\nexport async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> {\n return (await apiGetWithHeaders<T>(path, params)).data;\n}\n\nexport async function apiPost<T>(path: string, body: unknown, opts?: { timeoutMs?: number }): Promise<T> {\n const env = getEnv();\n const timeoutMs = opts?.timeoutMs ?? 60_000;\n const urlStr = new URL(path, env.BAKER_API_URL).toString();\n const startedAt = Date.now();\n let response: Response;\n try {\n response = await fetchWithRateLimitRetry(urlStr, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${env.BAKER_API_KEY}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...chatHeader(),\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (err) {\n debugLogHttp({\n source: \"cli\",\n method: \"POST\",\n url: urlStr,\n requestBody: body,\n durationMs: Date.now() - startedAt,\n error: err,\n });\n if (err instanceof Error && (err.name === \"TimeoutError\" || err.name === \"AbortError\")) {\n throw new ApiError(\"TIMEOUT\", `Request timed out after ${Math.round(timeoutMs / 1000)} seconds`);\n }\n throw new ApiError(\"NETWORK_ERROR\", `Request failed: ${err instanceof Error ? err.message : \"Unknown error\"}`);\n }\n\n debugLogHttp({\n source: \"cli\",\n method: \"POST\",\n url: urlStr,\n requestBody: body,\n status: response.status,\n ok: response.ok,\n responseBody: await readBodyForLog(response),\n durationMs: Date.now() - startedAt,\n });\n return handleResponse<T>(response);\n}\n"],"mappings":"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","names":[]}
@@ -17,9 +17,9 @@ import {
17
17
  shouldEscalate
18
18
  } from "./chunk-WFWU3CHS.js";
19
19
 
20
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
20
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js
21
21
  var require_safe_stable_stringify = __commonJS({
22
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js"(exports, module) {
23
23
  "use strict";
24
24
  var { hasOwnProperty } = Object.prototype;
25
25
  var stringify = configure2();
@@ -1098,7 +1098,7 @@ function resolveAdaptFormats(params) {
1098
1098
  return params.formats ?? [];
1099
1099
  }
1100
1100
 
1101
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
1101
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/esm/wrapper.js
1102
1102
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1103
1103
  var configure = import__.default.configure;
1104
1104
  var wrapper_default = import__.default;
@@ -1139,6 +1139,14 @@ function normalizeForCanonical(value) {
1139
1139
  return void 0;
1140
1140
  }
1141
1141
 
1142
+ // ../canvas-contract/src/frameRealism.ts
1143
+ function frameRealismDirection(opts = {}) {
1144
+ return " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script. Hands are kept simple: no close-up of fingers manipulating small parts, no hand gripping the edge of an object, and each person has exactly TWO arms and TWO legs, all attached and all visible or all out of frame. Anyone working does so the way the trade actually does it: nobody stands or kneels on the equipment being installed, nothing is fitted overhanging an edge or floating unsupported, and every part rests on the structure that would really carry it." + // The newest clause, and the one no route had. A testimonial came back with the customer
1145
+ // repairing the panel herself: nothing said she was not the installer, so the model cast
1146
+ // her as one. Who someone IS in the picture has to be stated, or it is guessed.
1147
+ (opts.role ? ` The person on camera is ${opts.role} \u2014 they are shown as that and never doing somebody else's job.` : "") + (opts.currency ? ` If a currency is unavoidably visible it is ${opts.currency}.` : "");
1148
+ }
1149
+
1142
1150
  // ../canvas-contract/src/registry.ts
1143
1151
  var OPENROUTER_IMAGE_AR = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
1144
1152
  var OPENROUTER_IMAGE_AR_EXTREME = [...OPENROUTER_IMAGE_AR, "1:4", "4:1", "1:8", "8:1"];
@@ -3141,7 +3149,7 @@ var VEO_DURATIONS = [4, 6, 8];
3141
3149
  var KLING_DURATIONS2 = [5, 10];
3142
3150
  var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
3143
3151
  var KLING_CFG_SCALE = 0.7;
3144
- var SPEAKS_PROSE = (line) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues). Dialogue: "${line}"`;
3152
+ var SPEAKS_PROSE = (line, accent) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues).${accent ? ` The speech is in ${accent}.` : ""} Dialogue: "${line}"`;
3145
3153
  var SEEDANCE_PROFILE = {
3146
3154
  id: "seedance",
3147
3155
  dialogueDirective: SPEAKS_PROSE,
@@ -7349,17 +7357,18 @@ var GATE_SECTIONS = ["lint", "runtime", "layout", "motion", "contrast"];
7349
7357
  function buildCheckArgs(dir, samples) {
7350
7358
  return ["hyperframes", "check", dir, "--json", "--samples", String(samples), "--at-transitions"];
7351
7359
  }
7352
- var PROMOTE_TO_BLOCKING = /GSAP target [^ ]+ not found/i;
7360
+ var PROMOTE_TO_BLOCKING = /GSAP target [^ ]+ not found|no deterministic font mapping/i;
7353
7361
  function toGateFinding(section, entry) {
7354
7362
  const f = entry;
7355
7363
  const code = typeof f.code === "string" ? f.code : "unknown";
7356
7364
  const message2 = typeof f.message === "string" ? f.message : "";
7357
7365
  const isError = f.severity === "error" || PROMOTE_TO_BLOCKING.test(message2);
7366
+ const fatal = PROMOTE_TO_BLOCKING.test(message2);
7358
7367
  return {
7359
7368
  source: section,
7360
7369
  code,
7361
7370
  message: message2,
7362
- severity: isError && !isAdvisory(code, message2) ? "blocking" : "warning"
7371
+ severity: fatal || isError && !isAdvisory(code, message2) ? "blocking" : "warning"
7363
7372
  };
7364
7373
  }
7365
7374
  function classifyGateOutput(raw) {
@@ -8477,7 +8486,7 @@ var imageSearchNode = delegated({
8477
8486
  id: "image_search",
8478
8487
  version: "1.0.0",
8479
8488
  category: "image",
8480
- summary: "Agentic image search across Google Images, stock photography (Freepik), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
8489
+ summary: "Agentic image search across Google Images, free stock photography (Pexels and Pixabay), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
8481
8490
  when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
8482
8491
  inputs: z23.object({}).loose(),
8483
8492
  params: ImageSearchParams,
@@ -9044,7 +9053,14 @@ async function tryExtractAudio(inputs, ctx) {
9044
9053
  import { z as z34 } from "zod";
9045
9054
  var voiceSelectNode = delegated({
9046
9055
  id: "voice_select",
9047
- version: "1.0.0",
9056
+ // 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
9057
+ // and no mime, which every consumer taking the id as an INPUT rejected —
9058
+ // `audio_voice_convert` died on `voice_ref.sha256: expected string, received undefined`.
9059
+ // The backend now uploads it as a real asset like the ranked branch, and the version has
9060
+ // to move with it: the cache key is built from this string, so entries written under
9061
+ // 1.0.0 kept replaying the broken shape long after the fix was deployed. A node whose
9062
+ // OUTPUT shape changes has to bump, not only one whose behaviour does.
9063
+ version: "1.1.0",
9048
9064
  category: "audio",
9049
9065
  summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
9050
9066
  when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
@@ -9055,6 +9071,11 @@ var voiceSelectNode = delegated({
9055
9071
  age: z34.string().optional(),
9056
9072
  accent: z34.string().optional(),
9057
9073
  language: z34.string().optional(),
9074
+ /**
9075
+ * A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
9076
+ * ranking is skipped and this id is emitted as-is.
9077
+ */
9078
+ voice_id: z34.string().min(1).max(120).optional(),
9058
9079
  limit: z34.number().int().min(1).max(20).optional()
9059
9080
  }).strict(),
9060
9081
  outputs: z34.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
@@ -9217,6 +9238,7 @@ export {
9217
9238
  describeFailureReason,
9218
9239
  AD_FORMAT_PLATFORMS,
9219
9240
  platformFormats,
9241
+ frameRealismDirection,
9220
9242
  SEEDANCE_DURATIONS,
9221
9243
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
9222
9244
  IMAGE_GENERATE_MODELS,
@@ -9272,4 +9294,4 @@ export {
9272
9294
  defaultRegistry,
9273
9295
  createEngineFromEnv
9274
9296
  };
9275
- //# sourceMappingURL=chunk-SZJWEOW2.js.map
9297
+ //# sourceMappingURL=chunk-LJ5DIC4F.js.map