@koda-sl/baker-cli 0.301.0-dev.50ab1c583 → 0.301.0-dev.c73cac21a
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 +128 -88
- package/canvas/tiktok-captions-composition/index.html +2 -23
- package/dist/{chunk-2IUN3S3E.js → chunk-2IB3IL4K.js} +2 -2
- package/dist/{chunk-4YF56EGK.js → chunk-D3HHUBIL.js} +4 -4
- package/dist/{chunk-WFWU3CHS.js → chunk-DZUVUGEP.js} +1 -9
- package/dist/{chunk-WFWU3CHS.js.map → chunk-DZUVUGEP.js.map} +1 -1
- package/dist/{chunk-SSXER76B.js → chunk-EFETQHTX.js} +123 -438
- package/dist/chunk-EFETQHTX.js.map +1 -0
- package/dist/{chunk-A2VZOTCV.js → chunk-EX2OIOVL.js} +3 -3
- package/dist/{chunk-KDTHRRAC.js → chunk-GU7IWEBC.js} +3 -3
- package/dist/cli.js +7754 -6708
- package/dist/cli.js.map +1 -1
- package/dist/{client-6KQHCXS2.js → client-VDCCDEHE.js} +4 -4
- package/dist/engine/index.d.ts +0 -13
- package/dist/engine/index.js +3 -3
- package/dist/{env-C7YXL6C6.js → env-FWMZXMQK.js} +2 -6
- package/dist/{output-RBM32FKJ.js → output-4H2RNBXL.js} +5 -5
- package/dist/{shared-WJIJTWST.js → shared-UKL36AXT.js} +6 -6
- package/package.json +2 -5
- package/dist/chunk-SSXER76B.js.map +0 -1
- /package/dist/{chunk-2IUN3S3E.js.map → chunk-2IB3IL4K.js.map} +0 -0
- /package/dist/{chunk-4YF56EGK.js.map → chunk-D3HHUBIL.js.map} +0 -0
- /package/dist/{chunk-A2VZOTCV.js.map → chunk-EX2OIOVL.js.map} +0 -0
- /package/dist/{chunk-KDTHRRAC.js.map → chunk-GU7IWEBC.js.map} +0 -0
- /package/dist/{client-6KQHCXS2.js.map → client-VDCCDEHE.js.map} +0 -0
- /package/dist/{env-C7YXL6C6.js.map → env-FWMZXMQK.js.map} +0 -0
- /package/dist/{output-RBM32FKJ.js.map → output-4H2RNBXL.js.map} +0 -0
- /package/dist/{shared-WJIJTWST.js.map → shared-UKL36AXT.js.map} +0 -0
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.
|
|
@@ -669,7 +672,7 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
|
|
|
669
672
|
|
|
670
673
|
**A device criterion can be added, but never removed** — `campaign-criteria add` with a device criterion works, but Google then rejects `campaign-criteria remove` on it forever, including on a criterion you added yourself (and Baker refuses the removal before publish). Adjust it instead with `campaign-criteria update customers/<cid>/campaignCriteria/<campaignId>~<criterionId> --bid-modifier <n>`: `1` for no adjustment, `1.25` to bid 25% more, `0.9` to bid 10% less, `0` to stop serving on that device. Locations, languages and ad schedules remove normally.
|
|
671
674
|
|
|
672
|
-
**Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach`
|
|
675
|
+
**Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach` **skips** it at stage time and names what to do instead — the op is never stored, the rest of a batch stages normally, and the skipped link is what records which images belong on which campaign. Staging an image, or asking to link one the account already holds, therefore also stages a **Task** — "Add N images to <campaign> in Google Ads" — which applies with the rest of the chat, so the one step Google keeps inside its own interface is tracked rather than mentioned. Attach the image to a Performance Max asset group (`asset-groups attach`) and the Task withdraws itself. The one image a Search campaign does take through a link is the advertiser's logo: `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
|
|
673
676
|
|
|
674
677
|
**Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
|
|
675
678
|
|
|
@@ -1265,10 +1268,30 @@ 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
|
|
1274
|
+
baker analytics sends # where each system event is sent, and what happened to each upload
|
|
1275
|
+
baker analytics sends --set --source-event page:deal_won --platform meta --tag <tagId> --event Purchase # staged; spends budget once published
|
|
1276
|
+
baker analytics sends --set --source-event page:deal_won --platform meta --tag <tagId> --event Purchase --parameter content_name={{properties.plan}} # with an extra Meta custom_data parameter
|
|
1277
|
+
baker analytics sends --uploads s1 --failed # the uploads Meta refused, each with the event and Meta's own words
|
|
1278
|
+
baker analytics sends --remove s1 # stop one, when the chat is published
|
|
1268
1279
|
baker analytics delivery --page 2 # the next page of a long list
|
|
1269
1280
|
baker analytics submissions --flow contact # every attempt at a Form, delivered or not
|
|
1281
|
+
baker analytics feed --source Zapier # the raw event feed, newest first, narrowed to what one source sent
|
|
1282
|
+
baker analytics feed --sends failed # only the uploads a conversion's sends made and the platform refused
|
|
1283
|
+
baker analytics sources # every source: the addresses the client's tools send events to, and the HubSpot reads
|
|
1284
|
+
baker analytics sources --create --name Zapier # a new address, learning; hand the URL to the user
|
|
1285
|
+
baker analytics sources --map --source <id> --field email={{contact.email}} --field value={{deal.amount}} --field currency=EUR
|
|
1286
|
+
baker analytics sources --create-hubspot --property lifecyclestage --value customer # an event HubSpot keeps for Baker each time the property changes
|
|
1287
|
+
baker analytics sources --create-hubspot --property deal_value --operator greaterThan --value 1000 # any condition a Form's filter can say
|
|
1288
|
+
baker analytics sources --map --source <id> --field email={{work_email}} --field value={{deal_value}} --field currency=EUR # repoint a HubSpot read
|
|
1270
1289
|
```
|
|
1271
1290
|
|
|
1291
|
+
**Sources: where events reach Baker from a server.** Measure → Sources is three built-in doors — the Baker landings, the website tag, and **the client's own server** — plus the connections Baker reads (HubSpot today). `--server` prints the server's one address: made once, the same for ever, `counting` at once, Baker's own format (`sending-events` is the contract; `X-Baker-Mode: test` rehearses). A HubSpot event is a trigger in HubSpot's own words, kept by HubSpot's journal each time the property changes and counted within a minute. `--create --name <tool>` still makes an address for a tool that cannot be told what to send — its first send is a masked sample counted as nothing until `--map` confirms the rows — but the dashboard no longer offers those; a tool worth reading is a connection, requested from the Sources page. Each `--field` is a **template**: text with `{{path}}` mentions, one thing per mention (`--field currency=EUR` types a value, `--field first_name="{{who.first}} {{who.last}}"` joins two; there is no `{{a|b}}` fallback); a bare path is read as its mention when the sample has it. From then on every arrival is an ordinary event carrying `baker_source: <name>`, listed by `feed --source <name>`, and `--pause`, `--resume` and `--remove` are by `--source <id>`.
|
|
1292
|
+
|
|
1293
|
+
**`--preview` then `--create-hubspot --property <name> [--operator <op>] --value <value>… [--name <name>] [--field <field>=<template>…] [--amount-property <n> --currency <c>]`: a HubSpot event.** HubSpot keeps each change to the property for Baker, filtered to the condition — on any plan, nothing to set up in HubSpot, an account of any size — and every contact whose new value satisfies the condition is one event, within a minute, named after the source (default: the trigger's words). The condition is any a Form's filter can say, by the property's kind: `is` / `isNot` (a dropdown, `--value` repeated for several), `contains` / `startsWith` / `endsWith` (text), `greaterThan` / `lessThanOrEqual` … (numbers), `isAfter` / `isBetween` (dates, `YYYY-MM-DD`), `isEmpty` / `isNotEmpty` (any); `--operator` is `is` when unsaid. Names, kinds and operators come from `baker hubspot contacts properties` and row properties from `baker hubspot contacts fields`; `--preview --property … [--operator …] --value … [--field …] [--contact <id or email>]` says how many contacts satisfy it today (NOT counted — only contacts that come to satisfy it from now on are) and returns `preview.contact`, the one most recently there or the one asked for, to check every row against before making anything. `--set-trigger --source <id> --property … [--operator …] --value …` changes what an event watches later: the rows stay and `countingSince` moves to now. The connection must be allowed to read contacts, and a refusal names the reconnect. `counting` at once, no URL; the source carries `hubspot { property, propertyLabel, kind, operator, values, valueLabels }`, `countingSince`, `listening` (whether HubSpot has been told to keep its changes: `listening`, `not_configured` or `failed`, with a `detail`), `lastError` and `mapping` — the same field → template table as an address, over the contact's properties plus `{{id}}`, defaulting to HubSpot's standard ones (email, phone, the contact id, name, address, `hs_google_click_id`, `hs_facebook_click_id`); a `--field` row at creation replaces the default it names, `--map --field` replaces the whole set later, and `--amount-property … --currency …` is the short spelling for the `value` and `currency` rows. `--field properties.<name>=<template>` adds an extra property of the event (`--field properties.plan={{plan_tier}}`): it breaks the event down on Measure → Events and a send reads it back as `{{properties.<name>}}`. A deal is watched the same way with `--object deal`. Where the event is sent on (Meta, Google Ads…) is `sends`, edited on the event's own card on the Sources page.
|
|
1294
|
+
|
|
1272
1295
|
Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-MM-DD`, end inclusive); `--full`; `--compare` to also return the preceding window of the same length; `--timezone <IANA>` and `--granularity hour|day|week|month` to shape a series. Every call reads live — there is no cache layer, so there is nothing to bypass.
|
|
1273
1296
|
|
|
1274
1297
|
**`--output md` when the numbers are going to be read.** Every report is emitted as compact JSON by default, and `--output md` renders each block as a Markdown table instead — one header rather than the keys repeated on every row, with the identical `warnings` and `hints`. Measured across all sixteen presets against a real deployment, the two changes together take the family from 110 KB to 44 KB — **about 60% smaller overall**, 38% at worst (`ads`, which is dense numeric data with nothing to prune) and 77% at best (`landings`). Three further economies apply to both formats, because the reader is usually an LLM with a context window rather than a browser:
|
|
@@ -1277,6 +1300,13 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1277
1300
|
- 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
1301
|
- Past ~25 rows the response carries a hint naming the row count and pointing at `--output md`.
|
|
1279
1302
|
|
|
1303
|
+
**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:
|
|
1304
|
+
|
|
1305
|
+
- **`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.
|
|
1306
|
+
- **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.
|
|
1307
|
+
|
|
1308
|
+
`--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.
|
|
1309
|
+
|
|
1280
1310
|
**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
1311
|
|
|
1282
1312
|
**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.
|
|
@@ -1285,6 +1315,8 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1285
1315
|
|
|
1286
1316
|
**`--flow <slug>` reads it from the Form's side**, listing that Form's endings already spelled as event keys with the name each is counted under, built from the Form's own file so it works before a single visitor arrives. With no `--flow`, the answer carries `flows[]` — every Form in the workspace with the step that counts for it, or nothing — which exists nowhere else: on every other report a Form that counts nothing looks exactly like a Form nobody converts on. The list of definitions is read from the store rather than from the reporting mirror, so an outcome named a second ago is in the reply that named it.
|
|
1287
1317
|
|
|
1318
|
+
**A send belongs to a system's event, and `baker analytics sends` is where it is set.** `page:deal_won` from HubSpot → Meta Purchase means every `deal_won` the client's system reports is uploaded to Meta with who the person is, the click that brought them and the amount forwarded automatically; there is nothing to map. What happens on a page or in a Form is never sent from here — the page's pixels and the Form's own side effects do that — and a send is never set on a funnel step (the step reads the sends of the events behind its ways in back as `sends[]`). With no flags it lists the sends in effect with `sends[].outcome` ("12 sent · 1 failed", `lastError` in the platform's words), `platforms[]` with the tags on Tracking that can send, and `events[]`, the system events that can be sent (a Source's own key, or a name the server posts). `--set --source-event <key> --platform <p> --tag <tagId> --event <e>` **stages** a send: it lands when the chat is published, and from then on it **spends ad budget** on every event under the key — say so plainly; never call a published send a preview. `--tag` is copied from `platforms[].tags[].tagId`, which lists only the tags that hold their server credential — one typed from memory is refused, and a platform with none says what to add on Tracking. A key no system raises is refused too. `--remove <id>` stops one, staged the same way. `--parameter <name>=<template>` (repeatable, with `--set`) adds an extra parameter the platform receives with every upload — Meta's `custom_data`, a GA4 event parameter, a Google Ads custom variable, X's contents; LinkedIn takes none — written from the event: `{{properties.<key>}}` for a property it carries, `{{value}}`, `{{page_path}}`, `{{city}}`… (`templateFields` under `--full`; `parameters[]` says what each platform takes). `--uploads <id> [--failed]` lists the rows behind `sends[].outcome`: which event went, when, its `outcome` and the platform's `error`.
|
|
1319
|
+
|
|
1288
1320
|
**Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
|
|
1289
1321
|
|
|
1290
1322
|
**Mapping is staged on the chat, not applied.** Answers appear under **Campaign parameters** in the dashboard and take effect when the chat is published; a discard takes them all back, and a report run in the same turn still reads the old vocabulary. Reading the mapping back inside the chat *does* include what it has staged, so a second turn never re-maps the same parameter.
|
|
@@ -1318,6 +1350,41 @@ Data is retained for 400 days; a window reaching further back comes back with a
|
|
|
1318
1350
|
|
|
1319
1351
|
---
|
|
1320
1352
|
|
|
1353
|
+
### The company's funnel (`baker funnel`)
|
|
1354
|
+
|
|
1355
|
+
The steps a company's business actually has, wired as a graph, each reached by an event Baker already records — and counted in PEOPLE on every read. Every company has one from the day it exists (Visited → Became a lead → Became a customer). It is what makes "how far did the people from this campaign get" answerable: a conversion count can say how many converted on a page, never how many reached Meeting booked and where the rest stopped.
|
|
1356
|
+
|
|
1357
|
+
```bash
|
|
1358
|
+
baker funnel # the funnel this company has, with 30 days of people per step
|
|
1359
|
+
baker funnel --days 90 # a longer window
|
|
1360
|
+
baker funnel --moments # everything a way in can name — read before writing any trigger
|
|
1361
|
+
baker funnel --step '{"key":"lead","displayName":"Enquiry","stepClass":"lead","after":["visited"],"triggers":[{"id":"t1","kind":"event","eventKey":"submit:contact"}]}' # one step, added or replaced by key
|
|
1362
|
+
baker funnel --remove-step lead # one step off; the chain stays joined
|
|
1363
|
+
baker funnel --templates # the step library and the presets
|
|
1364
|
+
baker funnel --preset b2b_sales # draw the closest graph over the whole funnel
|
|
1365
|
+
baker funnel --steps '[...]' # the whole funnel, restated — it replaces
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
**Every step comes back counted.** `people` reached it, `atLeast` got at least that far (the step or anything after it), `before` got as far as the step before, `share` is `atLeast / before`, and `money` is what its events said they were worth, by currency. `window` says which days, `worstStep` names where the most people are lost, and the hints say it in a sentence. Counted in people across visits and devices — a landing viewed on a phone, a Form completed on a laptop and a deal a CRM posted under its own id are one person when a contact digest joins them — and unordered, so a deal the CRM posted before the site's submit landed still counts. Never quote these as visits.
|
|
1369
|
+
|
|
1370
|
+
**The vocabulary is the company's; the stage is closed.** A company invents its own steps and names them whatever it calls them — `displayName` is unconstrained. `key` is the stable identifier (lowercase letters, digits, underscores) and never changes when the name does. Every step also carries one of six stages — `visited`, `lead`, `engaged`, `customer`, `repeat`, `lost` — plain words every business has, used to compare one company's funnel with another's.
|
|
1371
|
+
|
|
1372
|
+
**It is a graph, not a ladder.** `after` is a list of predecessors, so two ways into one step is ordinary rather than an anomaly — people arrive by seeing the landing *or* by asking for a demo. An empty `after` means people come in there. A cycle or a predecessor that does not exist is refused with the step named.
|
|
1373
|
+
|
|
1374
|
+
**Every write says what it dropped.** `--step` changes one step and leaves the rest as it stands; `--steps` replaces the whole funnel and `--preset` draws over it. Every write returns `removed[]` and a `REMOVED steps:` hint when a step went, so a restatement written from a stale read is caught in the same turn.
|
|
1375
|
+
|
|
1376
|
+
**A step says what triggers it, and every trigger is PICKED from `--moments`.** `triggers` is a list and any one of them firing is enough. Three kinds: `{"kind":"event","eventKey":"submit:contact"}` for anything the company's Forms, pages or systems produced (`moments.events`, `moments.sources`), `{"kind":"landing","landingId":"<id>"}` for one landing being viewed (`moments.landings`), and `{"kind":"any_landing"}` for any landing — what the default first step listens for. Nothing has to be named first: the event is used by its own key, and the step shows it by its own words. `moments.events[].origin` tells a page's event (`site`) from one the client's own systems sent (`systems`); `moments.sources` lists the client's systems by the `page:<name>` key each raises, pickable before the first send arrives. A key typed from memory saves, publishes and counts nobody forever — the hint "has a trigger and nobody reached it while N people entered" is that mistake caught after the fact. A step written with `offlineConversions`, a `conversion` trigger or an `analytics` trigger is refused before any round trip, with the shape to use.
|
|
1377
|
+
|
|
1378
|
+
**A landing being viewed can be a step** — where most funnels start — and never a conversion: `baker analytics conversions` refuses a `view:` key.
|
|
1379
|
+
|
|
1380
|
+
**A step nothing enters advances nobody, silently.** No `triggers` means the report shows zero, nothing errors, and it looks exactly like a stage people never reach. Every response names these in `hints`. **There is no default door either:** a Form is counted only where a trigger names it, and `hints` names every Form no step counts.
|
|
1381
|
+
|
|
1382
|
+
**Where a step sends is read here and set on the system's event.** Every step comes back with `sends[]` — one entry per send on a system's event among its triggers (`id`, `eventKey`, `platform`, `event`, and `outcome`: "12 sent · 1 failed", or "Nothing sent yet"). It is read-only: a send belongs to the event, so it is set with `baker analytics sends --set --source-event <key> --platform <p> --tag <tagId> --event <e>`, never on a step. A send that failed is named in the hints (`SEND FAILED on step <key>: …`) with the platform's own error.
|
|
1383
|
+
|
|
1384
|
+
**Staged, and published in one piece.** A change becomes the company's funnel when the chat is published; discarding takes it back. The client sees what you staged on the funnel canvas inside the chat and can edit it there — the same canvas, the same publish — so read the funnel back before restating it. The version it replaces is kept, so removing a step does not remove what was measured under it.
|
|
1385
|
+
|
|
1386
|
+
---
|
|
1387
|
+
|
|
1321
1388
|
### A/B tests (`baker experiment`)
|
|
1322
1389
|
|
|
1323
1390
|
Two versions of one landing page, split 50/50 at the edge, read on one of the conversions the company already counts, and decided by a person on a verdict rather than by two rates somebody reads.
|
|
@@ -1342,6 +1409,8 @@ baker experiment fold # make the page
|
|
|
1342
1409
|
|
|
1343
1410
|
**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
1411
|
|
|
1412
|
+
**`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.
|
|
1413
|
+
|
|
1345
1414
|
**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
1415
|
|
|
1347
1416
|
**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 +2196,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
|
|
|
2127
2196
|
| `baker images get <id>` | Single record | n/a |
|
|
2128
2197
|
| `baker images upload <file\|url>` | Upload a local file OR remote URL to the library | always on |
|
|
2129
2198
|
| `baker images delete <id>` | Delete a record | n/a |
|
|
2199
|
+
| `baker images describe <id>` | Correct a record's stored name / description / tags | n/a |
|
|
2130
2200
|
| `baker images normalize <files>` | Local recolor + bg removal + trim + resize | n/a (operates on disk, no upload) |
|
|
2131
2201
|
| `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked) | n/a (operates on library image) |
|
|
2132
2202
|
| `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 +2637,20 @@ baker images delete j571abc123def --dry-run
|
|
|
2567
2637
|
baker images delete j571abc123def
|
|
2568
2638
|
```
|
|
2569
2639
|
|
|
2640
|
+
### `baker images describe <id>`
|
|
2641
|
+
|
|
2642
|
+
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.
|
|
2643
|
+
|
|
2644
|
+
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.
|
|
2645
|
+
|
|
2646
|
+
```bash
|
|
2647
|
+
baker images describe j571abc123def --description "Founder on stage at SaaStr, blue backdrop, mid-gesture"
|
|
2648
|
+
baker images describe j571abc123def --name "SaaStr keynote" --tags event,team
|
|
2649
|
+
baker images describe j571abc123def --description "…" --full # also returns the whole library row
|
|
2650
|
+
```
|
|
2651
|
+
|
|
2652
|
+
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`.
|
|
2653
|
+
|
|
2570
2654
|
### `baker images normalize <files> [options]`
|
|
2571
2655
|
|
|
2572
2656
|
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 +2917,15 @@ Requires `yt-dlp` on `PATH` (present in the Baker Runtime). Private, age-restric
|
|
|
2833
2917
|
|
|
2834
2918
|
Delete a video by ID.
|
|
2835
2919
|
|
|
2920
|
+
### `baker videos describe <id>`
|
|
2921
|
+
|
|
2922
|
+
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.
|
|
2923
|
+
|
|
2924
|
+
```bash
|
|
2925
|
+
baker videos describe j571abc123def --description "Customer explaining how onboarding cut setup from a week to a day"
|
|
2926
|
+
baker videos describe j571abc123def --tags testimonial
|
|
2927
|
+
```
|
|
2928
|
+
|
|
2836
2929
|
### `baker videos tags`
|
|
2837
2930
|
|
|
2838
2931
|
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).
|
|
@@ -2935,7 +3028,7 @@ baker avatars create --name "Marta" --subject "…" \
|
|
|
2935
3028
|
--source-image "<imageId1>,<imageId2>" --likeness-confirmed
|
|
2936
3029
|
```
|
|
2937
3030
|
|
|
2938
|
-
Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--
|
|
3031
|
+
Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--voice-id`, `--voice-description`. Returns `{ avatarId, handle, likeness, status }`.
|
|
2939
3032
|
|
|
2940
3033
|
#### `baker avatars update <handle>`
|
|
2941
3034
|
|
|
@@ -2944,7 +3037,7 @@ Edit an avatar. Profile flags are merged over the stored profile, so `--persona`
|
|
|
2944
3037
|
```bash
|
|
2945
3038
|
baker avatars update marta --wardrobe "charcoal blazer over a white tee"
|
|
2946
3039
|
baker avatars update marta --subject "woman in her early 50s, silver bob, …" --regenerate-sheet
|
|
2947
|
-
baker avatars update marta --new-handle marta-founder --
|
|
3040
|
+
baker avatars update marta --new-handle marta-founder --voice-id <voiceId>
|
|
2948
3041
|
```
|
|
2949
3042
|
|
|
2950
3043
|
#### `baker avatars delete <handle>`
|
|
@@ -3216,6 +3309,7 @@ baker tags draft # review the staged changes awaiting publish
|
|
|
3216
3309
|
|
|
3217
3310
|
Notes:
|
|
3218
3311
|
|
|
3312
|
+
- **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
3313
|
- **`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
3314
|
- **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
3315
|
- 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.
|
|
@@ -3439,7 +3533,7 @@ baker history list --category ads --full # ad writes with raw metadata
|
|
|
3439
3533
|
|
|
3440
3534
|
---
|
|
3441
3535
|
|
|
3442
|
-
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup` · `baker hubspot meetings list | view`
|
|
3536
|
+
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
|
|
3443
3537
|
|
|
3444
3538
|
Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
|
|
3445
3539
|
|
|
@@ -3460,6 +3554,7 @@ baker hubspot workflows view <workflowId> --full # + the values each branch com
|
|
|
3460
3554
|
baker hubspot pipelines list # where a lead lands, and the stages after it
|
|
3461
3555
|
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
3462
3556
|
baker hubspot contacts lookup lead@example.com # was this one lead picked up?
|
|
3557
|
+
baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
|
|
3463
3558
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
3464
3559
|
baker hubspot meetings view <slug> # booking-form fields for one calendar
|
|
3465
3560
|
baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
|
|
@@ -3473,6 +3568,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
|
|
|
3473
3568
|
**`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
|
|
3474
3569
|
**`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
|
|
3475
3570
|
**`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
|
|
3571
|
+
**`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
|
|
3476
3572
|
**`meetings list` flags:** `--search <text>`, `--redirecting-only`.
|
|
3477
3573
|
**`meetings view`:** positional `<slug>` — the only reliable source for a calendar's booking fields (HubSpot omits `customParams` from the list endpoint, so `fieldCount: null` there means unknown, not zero); `--as-node` as above.
|
|
3478
3574
|
|
|
@@ -3636,7 +3732,6 @@ Reach for it when you are relaying the **client's** own words. When you are writ
|
|
|
3636
3732
|
|
|
3637
3733
|
```bash
|
|
3638
3734
|
baker studio animate "slow push in, model turns to camera and smiles" --image j57abc123def456ghi789
|
|
3639
|
-
baker studio animate "she looks to camera and says: \"Hola, soy Elena\"" --avatar elena --quality 720p --aspect-ratio 9:16
|
|
3640
3735
|
baker studio animate "handheld drift right, steam rising from the cup" \
|
|
3641
3736
|
--image ./out/hero.png --duration 6 --quality 1080p
|
|
3642
3737
|
baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
|
|
@@ -3786,65 +3881,6 @@ baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and br
|
|
|
3786
3881
|
baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
|
|
3787
3882
|
```
|
|
3788
3883
|
|
|
3789
|
-
#### The brand, the ending and the cast are drawn, not generated
|
|
3790
|
-
|
|
3791
|
-
`baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
|
|
3792
|
-
because a video model garbles a wordmark every time:
|
|
3793
|
-
|
|
3794
|
-
- **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
|
|
3795
|
-
and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
|
|
3796
|
-
- **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
|
|
3797
|
-
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
3798
|
-
the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
|
|
3799
|
-
the words on the button, which otherwise default to the last line of the script.
|
|
3800
|
-
- **The brand fills itself.** When the spec leaves `brand.logo` or `brand.palette` empty,
|
|
3801
|
-
they are read from the workspace — `src/brand/BRAND.md` for the colours (in document
|
|
3802
|
-
order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
|
|
3803
|
-
(vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
|
|
3804
|
-
the ad needs something other than the client's default.
|
|
3805
|
-
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
3806
|
-
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
3807
|
-
- **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
|
|
3808
|
-
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
3809
|
-
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
3810
|
-
or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
|
|
3811
|
-
- **A real brand mark is required.** `scaffold-ad` refuses to build without one in
|
|
3812
|
-
`src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
|
|
3813
|
-
card, so a stand-in ships an ad wearing the wrong brand.
|
|
3814
|
-
- **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
|
|
3815
|
-
a separately cast voice, which is dubbing and looks like it.
|
|
3816
|
-
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
3817
|
-
become on-screen text and are captioned straight from the script, so the words are
|
|
3818
|
-
exact. Give it `music` too.
|
|
3819
|
-
- **`--avatar <handle>`** — the same flag as on `studio generate` / `studio animate`, and the way to cast a person into an ad. Overrides `cast.avatar` in the spec.
|
|
3820
|
-
- **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
|
|
3821
|
-
every beat they appear in on that avatar's identity sheet and copies its subject
|
|
3822
|
-
description verbatim, so the ad shows the same face as the rest of the company's work.
|
|
3823
|
-
This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
|
|
3824
|
-
- **`cast.description`** — a person described in words, when there is no avatar.
|
|
3825
|
-
|
|
3826
|
-
Each of these also removes a generation: the closing beat costs no image and no clip.
|
|
3827
|
-
|
|
3828
|
-
#### The finished video is reviewed
|
|
3829
|
-
|
|
3830
|
-
When a run's output is a video, `baker canvas run` samples twelve frames across it and
|
|
3831
|
-
looks at them before reporting success. It reports, never blocks — the clips are already
|
|
3832
|
-
paid for by the time it runs — and the findings come back as `review` and `hints`:
|
|
3833
|
-
|
|
3834
|
-
- **Per frame:** anything that could not physically happen, or shows a job being done
|
|
3835
|
-
incorrectly, plus objects floating unattached in shot. Malformed hands and limbs are
|
|
3836
|
-
rolled up into one finding across the whole render rather than repeated per frame.
|
|
3837
|
-
- **Across the reel:** whether the cast stays the same people, whether a brand mark is
|
|
3838
|
-
visible anywhere, and whether the last frames close the ad or simply stop.
|
|
3839
|
-
|
|
3840
|
-
Each per-frame finding names the **scene file** that produced that second, so a defect is
|
|
3841
|
-
an edit rather than a hunt — and re-running re-renders only the scenes you changed.
|
|
3842
|
-
|
|
3843
|
-
It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
|
|
3844
|
-
says the video was **not** reviewed rather than reporting it clean, and a frame that
|
|
3845
|
-
could not be read is reported as unchecked for the same reason.
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
3884
|
> Scaffolded canvases use **workspace-relative** composition/asset paths and are validated
|
|
3849
3885
|
> against the engine's hyperframe pre-render gate at `validate` time, so a broken overlay
|
|
3850
3886
|
> (or a moved/renamed workspace) is caught before any billed clip. The scaffold also adds
|
|
@@ -4073,7 +4109,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
|
|
|
4073
4109
|
|
|
4074
4110
|
**Params** — discriminated on `source`:
|
|
4075
4111
|
|
|
4076
|
-
`source: "url"` (yt-dlp / direct fetch /
|
|
4112
|
+
`source: "url"` (yt-dlp / direct fetch / Firecrawl):
|
|
4077
4113
|
|
|
4078
4114
|
| Name | Type | Required | Constraint |
|
|
4079
4115
|
|---|---|---|---|
|
|
@@ -4093,7 +4129,7 @@ Pick a `source` discriminator and declare the kind you expect. See [Ingestion](#
|
|
|
4093
4129
|
|
|
4094
4130
|
**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
4131
|
|
|
4096
|
-
**Cost:** 0 engine credits for direct fetch + yt-dlp + local file.
|
|
4132
|
+
**Cost:** 0 engine credits for direct fetch + yt-dlp + local file. Firecrawl charges per scrape.
|
|
4097
4133
|
|
|
4098
4134
|
---
|
|
4099
4135
|
|
|
@@ -5407,10 +5443,10 @@ baker canvas catalog | jq '.compositions[].id'
|
|
|
5407
5443
|
// URL source — YouTube audio-only (yt-dlp -x)
|
|
5408
5444
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://youtu.be/dQw4w9WgXcQ", "expect": "audio" } }
|
|
5409
5445
|
|
|
5410
|
-
// URL source — blog post → clean markdown (
|
|
5446
|
+
// URL source — blog post → clean markdown (Firecrawl)
|
|
5411
5447
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/blog/post", "expect": "text" } }
|
|
5412
5448
|
|
|
5413
|
-
// URL source — PDF → markdown (
|
|
5449
|
+
// URL source — PDF → markdown (Firecrawl handles PDFs)
|
|
5414
5450
|
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://example.com/whitepaper.pdf", "expect": "text" } }
|
|
5415
5451
|
|
|
5416
5452
|
// URL source — raw markdown file (direct fetch)
|
|
@@ -5438,7 +5474,7 @@ baker canvas catalog | jq '.compositions[].id'
|
|
|
5438
5474
|
| url | video / audio | any | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor) |
|
|
5439
5475
|
| url | image / json / font | any | `direct_fetch` (HTTP GET) |
|
|
5440
5476
|
| url | text | ends in `.txt` or `.md` | `direct_fetch` |
|
|
5441
|
-
| url | text | anything else (HTML, PDF, …) | `
|
|
5477
|
+
| url | text | anything else (HTML, PDF, …) | `firecrawl` `/v2/scrape` (extracted markdown — HTML/PDF made readable) |
|
|
5442
5478
|
|
|
5443
5479
|
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
5480
|
|
|
@@ -5481,7 +5517,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
|
|
|
5481
5517
|
file_size?: number, // bytes
|
|
5482
5518
|
original_filename?: string, // basename of the source path
|
|
5483
5519
|
|
|
5484
|
-
strategy: "direct_fetch" | "
|
|
5520
|
+
strategy: "direct_fetch" | "firecrawl" | "yt_dlp" | "local_file",
|
|
5485
5521
|
ingested_at: string, // ISO 8601 timestamp
|
|
5486
5522
|
|
|
5487
5523
|
// yt-dlp adds (when available from --dump-json):
|
|
@@ -5492,7 +5528,7 @@ If the bytes' mime contradicts `expect` (e.g. `expect: "image"` but the URL serv
|
|
|
5492
5528
|
width?: number,
|
|
5493
5529
|
height?: number,
|
|
5494
5530
|
|
|
5495
|
-
//
|
|
5531
|
+
// Firecrawl adds:
|
|
5496
5532
|
word_count?: number,
|
|
5497
5533
|
},
|
|
5498
5534
|
|
|
@@ -5511,8 +5547,8 @@ The `url` is a stable R2 URL — remote downstream nodes (e.g. `video_lipsync`)
|
|
|
5511
5547
|
|-----------------------|----------------|------------------------------------------------------------------------------|
|
|
5512
5548
|
| `INVALID_PARAMS` | engine validator | Missing/malformed fields, mixing `url` + `path`, unknown `source`, unknown `expect`. |
|
|
5513
5549
|
| `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 +
|
|
5550
|
+
| `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). |
|
|
5551
|
+
| `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
5552
|
| `file_not_found` | path + local_file | `stat()` returned `ENOENT`. Error names the absolute resolved path. |
|
|
5517
5553
|
| `not_a_file` | path + local_file | Path exists but is a directory / block device / socket. |
|
|
5518
5554
|
| `permission_denied` | path + local_file | `stat()` or `readFile()` returned `EACCES`. |
|
|
@@ -5529,7 +5565,7 @@ All execution-time failures flow through `NodeExecutionError` so they appear in
|
|
|
5529
5565
|
- **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
5566
|
- **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
5567
|
- **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.**
|
|
5568
|
+
- **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
5569
|
|
|
5534
5570
|
**Local files (`source: "path"`):**
|
|
5535
5571
|
|
|
@@ -5552,10 +5588,10 @@ ingest { source: "path", expect: "video" } → video_transcribe # Local
|
|
|
5552
5588
|
**Requirements:**
|
|
5553
5589
|
|
|
5554
5590
|
- `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
|
-
- `
|
|
5591
|
+
- `FIRECRAWL_API_KEY` on the Convex backend for `source: "url"` with `expect: "text"` on non-`.md`/`.txt` URLs.
|
|
5556
5592
|
- No additional dependencies for `source: "path"` — just filesystem access.
|
|
5557
5593
|
|
|
5558
|
-
**Cost:** zero engine credits for direct_fetch + yt_dlp.
|
|
5594
|
+
**Cost:** zero engine credits for direct_fetch + yt_dlp. Firecrawl charges per scrape on your Firecrawl account (configured outside Baker).
|
|
5559
5595
|
|
|
5560
5596
|
---
|
|
5561
5597
|
|
|
@@ -5930,6 +5966,8 @@ baker landing critique spring-offer summer-offer claude # score three landings
|
|
|
5930
5966
|
|
|
5931
5967
|
- **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a comparative sentence outside a `<table>` — a comparison table is a section the landing skill teaches, but naming a rival anywhere else on paid traffic is the client's call, not the build's. Spanish, Portuguese and English.
|
|
5932
5968
|
|
|
5969
|
+
- **Comments are not markup, and no rule reads one.** Astro expression comments (`{/* … */}`) and HTML comments (`<!-- … -->`) are blanked before detection, offsets preserved so `file:line` stays exact. A shared scaffold component explaining its `preconnect` in prose that spelled `<img>` twice was read as two broken images — block-tier — so every landing rendering a video failed the publish gate on a file the client cannot edit. Comment prose is also where a component's reasoning lives, em-dashes and buzzwords included, so reading it inflated the count-threshold tells too.
|
|
5970
|
+
|
|
5933
5971
|
- **Copy held in component frontmatter is read like markup copy.** Astro landings keep FAQ pairs, testimonial quotes, feature cards and pricing rows as a `const items = [{ q, a }]` array above the `---` fence and render them in a `.map()`, so a critic that blanked the whole block was blind to a large share of every page's actual words. Prose string literals in that block now feed every copy rule; imports, class strings, URLs and config do not, and the bar is deliberately high in that direction — a Tailwind class list in front of the copy rules is worse than one missed tell.
|
|
5934
5972
|
|
|
5935
5973
|
Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, slug, overall, counts, dimensions, findings }`. `dimensions` scores seven design families (typography, color, borders_depth, motion, spacing, copy, integrity) 0–1 (higher is better); `counts` is the block/warn/advisory tally.
|
|
@@ -6008,6 +6046,9 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
6008
6046
|
- **0.150.0**: `baker landing critique` gains an `agent-washing` rule in the `copy` family — an autonomy claim ("fully autonomous", "while you sleep", "no human intervention") with no signal anywhere on the page about who oversees the agent (approval, review, override, undo/rollback, audit log, escalation) scores a warn. Page-scope, so a hero may defer the trust story to a later section; generic privacy boilerplate does not clear it. `CRITIC_VERSION` bumps to `2`.
|
|
6009
6047
|
- **0.152.0**: `baker ads linkedin campaigns url-params <ad-set-id>` stages an ad set's URL tracking parameters (LinkedIn's `adTrackingParameters`) — `--param key=value` for fixed values (repeatable or `&`-joined), `--dynamic key=PLACEHOLDER` for values LinkedIn fills in per ad (`CAMPAIGN_NAME`, `CREATIVE_ID`, …), `--clear` to remove them. LinkedIn appends these to the landing URL of every ad in the ad set, including ads already running, so this replaces stamping the same UTM onto each ad's `--landing-url` — which missed later ads and double-appended keys the ad set already set. Account-level parameters remain UI-only (LinkedIn exposes no API for them).
|
|
6010
6048
|
- **0.153.0**: `baker landing critique` accepts several slugs in one call (`baker landing critique a b c`). `data.landings[]` always carries one entry per slug; a single slug additionally keeps the old top-level `data.slug`/`counts`/`dimensions`/`findings` shape, so existing readers are unaffected. An invalid or missing slug anywhere in the batch still fails the whole call up front. The scaffold's Stop-hook critique gate now emits one batched command instead of one line per stale landing.
|
|
6049
|
+
- **0.289.0**: `baker analytics sends --set … --parameter <name>=<template>` (repeatable) adds extra platform parameters to a send — Meta `custom_data`, GA4 event parameters, Google Ads custom variables, X contents — written from the event with `{{properties.<key>}}` and its readable fields; `parameters[]` and `templateFields` (`--full`) describe what each platform takes. `baker analytics sends --uploads <id> [--failed]` lists the rows behind a send's outcome with the platform's own reason per upload, and `baker analytics feed --sends any|failed` shows the same uploads in the raw feed. `baker analytics sources --field properties.<name>=<template>` adds an extra property of a source's events (HubSpot reads and addresses alike). `baker analytics conversions` catalogue rows carry `sources[]`, the doors on Sources that sent each key.
|
|
6050
|
+
- **0.291.0**: **HubSpot keeps the changes; the five-minute read of the CRM is gone.** A HubSpot event is read off HubSpot's webhooks journal: Baker's app subscribes the account to the one property the event watches, with the trigger as HubSpot's own value filter where it can be said (never every edit, never a search of the CRM, an account of any size costs nothing to watch), Baker reads the journal forward every half minute, and only a change *to* a value the trigger accepts is an event. `baker analytics sources` answers with `listening` on every HubSpot source — `listening`, or `not_configured` / `failed` with a `detail` — and the hint says so; `lastPolledAt` is gone.
|
|
6051
|
+
- **0.290.0**: A HubSpot event can be on a **deal**: `baker hubspot deals properties` / `deals fields` list what a deal can be watched on and read, and `baker analytics sources --create-hubspot --object deal --property dealstage --value closedwon [--contacts first|every]` makes an event about the deal's contact (its first, or every one, each its own event), worth the deal's own amount; rows read the deal bare (`{{amount}}`) and its contact as `{{contact.email}}`; `--preview --object deal` answers with `preview.deal` and its `contacts`. **Inside a chat, `baker analytics sources` stages** — a HubSpot event made, its trigger or rows changed, a source paused, resumed or removed lands at Publish and is drawn under Sources in the chat's change list (`staged[]` in the response); `--server` and `--create` still answer at once. `baker analytics feed --send <id>` keeps one send's uploads and `--uid <event_uid>` one event with every upload made for it, each with the platform's verdict.
|
|
6011
6052
|
- **0.159.0**: `baker ads google ads update` takes the same content flags as `ads create` (`--format` + `--headlines`/`--descriptions`/image refs/…), and `--format app` becomes a flag-buildable format on both, with `--image-assets` wiring an App campaign ad's images (`AppAdInfo.images`) the same way Responsive Display does. Content flags without `--format` now fail instead of being silently dropped. An ad content update replaces **every field it sends** — a list has to include the entries you want to keep, while an optional asset list you omit is left untouched on the ad — and a partial payload (only the images, or Google's read-shaped `{"appAd":{…}}`) is rejected at stage time with the fields it's missing, instead of staging cleanly and blocking publish later. Content updates also record the staged image assets they consume as dependencies, so discarding an asset no longer leaves the ad pointing at nothing.
|
|
6012
6053
|
- **0.167.0**: `baker ads google conversions update` stops staging two conversion-action changes Google always rejects. `status: "HIDDEN"` leaves the write contract — Google denies it on every conversion action ("the field's value is on a deny-list for this field"), and offering it made it the obvious pick for retiring an action gently, so a whole cleanup published nothing; the refusal now names the two changes that do work (`--no-primary` to stop bidding toward it while it keeps measuring, `status: "REMOVED"` to stop it counting). `HIDDEN` is unchanged everywhere it is *read* — an action hidden long ago still reads back. And an update against a conversion action **Google owns** — Smart campaign calls/directions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions — is refused at stage time, renames included, since Google rejects every write against one and its own interface will not change them either. Both refusals name what to do instead and tell the agent to carry on with the rest of the job.
|
|
6013
6054
|
- **0.166.0**: `baker ads google conversions` can set a conversion action's primary/secondary role (`ConversionAction.primary_for_goal`) — `update <id> --primary` to have automated bidding optimize toward it, `--no-primary` to demote it to reported-only. Demoting an action was previously manual work in the Google Ads UI. `conversions create` now states the role instead of leaving it to Google, defaulting to **secondary** so a new action can't silently join what a Maximize Conversions / Target CPA campaign bids toward. The staged card gains an "Action optimization" row, and re-staging the role an action already has is reported as a no-op.
|
|
@@ -6030,18 +6071,9 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
6030
6071
|
|
|
6031
6072
|
- **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
|
|
6032
6073
|
|
|
6033
|
-
- **0.
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
- **0.253.0**: the pre-render gate runs `hyperframes check` — lint, runtime, layout, motion and contrast in one browser session, sampled at transition seams — instead of the static `lint` + `inspect` pair, falling back to them when the installed binary predates `check`. The browser half is signal the parser could never produce: measured on HyperFrames' own `warm-grain` example, lint/runtime/layout/motion all reported `ok: true` while the composition rendered an almost entirely blank ten-second video, the only trace being six `GSAP target #a-roll not found` warnings — a tween pointed at an element that does not exist, so nothing it animates ever appears. Baker promotes that warning to blocking, alongside `sweep_static` ("the timeline never advanced", i.e. a still image billed as a video). Warnings are grouped by code with a count (one run returned 62, 53 of them identical) and the highest-value findings carry advice on what to do rather than only what was seen. Requires `hyperframes@0.8.17` in the sandbox, bumped from 0.7.5 in the same change. Reasoning: `docs/adr/0007-the-render-gate-runs-in-a-browser.md`.
|
|
6037
|
-
- **0.254.0**: the nested-composition smoke test now looks at the frames instead of trusting the exit code — exiting 0 is what a blank render does too. `hyperframes snapshot --describe` sends the captured frames to Gemini and writes back what is actually in them, and the credential Studio already uses for Gemini and Omni (`GOOGLE_GENERATIVE_AI_API_KEY`) is the `GEMINI_API_KEY` it reads; the backend now ships it to the sandbox by omission, so a deployment without one keeps the plain smoke test. Verified against a real render: the pass reported "the image is completely empty" and named the single visible element without being told what to look for. Frame verdicts are condensed into the node log, one line per frame — a file the agent would have to know to open is a file it does not read.
|
|
6038
|
-
- **0.263.0**: shot briefs refuse the frame that contradicts its own caption, and the vision pass reads the numbers back. Observed verbatim: a phone screen reading "$215.40 SAVED THIS MONTH" under a caption reading "Save over £900 a year" — wrong currency, and a figure that contradicts the claim it sits beneath, since 215.40 a month is 2,584 a year. The model invents a number whenever a screen or document is in shot, so the frames no longer carry legible text at all: the caption track is where an ad's numbers live. Same brief also pins each object's real-world FORM after a phone rendered with a second screen on its back, and names the market's currency for anything unavoidably visible. The frame-vision question now reads out every number and word inside the picture and says whether it contradicts the caption — the defect only exists between the two, so neither alone can see it.
|
|
6039
|
-
- **0.262.0**: `scaffold-ad` carries the brand. The spec takes `brand.palette` (hex tokens) and `brand.logo` (repo path to the mark), and emits them as the `style.palette` and `branding` the engine already reads to colour its brand plate and CTA. The palette dresses the plate and the type and is deliberately kept OUT of the shot briefs — hex in a frame prompt makes the model paint the whole scene that colour, which reads as a filter. Also takes `market`, since an ad that does not say where it is set is an ad the model sets wherever it likes.
|
|
6040
|
-
- **0.261.0**: four defects that reached a finished ad, fixed in `scaffold-ad` where they originate. Frames render on **`openai/gpt-image-2`**, pinned rather than inheriting the catalogue default (Gemini Flash — right for cheap high-volume work, wrong for the opening still a five-second clip is built from). Every shot brief now states the MARKET, inferred from the voice language when unstated: an ad for Spain came back full of British houses because nothing said where it was. Every shot brief also rules out the impossible — paper and screens are opaque, objects are at real-world scale — after a bill rendered with the text from its reverse showing through, and a solar panel five metres tall. And beat durations snap to lengths the video model can actually render: free-form durations were rounded down per clip, so 11 beats summed to 27.0s of picture against a 31.4s timeline and the 4.4s difference rendered as a black tail. The read rate was re-measured off a real Spanish ad (81 words in 26.48s = 3.06 w/s); the old 2.6 over-predicted every beat by 18%.
|
|
6041
|
-
- **0.260.0**: burned-in captions break where the script punctuates instead of every N words. The karaoke composition sliced the transcript into fixed groups, and a word count knows nothing about the sentence it is cutting — an ad the engine rendered showed cards reading "OTRA VEZ, EN" and "TU CASA GENERA", half-thoughts ending on a preposition. A card now closes when the clause does; the word count stays as a ceiling so a long clause is still split, and a linking word ("de", "al", "y") never ends a card because it belongs to what follows. Same rule the pre-render gate already checked for, now enforced where the cards are actually built.
|
|
6042
|
-
- **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.
|
|
6043
|
-
- **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.
|
|
6044
|
-
- **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.
|
|
6074
|
+
- **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.
|
|
6075
|
+
|
|
6076
|
+
## Publishing
|
|
6045
6077
|
|
|
6046
6078
|
### Auto-publish (CI)
|
|
6047
6079
|
|
|
@@ -6086,6 +6118,14 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
|
|
|
6086
6118
|
|
|
6087
6119
|
- **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.
|
|
6088
6120
|
|
|
6121
|
+
- **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.
|
|
6122
|
+
|
|
6123
|
+
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.
|
|
6124
|
+
|
|
6125
|
+
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.
|
|
6126
|
+
|
|
6127
|
+
- **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.
|
|
6128
|
+
|
|
6089
6129
|
### Testing a pre-release in sandboxes
|
|
6090
6130
|
|
|
6091
6131
|
```bash
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
|
|
36
36
|
.caption-group {
|
|
37
37
|
position: absolute;
|
|
38
|
-
/* Absolutely positioned with no width, a card sizes to its content and
|
|
39
|
-
runs off the 1080px frame rather than wrapping. Matches the container's
|
|
40
|
-
60px gutters, so a long clause becomes two lines instead of an overflow. */
|
|
41
|
-
max-width: 960px;
|
|
42
38
|
font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
|
|
43
39
|
font-size: {{font_size}}px;
|
|
44
40
|
font-weight: 900;
|
|
@@ -84,27 +80,10 @@
|
|
|
84
80
|
const container = document.getElementById('captions');
|
|
85
81
|
const tl = gsap.timeline({ paused: true });
|
|
86
82
|
|
|
87
|
-
// A card closes where the SCRIPT closes, not on a word count. Slicing every
|
|
88
|
-
// N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
|
|
89
|
-
// preposition. The count stays as a ceiling (a long clause still has to be
|
|
90
|
-
// split), and a linking word never ends a card: it belongs to what follows.
|
|
91
|
-
const CLOSES = /[.,;:!?\u2026]$/;
|
|
92
|
-
const CARRIES_ON = new Set([
|
|
93
|
-
'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
|
|
94
|
-
'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
|
|
95
|
-
]);
|
|
96
|
-
const carriesOn = (w) => CARRIES_ON.has(String(w.text || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
|
|
97
|
-
|
|
98
83
|
const groups = [];
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
current.push(word);
|
|
102
|
-
const last = current[current.length - 1];
|
|
103
|
-
const closes = CLOSES.test(String(last.text || '')) ||
|
|
104
|
-
(current.length >= WORDS_PER_GROUP && !carriesOn(last));
|
|
105
|
-
if (closes) { groups.push(current); current = []; }
|
|
84
|
+
for (let i = 0; i < transcript.length; i += WORDS_PER_GROUP) {
|
|
85
|
+
groups.push(transcript.slice(i, i + WORDS_PER_GROUP));
|
|
106
86
|
}
|
|
107
|
-
if (current.length > 0) groups.push(current);
|
|
108
87
|
|
|
109
88
|
groups.forEach((group, gi) => {
|
|
110
89
|
const el = document.createElement('div');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debugLogSetting
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DZUVUGEP.js";
|
|
4
4
|
|
|
5
5
|
// src/debugLog.ts
|
|
6
6
|
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
@@ -150,4 +150,4 @@ export {
|
|
|
150
150
|
readBodyForLog,
|
|
151
151
|
installStreamTaps
|
|
152
152
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-2IB3IL4K.js.map
|