@koda-sl/baker-cli 0.305.0-dev.a04fd407d → 0.306.0-dev.bb28562ac
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 +67 -13
- package/dist/{chunk-ZNN6ZHDS.js → chunk-3TEVIAAV.js} +41 -12
- package/dist/chunk-3TEVIAAV.js.map +1 -0
- package/dist/cli.js +3660 -2794
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +2 -5
- package/dist/chunk-ZNN6ZHDS.js.map +0 -1
package/README.md
CHANGED
|
@@ -352,6 +352,7 @@ The same lines also go to stderr. A read with nothing to report prints none of i
|
|
|
352
352
|
| Preset | Description | Default date range |
|
|
353
353
|
|------------------------|------------------------------------------|--------------------|
|
|
354
354
|
| `campaign-performance` | Campaign metrics overview | LAST_30_DAYS |
|
|
355
|
+
| `conversions-by-action` | Which conversion action each campaign's conversions actually are | LAST_30_DAYS |
|
|
355
356
|
| `keyword-analysis` | Keyword performance per ad group | LAST_30_DAYS |
|
|
356
357
|
| `keyword-serving` | Why a keyword is limited — status reasons, first-page bid estimate vs max CPC, quality score | ALL_TIME |
|
|
357
358
|
| `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
|
|
@@ -366,6 +367,8 @@ The same lines also go to stderr. A read with nothing to report prints none of i
|
|
|
366
367
|
|
|
367
368
|
Every preset except the negatives lists and `account-summary` filters to **actually-serving entities** by default — the full status chain (`campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED'`, as applicable) — and selects the status columns so you can see them. Pass `--include-paused` to widen to `!= 'REMOVED'` (keeps paused, still drops removed). Raw GAQL (no `--preset`) has no such default: a query over a serving-hierarchy resource without a status filter emits a `SERVING_SCOPE` warning reminding you to add the chain, because Google Ads has no single serving flag.
|
|
368
369
|
|
|
370
|
+
**Conversion rates across campaigns are not comparable by default.** `metrics.conversions` is a sum over whatever conversion actions are primary for each campaign, and those differ campaign by campaign — a webinar registration in one, an info request in the next. A read that selects a conversion metric, is not segmented by conversion action, and is not pinned to one campaign (`WHERE campaign.id = …`) comes back with a `CONVERSION_MIX` warning: a total across those rows adds different events, and one campaign's rate is not comparable with another's. `--preset conversions-by-action` is the read that settles it. Like every other warning it travels in `--output csv|jsonl|md` and on the `--out` file summary.
|
|
371
|
+
|
|
369
372
|
**"Eligible (Limited)" keywords:** an enabled keyword that barely shows is invisible in the performance presets — near-zero impressions look the same whether it is outbid, low quality, or rarely searched. `--preset keyword-serving` reads the signals behind Google Ads' status column: `ad_group_criterion.primary_status` and `primary_status_reasons` (the status enum has no `LIMITED` value — "(Limited)" lives in the reasons while the status stays `ELIGIBLE`), `position_estimates.first_page_cpc_micros` (the number in "Below first page bid (€3.63)"), the max CPC actually in force and where it comes from, quality score split into ad relevance / landing page / expected CTR, approval status, and `system_serving_status`. Any read whose rows carry those fields — preset or raw GAQL — comes back with a `KEYWORD_SERVING_LIMITED` warning naming the affected keywords (each by its ad group, since one keyword's copies share a criterion id) and the bid each would need. Below-first-page-bid and low-quality want opposite fixes (raise the bid vs. improve relevance), so the warning reports them separately. On `--out` the verdict is folded across pages as they stream to disk and returned on the file summary; like every other warning it also travels in `--output csv|jsonl|md`.
|
|
370
373
|
|
|
371
374
|
The warning defers to Google: when the query selects `primary_status_reasons`, a keyword Google did not flag is not reported, even if its bid sits under the first-page estimate — the estimate is advisory and the bid may not be the criterion's to set under a portfolio strategy. Only an estimates-only read (no reasons selected) falls back to inferring the limit from the bid gap.
|
|
@@ -1265,6 +1268,9 @@ baker analytics conversions --event page:a --count-mode every_time # re
|
|
|
1265
1268
|
baker analytics conversions --rename "Lead form" --to "Lead" # merge two into one row
|
|
1266
1269
|
baker analytics conversions --remove page:request_demo
|
|
1267
1270
|
baker analytics conversions --remove-name "Lead"
|
|
1271
|
+
baker analytics sites # the websites the measurement tag is accepted from
|
|
1272
|
+
baker analytics sites --add example.com # and every subdomain of it — applies at once, no publish
|
|
1273
|
+
baker analytics website-tag # the tag for a site Baker does not publish, with the key in it
|
|
1268
1274
|
baker analytics delivery --page 2 # the next page of a long list
|
|
1269
1275
|
baker analytics submissions --flow contact # every attempt at a Form, delivered or not
|
|
1270
1276
|
```
|
|
@@ -1277,6 +1283,13 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1277
1283
|
- The `sparkline` on `landings` is dropped unless `--full`, and resampled to 30 points when present. It is bucketed by `--granularity` upstream, so ninety days at `hour` is 2,160 numbers per page.
|
|
1278
1284
|
- Past ~25 rows the response carries a hint naming the row count and pointing at `--output md`.
|
|
1279
1285
|
|
|
1286
|
+
**Measuring a website Baker does not publish.** `baker analytics website-tag` returns the tag with this company's site key in it, the websites it is accepted from, and the whole install as Markdown. Two things decide whether it does anything:
|
|
1287
|
+
|
|
1288
|
+
- **`baker analytics sites` first.** The key is public and is **silently ignored** from any origin not on the declared list, so a tag on an undeclared website is a perfect install that measures nothing, with no failing request anywhere. `--add` / `--remove` are repeatable, apply immediately, need no publish, and a tag already installed starts being accepted at once. Declare the **domain**, not the subdomain you happen to be installing on: one entry covers everything under it (`example.com` answers for `www.`, `go.` and `shop.eu.`) and it never widens upward. The response also carries `suggestedOrigins` — domains Baker already serves this company's landings on that nothing covers yet.
|
|
1289
|
+
- **The snippet is two scripts and both are required.** The first creates `window.baker` synchronously and queues calls; the second is the tag, which is async. Paste only the second and any outcome reported before it finishes loading reaches nothing — which on a thank-you page is the normal case. For the same reason, never guard a call with `if (!window.baker) return`: on the fastest pages that is always true, and it throws the conversion away with no error anywhere. Call `window.baker.track(…)` directly.
|
|
1290
|
+
|
|
1291
|
+
`--measure` decides whose activity is measured and defaults to `campaigns` (only visitors a Baker landing brought). `--measure all` measures the client's entire website and is a decision somebody has to ask for. `host` is the client's own landing domain when one shares a domain with the site being measured, so the script and its events stay first-party; `firstParty` in the response says which, and removing that domain from Baker means re-running this command.
|
|
1292
|
+
|
|
1280
1293
|
**Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
|
|
1281
1294
|
|
|
1282
1295
|
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
|
|
@@ -1342,6 +1355,8 @@ baker experiment fold # make the page
|
|
|
1342
1355
|
|
|
1343
1356
|
**The goal is one event, named by its key.** `--goal submit:book-a-call` — the same vocabulary the event stream uses (`form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>`, `exit:<host>`), and `baker analytics conversions --candidates` lists the keys these pages produce. Not a conversion *name*, which groups several keys: a variant that swaps the page's Form for a shorter one converts on a different key, and a name would either count both endings as one outcome or count the new version as never converting at all — `--variant-goal` gives the new version its own event when it has one. The goal is fixed for the life of the test and the verdict is read on it alone. Every other conversion the company counts comes back under `secondary` on every result, with the same interval and a one-word reading, and decides nothing. A conversion counts for a visitor wherever it happens after they were shown a version — on the page, on another page, in a delivered lead, or in a deal the CRM posted a week later by email.
|
|
1344
1357
|
|
|
1358
|
+
**`start` checks that both versions still fire a conversion on the same action.** A conversion is a name matched against each page's own `data-baker-*` declarations, so the two versions agree about what an event means only because a person made them agree — and nothing downstream can tell when they stop. `start` compares the sections the two versions do not share and names any conversion whose markup differs, because a step moved from a pop-up to the open page keeps its name and starts counting a scroll instead of a click. A forked section is supposed to change, so most of these are fine; the one that is not is the one nobody looked at. Give the new version's step its own name when it is a different action.
|
|
1359
|
+
|
|
1345
1360
|
**Nothing ends a test on its own.** `status` returns a verdict; `finish --keep original|variant` is the decision, and it takes effect within seconds with nothing to publish. Ending a `keep_running` test records `inconclusive`. Going against the verdict is allowed and recorded. `pause` and `resume` switch the split off and on without losing the test; `cancel` is `finish --keep original` for a live test and takes a staged one back off the session. `update` changes what a test *says* — `--because`, `--change`, `--evidence` (repeatable, `source:what it showed`, optionally `|link`) — at any time, writes down what a finished one taught (`--learning`), and changes what it *measures* — `--goal`, `--lift`, `--expect` — only while it is still staged.
|
|
1346
1361
|
|
|
1347
1362
|
**A finished test is a record.** Its counts are frozen when it ends, so it reads the same next year as it did that day; its `learning` is what it taught, in a person's words. `history` returns every finished test — the claim, what was different, the numbers with their interval and confidence, what was kept and whether that went against the numbers, whether the belief held, and the learning — and `plan` returns the same entries as `pastTests` for the page it sizes, so a hypothesis is chosen against the page's record rather than in ignorance of it.
|
|
@@ -2127,6 +2142,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
|
|
|
2127
2142
|
| `baker images get <id>` | Single record | n/a |
|
|
2128
2143
|
| `baker images upload <file\|url>` | Upload a local file OR remote URL to the library | always on |
|
|
2129
2144
|
| `baker images delete <id>` | Delete a record | n/a |
|
|
2145
|
+
| `baker images describe <id>` | Correct a record's stored name / description / tags | n/a |
|
|
2130
2146
|
| `baker images normalize <files>` | Local recolor + bg removal + trim + resize | n/a (operates on disk, no upload) |
|
|
2131
2147
|
| `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked) | n/a (operates on library image) |
|
|
2132
2148
|
| `baker images layerize <imageId>` | Split into editable layers — transparent cutouts per element plus baked-in copy recovered as editable text (costs credits) | n/a (operates on library image) |
|
|
@@ -2567,6 +2583,20 @@ baker images delete j571abc123def --dry-run
|
|
|
2567
2583
|
baker images delete j571abc123def
|
|
2568
2584
|
```
|
|
2569
2585
|
|
|
2586
|
+
### `baker images describe <id>`
|
|
2587
|
+
|
|
2588
|
+
Correct what the library says an image is. The stored `description` is what `baker images library` retrieves on, so an AI description that came out wrong is what makes an asset unfindable — this is the fix, rather than another query.
|
|
2589
|
+
|
|
2590
|
+
Only the flags you pass change; everything else keeps its current value. `--tags` **replaces** the whole tag set rather than adding to it, so read the current one with `baker images get <id>` first if you mean to extend it. It is repeatable (`--tags a --tags b`) as well as comma-separated — both spellings keep every value.
|
|
2591
|
+
|
|
2592
|
+
```bash
|
|
2593
|
+
baker images describe j571abc123def --description "Founder on stage at SaaStr, blue backdrop, mid-gesture"
|
|
2594
|
+
baker images describe j571abc123def --name "SaaStr keynote" --tags event,team
|
|
2595
|
+
baker images describe j571abc123def --description "…" --full # also returns the whole library row
|
|
2596
|
+
```
|
|
2597
|
+
|
|
2598
|
+
The semantic index rebuilds in the background, so a `library` search run immediately afterwards may still rank on the old wording. The response says so in `hints`.
|
|
2599
|
+
|
|
2570
2600
|
### `baker images normalize <files> [options]`
|
|
2571
2601
|
|
|
2572
2602
|
Local-file image normalization for logos and brand assets. Declarative — describe the desired final state via flags and the pipeline runs the needed stages in order. Operates on files in the sandbox; pair with `baker images upload` to ship results to the library. For super-resolution, upload first then use `baker images upscale <imageId>` (runs on the Convex backend, cost-tracked).
|
|
@@ -2833,6 +2863,15 @@ Requires `yt-dlp` on `PATH` (present in the Baker Runtime). Private, age-restric
|
|
|
2833
2863
|
|
|
2834
2864
|
Delete a video by ID.
|
|
2835
2865
|
|
|
2866
|
+
### `baker videos describe <id>`
|
|
2867
|
+
|
|
2868
|
+
The same correction for a clip: fix the stored `name`, `description` or `tags` when the analysis read the video wrong. Only the flags you pass change, `--tags` replaces the whole set, and the search index rebuilds in the background.
|
|
2869
|
+
|
|
2870
|
+
```bash
|
|
2871
|
+
baker videos describe j571abc123def --description "Customer explaining how onboarding cut setup from a week to a day"
|
|
2872
|
+
baker videos describe j571abc123def --tags testimonial
|
|
2873
|
+
```
|
|
2874
|
+
|
|
2836
2875
|
### `baker videos tags`
|
|
2837
2876
|
|
|
2838
2877
|
List the available video tag names — built-in defaults plus the company's custom tags — straight from the backend. Use it before filtering with `--tags`. Defaults to a markdown list (`--output json` for the `{ ok, data }` envelope).
|
|
@@ -3216,6 +3255,7 @@ baker tags draft # review the staged changes awaiting publish
|
|
|
3216
3255
|
|
|
3217
3256
|
Notes:
|
|
3218
3257
|
|
|
3258
|
+
- **Both reads restate the write path, on stderr.** `list` and `draft` end with the two lines that say the approval form is the only way to change a tag, that it rides on every turn, and that a tag change is never a Task. They are there because a chat concluded from its tool list alone that the form did not exist, never called it, and filed the change as a Task for the user to carry out by hand. stdout stays the list, so piping it is unaffected; `--json` carries the same lines in the envelope's `hints`.
|
|
3219
3259
|
- **`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.)
|
|
3220
3260
|
- **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.
|
|
3221
3261
|
- 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.
|
|
@@ -3796,7 +3836,10 @@ because a video model garbles a wordmark every time:
|
|
|
3796
3836
|
- **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
|
|
3797
3837
|
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
3798
3838
|
the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
|
|
3799
|
-
the words on the button
|
|
3839
|
+
the words on the button. There is **no default**: with no `cta` the card carries the mark
|
|
3840
|
+
alone. It used to fall back to the last line of the script, which printed that sentence
|
|
3841
|
+
twice on the closing frame — once as the button and once as the caption of the line being
|
|
3842
|
+
spoken over it. Repeat the closing line as the `cta` and it is dropped for the same reason.
|
|
3800
3843
|
- **The brand fills itself.** When the spec leaves `brand.logo` or `brand.palette` empty,
|
|
3801
3844
|
they are read from the workspace — `src/brand/BRAND.md` for the colours (in document
|
|
3802
3845
|
order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
|
|
@@ -4073,7 +4116,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
|
|
|
4073
4116
|
|
|
4074
4117
|
**Params** — discriminated on `source`:
|
|
4075
4118
|
|
|
4076
|
-
`source: "url"` (yt-dlp / direct fetch /
|
|
4119
|
+
`source: "url"` (yt-dlp / direct fetch / Firecrawl):
|
|
4077
4120
|
|
|
4078
4121
|
| Name | Type | Required | Constraint |
|
|
4079
4122
|
|---|---|---|---|
|
|
@@ -4093,7 +4136,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
|
|
|
4093
4136
|
|
|
4094
4137
|
**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.
|
|
4095
4138
|
|
|
4096
|
-
**Cost:** 0 engine credits for direct fetch + yt-dlp + local file.
|
|
4139
|
+
**Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Firecrawl charges per scrape.
|
|
4097
4140
|
|
|
4098
4141
|
---
|
|
4099
4142
|
|
|
@@ -5407,10 +5450,10 @@ baker canvas catalog | jq '.compositions[].id'
|
|
|
5407
5450
|
// URL source — YouTube audio-only (yt-dlp -x)
|
|
5408
5451
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://youtu.be/dQw4w9WgXcQ", "expect": "audio" } }
|
|
5409
5452
|
|
|
5410
|
-
// URL source — blog post → clean markdown (
|
|
5453
|
+
// URL source — blog post → clean markdown (Firecrawl)
|
|
5411
5454
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/blog/post", "expect": "text" } }
|
|
5412
5455
|
|
|
5413
|
-
// URL source — PDF → markdown (
|
|
5456
|
+
// URL source — PDF → markdown (Firecrawl handles PDFs)
|
|
5414
5457
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/whitepaper.pdf", "expect": "text" } }
|
|
5415
5458
|
|
|
5416
5459
|
// URL source — raw markdown file (direct fetch)
|
|
@@ -5438,7 +5481,7 @@ baker canvas catalog | jq '.compositions[].id'
|
|
|
5438
5481
|
| url | video / audio | any | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor) |
|
|
5439
5482
|
| url | image / json / font | any | `direct_fetch` (HTTP GET) |
|
|
5440
5483
|
| url | text | ends in `.txt` or `.md` | `direct_fetch` |
|
|
5441
|
-
| url | text | anything else (HTML, PDF, …) | `
|
|
5484
|
+
| url | text | anything else (HTML, PDF, …) | `firecrawl` `/v2/scrape` (extracted markdown — HTML/PDF made readable) |
|
|
5442
5485
|
|
|
5443
5486
|
If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serves `text/html`, or a `.pdf` file is passed as `expect: "image"`), the node fails with a clear `kind_mismatch` error instead of forwarding bad bytes downstream.
|
|
5444
5487
|
|
|
@@ -5481,7 +5524,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
|
|
|
5481
5524
|
file_size?: number, // bytes
|
|
5482
5525
|
original_filename?: string, // basename of the source path
|
|
5483
5526
|
|
|
5484
|
-
strategy: "direct_fetch" | "
|
|
5527
|
+
strategy: "direct_fetch" | "firecrawl" | "yt_dlp" | "local_file",
|
|
5485
5528
|
ingested_at: string, // ISO 8601 timestamp
|
|
5486
5529
|
|
|
5487
5530
|
// yt-dlp adds (when available from --dump-json):
|
|
@@ -5492,7 +5535,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
|
|
|
5492
5535
|
width?: number,
|
|
5493
5536
|
height?: number,
|
|
5494
5537
|
|
|
5495
|
-
//
|
|
5538
|
+
// Firecrawl adds:
|
|
5496
5539
|
word_count?: number,
|
|
5497
5540
|
},
|
|
5498
5541
|
|
|
@@ -5511,8 +5554,8 @@ The `url` is a stable R2 URL — remote downstream nodes (e.g. `video_lipsync`)
|
|
|
5511
5554
|
|-----------------------|----------------|------------------------------------------------------------------------------|
|
|
5512
5555
|
| `INVALID_PARAMS` | engine validator | Missing/malformed fields, mixing `url` + `path`, unknown `source`, unknown `expect`. |
|
|
5513
5556
|
| `kind_mismatch` | url + direct_fetch | URL returned bytes whose mime doesn't match `expect`. Message: `ingest expect=<X> but <url> returned <mime> (kind=<Y>)`. |
|
|
5514
|
-
| `kind_mismatch` | path + local_file | File mime (from extension or magic-byte sniff) doesn't match `expect`, *or* mime doesn't map to any of our kinds (e.g. local PDFs aren't supported as `text` — host them via URL for
|
|
5515
|
-
| `provider_error` | url +
|
|
5557
|
+
| `kind_mismatch` | path + local_file | File mime (from extension or magic-byte sniff) doesn't match `expect`, *or* mime doesn't map to any of our kinds (e.g. local PDFs aren't supported as `text` — host them via URL for Firecrawl to extract). |
|
|
5558
|
+
| `provider_error` | url + firecrawl / direct_fetch | Firecrawl `/v2/scrape` non-2xx or no markdown in the response, **or the target site answering 4xx/5xx** (Firecrawl reports that inside a 200 carrying the error page, so it is refused rather than stored as the page), or upstream `fetch` non-ok. Terminal: a bad host, a 404 and a block page fail identically on a retry and each retry is billed. Retried: a 429 from either side, a Firecrawl-side 5xx, a dropped connection. |
|
|
5516
5559
|
| `file_not_found` | path + local_file | `stat()` returned `ENOENT`. Error names the absolute resolved path. |
|
|
5517
5560
|
| `not_a_file` | path + local_file | Path exists but is a directory / block device / socket. |
|
|
5518
5561
|
| `permission_denied` | path + local_file | `stat()` or `readFile()` returned `EACCES`. |
|
|
@@ -5529,7 +5572,7 @@ All execution-time failures flow through `NodeExecutionError` so they appear in
|
|
|
5529
5572
|
- **Engine-level cache** (`canvas/.cache/index/`) keys by node params + node version + node-supplied cache extras. Re-running the same canvas with the same params is a cache hit and skips the network/yt-dlp/local-read entirely.
|
|
5530
5573
|
- **yt-dlp version is folded into the cache key** for `source: "url"` with `expect: video | audio`. Upgrading yt-dlp busts the cache automatically, since new versions can produce different bitstreams.
|
|
5531
5574
|
- **Local file `mtime + size` is folded into the cache key** for `source: "path"`. Editing the file (any save bumps mtime) invalidates the cache so you get fresh ingestion. mtime+size is cheap to read at validate time; for cases where mtime is unreliable (`cp -p`, content-only changes), force-bypass with `--cache-policy bypass` or change the canvas's `cache_salt`.
|
|
5532
|
-
- **No TTL.**
|
|
5575
|
+
- **No TTL.** Firecrawl and direct fetch don't cache-bust on page changes — if the source page updates, use `--cache-policy bypass` or change `cache_salt`.
|
|
5533
5576
|
|
|
5534
5577
|
**Local files (`source: "path"`):**
|
|
5535
5578
|
|
|
@@ -5552,10 +5595,10 @@ ingest { source: "path", expect: "video" } → video_transcribe # Local
|
|
|
5552
5595
|
**Requirements:**
|
|
5553
5596
|
|
|
5554
5597
|
- `yt-dlp` and `ffmpeg` on PATH for `source: "url"` with `expect: "video" | "audio"` (already in the E2B sandbox image; install locally with `pip install yt-dlp` and `brew install ffmpeg` / equivalent).
|
|
5555
|
-
- `
|
|
5598
|
+
- `FIRECRAWL_API_KEY` on the Convex backend for `source: "url"` with `expect: "text"` on non-`.md`/`.txt` URLs.
|
|
5556
5599
|
- No additional dependencies for `source: "path"` — just filesystem access.
|
|
5557
5600
|
|
|
5558
|
-
**Cost:** zero engine credits for direct_fetch + yt_dlp.
|
|
5601
|
+
**Cost:** zero engine credits for direct_fetch + yt_dlp. Firecrawl charges per scrape on your Firecrawl account (configured outside Baker).
|
|
5559
5602
|
|
|
5560
5603
|
---
|
|
5561
5604
|
|
|
@@ -6044,6 +6087,9 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
6044
6087
|
- **0.259.0**: `baker canvas scaffold-ad` is listed in `baker canvas --help` with a description, and the three video routes are separated in the skill. It had a usage line and no description row, so an agent reading help saw two scaffolders explained and a bare name — and three runs from the same prompt took three different wrong paths: hand-directing the flow, reproducing a competitor ad found via `winning-ads`, and hand-authoring a canvas. The routes now state their own boundary: `scaffold-ad` is the default for a new ad, `scaffold-video` only reproduces a video the USER supplied, and `video-flow.md` opens by saying so rather than calling itself the default eleven times.
|
|
6045
6088
|
- **0.257.0**: the frame-vision pass now asks whether what is on screen could physically happen, not just whether the frame is empty. The defect that motivated it was a generated shot of a solar panel roughly five metres tall being lifted onto a roof by one person — well lit, on brief, perfectly legible, and impossible. That is what reads loudest as "AI-generated", and the previous question could not see it: nothing was missing, something present was the wrong size. Asked the new question, the pass called the rejected shot *"physically implausible due to the immense weight and surface area"* and the replacement *"consistent with what two people could reasonably maneuver"* — the same verdict a person reached, unprompted.
|
|
6046
6089
|
- **0.256.0**: the pre-render gate reports caption cards that end mid-clause. The first cut of this check asked for consistency and got it the wrong way round — stripping terminal punctuation makes the cards agree and leaves them wrong, because "En solo nueve días tu casa" is not a line anyone wrote, it is a sentence halved by a word count. Captions split where the script punctuates; each card is then a clause carrying its own mark, grammatical, and consistent as a by-product. The last card is exempt, since a CTA legitimately ends bare. Reported, never rewritten — the copy belongs to whoever wrote it.
|
|
6090
|
+
- **0.297.0**: `baker images describe <id>` and `baker videos describe <id>` correct a library asset's stored `name`, `description` or `tags`. Until now `--context` at ingest time was the only way to influence a description, so an AI description that came out wrong was unfixable by an agent — and that description is what every `baker images library` / `baker videos search` retrieves on, which made the asset quietly unfindable rather than merely mislabelled. Only the flags passed are changed; the rest keep their stored values, because the dashboard's editor always has all three in a form and an agent fixing one sentence does not. `--tags` replaces the whole set (as the dashboard's does) and is repeatable as well as comma-separated, via the shared `repeatedFlags` helper — read off `rawArgs` because citty collapses a repeated flag to its last value, which on a wholesale-replace write deletes the rest. Compact by default; `--full` returns the whole library row in the same round trip. Both re-index in the background, so the response hints that an immediate re-search may still rank on the old wording. A row that is still being analysed is refused with `CONFLICT` rather than written: the describe pass overwrites `name`/`description`/`tags` with the model's own output when it lands, so a correction made in that window disappeared minutes after the CLI reported success. The dashboard never hit it because a non-ready card is not clickable; the agent's door had no such gate. An `error` row stays writable — nothing is scheduled against it.
|
|
6091
|
+
|
|
6092
|
+
## Publishing
|
|
6047
6093
|
|
|
6048
6094
|
### Auto-publish (CI)
|
|
6049
6095
|
|
|
@@ -6088,8 +6134,16 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
|
|
|
6088
6134
|
|
|
6089
6135
|
- **0.271.0**: **`--format demandGenVideo` — the Demand Gen ad most Demand Gen spend actually runs through can be edited.** A `DEMAND_GEN_VIDEO_RESPONSIVE_AD` keeps its copy in `demandGenVideoResponsiveAd`, which has a `longHeadlines` list no other ad format has and takes its business name as a text asset rather than a string. Baker modelled only the multi-asset Demand Gen ad and labelled all four Demand Gen types as one format, so every served video ad was uneditable: the strict content schema refused `longHeadlines` and named a field instead of a missing format, and the only route left was the Google Ads interface. On the account that surfaced it, Demand Gen was 76% of the spend. New flags on `ads create|update`: `--long-headlines` (up to 5 × 90 chars, this format only), `--video-assets`, `--logo-image-assets`, `--call-to-action-assets`; a create with no video is refused with the `assets create` call to make first, and an omitted media list on an update leaves that media on the ad. Two more gates ship with it. **A content flag a format does not read is now refused instead of dropped** — `--long-headlines` on `--format demandGen` staged the copy without them and reported success, the same silent-drop the backend's strict schemas exist to stop. And **the format is checked against the live `ad_group_ad.ad.type` when it stages**: a mismatch is refused with the right format named, and the two Demand Gen types Baker writes no format for (`DEMAND_GEN_CAROUSEL_AD`, `DEMAND_GEN_PRODUCT_AD`) are refused by name, so "Baker cannot edit this ad" arrives at stage time rather than as a publish failure a Session too late.
|
|
6090
6136
|
|
|
6137
|
+
- **0.293.0**: an `assets detach` whose asset link is not on the account is refused **at stage time**, and an asset-link read that leaves the link's own status unscoped is refused in preflight. Both come from one publish: 74 of 85 staged ops came back `RESOURCE_NOT_FOUND` — 67 ad-group sitelinks and 7 campaign ones — twelve hours after the agent reported them staged, against a Session that had already merged. Nothing checked them: `google.assetLink.detach` had no entry in the stage-time snapshot map, so its target read back as "read failed" (silent) instead of "not found" (refused), exactly the gap that was closed for `audienceCriterion`. The read that produced the list had the same hole from the other side — it scoped `campaign.status` and `ad_group.status` and said nothing about `campaign_asset.status` / `ad_group_asset.status`, and Google returns a removed link like any other row, so already-detached extensions read as live. The snapshot query excludes `REMOVED` for that reason: a retired link is zero rows, which is the refusal. A bare-id target still only warns — it names no level, so there is no row to read and a refusal would be a guess.
|
|
6138
|
+
|
|
6139
|
+
Two rules the new chains brought with them. **`campaign_asset` and `ad_group_asset` segment on their owners**, so a `campaign.*` / `ad_group.*` field used in WHERE must also be SELECTed or Google refuses the whole statement (`EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE`) — the rule that already applied to `campaign_budget`, now keyed off a table rather than one resource, and applied to the query `INCOMPLETE_STATUS_CHAIN` hands back so the corrected command is one you can actually run. Checked live on v23 against 3865083115: `ad_group_ad`, `ad_group_criterion` and `asset_group_asset` filter their ancestors freely, these two do not. And **`asset_group_asset` carries the same status** its campaign and asset group do not, so its chain gained `asset_group_asset.status` too — the Performance Max `asset-performance` preset scopes and shows it.
|
|
6140
|
+
|
|
6141
|
+
Underneath both: **`query` now returns a `.resource_name` you selected.** Google injects `resource_name` into every row whether or not it was asked for, and the backend stripped all of them as noise — including the one the SELECT clause named. For an asset link that column *is* the identity (`customers/{cid}/adGroupAssets/{adGroupId}~{assetId}~{FIELD_TYPE}`; there is no id to detach by), so the read the docs prescribe — `SELECT ad_group_asset.resource_name …` — came back without it and composing the string by hand was the only route left. Auto-injected resource names are still stripped; a selected one is kept. This also repairs the `asset-groups detach` instruction, which pointed at `asset_group_asset.resource_name` and could not have worked either.
|
|
6142
|
+
|
|
6091
6143
|
- **0.291.0**: **the image handover Task no longer depends on having uploaded the images.** It was built only from staged `asset.create` image ops, so a Session that audited a live account — found the brand campaign carrying 22 image extensions and the generic one carrying none, and reached for `assets attach` on assets that were *already in the library* — got Google's refusal and nothing else: no upload to hang a Task on, and a CLI hint telling the agent not to raise one by hand because "Baker stages it itself". The one step only the user can take reached them as a sentence in a chat that scrolls away. An image `assets attach` is now **skipped rather than refused**: it still never reaches Google (re-verified live with `validate_only` on v23 *and* v25 against 3865083115, against a control op that validates clean in the same request — `AD_IMAGE` → `UNSUPPORTED_FIELD_TYPE` at customer and campaign level, `MARKETING_IMAGE` → `FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE`, `BUSINESS_LOGO` → `CUSTOMER_NOT_VERIFIED`, which is what proves the field type is the objection; **we are not on Google's allowlist for it**), but the skip carries the image and campaign names, recorded on the draft so the next staged op cannot recompute the Task away, and a batch that also carries sitelinks keeps them instead of failing whole. The schema stopped refusing the field type for the same reason: a boundary refusal has no chat to record a handover against.
|
|
6092
6144
|
|
|
6145
|
+
- **0.306.0**: review fixes across the avatar and ad paths. **A silent clip is 5 seconds again** — the 8s speaking floor was applied to every `studio animate` call, not just ones that cast an avatar, so plain b-roll was lengthened by three seconds and billed for them. **`scaffold-ad` validates the mark it will actually draw**: the logo gates read the path the spec named rather than the one `fetchClientLogo` had just put there, so a fetched mark either skipped both checks or was refused as unreadable. **The render review is told where the ad is set, who it is for and what the script says** — read off the blueprint beside the canvas; without them the watch pass judged an accent against nothing and still graded the verdict as blocking. **A beat is "too short" by its LINE, not by the clip floor** — the old test needed a 25-word clause, so it fired on all three beats of this command's own documented example. **A voiceover stays a voiceover** — a phrase nobody is shown speaking was being folded into the presenter's clip and said on camera, words that shot was never generated for; the fold now applies only where there is a mouth for the words to contradict. `--voice-id` / `--voice-description` are gone from `baker avatars create` (0.252.0 said so; the flags were still declared and still sent), `end_card.cta` is documented as having no default, and the dead Veo/Omni content-block fallbacks are removed.
|
|
6146
|
+
|
|
6093
6147
|
### Testing a pre-release in sandboxes
|
|
6094
6148
|
|
|
6095
6149
|
```bash
|
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
shouldEscalate
|
|
18
18
|
} from "./chunk-WFWU3CHS.js";
|
|
19
19
|
|
|
20
|
-
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/
|
|
20
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
|
|
21
21
|
var require_safe_stable_stringify = __commonJS({
|
|
22
|
-
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/
|
|
22
|
+
"../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
23
23
|
"use strict";
|
|
24
24
|
var { hasOwnProperty } = Object.prototype;
|
|
25
25
|
var stringify = configure2();
|
|
@@ -758,6 +758,19 @@ async function parseErrorBody(res) {
|
|
|
758
758
|
}
|
|
759
759
|
return classifyHttpError(res.status, errObj, message2);
|
|
760
760
|
}
|
|
761
|
+
function providerVerdict(status, errObj, message2) {
|
|
762
|
+
if (errObj.provider === void 0 && errObj.code !== "provider_error") return null;
|
|
763
|
+
return {
|
|
764
|
+
kind: "provider",
|
|
765
|
+
status,
|
|
766
|
+
provider: errObj.provider,
|
|
767
|
+
code: errObj.code ?? "provider_error",
|
|
768
|
+
message: message2,
|
|
769
|
+
// A 4xx is terminal unless the backend says otherwise — the opposite
|
|
770
|
+
// default from the 502 branch, where the status itself means "try again".
|
|
771
|
+
retryable: errObj.retryable ?? false
|
|
772
|
+
};
|
|
773
|
+
}
|
|
761
774
|
function classifyHttpError(status, errObj, message2) {
|
|
762
775
|
if (errObj.code === CONTENT_POLICY_CODE) {
|
|
763
776
|
return { kind: "content_policy", status, provider: errObj.provider, message: message2 };
|
|
@@ -766,7 +779,7 @@ function classifyHttpError(status, errObj, message2) {
|
|
|
766
779
|
return { kind: "unauthorized", status, message: message2 };
|
|
767
780
|
}
|
|
768
781
|
if (status === 400 || status === 422) {
|
|
769
|
-
return { kind: "validation", status, message: message2, details: errObj.details };
|
|
782
|
+
return providerVerdict(status, errObj, message2) ?? { kind: "validation", status, message: message2, details: errObj.details };
|
|
770
783
|
}
|
|
771
784
|
if (status === 502 || status === 504) {
|
|
772
785
|
if (errObj.code === "provider_timeout" || status === 504) {
|
|
@@ -1098,7 +1111,7 @@ function resolveAdaptFormats(params) {
|
|
|
1098
1111
|
return params.formats ?? [];
|
|
1099
1112
|
}
|
|
1100
1113
|
|
|
1101
|
-
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/
|
|
1114
|
+
// ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
1102
1115
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
1103
1116
|
var configure = import__.default.configure;
|
|
1104
1117
|
var wrapper_default = import__.default;
|
|
@@ -1139,6 +1152,21 @@ function normalizeForCanonical(value) {
|
|
|
1139
1152
|
return void 0;
|
|
1140
1153
|
}
|
|
1141
1154
|
|
|
1155
|
+
// ../canvas-contract/src/castName.ts
|
|
1156
|
+
function withoutCastName(text, names) {
|
|
1157
|
+
let out = text;
|
|
1158
|
+
for (const name of names) {
|
|
1159
|
+
const token = name.trim();
|
|
1160
|
+
if (token.length < 3) continue;
|
|
1161
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${escapeForPattern(token)}(?![\\p{L}\\p{N}])`, "giu");
|
|
1162
|
+
out = out.replace(pattern, "the person in the reference");
|
|
1163
|
+
}
|
|
1164
|
+
return out;
|
|
1165
|
+
}
|
|
1166
|
+
function escapeForPattern(value) {
|
|
1167
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1142
1170
|
// ../canvas-contract/src/clipWeight.ts
|
|
1143
1171
|
var MB_PER_SECOND_720P = 2.13;
|
|
1144
1172
|
var RESOLUTION_WEIGHT = {
|
|
@@ -6237,7 +6265,7 @@ function resolveStrategy(params) {
|
|
|
6237
6265
|
case "text": {
|
|
6238
6266
|
const p = safePathname(params.url);
|
|
6239
6267
|
const isRawText = RAW_TEXT_EXTENSIONS.some((ext) => p.endsWith(ext));
|
|
6240
|
-
return isRawText ? "direct_fetch" : "
|
|
6268
|
+
return isRawText ? "direct_fetch" : "firecrawl";
|
|
6241
6269
|
}
|
|
6242
6270
|
default: {
|
|
6243
6271
|
params.expect;
|
|
@@ -6254,10 +6282,10 @@ function safePathname(rawUrl) {
|
|
|
6254
6282
|
}
|
|
6255
6283
|
var ingestNode = defineNode({
|
|
6256
6284
|
id: "ingest",
|
|
6257
|
-
version: "1.
|
|
6285
|
+
version: "1.3.0",
|
|
6258
6286
|
category: "io",
|
|
6259
6287
|
location: "local",
|
|
6260
|
-
summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs),
|
|
6288
|
+
summary: "Ingest an external URL or a local file into the asset store. Declare the kind you expect (image/video/audio/text/json/font); the node picks the strategy. For source=url: yt-dlp for video/audio (YouTube/TikTok/Vimeo/etc. and direct file URLs), Firecrawl for HTML/PDF pages \u2192 markdown, direct HTTP fetch for binary URLs (images, fonts) and raw .txt/.md. For source=path: read from the local filesystem and upload to R2.",
|
|
6261
6289
|
when_to_use: 'Use as the on-ramp for any external asset a canvas needs. `expect` makes the output port statically typed so downstream wiring is checked before any byte is fetched. Pick `source: "url"` for portable canvas; pick `source: "path"` for local files (canvas is not portable across machines without the file). Compose with other nodes for richer pipelines (e.g. `ingest expect=video` \u2192 `video_transcribe`).',
|
|
6262
6290
|
inputs: IngestInputs,
|
|
6263
6291
|
params: IngestParams,
|
|
@@ -6284,8 +6312,8 @@ function runStrategy(strategy, params, ctx) {
|
|
|
6284
6312
|
switch (strategy) {
|
|
6285
6313
|
case "direct_fetch":
|
|
6286
6314
|
return execDirectFetch(params, ctx);
|
|
6287
|
-
case "
|
|
6288
|
-
return
|
|
6315
|
+
case "firecrawl":
|
|
6316
|
+
return execFirecrawl(params, ctx);
|
|
6289
6317
|
case "yt_dlp":
|
|
6290
6318
|
return execYtDlp(params, ctx);
|
|
6291
6319
|
case "local_file":
|
|
@@ -6338,11 +6366,11 @@ async function ingestImageUrl(url, ctx) {
|
|
|
6338
6366
|
ctx
|
|
6339
6367
|
});
|
|
6340
6368
|
}
|
|
6341
|
-
async function
|
|
6369
|
+
async function execFirecrawl(params, ctx) {
|
|
6342
6370
|
const result = await callBackendExec({
|
|
6343
6371
|
nodeType: "ingest",
|
|
6344
6372
|
nodeVersion: ingestNode.version,
|
|
6345
|
-
params: { strategy: "
|
|
6373
|
+
params: { strategy: "firecrawl", url: params.url },
|
|
6346
6374
|
inputs: {},
|
|
6347
6375
|
outputKinds: { asset: "text" },
|
|
6348
6376
|
ctx
|
|
@@ -9570,6 +9598,7 @@ export {
|
|
|
9570
9598
|
describeFailureReason,
|
|
9571
9599
|
AD_FORMAT_PLATFORMS,
|
|
9572
9600
|
platformFormats,
|
|
9601
|
+
withoutCastName,
|
|
9573
9602
|
frameRealismDirection,
|
|
9574
9603
|
SEEDANCE_DURATIONS,
|
|
9575
9604
|
ELEVENLABS_MAX_MUSIC_LENGTH_MS,
|
|
@@ -9633,4 +9662,4 @@ export {
|
|
|
9633
9662
|
defaultRegistry,
|
|
9634
9663
|
createEngineFromEnv
|
|
9635
9664
|
};
|
|
9636
|
-
//# sourceMappingURL=chunk-
|
|
9665
|
+
//# sourceMappingURL=chunk-3TEVIAAV.js.map
|