@koda-sl/baker-cli 0.121.0-dev.775026ddd → 0.121.0-dev.a2cdb40bc
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 +108 -43
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/chunk-5WRI5ZAA.js.map +1 -0
- package/dist/chunk-AVJ3B6LC.js +97 -0
- package/dist/chunk-AVJ3B6LC.js.map +1 -0
- package/dist/chunk-ISFSUDEQ.js +264 -0
- package/dist/chunk-ISFSUDEQ.js.map +1 -0
- package/dist/chunk-K47Q73CK.js +156 -0
- package/dist/chunk-K47Q73CK.js.map +1 -0
- package/dist/{chunk-GQIOFHVI.js → chunk-MWFJ5NOP.js} +132 -275
- package/dist/chunk-MWFJ5NOP.js.map +1 -0
- package/dist/chunk-YTEAWSEM.js +39 -0
- package/dist/chunk-YTEAWSEM.js.map +1 -0
- package/dist/cli.js +4389 -2321
- package/dist/cli.js.map +1 -1
- package/dist/client-PGOTU24X.js +15 -0
- package/dist/client-PGOTU24X.js.map +1 -0
- package/dist/engine/index.d.ts +1 -30
- package/dist/engine/index.js +2 -1
- package/dist/env-TD4VXCQ7.js +10 -0
- package/dist/env-TD4VXCQ7.js.map +1 -0
- package/dist/output-2LGBISBQ.js +18 -0
- package/dist/output-2LGBISBQ.js.map +1 -0
- package/dist/shared-CQC3YRBA.js +23 -0
- package/dist/shared-CQC3YRBA.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-GQIOFHVI.js.map +0 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Use `--output` to change format:
|
|
|
82
82
|
|
|
83
83
|
### Ad Platforms (`baker ads`)
|
|
84
84
|
|
|
85
|
-
Multi-platform ad
|
|
85
|
+
Multi-platform ad commands. Google, Meta, and LinkedIn support both reads and **staged writes** (create/update campaigns, ad sets/ad groups, ads, creatives, audiences — applied on chat publish); X (Twitter) is read + analytics. TikTok is coming.
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
@@ -687,18 +687,39 @@ baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgr
|
|
|
687
687
|
--headlines "Come back and save" --long-headline "Finish signing up and get 20% off" \
|
|
688
688
|
--descriptions "Fast setup, no card required" --business-name "Acme" --final-url https://example.com \
|
|
689
689
|
--marketing-images g_temp_<img_landscape> --square-marketing-images g_temp_<img_square> --logo-images g_temp_<img_logo>
|
|
690
|
-
|
|
691
|
-
|
|
690
|
+
# Video ad — the YouTube video is its own asset first, then referenced by --video-assets
|
|
691
|
+
baker ads google assets create --customer-id 1234567890 --file '{"type":"youtubeVideo","youtubeVideoId":"dQw4w9WgXcQ"}'
|
|
692
|
+
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format video \
|
|
693
|
+
--video-assets g_temp_<video> --final-url https://example.com
|
|
694
|
+
# Demand Gen ad — image refs the same way
|
|
695
|
+
baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --format demandGen \
|
|
696
|
+
--headlines "Discover the app" --descriptions "Try it free today" --business-name "Acme" --final-url https://example.com \
|
|
697
|
+
--image-assets g_temp_<img> --square-image-assets g_temp_<img_square> --logo-image-assets g_temp_<img_logo>
|
|
698
|
+
# Update a staged/live asset in place (flat partial — set only what changed)
|
|
699
|
+
baker ads google assets update <assetId> --customer-id 1234567890 --link-text "New sitelink text" --final-urls https://example.com/new
|
|
700
|
+
|
|
701
|
+
# Performance Max — PMax has no ad format of its own; its copy/images stage as an asset group
|
|
702
|
+
baker ads google campaigns create --customer-id 1234567890 --name "PMax — Full Funnel" --channel-type PERFORMANCE_MAX --budget-ref g_temp_<budget>
|
|
703
|
+
baker ads google asset-groups create --customer-id 1234567890 --campaign-ref g_temp_<campaign> --name "Core" \
|
|
704
|
+
--headlines "Fast Widgets,Buy Online,Free Shipping" --long-headlines "The widget everyone is talking about" \
|
|
705
|
+
--descriptions "Best widgets around.,Ships tomorrow." --business-name "Acme" --final-urls https://example.com
|
|
706
|
+
baker ads google asset-groups update g_temp_<assetgroup> --status ENABLED
|
|
707
|
+
|
|
708
|
+
# Review / undo / amend staged changes
|
|
692
709
|
baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
|
|
693
710
|
baker ads google draft list --json # raw JSON envelope for scripting
|
|
711
|
+
baker ads google draft show g_temp_ab12 # full staged payload — the receipt before publish
|
|
712
|
+
baker ads google draft amend g_temp_ab12 --patch '{"amountMicros": 60000000}' # update in place, never remove+recreate
|
|
694
713
|
baker ads google draft remove g_temp_ab12 # cascades to dependents
|
|
695
714
|
baker ads google draft clear
|
|
696
715
|
```
|
|
697
716
|
|
|
698
|
-
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets
|
|
717
|
+
Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update — Performance Max), `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa` take major units; `--target-roas` a ratio. 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**.
|
|
699
718
|
|
|
700
719
|
**Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with the simulated/live mode banner and non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
|
|
701
720
|
|
|
721
|
+
**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.
|
|
722
|
+
|
|
702
723
|
**Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
|
|
703
724
|
|
|
704
725
|
---
|
|
@@ -862,6 +883,36 @@ baker ads meta pixels --account-id act_123
|
|
|
862
883
|
baker ads meta pixels --pixel-id 9988 --stats --days 7
|
|
863
884
|
```
|
|
864
885
|
|
|
886
|
+
#### Staged writes (`baker ads meta campaigns|adsets|ads|creatives|audiences|media|draft`)
|
|
887
|
+
|
|
888
|
+
Meta writes mirror the Google/LinkedIn model: they **never hit the Graph API when you run them**. Each op is validated at stage time (field shapes, budget rules, page/Instagram identity ownership, CBO/ABO exclusivity, status-transition legality), shows in the dashboard chat as a pending Meta change, and applies only when the chat is published. Meta has no atomic batch mutate, so ops apply **sequentially** on publish — one failure skips its dependents but the publish completes.
|
|
889
|
+
|
|
890
|
+
If the company's `metaAdsWriteEnabled` flag is off (the default), publish runs the identical lifecycle **simulated** — fake `sim_meta_*` ids, zero Meta calls. The stage response and `baker ads meta draft` show `mode: "simulated"`.
|
|
891
|
+
|
|
892
|
+
Chain a full ad from scratch with `meta_temp_*` refs (each create returns one):
|
|
893
|
+
|
|
894
|
+
```bash
|
|
895
|
+
# 1. (optional) upload creative media from the Baker library → publishes an image hash
|
|
896
|
+
baker ads meta media upload --account-id act_123 --kind image --image-id <bakerImageId> # → meta_temp_img
|
|
897
|
+
|
|
898
|
+
# 2. campaign → ad set → creative → ad
|
|
899
|
+
baker ads meta campaigns create --account-id act_123 --name "Q3 Prospecting" --objective OUTCOME_LEADS --daily-budget 50 # → meta_temp_camp
|
|
900
|
+
baker ads meta adsets create --account-id act_123 --campaign meta_temp_camp --name "US 25-54" \
|
|
901
|
+
--optimization-goal OFFSITE_CONVERSIONS --billing-event IMPRESSIONS --pixel-id 111 --custom-event-type LEAD \
|
|
902
|
+
--targeting-file targeting.json # → meta_temp_as
|
|
903
|
+
baker ads meta creatives create --account-id act_123 --page 555 --instagram-user 777 \
|
|
904
|
+
--message "Save money on pet insurance" --link https://example.com --headline "Best cover" \
|
|
905
|
+
--image-ref meta_temp_img --cta SHOP_NOW --standard-enhancements on # → meta_temp_cr
|
|
906
|
+
baker ads meta ads create --account-id act_123 --name "Ad 1" --adset meta_temp_as --creative meta_temp_cr
|
|
907
|
+
|
|
908
|
+
# review + edit before publish
|
|
909
|
+
baker ads meta draft
|
|
910
|
+
baker ads meta draft remove meta_temp_cr # removing a create cascades to dependents
|
|
911
|
+
baker ads meta draft clear
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`ads`. `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
|
|
915
|
+
|
|
865
916
|
#### Smart defaults (so agents don't enumerate the API)
|
|
866
917
|
|
|
867
918
|
**`insights`** — the workhorse:
|
|
@@ -909,7 +960,7 @@ The HTTP backend exposes more endpoints (catalogs, ad-studies, ad-images, labels
|
|
|
909
960
|
|
|
910
961
|
- `spend` is a decimal string in account currency, not an integer.
|
|
911
962
|
- `effective_status` ≠ `status`. The dashboard shows effective_status (e.g. `WITH_ISSUES`, `PENDING_REVIEW`, `DISAPPROVED`, `ADSET_PAUSED`).
|
|
912
|
-
- Meta creatives are effectively immutable once attached. Editing copy/image/CTA =
|
|
963
|
+
- Meta creatives are effectively immutable once attached. Editing copy/image/CTA = stage a new creative + `ads update <id> --creative <ref>` (or `creatives duplicate`), not an edit of the live creative — see [Staged writes](#staged-writes-baker-ads-meta-campaignsadsetsadscreativesaudiencesmediadraft) above.
|
|
913
964
|
- Currency offsets are non-uniform (JPY/KRW = 1, KWD/BHD = 1000, most = 100). Read `account.currency` before doing budget math.
|
|
914
965
|
|
|
915
966
|
---
|
|
@@ -1024,7 +1075,7 @@ creatives create|update|pause|resume|duplicate
|
|
|
1024
1075
|
audiences create|upload
|
|
1025
1076
|
conversions create|update
|
|
1026
1077
|
lead-forms create|update
|
|
1027
|
-
draft [remove <ref
|
|
1078
|
+
draft [list|remove <ref>|clear|amend <ref>|show <ref>] # review/undo/amend staged write ops
|
|
1028
1079
|
```
|
|
1029
1080
|
|
|
1030
1081
|
#### Staged writes
|
|
@@ -1067,10 +1118,21 @@ baker ads linkedin creatives duplicate 1458413484 --headline "New headline" --re
|
|
|
1067
1118
|
# Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
|
|
1068
1119
|
baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
|
|
1069
1120
|
baker ads linkedin campaigns update li_temp_x2 --daily-budget 100 --currency EUR
|
|
1121
|
+
# --campaign re-parents a staged creative to a different ad set (rejected once the creative is live)
|
|
1122
|
+
baker ads linkedin creatives update li_temp_x3 --campaign li_temp_x2
|
|
1123
|
+
# campaigns update passes --group/--type/--locale/--associated-entity through when the target is li_temp_*
|
|
1124
|
+
# (they're create-only once live) — so amending a staged campaign can still change its group or type
|
|
1125
|
+
baker ads linkedin campaigns update li_temp_x2 --group li_temp_x1_alt
|
|
1070
1126
|
# A second update to the same real URN also merges into the already-staged update op.
|
|
1071
1127
|
|
|
1128
|
+
# Generic amend — merge a JSON patch into ANY staged op's payload (objects deep-merge, null deletes
|
|
1129
|
+
# a key, arrays/scalars replace); always prefer this over remove + re-create
|
|
1130
|
+
baker ads linkedin draft amend li_temp_x2 --patch '{"dailyBudget": {"amount": "100"}}'
|
|
1131
|
+
baker ads linkedin draft show li_temp_x2 # full staged payload — the receipt before publish
|
|
1132
|
+
|
|
1072
1133
|
# Review / undo before publish; after publish shows per-op results (applied/simulated/failed/skipped)
|
|
1073
|
-
baker ads linkedin draft
|
|
1134
|
+
baker ads linkedin draft # readable Campaign group ▸ Campaign ▸ Creative tree
|
|
1135
|
+
baker ads linkedin draft list --json # raw JSON envelope for scripting
|
|
1074
1136
|
baker ads linkedin draft remove li_temp_x2 # removing a create cascades to dependents
|
|
1075
1137
|
baker ads linkedin draft clear
|
|
1076
1138
|
```
|
|
@@ -1080,7 +1142,7 @@ Notes:
|
|
|
1080
1142
|
- All write commands take `--file <json>` payloads; explicit flags override file keys. `baker schema ads.linkedin.campaigns.create` for exact args.
|
|
1081
1143
|
- Money flags (`--bid`, `--daily-budget`, `--total-budget`) require `--currency`.
|
|
1082
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`.
|
|
1083
|
-
- Lead forms are file-first (`lead-forms create --file form.json`): name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion).
|
|
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), `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.
|
|
1084
1146
|
|
|
1085
1147
|
#### `audit` — playbook diagnostic
|
|
1086
1148
|
|
|
@@ -2440,6 +2502,31 @@ Rules:
|
|
|
2440
2502
|
|
|
2441
2503
|
---
|
|
2442
2504
|
|
|
2505
|
+
### Marketing Tags (`baker tags`)
|
|
2506
|
+
|
|
2507
|
+
Manage the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, PostHog, …) as per-chat **staged** changes. Nothing touches production tags until the chat is published; discarding the chat drops the draft.
|
|
2508
|
+
|
|
2509
|
+
`BAKER_CHAT_ID` must be set.
|
|
2510
|
+
|
|
2511
|
+
```bash
|
|
2512
|
+
baker tags list # effective view: production + staged, with secret status
|
|
2513
|
+
baker tags add clarity --set projectId=abcde12345 # client-only tag, no secrets needed
|
|
2514
|
+
baker tags add meta --set pixelId=123 --request-secret accessToken
|
|
2515
|
+
baker tags update <ref> --set pixelId=999 --clear testEventCode
|
|
2516
|
+
baker tags remove <ref> # stage deletion (tag_temp_* ref drops the staged create)
|
|
2517
|
+
baker tags draft # review staged ops
|
|
2518
|
+
baker tags draft remove <ref> # drop one staged op
|
|
2519
|
+
baker tags draft clear # drop everything staged
|
|
2520
|
+
```
|
|
2521
|
+
|
|
2522
|
+
Notes:
|
|
2523
|
+
|
|
2524
|
+
- **Secrets never travel through this CLI.** Secret fields (`accessToken`, `apiSecret`, `authorizationToken`, `apiKey`, `conversionToken`, `oauthProviderId`) are rejected in `--set`. Stage with `--request-secret <field>`; the user provides values via the secure tag form in the chat (`request_tag_input`), which writes them straight into the staged draft. Responses only ever name which secret fields are set/pending.
|
|
2525
|
+
- Staged creates get a server-generated `tag_temp_*` ref. Use it (or a real tag id) as flow side-effect `tagIds` — the published tag keeps resolving under the temp ref.
|
|
2526
|
+
- `update`/`remove` with a `tag_temp_*` ref amend/drop the staged create in place.
|
|
2527
|
+
- Single-instance types (`code`, `posthog`, `datafast`) reject a second instance against the chat's effective view.
|
|
2528
|
+
- Conflicts at publish (tag deleted in the dashboard, config invalid) skip the op with a recorded reason — they never block the publish.
|
|
2529
|
+
|
|
2443
2530
|
### Action Items (`baker actions`)
|
|
2444
2531
|
|
|
2445
2532
|
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.
|
|
@@ -2599,17 +2686,6 @@ baker canvas run my-canvas.json
|
|
|
2599
2686
|
# single node — that old serial workaround is obsolete.
|
|
2600
2687
|
baker canvas run my-canvas.json --parallel 8
|
|
2601
2688
|
|
|
2602
|
-
# 2c. Runs persist across sandboxes/sessions by default: node results sync to a
|
|
2603
|
-
# company-scoped remote cache (small JSON pointers; bytes stay in R2), so a FRESH
|
|
2604
|
-
# sandbox re-runs an already-computed canvas at zero credits — assets rehydrate
|
|
2605
|
-
# from R2, sha-verified. Every run also posts a durable history record (per-node
|
|
2606
|
-
# outputs, credits, cached/fresh) that powers the dashboard's Creatives
|
|
2607
|
-
# generations timeline. Opt out with --remote-cache off (env
|
|
2608
|
-
# BAKER_CANVAS_REMOTE_CACHE=off) and --no-record. With --remote-cache off,
|
|
2609
|
-
# assets are not uploaded, so a recorded run keeps its stats but has no
|
|
2610
|
-
# browsable outputs — pass --no-record too if you want nothing persisted.
|
|
2611
|
-
baker canvas run my-canvas.json --remote-cache off --no-record
|
|
2612
|
-
|
|
2613
2689
|
# 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
|
|
2614
2690
|
baker canvas inspect <run_id>
|
|
2615
2691
|
|
|
@@ -3866,7 +3942,7 @@ Turn a reference video into a **runnable, self-validated reproduction canvas** i
|
|
|
3866
3942
|
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.
|
|
3867
3943
|
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.
|
|
3868
3944
|
|
|
3869
|
-
Before the deconstruct it runs a **local shot-cut pass** on the source file with **[PySceneDetect](https://www.scenedetect.com)** (`scenedetect` CLI, `detect-content` — the battle-tested HSV content detector, installed in the canvas sandbox) and passes the cut timestamps as `video_deconstruct`'s `shot_cuts`. The deconstruct snaps its scene boundaries onto those real cuts and **splits any scene that spans one**, so a scene's frames can never straddle a hard cut (the failure where a scene's start frame was the couch and its end frame the b-roll). Two knobs tuned for fast social ads: the content **threshold defaults to 18** (PySceneDetect's own default of 27 misses soft reframes) and the **minimum scene length is dropped to 0.25s** (its default ~0.6s merges away rapid montage flashes) — so super-fast cuts survive and become cheap still-holds downstream. The threshold is **adaptive**: if the first pass looks like a continuous shot shredded into many close micro-cuts (a talking-head selfie's natural motion), it re-runs at PySceneDetect's own default of 27 and
|
|
3945
|
+
Before the deconstruct it runs a **local shot-cut pass** on the source file with **[PySceneDetect](https://www.scenedetect.com)** (`scenedetect` CLI, `detect-content` — the battle-tested HSV content detector, installed in the canvas sandbox) and passes the cut timestamps as `video_deconstruct`'s `shot_cuts`. The deconstruct snaps its scene boundaries onto those real cuts and **splits any scene that spans one**, so a scene's frames can never straddle a hard cut (the failure where a scene's start frame was the couch and its end frame the b-roll). Two knobs tuned for fast social ads: the content **threshold defaults to 18** (PySceneDetect's own default of 27 misses soft reframes) and the **minimum scene length is dropped to 0.25s** (its default ~0.6s merges away rapid montage flashes) — so super-fast cuts survive and become cheap still-holds downstream. The threshold is **adaptive**: if the first pass looks like a continuous shot shredded into many close micro-cuts (a talking-head selfie's natural motion), it re-runs at PySceneDetect's own default of 27 and **merges the two passes** — the high-threshold set is the base, and the low pass's *isolated* extras (real soft blur-morph transitions that vanish at 27) are added back while clustered extras (motion shred) stay dropped. Pinning **`--shot-threshold N`** disables the re-check (lower = more cuts). The backend snap window is likewise **adaptive** (up to 1s onto an unambiguous nearest cut, shrinking around dense cut pairs so a boundary never jumps past the wrong cut), any scene spanning an interior cut is split, and the residual-sliver coalesce is **cut-aware**: a drift sliver folds backward across its non-cut edge and never re-merges across a real cut. If `scenedetect` is unavailable it warns loudly and degrades to LLM-only boundaries.
|
|
3870
3946
|
|
|
3871
3947
|
A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
|
|
3872
3948
|
|
|
@@ -3874,9 +3950,17 @@ It then scaffolds the full pipeline like an **editing timeline**: each clip gets
|
|
|
3874
3950
|
|
|
3875
3951
|
**Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in `prompt.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
3876
3952
|
|
|
3953
|
+
**Typed region kinds & real screen surfaces.** Each composition region now carries a `kind` — `camera` (filmed footage, re-generated), `screen_capture` (app/site/document screen recording), `static_graphic` (designed text/graphic panel), or `generated` (3D/motion graphics) — plus an optional `nested` list for video-in-video (a Loom-style camera bubble inside a screen share). `kind` is authoritative for routing (prose keywords remain the fallback for older blueprints): `screen_capture`/`static_graphic` regions are **never generated by the video model** — the scene renders as a clean background plate (its clip prompt is scrubbed of all screen narration and forbids rendering UI) and the real surface is composited on the overlay layer. The route is decided **once per persistent layout run** (consecutive scenes sharing one composition signature), so a layout that runs unbroken across many scenes can't flip between pipelines on wording differences. A persistent surface seeds **ONE grouped stub** in `video-overlay-composition/index.html` spanning its whole window, with a per-scene **state timeline** — build one continuous screen recording/mockup, not one screenshot per scene. A `screen_capture` region also carries `surface_id`: a source video routinely **splices two unrelated screen recordings** under one persistent layout (a live app-processing capture, then an unrelated pre-made demo note) — the deconstruct assigns a stable id while the SAME recording continues and a new one when the on-screen content genuinely changes, so the run splits into **separate stubs** at the splice instead of asking for one screenshot that can't cover both. `baker canvas validate` additionally warns (`VIDEO_UI_IN_PROMPT`) if any clip prompt still narrates a screen surface, and (`VIDEO_BRANDMARK_IN_PROMPT`) if a generate prompt asks the model to paint a brand logo/wordmark (generation garbles marks; source the real one with `baker images logo` and composite it on the overlay layer).
|
|
3954
|
+
|
|
3955
|
+
**Designed graphics are rebuilt, not generated.** A `static_graphic` surface (a newspaper-collage panel, a meme card, a marketing composition) seeds a **GRAPHIC PANEL** stub — rebuild it as brand HTML or drop the design asset; it never gets the "screenshot the live page" instruction (there is no live page). A **full-frame** designed-graphic scene (the deconstruct emits one full-frame `static_graphic` region for meme/collage/motion-graphic beats) routes to a real design plate the same way screens do — no `image_generate`/`video_generate` — and dialogue over an all-graphic scene is voiceover by definition (nobody is on screen to lip-sync). A region typed `generated` whose own prose reads like a UI/designed panel is treated as a surface candidate too (the frame-grounded continuity checker delivers the verdict and corrects the kind), so one mistyped kind can't re-open the Seedance-paints-UI hole. Floating FX elements (hearts, sparkles, badges) ride the overlay layer: their narration is **scrubbed from clip briefs** and a categorical no-decorations directive is added, so the model can't bake a second, uneditable copy under the real composited one.
|
|
3956
|
+
|
|
3957
|
+
**Same-take continuity.** Scene boundaries that don't land on a detected shot cut are marked `continues_previous` (they're content beats inside one camera take — a genuinely cut-less video is ONE take), and a mid-take phrase clip **chains its start keyframe from the previous phrase's end keyframe**, so one continuous shot never jumps identity/pose at an invented boundary.
|
|
3958
|
+
|
|
3959
|
+
**Speech budgets use the speaker's observed pace.** `metadata.video.talking_scenes` stamps `est_speech_s` measured from the deconstruct's word timings plus `speech_words`; the `VIDEO_SPEECH_OVERRUN` check budgets a (re-authored) line at that real rate instead of a fixed words-per-second guess, so a fast talker's line that demonstrably fits its clip no longer fails validation. Numeral spelling for native clips applies **only to the spoken dialogue/transcript**, never to visual descriptions (a UI progress bar's "4%" stays "4%"). And on a blocking validation issue the canvas is **still written** (with the issues under `metadata.todo.blocking_validation_issues`) — the paid deconstruct is never discarded; fix in place and re-`validate`.
|
|
3960
|
+
|
|
3877
3961
|
**True output aspect, even when the video model can't generate it.** The scaffold resolves TWO ratios: the ad's **output** ratio (the source video's detected `aspect_ratio`, or **`--aspect`**, e.g. `4:5` → a 1080×1350 canvas) and the **generation** ratio — the nearest ratio the video model supports (Seedance has no 4:5, so clips + conditioning keyframes generate at `3:4`). When they differ the scaffold warns loudly, records `metadata.todo.aspect_remap`, normalizes **every clip** onto the output canvas (scale-to-fill, center-crop — keep subjects centered), sizes every ffmpeg plate/still/composite to the output dims, and stamps the copied overlay/caption compositions' `meta.json` to match. Nothing is silently dropped: an unknown ratio snaps to the nearest known one and stays explicit on every node.
|
|
3878
3962
|
|
|
3879
|
-
**Montage flashes held as stills.** A rapid-cut beat shorter than ~2s with no spoken line is a **flash** — Seedance's shortest clip is 4s, so generating one (then trimming away most of it) burns credits for motion no viewer perceives. The scaffold instead **holds one keyframe as a still** for the scene length (a cheap ffmpeg loop, no billed `video_generate`), same look at a fraction of the cost. Talking/ambient beats keep a real clip (they need motion + native audio).
|
|
3963
|
+
**Montage flashes held as stills — unless the picture really moves.** A rapid-cut beat shorter than ~2s with no spoken line is a **flash** — Seedance's shortest clip is 4s, so generating one (then trimming away most of it) burns credits for motion no viewer perceives. The scaffold instead **holds one keyframe as a still** for the scene length (a cheap ffmpeg loop, no billed `video_generate`), same look at a fraction of the cost. The deconstruct now stamps each scene's **`motion_level`** (`static` / `subtle` / `dynamic`): a **dynamic** flash (pouring chocolate, hands working, walking) keeps a **real trimmed clip** — freezing a moving montage turns it into a slideshow — while genuinely static beats (a logo card, a pinned photo, a product still) keep the cheap hold. Talking/ambient beats always keep a real clip (they need motion + native audio). The deconstruct also stamps each dialogue line's **`on_camera`** flag — a voice playing over b-roll, a graphic, or a mere *photo* of the speaker stays voiceover, so the scaffold never lip-syncs a scene with no speaking face (the polaroid close-up failure).
|
|
3880
3964
|
|
|
3881
3965
|
**The phrase model (voice cut at pauses, not at visual cuts).** The voice is grouped into **phrases** — runs of continuous speech with no real pause, which may span several visual scenes. A phrase is voiced ONCE (so a sentence the deconstruct split at a visual cut never breaks mid-word): if the speaker is **shown** anywhere in the phrase it's a single Seedance clip (`s<anchor>_clip`, native lip-sync + audio) re-voiced to the brand voice; if the speaker is **never shown** it's one ElevenLabs `tts` read. The picture is then assembled **scene by scene**: a scene that shows the speaker **slices its window** out of the phrase clip (`s<i>_seg`, an ffmpeg `-ss`/`-t` cut — video and audio come from the *same* clip, so lip-sync holds), and a **b-roll cutaway** gets its own silent clip while the phrase's voice plays underneath. "Shown" is decided by the **presenter element's per-scene presence**, not just who's speaking — a scene where a cast member narrates over b-roll (their element absent) is treated as a cutaway, so the talking head never appears where the original cut away. A presenter run longer than the **gateway-safe ~10s clip ceiling splits at a scene boundary** into contiguous takes (each its own clip + convert), so a sliced window never reads past its clip. (Seedance's *API* max is 15s, but the generation gateway frequently times out — **HTTP 524** — before it can deliver a clip longer than ~10s, so the scaffold never asks for one that long; 10s is a Seedance-allowed duration, so the split clip still snaps cleanly.) A b-roll cutaway *inside* a phrase lands at an **approximate** time (Seedance exposes no word timing) — nudge the scene boundary if it's off its beat.
|
|
3882
3966
|
|
|
@@ -3913,7 +3997,6 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
3913
3997
|
| Flag | Default | Effect |
|
|
3914
3998
|
|---|---|---|
|
|
3915
3999
|
| `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
|
|
3916
|
-
| `--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`. |
|
|
3917
4000
|
| `--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). |
|
|
3918
4001
|
| `--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. |
|
|
3919
4002
|
| `--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. |
|
|
@@ -3955,7 +4038,6 @@ baker canvas run ./static-ad.canvas.json
|
|
|
3955
4038
|
|---|---|---|
|
|
3956
4039
|
| `--context <text>` | — | Known provenance (advertiser, category, market) to ground the describe. |
|
|
3957
4040
|
| `--out <path>` | `<image-dir>/static-ad.canvas.json` (cwd when `<image>` is a URL) | Where to write the canvas (`prompt.json` is written alongside). |
|
|
3958
|
-
| `--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`. |
|
|
3959
4041
|
| `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
|
|
3960
4042
|
| `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
|
|
3961
4043
|
| `--layout-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the global-layout `text_generate` model. |
|
|
@@ -4427,26 +4509,6 @@ import {
|
|
|
4427
4509
|
} from "@koda-sl/baker-cli/engine";
|
|
4428
4510
|
```
|
|
4429
4511
|
|
|
4430
|
-
## Creatives
|
|
4431
|
-
|
|
4432
|
-
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.
|
|
4433
|
-
|
|
4434
|
-
```bash
|
|
4435
|
-
baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
|
|
4436
|
-
--slug spring-offer-4x5 --run-id r_01JXYZ... \
|
|
4437
|
-
--source-reference-url "https://www.facebook.com/ads/library/?id=..."
|
|
4438
|
-
```
|
|
4439
|
-
|
|
4440
|
-
| Flag | Effect |
|
|
4441
|
-
|---|---|
|
|
4442
|
-
| `--title <text>` | Required. Human title for the creative. |
|
|
4443
|
-
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`) — attaches the image to that creative's row, marks it `published`. |
|
|
4444
|
-
| `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
|
|
4445
|
-
| `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
|
|
4446
|
-
| `--context <text>` | Optional describe context for the uploaded image asset. |
|
|
4447
|
-
|
|
4448
|
-
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.
|
|
4449
|
-
|
|
4450
4512
|
## Help & Discovery
|
|
4451
4513
|
|
|
4452
4514
|
Every command supports `--help` for usage info:
|
|
@@ -4482,6 +4544,9 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
4482
4544
|
- **0.104.0**: `baker actions tags ...` commands now type their `/api/actions/tags...` request/response payloads from the shared `@baker/api` contract package instead of hand-written local interfaces. No command, flag, or output-shape changes.
|
|
4483
4545
|
- **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.
|
|
4484
4546
|
- **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.
|
|
4547
|
+
- **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.
|
|
4548
|
+
- **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.)
|
|
4549
|
+
- **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.
|
|
4485
4550
|
|
|
4486
4551
|
## Publishing
|
|
4487
4552
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
__commonJS,
|
|
29
|
+
__toESM
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-5WRI5ZAA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleConnectionError,
|
|
3
|
+
writeAdsJson
|
|
4
|
+
} from "./chunk-ISFSUDEQ.js";
|
|
5
|
+
import {
|
|
6
|
+
ApiError
|
|
7
|
+
} from "./chunk-K47Q73CK.js";
|
|
8
|
+
import {
|
|
9
|
+
getEnv
|
|
10
|
+
} from "./chunk-YTEAWSEM.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/ads/meta/shared.ts
|
|
13
|
+
var DAY_MS = 864e5;
|
|
14
|
+
function handleMetaError(err) {
|
|
15
|
+
if (err instanceof ApiError) {
|
|
16
|
+
if (err.code === "UNAUTHORIZED" || err.code === "NOT_FOUND") {
|
|
17
|
+
handleConnectionError("meta_ads", err.message);
|
|
18
|
+
}
|
|
19
|
+
const envelope = {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: {
|
|
22
|
+
code: err.code,
|
|
23
|
+
message: err.message,
|
|
24
|
+
fix: { action: "reject", explanation: err.message },
|
|
25
|
+
retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
writeAdsJson(envelope);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
32
|
+
writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message } });
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
var ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;
|
|
36
|
+
function resolveAccountIdArg(args) {
|
|
37
|
+
const fromArgs = args["account-id"];
|
|
38
|
+
const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;
|
|
39
|
+
if (!id) {
|
|
40
|
+
writeAdsJson({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: "MISSING_ACCOUNT_ID",
|
|
44
|
+
message: "Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs."
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!ACCOUNT_ID_RE.test(id)) {
|
|
50
|
+
writeAdsJson({
|
|
51
|
+
ok: false,
|
|
52
|
+
error: {
|
|
53
|
+
code: "INVALID_ACCOUNT_ID",
|
|
54
|
+
message: `Invalid account ID "${id}". Expected numeric ID, optionally prefixed with "act_".`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
return id.startsWith("act_") ? id : `act_${id}`;
|
|
60
|
+
}
|
|
61
|
+
function getDateRange(args) {
|
|
62
|
+
const datePreset = args["date-preset"];
|
|
63
|
+
const since = args.since;
|
|
64
|
+
const until = args.until;
|
|
65
|
+
if (since && until) return { since, until };
|
|
66
|
+
return { datePreset: datePreset ?? "last_7d" };
|
|
67
|
+
}
|
|
68
|
+
function todayIso() {
|
|
69
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
70
|
+
}
|
|
71
|
+
function daysAgoIso(days) {
|
|
72
|
+
return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
function csvOrJson(args) {
|
|
75
|
+
return args.output ?? "json";
|
|
76
|
+
}
|
|
77
|
+
function resolveEffectiveStatus(args) {
|
|
78
|
+
if (args["all-statuses"]) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const explicit = args["effective-status"];
|
|
82
|
+
if (explicit) {
|
|
83
|
+
return explicit;
|
|
84
|
+
}
|
|
85
|
+
return "ACTIVE";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
handleMetaError,
|
|
90
|
+
resolveAccountIdArg,
|
|
91
|
+
getDateRange,
|
|
92
|
+
todayIso,
|
|
93
|
+
daysAgoIso,
|
|
94
|
+
csvOrJson,
|
|
95
|
+
resolveEffectiveStatus
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=chunk-AVJ3B6LC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (err.code === \"UNAUTHORIZED\" || err.code === \"NOT_FOUND\") {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n"],"mappings":";;;;;;;;;;;;AAMA,IAAM,SAAS;AAER,SAAS,gBAAgB,KAAqB;AACnD,MAAI,eAAe,UAAU;AAC3B,QAAI,IAAI,SAAS,kBAAkB,IAAI,SAAS,aAAa;AAC3D,4BAAsB,YAAY,IAAI,OAAO;AAAA,IAC/C;AACA,UAAM,WAA6B;AAAA,MACjC,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM,IAAI;AAAA,QACV,SAAS,IAAI;AAAA,QACb,KAAK,EAAE,QAAQ,UAAU,aAAa,IAAI,QAAQ;AAAA,QAClD,WAAW,IAAI,SAAS,kBAAkB,IAAI,SAAS;AAAA,MACzD;AAAA,IACF;AACA,iBAAa,QAAQ;AACrB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,eAAa,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,QAAQ,EAAE,CAAC;AACrE,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,gBAAgB;AAGf,SAAS,oBAAoB,MAAuC;AACzE,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,KAAK,YAAY,OAAO,EAAE;AAChC,MAAI,CAAC,IAAI;AACP,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,CAAC,cAAc,KAAK,EAAE,GAAG;AAC3B,iBAAa;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,uBAAuB,EAAE;AAAA,MACpC;AAAA,IACF,CAAC;AACD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,SAAO,GAAG,WAAW,MAAM,IAAI,KAAK,OAAO,EAAE;AAC/C;AAEO,SAAS,aAAa,MAAwF;AACnH,QAAM,aAAa,KAAK,aAAa;AACrC,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAQ,KAAK;AACnB,MAAI,SAAS,MAAO,QAAO,EAAE,OAAO,MAAM;AAC1C,SAAO,EAAE,YAAY,cAAc,UAAU;AAC/C;AAEO,SAAS,WAAmB;AACjC,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAEO,SAAS,WAAW,MAAsB;AAC/C,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACvE;AAEO,SAAS,UAAU,MAAuC;AAC/D,SAAQ,KAAK,UAAiC;AAChD;AAOO,SAAS,uBAAuB,MAAmD;AACxF,MAAI,KAAK,cAAc,GAAG;AACxB,WAAO;AAAA,EACT;AACA,QAAM,WAAW,KAAK,kBAAkB;AACxC,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|