@koda-sl/baker-cli 0.382.0 → 0.383.0-dev.9779e8b49

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
@@ -942,8 +942,6 @@ baker ads meta custom-conversions --account-id act_123 --full # raw rule JSON,
942
942
 
943
943
  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.
944
944
 
945
- Staging also refuses what LinkedIn would reject at publish: a campaign group created `PAUSED` (only `DRAFT`/`ACTIVE`), a group `--total-budget` without `--end`, a `PAUSED` campaign in a `DRAFT` group, archiving a `DRAFT` ad, and a library image that is not JPG, PNG or GIF. Every campaign carries `--political-intent` (`NOT_DECLARED` by default; `NOT_POLITICAL` or `POLITICAL` are the advertiser's declaration). A write to an ad account LinkedIn has not enabled for Baker fails with a message saying so.
946
-
947
945
  **That publish reaches the real ad account for every company** — Meta writes are generally available, with no per-company flag. Discarding the chat before it completes is what undoes a staged change.
948
946
 
949
947
  Chain a full ad from scratch with `meta_temp_*` refs (each create returns one):
@@ -1109,7 +1107,7 @@ baker ads linkedin bid-pricing --account-id 503001492 \
1109
1107
  - URN normalization: every URN field becomes `{type, id, urn}` so rows are self-describing.
1110
1108
  - Wide windows (>180d) auto-segment into 30-day chunks and combine, since `q=analytics` caps at 15,000 rows with no pagination.
1111
1109
 
1112
- **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set. Only campaigns can be `COMPLETED`; each listing's `--help` names the statuses it accepts, and any other value is refused with `INVALID_STATUS` and that list before LinkedIn is called.
1110
+ **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set.
1113
1111
 
1114
1112
  **`creatives` is compact by default** — `id`, `urn`, `campaign`, `intendedStatus`, `isServing`, `reviewStatus`, `format`, and the resolved destination. The destination is read out of the format-specific `content` block for you: direct-content ads (text, spotlight) report a `landingUrl`; post-based ads (image, video, carousel, document, article, event, TLA) report a `postUrn`, because their destination lives in the sponsored post and is not reachable through the ads API. Pass `--fields id,content.textAd.landingPage` for specific dotted paths, or `--full` for the complete raw records.
1115
1113
 
@@ -1170,7 +1168,7 @@ Write commands never call the LinkedIn API at stage time. Each stages an op on t
1170
1168
 
1171
1169
  ```bash
1172
1170
  # Chain creates via li_temp_* refs — group → campaign → creative in one chat
1173
- baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-08-01 --end 2026-10-31 --total-budget 5000 --currency EUR # → li_temp_x1
1171
+ baker ads linkedin campaign-groups create --name "Q3 ABM" --total-budget 5000 --currency EUR # → li_temp_x1
1174
1172
  baker ads linkedin campaigns create --name "ABM Tier-1" --group li_temp_x1 \
1175
1173
  --objective WEBSITE_VISIT --type SPONSORED_UPDATES --cost-type CPC \
1176
1174
  --bid 8.50 --daily-budget 75 --currency EUR --locale en_US --targeting-file targeting.json # → li_temp_x2
@@ -2623,7 +2621,7 @@ baker images ingest https://images.pexels.com/photos/13219418/pexels-photo-13219
2623
2621
  baker images ingest https://acme.com/hero.png --source firecrawl --external-url https://acme.com/pricing --context "competitor pricing hero"
2624
2622
  ```
2625
2623
 
2626
- `--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `pexels`, `pixabay`, `magnific` (retired provider, kept for existing rows), `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
2624
+ `--source` is a closed set, validated before the request so a wrong value costs nothing: `uploaded`, `website`, `google_testimonial`, `trustpilot_testimonial`, `instagram`, `tiktok`, `linkedin`, `facebook`, `pexels`, `pixabay`, `magnific` (retired provider, kept for existing rows), `brandfetch`, `google_images`, `firecrawl`, `screenshotone`, `iconify`, `giphy`, `pinterest`, `ai_generated`, `layer_edit`. Use `website` for an asset pulled off a company's own site.
2627
2625
 
2628
2626
  Returns `{ imageId, deduped, contentHash }`. When `deduped: true`, an existing library row is returned — no new bytes are stored. Max ingest size 25MB.
2629
2627
 
@@ -2957,7 +2955,7 @@ Re-ingesting the same link returns the existing row with `deduped: true` instead
2957
2955
 
2958
2956
  | Flag | Purpose |
2959
2957
  |---|---|
2960
- | `--source` | Provenance. Inferred from the link's host when omitted, and that inference covers 20 video platforms — `youtube`, `instagram`, `tiktok`, `twitter`, `facebook`, `reddit`, `vimeo`, `dailymotion`, `twitch`, `bluesky`, `bilibili`, `vk`, `ok`, `pinterest`, `tumblr`, `snapchat`, `loom`, `streamable`, `newgrounds`, `rutube` — plus `uploaded` and `url`. A link from anywhere else still ingests, recorded as `url`. |
2958
+ | `--source` | Provenance. Inferred from the link's host when omitted, and that inference covers 21 video platforms — `youtube`, `instagram`, `tiktok`, `twitter`, `facebook`, `linkedin`, `reddit`, `vimeo`, `dailymotion`, `twitch`, `bluesky`, `bilibili`, `vk`, `ok`, `pinterest`, `tumblr`, `snapchat`, `loom`, `streamable`, `newgrounds`, `rutube` — plus `uploaded` and `url`. A link from anywhere else still ingests, recorded as `url`. |
2961
2959
  | `--external-id` | Dedupe key — re-running the same link returns the existing video. Derived from the link automatically on the download route. |
2962
2960
  | `--external-url` | Canonical page URL. Defaults to the input for a page URL. |
2963
2961
  | `--download` | Skip the direct attempt and always download. |
@@ -2989,6 +2987,16 @@ Requires `yt-dlp` on `PATH` (present in the Baker Runtime). Private, age-restric
2989
2987
 
2990
2988
  **yt-dlp goes stale, and that looks like a broken video.** Large platforms change their players every few weeks and an out-of-date `yt-dlp` then resolves *no* formats at all, failing with "Requested format is not available" or "Only images are available" rather than anything that names the real cause. The error classifies this case separately and says the link is probably fine. The Runtime image installs `yt-dlp` unpinned at build time (`packages/e2b-template/src/template.ts`), so it ages with the image — if platform ingests start failing across the board, rebuild the template before investigating anything else. Direct file URLs don't depend on `yt-dlp` at all and keep working regardless.
2991
2989
 
2990
+ ### `baker videos fetch <url> --out <dir>`
2991
+
2992
+ Download a video page (YouTube, TikTok, Vimeo, …) to a local `.mp4` without adding it to the library — the download half of `ingest`, with the same proxy fallback and duration check. `--max-seconds` lowers the length limit (default 3 hours); `--timeout-ms` bounds the download. By default it keeps the best quality the site offers; `--max-height 1080` keeps a version whose shorter side is at most that when there is one (a vertical Short keeps its 1080×1920) (a 4K master can be five times the bytes). Returns `{ file, durationSeconds, egress }`. The file is named after the video (e.g. `youtube-abc123.mp4`), so several fetches can share one folder; an existing file is never overwritten — a second copy gets a `-2` suffix. Nothing else is left in the folder.
2993
+
2994
+ ```bash
2995
+ baker videos fetch https://www.youtube.com/watch?v=abc123 --out ./clips
2996
+ ```
2997
+
2998
+ Baker's YouTube source sync runs this same command on a disposable machine, with `--max-height 1080`, because a YouTube file URL only works from the IP that requested it.
2999
+
2992
3000
  ### `baker videos delete <id>`
2993
3001
 
2994
3002
  Delete a video by ID.
@@ -3142,6 +3150,32 @@ baker avatars delete marta --archive
3142
3150
 
3143
3151
  ---
3144
3152
 
3153
+ ### Media Sources (`baker media-sources`)
3154
+
3155
+ Connect a public social account so its posts fill the image and video libraries: **Instagram**, **TikTok**, **YouTube** (videos and Shorts only), **LinkedIn** company pages and **Facebook** pages — or a **Google Drive folder** the user picks from the company's Google Drive connection. Each social sync reaches back up to a year, stops paging at the first page it already has, imports only what the library doesn't have yet, keeps carousels/albums/documents together as a set (`images group` / `videos group`), and passes the post caption to the analysis. A source is named by its key, `<platform>:<handle>`.
3156
+
3157
+ Connecting and removing apply immediately and show on the chat as an already-applied change.
3158
+
3159
+ | Command | What it does |
3160
+ |---|---|
3161
+ | `baker media-sources add <profile URL \| @handle> [--platform <p>] [--every never\|daily\|weekly\|monthly]` | Look the account up (must exist and be public) and connect it; the import starts in the background. `--platform` only for a bare handle. Idempotent. |
3162
+ | `baker media-sources list` | Every source: key, platform, label, auto-sync, and per library `{ status, stored, pending?, failed?, error? }`. |
3163
+ | `baker media-sources sync <key>` | Import new posts now. |
3164
+ | `baker media-sources schedule <key> --every <interval>` | Set the auto-sync cadence. |
3165
+ | `baker media-sources remove <key>` | Disconnect; imported media stays in the library. |
3166
+
3167
+ ```bash
3168
+ baker media-sources add https://www.tiktok.com/@acme
3169
+ baker media-sources add @acme --platform instagram --every daily
3170
+ baker media-sources list
3171
+ ```
3172
+
3173
+ YouTube downloads are the expensive import (a disposable machine per batch), so a YouTube sync takes the newest 25 videos it hasn't stored, skips videos over 20 minutes, and continues with older ones on the next sync.
3174
+
3175
+ A Google Drive folder is never added with `add`: the agent calls `request_connection` with `platform: "google-drive"` and `choose: "media-folder"`, and the user picks the folder in the chat card (or in Baker, Add source → Google Drive) — only the Baker team can for now. `add <drive link>` is refused with that `error.fix`; the card's result carries the new source's `mediaSourceKey`. Once connected it is managed by key like any source. Everything in it and its subfolders is imported in batches of 150 images and 20 videos (60 minutes / 5 GB at most each), new files arrive with each scheduled sync (weekly unless changed), and images are stored resized to 2560 px. Imported images from every source are resized the same way.
3176
+
3177
+ ---
3178
+
3145
3179
  ### Winning Ads (`baker winning-ads`)
3146
3180
 
3147
3181
  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` / `feed` / `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.
@@ -5474,7 +5508,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5474
5508
 
5475
5509
  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`.
5476
5510
 
5477
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5511
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5478
5512
 
5479
5513
  **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 that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5480
5514
 
@@ -5528,7 +5562,7 @@ baker canvas run ./reference-ad.video.canvas.json
5528
5562
  |---|---|---|
5529
5563
  | `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
5530
5564
  | `--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`. |
5531
- | `--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) — only on a video with nobody in it; on one showing people it is refused and every frame is generated. |
5565
+ | `--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). |
5532
5566
  | `--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. |
5533
5567
  | `--seam-dedup <mode>` | `head` | How to dedup the boundary frame two clips SHARE when a long shot is split for length (the second clip's first frame IS the first clip's last frame, so a plain concat freezes on it for a frame). `head` drops the second clip's first frame, `tail` drops the first clip's last frame, `off` keeps both. Only touches shared-frame continuation joins — a hard cut between two shots shares no frame. |
5534
5568
  | `--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. |
@@ -20,9 +20,9 @@ import {
20
20
  shouldEscalate
21
21
  } from "./chunk-HJEMOSPH.js";
22
22
 
23
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
23
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
24
24
  var require_safe_stable_stringify = __commonJS({
25
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
25
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
26
26
  "use strict";
27
27
  var { hasOwnProperty } = Object.prototype;
28
28
  var stringify = configure2();
@@ -1123,7 +1123,7 @@ function resolveAdaptFormats(params) {
1123
1123
  return params.formats ?? [];
1124
1124
  }
1125
1125
 
1126
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1126
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
1127
1127
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1128
1128
  var configure = import__.default.configure;
1129
1129
  var wrapper_default = import__.default;
@@ -5999,6 +5999,12 @@ function isVimeoAuthFailure(detail) {
5999
5999
  return VIMEO_AUTH_SIGNATURES.some((signature) => haystack.includes(signature));
6000
6000
  }
6001
6001
 
6002
+ // src/lib/ytDlpFormat.ts
6003
+ function videoFormatArgs(maxResolution) {
6004
+ const sort = maxResolution ? ["-S", `res:${maxResolution}`] : [];
6005
+ return ["--format", "bv*+ba/b", ...sort, "--merge-output-format", "mp4"];
6006
+ }
6007
+
6002
6008
  // src/lib/ytDlpPlayerClient.ts
6003
6009
  var YOUTUBE_FALLBACK_CLIENT_ARGS = ["--extractor-args", "youtube:player_client=android"];
6004
6010
  var MEDIA_FETCH_REFUSED = "unable to download video data";
@@ -6067,7 +6073,7 @@ async function runYtDlp(args) {
6067
6073
  if (args.audioOnly) {
6068
6074
  argv.push("-x", "--audio-format", "m4a");
6069
6075
  } else {
6070
- argv.push("--format", "bv*+ba/b", "--merge-output-format", "mp4");
6076
+ argv.push(...videoFormatArgs(args.maxHeight));
6071
6077
  }
6072
6078
  const cookieFile = await writeVimeoCookieFile(args.vimeoCookie, args.workDir);
6073
6079
  if (cookieFile) argv.push("--cookies", cookieFile);
@@ -9986,4 +9992,4 @@ export {
9986
9992
  defaultRegistry,
9987
9993
  createEngineFromEnv
9988
9994
  };
9989
- //# sourceMappingURL=chunk-FGHMWBUD.js.map
9995
+ //# sourceMappingURL=chunk-ZW6DHSDG.js.map