@koda-sl/baker-cli 0.125.0 → 0.128.0-dev.70bf43ce4

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
@@ -223,8 +223,6 @@ baker ads google query --list-presets
223
223
  | `keyword-analysis` | Keyword performance with match type | LAST_30_DAYS |
224
224
  | `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
225
225
  | `negative-keywords` | Negative (blocking) keywords only | ALL_TIME |
226
- | `negative-keyword-lists` | Shared negative lists + their terms | ALL_TIME |
227
- | `negative-list-attachments`| Which campaigns each shared list covers | ALL_TIME |
228
226
  | `search-terms` | Actual user queries triggering ads | LAST_7_DAYS |
229
227
  | `ad-copy-performance` | Ad headline/description effectiveness | LAST_30_DAYS |
230
228
  | `asset-performance` | PMax asset performance labels | LAST_30_DAYS |
@@ -399,6 +397,262 @@ baker ads google keywords metrics --customer-id 1234567890 --keywords "running s
399
397
 
400
398
  ---
401
399
 
400
+ ### Google Ads Library (`baker ads google library`)
401
+
402
+ Manage and search the Google Ads Transparency Center. Track competitor advertisers, browse their ad creatives, and discover who's bidding on keywords.
403
+
404
+ **Typical workflow:** `search-advertiser` → `track` → `search-ads`
405
+
406
+ ---
407
+
408
+ ### `baker ads google library search-advertiser "query"`
409
+
410
+ Search for an advertiser on the Google Ads Transparency Center.
411
+
412
+ > **Recommended:** use the domain running the ads (e.g. `example.com`) for more accurate results.
413
+
414
+ ```bash
415
+ baker ads google library search-advertiser "example.com"
416
+ baker ads google library search-advertiser "Nike"
417
+ ```
418
+
419
+ **Response:**
420
+
421
+ ```json
422
+ {
423
+ "ok": true,
424
+ "data": {
425
+ "results": [
426
+ { "advertiserId": "AR12345678901234567", "name": "Nike, Inc.", "region": "US", "format": "TEXT_IMAGE_VIDEO" }
427
+ ]
428
+ }
429
+ }
430
+ ```
431
+
432
+ **Flags:**
433
+
434
+ | Flag | Description |
435
+ |------------|--------------------------------|
436
+ | `--output` | Format: `json` \| `csv` \| `md` |
437
+
438
+ ---
439
+
440
+ ### `baker ads google library track <id> <name>`
441
+
442
+ Track a new Google advertiser and wait for the initial ad sync to complete. Polls every 5 seconds with a 10-minute timeout. Progress is written to stderr.
443
+
444
+ ```bash
445
+ baker ads google library track AR12345678901234567 "Nike, Inc."
446
+ baker ads google library track AR12345678901234567 "Nike, Inc." --json
447
+ ```
448
+
449
+ **Response (with `--json`):**
450
+
451
+ ```json
452
+ {
453
+ "ok": true,
454
+ "data": {
455
+ "advertiserId": "ar_abc123",
456
+ "accountId": "acc_def456",
457
+ "totalAdCount": 342,
458
+ "activeAdCount": 89
459
+ }
460
+ }
461
+ ```
462
+
463
+ **Flags:**
464
+
465
+ | Flag | Description |
466
+ |----------|----------------------|
467
+ | `--json` | Output in JSON format |
468
+
469
+ ---
470
+
471
+ ### `baker ads google library list-advertisers`
472
+
473
+ List all tracked Google advertisers and their accounts.
474
+
475
+ ```bash
476
+ baker ads google library list-advertisers
477
+ baker ads google library list-advertisers --output md
478
+ ```
479
+
480
+ **Flags:**
481
+
482
+ | Flag | Description |
483
+ |------------|--------------------------------|
484
+ | `--output` | Format: `json` \| `csv` \| `md` |
485
+
486
+ ---
487
+
488
+ ### `baker ads google library sync-status <accountId>`
489
+
490
+ Check the sync status and ad counts of a tracked account.
491
+
492
+ ```bash
493
+ baker ads google library sync-status acc_def456
494
+ ```
495
+
496
+ **Response:**
497
+
498
+ ```json
499
+ {
500
+ "ok": true,
501
+ "data": {
502
+ "syncStatus": null,
503
+ "totalAdCount": 342,
504
+ "activeAdCount": 89
505
+ }
506
+ }
507
+ ```
508
+
509
+ `syncStatus` is `null` when idle, `"syncing"` during a sync, or `"error"` if the last sync failed.
510
+
511
+ ---
512
+
513
+ ### `baker ads google library search-ads <accountId>`
514
+
515
+ Search and filter ads for a tracked account. Supports pagination.
516
+
517
+ ```bash
518
+ baker ads google library search-ads acc_def456
519
+ baker ads google library search-ads acc_def456 --search "summer sale" --isActive --mediaType image
520
+ baker ads google library search-ads acc_def456 --sort newest --limit 50
521
+ baker ads google library search-ads acc_def456 --cursor "eyJwYWdl..."
522
+ ```
523
+
524
+ **Response:**
525
+
526
+ ```json
527
+ {
528
+ "ok": true,
529
+ "data": {
530
+ "page": [
531
+ {
532
+ "_id": "abc123",
533
+ "platform": "google",
534
+ "externalId": "CR_1234567890",
535
+ "isActive": true,
536
+ "mediaType": "image",
537
+ "headline": "Summer Sale — 50% Off Everything",
538
+ "description": "Shop our biggest sale of the year. Free shipping on all orders.",
539
+ "destinationUrl": "https://example.com/summer-sale",
540
+ "bodyText": "Summer Sale — 50% Off Everything",
541
+ "pageName": "Example Store",
542
+ "impressionsMin": 100000,
543
+ "impressionsMax": 200000,
544
+ "startDate": "2025-06-01",
545
+ "endDate": "2025-06-30",
546
+ "firstSeenAt": 1717200000000,
547
+ "lastSeenAt": 1719792000000,
548
+ "publisherPlatforms": ["GOOGLE_ADS"],
549
+ "regionCodes": ["US", "GB"],
550
+ "variations": [
551
+ {
552
+ "headline": "Summer Sale — 50% Off",
553
+ "description": "Shop our biggest sale of the year.",
554
+ "destinationUrl": "https://example.com/summer-sale",
555
+ "imageUrl": "https://...",
556
+ "visibleUrl": "example.com"
557
+ }
558
+ ],
559
+ "regions": [
560
+ { "code": "US", "name": "United States" }
561
+ ],
562
+ "analysisStatus": "completed",
563
+ "aiAnalysis": {
564
+ "aiSummary": "Promotional display ad for a seasonal sale with urgency-driven CTA",
565
+ "hookAngle": "Discount/Price",
566
+ "offerType": "Percentage Discount",
567
+ "ctaStrategy": "Shop Now",
568
+ "funnelStage": "Bottom",
569
+ "targetAudience": "Price-sensitive shoppers",
570
+ "adFormat": "responsive_display",
571
+ "tags": ["sale", "discount", "ecommerce"],
572
+ "trustSignals": ["Free shipping"],
573
+ "keyMessages": ["50% off", "Free shipping"],
574
+ "competitiveAngle": "Price leadership",
575
+ "dominantColors": ["#FF5733", "#FFFFFF"],
576
+ "analyzedAt": 1719792000000
577
+ }
578
+ }
579
+ ],
580
+ "continueCursor": "eyJwYWdl...",
581
+ "isDone": false
582
+ }
583
+ }
584
+ ```
585
+
586
+ **Key response fields:**
587
+
588
+ | Field | Description |
589
+ |-------|-------------|
590
+ | `headline`, `description` | Top-level ad copy (first variation) |
591
+ | `variations[]` | All ad variations with copy, images, videos, and URLs |
592
+ | `regions[]` | Geographic targeting regions |
593
+ | `impressionsMin/Max` | Estimated impression range (Google Ads Transparency data) |
594
+ | `publisherPlatforms` | Where the ad ran (GOOGLE_ADS, YOUTUBE, etc.) |
595
+ | `analysisStatus` | AI analysis state: `pending`, `processing`, `completed`, `failed` |
596
+ | `aiAnalysis` | AI-generated creative analysis (only present when `analysisStatus` is `completed`) |
597
+ | `aiAnalysis.aiSummary` | One-line AI summary of the ad |
598
+ | `aiAnalysis.hookAngle` | Creative hook (Discount, Fear, Social Proof, etc.) |
599
+ | `aiAnalysis.funnelStage` | Funnel position: Top, Middle, Bottom |
600
+ | `aiAnalysis.tags` | AI-generated tags for filtering |
601
+
602
+ **Flags:**
603
+
604
+ | Flag | Description |
605
+ |---------------|------------------------------------------------|
606
+ | `--search` | Search term for ad text |
607
+ | `--isActive` | Filter by active ads only |
608
+ | `--mediaType` | Filter by media type: `image`, `video`, `text` |
609
+ | `--sort` | Sort: `newest` or `oldest` |
610
+ | `--limit` | Max results per page (default 20, max 100) |
611
+ | `--cursor` | Pagination cursor from previous response |
612
+ | `--output` | Format: `json` \| `csv` \| `md` |
613
+
614
+ ---
615
+
616
+ ### `baker ads google library sync <accountId>`
617
+
618
+ Trigger an immediate re-sync for a tracked account. Polls every 5 seconds until complete (10-minute timeout). Progress is written to stderr.
619
+
620
+ ```bash
621
+ baker ads google library sync acc_def456
622
+ ```
623
+
624
+ **Response:**
625
+
626
+ ```json
627
+ {
628
+ "ok": true,
629
+ "data": {
630
+ "totalAdCount": 350,
631
+ "activeAdCount": 92
632
+ }
633
+ }
634
+ ```
635
+
636
+ ---
637
+
638
+ ### `baker ads google library search-competitors "keyword"`
639
+
640
+ Search for competitors running Google ads for a keyword. Uses DataForSEO (same data as `baker research advertisers`).
641
+
642
+ ```bash
643
+ baker ads google library search-competitors "running shoes"
644
+ baker ads google library search-competitors "crm software" --location uk
645
+ ```
646
+
647
+ **Flags:**
648
+
649
+ | Flag | Description |
650
+ |--------------|----------------------------|
651
+ | `--location` | Location name or code |
652
+ | `--json` | Output in JSON format |
653
+
654
+ ---
655
+
402
656
  ### Staged writes (`baker ads google budgets|campaigns|...`)
403
657
 
404
658
  Write commands **never touch the Google Ads API at stage time**. Each command stages a create/update/pause/resume/remove op against the current chat's draft (`BAKER_CHAT_ID`); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic `GoogleAdsService.Mutate` when the chat is published. Feature-flagged per company (`companies.googleAdsWriteEnabled`) — off by default = a fully simulated publish with zero real API calls.
@@ -888,7 +1142,7 @@ Notes:
888
1142
  - All write commands take `--file <json>` payloads; explicit flags override file keys. `baker schema ads.linkedin.campaigns.create` for exact args.
889
1143
  - Money flags (`--bid`, `--daily-budget`, `--total-budget`) require `--currency`.
890
1144
  - Creative media comes from the Baker library (`--image-id`/`--video-id` from `baker images`/`baker videos` — uploaded to LinkedIn at publish) or as LinkedIn URNs (`--image-urn`/`--video-urn`). Formats: `image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs`; complex formats take `--file` with the full content object; conversation ads take `--file` with the message flow (`{message: {subject, body, senderName?, buttons[]}}` — buttons `NESTED` (with `nestedMessage`) or `LANDING_PAGE` (with `landingPageUrl`), ≤25 messages, bodies ≤500 chars, labels ≤25). Limits: headline ≤70, text-ad 25/75, intro soft-truncates at 600 chars. TLA sponsors an existing post via `--post-urn`.
891
- - Lead forms are file-first (`lead-forms create --file form.json`). Required: name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion). Each question is a predefined profile field (`{ name, predefinedField: "EMAIL" }` — Contact/Work/Company/Education/Demographic library) or a custom question (`{ name, questionType: "SINGLE_LINE_TEXT" | "MULTIPLE_CHOICE", options?: [...] }`; ≤3 custom, MULTIPLE_CHOICE needs 2–30 options). Also supported: `locale {country,language}`, `formImageId`/`formImageUrn` (banner), `consents[]` (≤5 disclosure checkboxes `{text, required}`), `hiddenFields[]` (≤20 `{name, value}` tracking fields), `legalDisclaimer`, `thankYou {message, cta, landingUrl | appointmentUrl}` (Calendly/Chili Piper booking link). The staged preview emits non-blocking best-practice warnings when a form has no qualifying question, no confirmation message/action, or no consent checkbox.
1145
+ - Lead forms are file-first (`lead-forms create --file form.json`). Required: name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion). Each question is a predefined profile field (`{ name, predefinedField: "EMAIL" }` — Contact/Work/Company/Education/Demographic library) or a custom question (`{ name, questionType: "SINGLE_LINE_TEXT" | "MULTIPLE_CHOICE", options?: [...] }`; ≤3 custom, MULTIPLE_CHOICE needs 2–30 options). Also supported: `locale {country,language}`, `formImageId`/`formImageUrn` (banner), `privacyPolicyText`, `consents[]` (≤5 disclosure checkboxes `{text, required}`), `hiddenFields[]` (≤20 `{name, value}` tracking fields), `legalDisclaimer`, `thankYou {message, cta, landingUrl | appointmentUrl}` (Calendly/Chili Piper booking link). The staged preview emits non-blocking best-practice warnings when a form has no qualifying question, no confirmation message/action, or no consent checkbox.
892
1146
 
893
1147
  #### `audit` — playbook diagnostic
894
1148
 
@@ -1690,14 +1944,14 @@ baker images generate "flat geometric mascot, brand palette" \
1690
1944
  --model recraft/recraft-v4.1-pro-vector --rgb-colors "[[10,10,10],[255,80,0]]" --bg-rgb "[255,255,255]"
1691
1945
  ```
1692
1946
 
1693
- **Models** (`--model`, default `google/gemini-3.1-flash-image-preview`):
1947
+ **Models** (`--model`, default `openai/gpt-5.4-image-2`):
1694
1948
 
1695
1949
  | Model | Best for | Aspect ratios | Sizes |
1696
1950
  |---|---|---|---|
1697
- | `google/gemini-3.1-flash-image-preview` **(default)** | Fast iteration, reference editing, extreme aspect ratios (Nano Banana flash) | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
1951
+ | `openai/gpt-5.4-image-2` **(default)** | Photoreal + cleanest in-image text ad/landing reproduction | standard set | `1K` `2K` `4K` |
1952
+ | `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) | standard set | `1K` `2K` `4K` |
1698
1953
  | `google/gemini-3.5-flash` | Fast; extreme aspect ratios | standard **+** `1:4` `4:1` `1:8` `8:1` | `0.5K`–`4K` |
1699
- | `google/gemini-3-pro-image-preview` | Highest fidelity (Nano Banana Pro) — strong prompt adherence and reference grounding | standard set | `1K` `2K` `4K` |
1700
- | `openai/gpt-5.4-image-2` | Photoreal + cleanest in-image text — ad/landing reproduction | standard set | `1K` `2K` `4K` |
1954
+ | `google/gemini-3.1-flash-image-preview` | Same as 3.5 flash (preview) | extreme set | `0.5K`–`4K` |
1701
1955
  | `recraft/recraft-v4.1-pro-vector` | Vector/flat/SVG-style with palette control | standard set | `1K` `2K` `4K` |
1702
1956
 
1703
1957
  Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9` `21:9`.
@@ -1706,7 +1960,7 @@ Standard aspect ratios: `1:1` `2:3` `3:2` `3:4` `4:3` `4:5` `5:4` `9:16` `16:9`
1706
1960
 
1707
1961
  | Flag | Description |
1708
1962
  |---|---|
1709
- | `--model` | Model id (default `google/gemini-3.1-flash-image-preview`) |
1963
+ | `--model` | Model id (default `openai/gpt-5.4-image-2`) |
1710
1964
  | `--aspect-ratio` | Output aspect ratio (default `1:1`) |
1711
1965
  | `--image-size` | Resolution: `1K` (default) `2K` `4K` (Gemini flash also `0.5K`) |
1712
1966
  | `--reference` | Comma-separated visual references, each either a **public image URL** (Pinterest / stock / library `imageUrl`) **or a local file path** (a sandbox image — brand logo, product shot, cropped photo, screenshot). Local files are downscaled (≤1536px) and inlined automatically — no manual upload. Applied in order; the biggest quality lever for photographed, on-brand output. Split is on `,`, so a URL containing a literal comma in its query string would be torn in two (rare for image CDNs — pass it alone if it occurs); a single `data:` URL is taken whole. |
@@ -2163,17 +2417,13 @@ baker testimonials tags
2163
2417
 
2164
2418
  ### Winning Ads (`baker winning-ads`)
2165
2419
 
2166
- Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`), and manage the brands your library tracks (`follow` / `following` / `unfollow`). Each result carries a presigned media URL (~1h TTL), the ad's DNA summary, and scores. The CLI authenticates with the normal `BAKER_API_KEY`; the Baker backend proxies the request to the ad-dna service with a server-held token — no extra credential in the sandbox.
2167
-
2168
- > The corpus has **Meta + LinkedIn** connectors, so `--platform` inputs are limited to `meta,linkedin`. (Older result rows may still carry a legacy platform string.)
2420
+ Search the **ad-dna** corpus of scored "winning" competitor ads for reference creatives to reproduce (e.g. with `baker canvas`). Each result carries a presigned media URL (~1h TTL), the ad's DNA summary, and scores. The CLI authenticates with the normal `BAKER_API_KEY`; the Baker backend proxies the request to the ad-dna service with a server-held token — no extra credential in the sandbox.
2169
2421
 
2170
2422
  > Backend env: the Convex deployment must have `AD_DNA_API_TOKEN` set (`npx convex env set AD_DNA_API_TOKEN …`). `AD_DNA_API_URL` is optional and defaults to `https://ads.withbaker.com`.
2171
2423
 
2172
- > Replaces the old `baker ads google library` tree, which has been removed. Competitor-by-keyword discovery still lives at `baker research advertisers`.
2173
-
2174
2424
  ### `baker winning-ads search <query>`
2175
2425
 
2176
- Semantic search (dense recall + BM25 + rerank) → `POST /api/ad-library/winners/search`. The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`; plus top-level `pool_size`, `match_confidence` (`high|medium|low`), and `below_floor_count` (matches dropped under the relevance floor). `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + longevity (`days_active`, `reach`, `active`, `winner_category`, `media_kind`). `--output json` (default) returns the lean objects; `--output md` prints a table.
2426
+ Semantic search (dense recall + BM25 + rerank). The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`; plus top-level `pool_size` and `match_confidence`. `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + longevity (`days_active`, `reach`, `active`, `winner_category`, `media_kind`). `--output json` (default) returns the lean objects; `--output md` prints a table.
2177
2427
 
2178
2428
  > `media_url` is the creative itself: for `static` it's the image, for `video` it's the video file. ad-dna stores **no separate poster** for videos, so a video result has only the video URL.
2179
2429
 
@@ -2195,7 +2445,7 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
2195
2445
  | `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
2196
2446
  | `--max-per-advertiser <n>` | Cap results per advertiser 1–50 (default 3) |
2197
2447
  | `--min-relevance <0-1>` | Relevance floor; trims weak matches |
2198
- | `--platform <list>` | `meta,linkedin` — pass a single value to search **only** that platform |
2448
+ | `--platform <list>` | One or many of `meta,tiktok,linkedin,google_search,google_display,youtube,reddit,x,pinterest,snapchat` — pass a single value to search **only** that platform |
2199
2449
  | `--format <list>` | `video,static,carousel` |
2200
2450
  | `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
2201
2451
  | `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
@@ -2210,66 +2460,13 @@ Reading the scores: **`relevance`** (0–1) = match of the creative to your quer
2210
2460
 
2211
2461
  ### `baker winning-ads advertisers <brand>`
2212
2462
 
2213
- List corpus brands by name or domain → `GET /api/ad-library/advertisers`. Use it to find **your own** advertiser (to `--exclude-advertiser`) or a **competitor** (to `--advertiser-id` / `winners`). Lean default fields: `advertiser_id`, `label`, `platform_count`, `family_count`, `active_ad_count`, `total_ad_count`; `--full` adds `scraped_name`, `image_url`, `platforms`, `total_reach`, `last_synced_at`. Supports `--platform meta|linkedin`, `--limit`, `--offset`.
2463
+ Resolve a brand name → `advertiser_id`(s) in the corpus. Use it to find **your own** advertiser (to `--exclude-advertiser`) or a **competitor** (to `--advertiser-id`). Returns `advertiser_id`, `label`, `active_ads`, `total_ads`.
2214
2464
 
2215
2465
  ```bash
2216
2466
  baker winning-ads advertisers "Acme" --output md # find our own advertiser id
2217
2467
  baker winning-ads advertisers "Deel" --platform meta --output md
2218
2468
  ```
2219
2469
 
2220
- ### `baker winning-ads follow "<domain | profile URL | brand>" --platform meta|linkedin`
2221
-
2222
- Add **all** of a brand's ads to your library — every platform (Meta + LinkedIn) and every country → `POST /api/ad-library/follow`. `--platform` is **required**, but it only tells us how to read your `input` (Facebook vs LinkedIn URL); it does **not** limit what we track. A bare domain is best: we resolve both the Meta page and the LinkedIn company from it and track both. Discovery is never region-scoped. The result `status` is one of:
2223
-
2224
- - `following` — the brand is already in the corpus; you're now subscribed (no wait).
2225
- - `added` — a new brand was queued for ingestion; its ads appear as discovery completes (a `hints[]` note flags this).
2226
- - `ambiguous` — the input mapped to multiple brands; pick one from `candidates` and re-run with a more specific domain/URL.
2227
-
2228
- ```bash
2229
- baker winning-ads follow "deel.com" --platform meta
2230
- baker winning-ads follow "https://www.linkedin.com/company/acme" --platform linkedin --label "Acme (competitor)"
2231
- ```
2232
-
2233
- ### `baker winning-ads following`
2234
-
2235
- List the brands you follow → `GET /api/ad-library/following`. Each row shows `status` (`ready` vs `adding…`) plus cached counts (`active_ad_count`, `total_ad_count`, `family_count`) and discovery progress (`adding_discovered`, `adding_enqueued`). `--full` adds `image_url` + `platforms`.
2236
-
2237
- ```bash
2238
- baker winning-ads following --output md
2239
- ```
2240
-
2241
- ### `baker winning-ads winners <advertiser>`
2242
-
2243
- Top winning ads for one advertiser id → `GET /api/ad-library/advertiser-winners`. Same lean winner cards as `search` (add `--full` for DNA + longevity). Supports `--top N` and `--platform meta|linkedin`.
2244
-
2245
- ```bash
2246
- baker winning-ads winners adv_123 --top 15 --output md
2247
- ```
2248
-
2249
- ### `baker winning-ads unfollow <advertiser>`
2250
-
2251
- Stop following a brand by advertiser id → `POST /api/ad-library/unfollow`. Returns `{ removed }`.
2252
-
2253
- ```bash
2254
- baker winning-ads unfollow adv_123
2255
- ```
2256
-
2257
- ### `baker winning-ads brief`
2258
-
2259
- Generate a creative brief grounded in strategically-similar winners → `POST /api/ad-library/brief`. Optionally describe the target creative with `--dna` (a JSON object), steer with `--notes`, and cap references with `--k`. Returns `brief_markdown` + `reference_ad_ids`.
2260
-
2261
- ```bash
2262
- baker winning-ads brief --dna '{"angle":"cost savings","awareness_stage":"solution_aware"}' --notes "B2B, LinkedIn video" --k 8
2263
- ```
2264
-
2265
- ### `baker winning-ads patterns --winners <adIds> --duds <adIds>`
2266
-
2267
- Mine what separates two cohorts of ads → `POST /api/ad-library/patterns`. Pass a comma-list of winning ad ids (`--winners`, cohort A) and weaker/dud ad ids (`--duds`, cohort B), plus optional `--top-n`. Returns each discriminating DNA field with the cohort it `favors` (`winners`/`duds`), a `score`, and the top values on each side.
2268
-
2269
- ```bash
2270
- baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --top-n 10 --output md
2271
- ```
2272
-
2273
2470
  ---
2274
2471
 
2275
2472
  ### Scheduled Actions (`baker scheduled-actions`)
@@ -2305,46 +2502,6 @@ Rules:
2305
2502
 
2306
2503
  ---
2307
2504
 
2308
- ### Marketing Tags (`baker tags`)
2309
-
2310
- Read the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, PostHog, …) — production tags overlaid with the changes staged in this chat.
2311
-
2312
- **The CLI is read-only.** Every tag change (create, edit, delete) goes through the `request_tag_input` tool (`baker_ui` MCP server): the agent proposes one or more changes — each becomes a tab in one blocking approval form — pre-filling the non-secret fields it knows; the user reviews, edits, fills secret fields, and approves or skips each tab. Approved changes stage on the chat and apply when the chat is published; discarding the chat drops them.
2313
-
2314
- `BAKER_CHAT_ID` must be set.
2315
-
2316
- ```bash
2317
- baker tags list # effective view: production + staged, full readable config + secret status
2318
- baker tags draft # review the staged changes awaiting publish
2319
- ```
2320
-
2321
- Notes:
2322
-
2323
- - **`list` prints each tag's full readable config.** Every non-secret field is shown in full under the tag — the Clarity `projectId`, GA4 `measurementId`, the entire custom `code` snippet, etc. — so the agent reuses a stored value to pre-fill a follow-up change instead of asking the user for something already installed. Secrets are never among these fields. (`--json` emits the same data as the raw envelope.)
2324
- - **Secrets never travel through this CLI or the chat.** Secret fields (`accessToken`, `apiSecret`, `authorizationToken`, `apiKey`, `conversionToken`, `oauthProviderId`) are entered only in the dashboard's secure tag form and flow straight into the staged draft; tool responses only ever name which secret fields are set/pending.
2325
- - Staged creates get a server-generated `tag_temp_*` ref (returned in the tool result and printed by `list`). Use it (or a real tag id) as flow side-effect `tagIds` — the published tag keeps resolving under the temp ref.
2326
- - Proposing a delete on a `tag_temp_*` ref drops the staged create instead.
2327
- - Single-instance types (`code`, `posthog`, `datafast`) reject a second instance against the chat's effective view.
2328
- - Conflicts at publish (tag deleted in the dashboard, config invalid) skip the op with a recorded reason — they never block the publish.
2329
-
2330
- ### Forms / Flows (`baker flows`)
2331
-
2332
- Read this workspace's Forms (flows) and the configuration status of each one's **confidential fields** — side-effect connection secrets, OAuth connections, and third-party field definitions (HubSpot, Calendly, HighLevel, SavvyCal). Reads the local `_data.json` files; no secret values are ever decrypted or printed.
2333
-
2334
- **The CLI is read-only.** Author a Form's structure (nodes, conditions, side effects, field mappings) by editing its `_data.json` with the flow-builder skill. Configure the confidential fields it reports as `[missing]` / `[needs connection]` / `not selected` through the `request_flow_input` tool (`baker_ui` MCP server): the agent authors the surrounding structure, references it by `nodeId`/`sideEffectId`, and the user enters secrets, connects/picks OAuth accounts, and picks third-party forms in the dashboard. Those values are written (encrypted, where a secret) into the Form on the chat's branch and go live when the chat is published.
2335
-
2336
- ```bash
2337
- baker flows list # every Form + how many confidential fields still need setup
2338
- baker flows show contact # one Form's confidential fields and their status
2339
- baker flows show contact --full # include the full flow tree (secret values redacted)
2340
- ```
2341
-
2342
- Notes:
2343
-
2344
- - **Secrets never travel through this CLI or the chat.** `show` reports only whether each field is configured (`[set]`/`[missing]`, `[connected]`/`[needs connection]`, selected/not selected) — never a value. Configured secrets appear only as `[configured]` ciphertext markers.
2345
- - Confidential side-effect types: `httpWebhook`, `zapier`, `crmble` (typed credentials); `pipedrive`, `googleSpreadsheet`, `hubspotForm`, `goHighlevelContact` (OAuth connection + resource); `email`. Widget node types needing a resource pick: `calendly`, `savvycal`, `hubspot`, `hubspotMeeting`, `highlevel`, `highlevelForm`.
2346
- - `request_flow_input` changes stage on the chat's branch and apply when the chat is published; discarding the chat drops them.
2347
-
2348
2505
  ### Action Items (`baker actions`)
2349
2506
 
2350
2507
  Manage action items for the current chat. Most write operations stage on the chat's draft and apply atomically when the chat is published. Claim/release run live so other chats can see who's working on what.
@@ -2504,6 +2661,29 @@ baker canvas run my-canvas.json
2504
2661
  # single node — that old serial workaround is obsolete.
2505
2662
  baker canvas run my-canvas.json --parallel 8
2506
2663
 
2664
+ # 2c. Runs persist across sandboxes/sessions by default: node results sync to a
2665
+ # company-scoped remote cache (small JSON pointers; bytes stay in R2), so a FRESH
2666
+ # sandbox re-runs an already-computed canvas at zero credits — assets rehydrate
2667
+ # from R2, sha-verified. Every run also posts a durable history record (per-node
2668
+ # outputs, credits, cached/fresh) that powers the dashboard's Creatives
2669
+ # generations timeline. Opt out with --remote-cache off (env
2670
+ # BAKER_CANVAS_REMOTE_CACHE=off) and --no-record. With --remote-cache off,
2671
+ # assets are not uploaded, so a recorded run keeps its stats but has no
2672
+ # browsable outputs — pass --no-record too if you want nothing persisted.
2673
+ baker canvas run my-canvas.json --remote-cache off --no-record
2674
+
2675
+ # 2d. Regenerate a node whose prompt is fine (force a fresh roll). The engine is
2676
+ # content-addressed: re-running an UNCHANGED node returns the identical cached
2677
+ # render, never a new draw. To re-roll a node without editing its prompt (a
2678
+ # color drifted, a face came out wrong), force it fresh two ways — NEVER
2679
+ # restructure the canvas (repointing output / deleting nodes) to trick the cache:
2680
+ # • One-shot flag — forces the named nodes + everything downstream fresh this
2681
+ # run, leaving every other node cached (unknown ids fail loudly before billing):
2682
+ baker canvas run my-canvas.json --regenerate gen_4x5,gen_9x16
2683
+ # • Persistent — add/bump a node's `regenerate` field in the canvas JSON
2684
+ # (e.g. "regenerate": 2) and re-run; the fresh render is reproducible in any
2685
+ # later session. Bump it again (3, 4, …) for each additional draw.
2686
+
2507
2687
  # 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
2508
2688
  baker canvas inspect <run_id>
2509
2689
 
@@ -2748,7 +2928,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
2748
2928
 
2749
2929
  **Outputs:** `asset` → `<params.expect>` / content-determined (URL strategy table) or extension-inferred (path).
2750
2930
 
2751
- **Path-source notes:** the canvas is **not portable** to another machine without the file. Cache key folds the file's `mtime:size`, so editing the file invalidates the cache automatically. Supported extensions: `png`, `jpg`/`jpeg`, `webp`, `gif`, `avif`, `svg`, `mp4`, `webm`, `mov`, `m4v`, `mp3`, `wav`, `m4a`, `ogg`, `flac`, `json`, `txt`, `md`, `markdown`, `html`/`htm`, `csv`, `ttf`, `otf`, `woff`, `woff2`. Unknown extensions fall back to magic-byte sniffing for common image formats (and an SVG content sniff), else `kind_mismatch`. **SVG (`expect: "image"`) is rasterized to a transparent PNG on ingest** — brand logos are usually SVG, and image-generation models can't read SVG markup, so it's upscaled (longest edge near 2048px) with transparency preserved and the resulting asset carries `metadata.rasterized_from: "svg"`. **Video (`expect: "video"`) duration is probed from the file's ISO-BMFF (`mp4`/`mov`/`m4v`) header** and stamped as the canonical `duration_ms` (and `metadata.duration_ms`); other containers (e.g. `webm`) leave it unset. Downstream `video_deconstruct` uses this declared duration to size its ingest-poll timeout and preflight — without it those fall back to worst-case budgets and a single deconstruct step can hit the action time limit.
2931
+ **Path-source notes:** the canvas is **not portable** to another machine without the file. Cache key folds the file's `mtime:size`, so editing the file invalidates the cache automatically. Supported extensions: `png`, `jpg`/`jpeg`, `webp`, `gif`, `avif`, `svg`, `mp4`, `webm`, `mov`, `m4v`, `mp3`, `wav`, `m4a`, `ogg`, `flac`, `json`, `txt`, `md`, `markdown`, `html`/`htm`, `csv`, `ttf`, `otf`, `woff`, `woff2`. Unknown extensions fall back to magic-byte sniffing for common image formats (and an SVG content sniff), else `kind_mismatch`. **Any `expect: "image"` in a format image-generation models can't read (SVG, AVIF, HEIC, TIFF, BMP) is normalized to PNG on ingest** — model-safe rasters (`jpeg`/`png`/`gif`/`webp`) pass through untouched, everything else is transcoded so a reference can never 400 a generation. This applies to **both `source: "path"` and `source: "url"`** (URL images are fetched and normalized locally, since the backend can't run the rasterizer). SVG gets density-aware upscaling (longest edge near 2048px, transparency preserved). The normalized asset carries `metadata.rasterized_from` set to the source format (e.g. `"svg"`, `"avif"`). **Video (`expect: "video"`) duration is probed from the file's ISO-BMFF (`mp4`/`mov`/`m4v`) header** and stamped as the canonical `duration_ms` (and `metadata.duration_ms`); other containers (e.g. `webm`) leave it unset. Downstream `video_deconstruct` uses this declared duration to size its ingest-poll timeout and preflight — without it those fall back to worst-case budgets and a single deconstruct step can hit the action time limit.
2752
2932
 
2753
2933
  **Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Handinger charges per scrape.
2754
2934
 
@@ -3198,9 +3378,9 @@ Accepted ref-image MIMEs vary by model — see per-model sections below.
3198
3378
 
3199
3379
  ###### Model: `bytedance/seedance-2.0`
3200
3380
 
3201
- Production-quality ad-creative model. Routed via **fal.ai** (not OpenRouter) because OpenRouter's Seedance passthrough rejects photorealistic human reference frames via ByteDance's "real person" safety filter.
3381
+ Production-quality ad-creative model. Routed via **Replicate** (`bytedance/seedance-2.0`). NOTE: ByteDance's upstream "real person" likeness filter still blocks photorealistic human reference frames on **any** reseller — the escape is a synthetic/AI presenter face or routing real faces to Veo, not the provider.
3202
3382
 
3203
- Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp` (via fal.ai).
3383
+ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`.
3204
3384
 
3205
3385
  | Name | Type | Required | Notes |
3206
3386
  |---|---|---|---|
@@ -3254,7 +3434,7 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
3254
3434
  >
3255
3435
  > A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
3256
3436
 
3257
- > **Content-policy blocks are deterministic, not flaky.** fal.ai/Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422, **non-retryable**), even when fal's proxy chain masks it as a 5xx. Retrying **never** succeeds and wastes credits. Fix the cause: switch the clip to the other curated model (Veo routes around fal's filter), or make the source frame less photorealistic.
3437
+ > **Content-policy blocks are deterministic, not flaky.** ByteDance's Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422, **non-retryable**). This is ByteDance's upstream filter, so it fires on **any** reseller (Replicate or otherwise) — a provider swap does **not** route around it. Retrying **never** succeeds and wastes credits. Fix the cause: use a **synthetic/AI-generated** (non-identifiable) presenter face, or route a real face to **Veo** (`person_generation: allow_adult`), or make the source frame less photorealistic.
3258
3438
 
3259
3439
  ---
3260
3440
 
@@ -3305,7 +3485,7 @@ None.
3305
3485
 
3306
3486
  ##### `video_lipsync`
3307
3487
 
3308
- Lip-sync a video to an audio track via VEED (fal.ai).
3488
+ Lip-sync a video to an audio track via Sync Labs `sync/lipsync-2` (Replicate).
3309
3489
 
3310
3490
  **Inputs**
3311
3491
 
@@ -3332,7 +3512,7 @@ Lip-sync a video to an audio track via VEED (fal.ai).
3332
3512
 
3333
3513
  ##### `video_background_remove`
3334
3514
 
3335
- Strip a video's background → alpha WebM/H264. Powered by fal.ai VEED.
3515
+ Strip a video's background → transparent alpha WebM (VP9) or MOV (ProRes 4444). Powered by `sprited/birefnet-video` (Replicate).
3336
3516
 
3337
3517
  **Inputs**
3338
3518
 
@@ -3508,7 +3688,7 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
3508
3688
 
3509
3689
  ##### `image_background_remove`
3510
3690
 
3511
- Strip background → transparent PNG (or mask). Powered by fal.ai BiRefNet v2.
3691
+ Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate). (Note: mask-only output is no longer produced.)
3512
3692
 
3513
3693
  **Inputs**
3514
3694
 
@@ -3750,12 +3930,16 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
3750
3930
  | `--run-id <id>` | auto ULID | Override the generated run id. |
3751
3931
  | `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
3752
3932
  | `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
3933
+ | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Company-scoped remote cache + durable asset persistence. |
3934
+ | `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
3935
+
3936
+ **Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
3753
3937
 
3754
3938
  **Failures don't abandon sibling work.** Nodes in a layer run under the concurrency cap and every one **settles** — a failed clip no longer kills its in-flight siblings, whose finished results still land in the content-addressed cache. One failure re-throws as-is; several are reported together (each failed node named). Re-running `baker canvas run` resumes from the cache and re-executes **only** the failed nodes and their descendants — never hand-orchestrate per-node renders. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
3755
3939
 
3756
3940
  #### `baker canvas scaffold-video <video> [flags]`
3757
3941
 
3758
- Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. It runs **billed passes** up front:
3942
+ Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. The `<video>` positional is a **local path OR an http(s) URL** (a `baker winning-ads` `media_url`, a library URL, any reel link) — a URL is downloaded for you (no manual `curl` first), so pass `--slug`/`--out` with it to give the canvas a home. It runs **billed passes** up front:
3759
3943
 
3760
3944
  1. **`video_deconstruct`** (`~google/gemini-pro-latest`, full mode) — reverse-engineers the video into a scene-by-scene blueprint + word-level transcript, written next to the canvas as **`prompt.json`**. Each scene's `start_frame_prompt`/`end_frame_prompt` are inlined into the frame nodes (see below); `prompt.json` then rides along as the shared **global style reference** (palette, cast cohesion) and as provenance.
3761
3945
  2. **recurring-element selection** (`~google/gemini-flash-latest`) — picks only the **recurring, identity-critical** elements (each `global.cast` person, a recurring animal, a showcased product, the brand logo) and the scene indices each appears in. One real reference image grounds each element across **every** frame it appears in, so the same actor stays consistent the whole video. This selection runs as a **second pass over a slimmed blueprint** (cast/branding + each scene's frame prompts only) — a long ad's full blueprint can exceed the engine's inline-prompt limit, so the heavy per-scene detail (dialogue, overlays, transcript) the selector never reads is dropped before the prompt.
@@ -3815,6 +3999,7 @@ baker canvas run ./reference-ad.video.canvas.json
3815
3999
  | Flag | Default | Effect |
3816
4000
  |---|---|---|
3817
4001
  | `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
4002
+ | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
3818
4003
  | `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast). |
3819
4004
  | `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
3820
4005
  | `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
@@ -3856,6 +4041,7 @@ baker canvas run ./static-ad.canvas.json
3856
4041
  |---|---|---|
3857
4042
  | `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
3858
4043
  | `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
4044
+ | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is **downloaded into `src/creatives/<slug>/references/` and normalized to a model-safe format** (SVG/AVIF/HEIC/… → PNG), named from the actual bytes (not the URL string) so a presigned/extensionless URL never lands as PNG-bytes-in-`.jpg` — the canvas ingests that committed, portable path instead of the expiring URL. |
3859
4045
  | `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
3860
4046
  | `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
3861
4047
  | `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
@@ -3865,6 +4051,8 @@ baker canvas run ./static-ad.canvas.json
3865
4051
 
3866
4052
  Scaffolding runs (and bills) the two vision passes; **running** the result generates a billed image. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
3867
4053
 
4054
+ **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>`. Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
4055
+
3868
4056
  #### `baker canvas inspect <run_id> [--thumbnails]`
3869
4057
 
3870
4058
  One-page summary of a completed run: per-node duration + cache status, list of files in the run dir, optional video thumbnails (start/middle/end frames extracted via ffmpeg).
@@ -4327,6 +4515,26 @@ import {
4327
4515
  } from "@koda-sl/baker-cli/engine";
4328
4516
  ```
4329
4517
 
4518
+ ## Creatives
4519
+
4520
+ Publish an approved canvas render as a first-class Baker creative. The image uploads to the Baker image library (tagged `creative`), a creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
4521
+
4522
+ ```bash
4523
+ baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
4524
+ --slug spring-offer-4x5 --run-id r_01JXYZ... \
4525
+ --source-reference-url "https://www.facebook.com/ads/library/?id=..."
4526
+ ```
4527
+
4528
+ | Flag | Effect |
4529
+ |---|---|
4530
+ | `--title <text>` | Required. Human title for the creative. |
4531
+ | `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
4532
+ | `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
4533
+ | `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
4534
+ | `--context <text>` | Optional describe context for the uploaded image asset. |
4535
+
4536
+ Without `--slug` the command behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
4537
+
4330
4538
  ## Help & Discovery
4331
4539
 
4332
4540
  Every command supports `--help` for usage info:
@@ -4363,7 +4571,6 @@ This CLI is designed for AI agent consumption. Key patterns:
4363
4571
  - **0.105.0**: `baker images ...`, `baker videos ...`, and `baker testimonials ...` commands now type their `/api/{images,videos,testimonials}/...` request/response payloads from the shared `@baker/api` contract package instead of hand-written local interfaces. No command, flag, or output-shape changes.
4364
4572
  - **0.106.0**: `baker ads linkedin` gains staged write commands — `campaign-groups`/`campaigns`/`creatives` create|update|pause|resume|(archive|)duplicate, `audiences create|upload`, `conversions create|update`, `lead-forms create|update`, plus `draft [remove|clear]` for review/undo. Ops validate at stage time, apply on chat publish, and run simulated (`urn:li:simulated:*`) unless LinkedIn writes are enabled for the company.
4365
4573
  - **0.116.0**: `lead-forms create` models the full Campaign Manager form — `locale`, form banner image (`formImageId`/`formImageUrn`), predefined profile-field questions (validated enum) vs custom questions (`SINGLE_LINE_TEXT`/`MULTIPLE_CHOICE` with `options`, ≤3 custom), `privacyPolicyText`, disclosure `consents[]` (≤5), tracking `hiddenFields[]` (≤20), and `thankYou` confirmation CTA + landing/appointment link. Staged preview surfaces best-practice warnings (no qualifying question, no confirmation, no consent). No breaking flag changes.
4366
- - **0.121.0**: `lead-forms create` drops the `privacyPolicyText` field — LinkedIn's versioned lead-form API has no privacy-policy-text slot, so it was silently discarded on publish. Use `legalDisclaimer` (shown under the form) or `consents[]` (disclosure checkboxes) instead. (Companion backend fix: staged lead-form questions were serialized in a shape LinkedIn dropped — they now publish correctly, and the staged preview lists each question.)
4367
4574
  - **0.119.0**: `draft amend`/`draft show` land on both `baker ads google` and `baker ads linkedin` — a generic JSON-merge-patch to update any staged op in place plus a full-payload receipt, replacing remove+recreate as the correction path. Google gains `assets update` and `asset-groups create|update` (Performance Max asset groups are now their own entity — `ads create --format performanceMaxAssetGroup` never worked and is gone); `ads create --format video` moves from a bare YouTube id to `--video-assets` refs (**breaking flag change** — stage the video as an asset first); `--format demandGen` gains `--image-assets`/`--square-image-assets`/`--logo-image-assets` and flag-building for headlines/descriptions. LinkedIn's `draft list` now renders a readable Campaign group ▸ Campaign ▸ Creative tree by default (`--json` for raw), `creatives update` gains `--campaign` (re-parent while staged), and `campaigns update` passes create-only fields (`--group`/`--type`/`--locale`/`--associated-entity`) through when amending a `li_temp_*` staged create instead of always stripping them.
4368
4575
 
4369
4576
  ## Publishing