@koda-sl/baker-cli 0.419.0-dev.ce424234b → 0.420.0-dev.2a54c32e7
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 +312 -57
- package/dist/{chunk-4RN4CBIQ.js → chunk-6Z4ETVEZ.js} +3 -3
- package/dist/{chunk-TTBMFAGF.js → chunk-RM2TTRYQ.js} +4 -4
- package/dist/{chunk-VF77MVEM.js → chunk-UKGTWBKW.js} +11 -3
- package/dist/chunk-UKGTWBKW.js.map +1 -0
- package/dist/{chunk-D76DSHL2.js → chunk-WJIYNKLI.js} +42 -4
- package/dist/chunk-WJIYNKLI.js.map +1 -0
- package/dist/{chunk-FQ7WNNOG.js → chunk-X35R65UP.js} +12100 -5191
- package/dist/chunk-X35R65UP.js.map +1 -0
- package/dist/{chunk-HFH5HQTP.js → chunk-Y77DA7EO.js} +43 -4
- package/dist/chunk-Y77DA7EO.js.map +1 -0
- package/dist/cli.js +18922 -10431
- package/dist/cli.js.map +1 -1
- package/dist/{client-OA2QMHOI.js → client-UUKOCY3Q.js} +4 -4
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +3 -3
- package/dist/{env-Q5FUIVIJ.js → env-Q3KQLCU5.js} +8 -2
- package/dist/output-V44OD5O3.js +22 -0
- package/dist/{shared-PD226SU6.js → shared-3THDUK2O.js} +6 -6
- package/package.json +4 -1
- package/dist/chunk-D76DSHL2.js.map +0 -1
- package/dist/chunk-FQ7WNNOG.js.map +0 -1
- package/dist/chunk-HFH5HQTP.js.map +0 -1
- package/dist/chunk-VF77MVEM.js.map +0 -1
- package/dist/output-NMXF2VIL.js +0 -18
- /package/dist/{chunk-4RN4CBIQ.js.map → chunk-6Z4ETVEZ.js.map} +0 -0
- /package/dist/{chunk-TTBMFAGF.js.map → chunk-RM2TTRYQ.js.map} +0 -0
- /package/dist/{client-OA2QMHOI.js.map → client-UUKOCY3Q.js.map} +0 -0
- /package/dist/{env-Q5FUIVIJ.js.map → env-Q3KQLCU5.js.map} +0 -0
- /package/dist/{output-NMXF2VIL.js.map → output-V44OD5O3.js.map} +0 -0
- /package/dist/{shared-PD226SU6.js.map → shared-3THDUK2O.js.map} +0 -0
package/README.md
CHANGED
|
@@ -167,8 +167,11 @@ List the Google Ads accounts selected for this company. Run this first to find a
|
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
baker ads google accounts
|
|
170
|
+
baker ads google accounts --available
|
|
170
171
|
```
|
|
171
172
|
|
|
173
|
+
`--available` lists every account the connected login reaches instead, each `{ id, name, selected }`. Read-only: it never changes the selection. When the account the user means comes back `selected: false`, the hint names it and points at `request_connection` with `pick: true`, which opens the account picker in the chat. The same flag works on `ads meta accounts`, `ads linkedin accounts`, `ads x accounts`, `ga4 properties`, `gsc sites` and `tag-manager containers`.
|
|
174
|
+
|
|
172
175
|
**Response:**
|
|
173
176
|
|
|
174
177
|
```json
|
|
@@ -865,7 +868,7 @@ baker ads x targeting-constants --constant tv_shows --locale en-US
|
|
|
865
868
|
|
|
866
869
|
### ChatGPT Ads (`baker ads chatgpt`)
|
|
867
870
|
|
|
868
|
-
Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversations) and how it performs, and stage changes to it that apply when the chat is published. The company connects one account in the
|
|
871
|
+
Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversations) and how it performs, and stage changes to it that apply when the chat is published. The company connects one account with an API key from its Ads Manager, pasted in the chat's connect card (`request_connection` `{ platform: "chatgpt-ads" }`) or in Brain → Integrations → ChatGPT Ads; the key stays in Baker and never reaches the CLI, so there is no account id to pass.
|
|
869
872
|
|
|
870
873
|
| Subcommand | What it returns |
|
|
871
874
|
|---|---|
|
|
@@ -950,6 +953,10 @@ baker ads meta ads --account-id act_123 --all-statuses # widen to
|
|
|
950
953
|
# "Find the October ad" — Meta matches the name, so only that row is read
|
|
951
954
|
baker ads meta ads --account-id act_123 --all-statuses --name-contains octubre --fields id,name,adset_id,creative_id
|
|
952
955
|
|
|
956
|
+
# "Who will see an ad in this campaign?" — a viewer per ad set: cold/warm/hot, what they
|
|
957
|
+
# already know (brand/offer/product), placements, and why — from targeting, audiences, objective
|
|
958
|
+
baker ads meta adsets --campaign-id 6123 --all-statuses --audience --output json
|
|
959
|
+
|
|
953
960
|
# "Tell me about ad 9988"
|
|
954
961
|
baker ads meta creatives --creative-id 9988
|
|
955
962
|
baker ads meta preview --creative-id 9988 --ad-format MOBILE_FEED_STANDARD --out-file /tmp/p.html
|
|
@@ -1109,10 +1116,23 @@ baker ads linkedin analytics --level campaign --campaign-id 1234 \
|
|
|
1109
1116
|
# "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
|
|
1110
1117
|
baker ads linkedin top-companies --campaign-id 1234 --last-days 30
|
|
1111
1118
|
|
|
1119
|
+
# "Which companies are our ads reaching?" — one row per company over the whole period,
|
|
1120
|
+
# most engaged first ({org, name, impressions, clicks, engagements, costLocal}).
|
|
1121
|
+
# Defaults: last 12 calendar months, the only selected ad account, top 100 (--full for all).
|
|
1122
|
+
baker ads linkedin companies --account-id 1234
|
|
1123
|
+
baker ads linkedin companies --by month --min-impressions 50 # months[] per company
|
|
1124
|
+
baker ads linkedin companies --campaign-id 1,2 --by campaign # campaigns[] per company
|
|
1125
|
+
baker ads linkedin companies --with-domains --stats --output csv # + domain/domainSource; read-quality stats
|
|
1126
|
+
|
|
1112
1127
|
# "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
|
|
1113
1128
|
baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
|
|
1114
1129
|
baker ads linkedin resolve --ids 17719,19022 --output csv
|
|
1115
1130
|
|
|
1131
|
+
# "What are these companies' website domains?" — for matching against CRM companies.
|
|
1132
|
+
# Per org: vanityName (LinkedIn page), website, domain, domainSource (website | subdomain | none);
|
|
1133
|
+
# ids LinkedIn doesn't know land in `errors`. Shortlinks are never followed.
|
|
1134
|
+
baker ads linkedin resolve --ids 1035,1337 --with-domains
|
|
1135
|
+
|
|
1116
1136
|
# "Who is in our audience by industry/seniority/function/title — all at once?"
|
|
1117
1137
|
baker ads linkedin demographics --campaign-id 1234 --last-days 30
|
|
1118
1138
|
|
|
@@ -1181,6 +1201,7 @@ creatives # default ACTIVE intended-status
|
|
|
1181
1201
|
|
|
1182
1202
|
analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
|
|
1183
1203
|
demographics # sweep all firmographic pivots in one call
|
|
1204
|
+
companies # which companies the ads reached, one row per company (--by month|campaign, --with-domains)
|
|
1184
1205
|
top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
|
|
1185
1206
|
conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
|
|
1186
1207
|
|
|
@@ -1200,6 +1221,7 @@ audit --format md # deliverable-ready markdown table
|
|
|
1200
1221
|
campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
|
|
1201
1222
|
campaigns create|update|pause|resume|archive|duplicate
|
|
1202
1223
|
campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
|
|
1224
|
+
campaigns exclude-companies <ad-set-id> --org <ids> # stop an ad set reaching these companies
|
|
1203
1225
|
account url-params # UTMs for every ad in the whole account
|
|
1204
1226
|
url-params [--ad-sets] # read what is tagged now, and what lands twice
|
|
1205
1227
|
creatives create|update|pause|resume|duplicate
|
|
@@ -1240,9 +1262,16 @@ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=pa
|
|
|
1240
1262
|
baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
|
|
1241
1263
|
baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
|
|
1242
1264
|
|
|
1243
|
-
# Matched audiences (CSV with header; email columns SHA-256 hashed locally)
|
|
1265
|
+
# Matched audiences (CSV with header; email columns SHA-256 hashed locally).
|
|
1266
|
+
# Company lists take companyName,domain,organizationUrn — organizationUrn matches the company page exactly
|
|
1244
1267
|
baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
|
|
1245
1268
|
|
|
1269
|
+
# Stop an ad set reaching customers / open deals — add-only; at publish Baker re-reads the ad set
|
|
1270
|
+
# and merges into its targeting as it stands then. --org takes company ids, ≤200 per call;
|
|
1271
|
+
# a longer list goes into a company-list audience passed as --segment
|
|
1272
|
+
baker ads linkedin campaigns exclude-companies 123456 --org 1035,1441
|
|
1273
|
+
baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegment:42
|
|
1274
|
+
|
|
1246
1275
|
# Conversions with playbook-default windows
|
|
1247
1276
|
baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
|
|
1248
1277
|
--post-click-window 30 --view-window 7
|
|
@@ -1304,7 +1333,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
|
|
|
1304
1333
|
- OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
|
|
1305
1334
|
- Account scoping: backend rejects calls against an account not in the picker selection.
|
|
1306
1335
|
- Pinned API version: `Linkedin-Version: 202606` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. The floor is `TRACKING_PARAM_DYNAMIC_MIN_API_VERSION` (`CREATIVE_NAME` needs 202606), asserted in `ads/linkedin/utils.test.ts`.
|
|
1307
|
-
- Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
|
|
1336
|
+
- Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, org domains 7d, bid-pricing/forecast 6h, audience-size 1h.
|
|
1308
1337
|
- `--no-cache` on any command skips both cache layers (the CLI's own and the server's).
|
|
1309
1338
|
|
|
1310
1339
|
#### LinkedIn-specific gotchas
|
|
@@ -1360,6 +1389,7 @@ baker analytics tracking # do the ad URLs carry the campaign, a
|
|
|
1360
1389
|
baker analytics tracking --platform meta # the same for one platform, read from their own ad account
|
|
1361
1390
|
baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
|
|
1362
1391
|
baker analytics map --platform google --remove kw # take that answer back
|
|
1392
|
+
baker analytics unstage --kind mapping --ref google/kw # take back what this chat staged (mapping|conversion|send|source), or --all
|
|
1363
1393
|
baker analytics conversions # what counts, what each produced, and which Forms count nothing
|
|
1364
1394
|
baker analytics conversions --candidates --output md # everything these pages do, and which of it is counted, as tables
|
|
1365
1395
|
baker analytics conversions --candidates # the same as compact JSON: one line per page event, Form keys folded onto `flows`
|
|
@@ -1532,6 +1562,26 @@ baker experiment fold # make the page
|
|
|
1532
1562
|
|
|
1533
1563
|
**`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*; `plan` says how long that is at this page's traffic, lists what this page's visitors actually went on to do, and what a 14, 28, 56 or 90-day window could settle. The duration is a guide and never a finish line — the test ends when the numbers are clear, and a page with no traffic yet still runs, sized on a stand-in that `warning` names (`--baseline 0.05` states the rate to expect). What `plan` refuses is what cannot be measured: no conversion defined, or a goal the page has demonstrably never produced. `--lift` sets the smallest lift worth detecting (default `0.2`) — halving it roughly quadruples the traffic needed.
|
|
1534
1564
|
|
|
1565
|
+
### Site settings (`baker site`)
|
|
1566
|
+
|
|
1567
|
+
The company's site settings — cookie consent, what Baker records, whether search engines show the site, the home page, the time zone, the landing download format, and custom domains. Every change stages on the chat and applies when the chat is published; discarding the chat drops it.
|
|
1568
|
+
|
|
1569
|
+
```bash
|
|
1570
|
+
baker site show # settings after this chat publishes, and what it staged (before/after)
|
|
1571
|
+
baker site set --consent recovery # bypass | recovery | strict | external
|
|
1572
|
+
baker site set --collection aggregate # full | aggregate | off (+ --payloads on|off with full)
|
|
1573
|
+
baker site set --indexing off # on | off
|
|
1574
|
+
baker site set --home oferta --home-mode redirect # a landing slug (rewrite by default), or --home none
|
|
1575
|
+
baker site set --timezone Europe/Madrid # IANA name
|
|
1576
|
+
baker site set --export-profile @profile.json # landing download format, JSON or @file
|
|
1577
|
+
baker site domain add shop.acme.com # staged; the client adds CNAME shop.acme.com → customers.withbaker.com
|
|
1578
|
+
baker site domain list # status of every domain: staged, pending_verification, active, error
|
|
1579
|
+
baker site domain verify shop.acme.com # re-check a pending domain now
|
|
1580
|
+
baker site unstage consent # take back one staged setting (or --all)
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
`set` takes several flags in one call and stages all of them or none. A staged setting that cannot apply at publish — a home page whose landing is not published yet, a domain another company registered in the meantime — is skipped with its reason on the chat's row and in `baker chats view` → `effects.siteSettings`; everything else in the chat still publishes.
|
|
1584
|
+
|
|
1535
1585
|
### Google Analytics 4 (`baker ga4`)
|
|
1536
1586
|
|
|
1537
1587
|
GA4 commands for multi-channel audits **and** for configuring what the property measures. Playbook-aligned report presets, property health audits, free-form Data API queries, and staged Admin API writes.
|
|
@@ -2338,15 +2388,17 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
|
|
|
2338
2388
|
| `baker images get <id>` | Single record | n/a |
|
|
2339
2389
|
| `baker images upload <file\|url>` | Upload a local file OR remote URL to the library | always on |
|
|
2340
2390
|
| `baker images delete <id>` | Delete a record | n/a |
|
|
2391
|
+
| `baker images retry <id>` | Process a failed image (status `error`) again | n/a |
|
|
2341
2392
|
| `baker images describe <id>` | Correct a record's stored name / description / tags | n/a |
|
|
2342
2393
|
| `baker images normalize <files>` | Local recolor + bg removal + trim + resize | n/a (operates on disk, no upload) |
|
|
2343
|
-
| `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked) | n/a (operates on library image) |
|
|
2394
|
+
| `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked). `--remove` drops the upscaled copy and goes back to the original (free) | n/a (operates on library image) |
|
|
2344
2395
|
| `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) |
|
|
2345
2396
|
| `baker images layers <imageId>` | Read the layers of an already-split image (free) | n/a (operates on library image) |
|
|
2346
2397
|
| `baker images crop <file>` | Coordinate-based rectangular extract — local file or URL | n/a |
|
|
2347
2398
|
| `baker images dimensions <file\|url>` | Read width / height / aspect / format without decoding | n/a |
|
|
2348
2399
|
| `baker images download <targets>` | Remote URLs and/or library ids → local files, so the local transforms can read them | n/a (writes to disk, no upload) |
|
|
2349
2400
|
| `baker images tags` | List available image tag names (defaults + company custom tags) | n/a |
|
|
2401
|
+
| `baker images tags create\|update\|delete <name>` | Manage the company's custom tags — one pool shared by images, videos and testimonials (also under `baker videos tags` / `baker testimonials tags`) | n/a |
|
|
2350
2402
|
|
|
2351
2403
|
**Every image record carries `width` and `height`, without `--full`.** They are there to be passed straight to the component that places the image: a remote image rendered with no declared size makes the site build download the file purely to measure it, which is the only reason building a site needs the network at all — and a dropped connection there fails the entire quality-check run for a reason nothing on the page caused.
|
|
2352
2404
|
|
|
@@ -3083,6 +3135,10 @@ Baker's YouTube source sync runs this same command on a disposable machine, with
|
|
|
3083
3135
|
|
|
3084
3136
|
Delete a video by ID.
|
|
3085
3137
|
|
|
3138
|
+
### `baker videos retry <id>`
|
|
3139
|
+
|
|
3140
|
+
Process a video whose status is `error` again. When the upload itself never finished it answers `status: "error"` with a `message` saying the file has to be uploaded again.
|
|
3141
|
+
|
|
3086
3142
|
### `baker videos describe <id>`
|
|
3087
3143
|
|
|
3088
3144
|
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.
|
|
@@ -3098,8 +3154,13 @@ List the available video tag names — built-in defaults plus the company's cust
|
|
|
3098
3154
|
|
|
3099
3155
|
```bash
|
|
3100
3156
|
baker videos tags
|
|
3157
|
+
baker videos tags create "summer sale" --description "Assets for the summer sale campaign"
|
|
3158
|
+
baker videos tags update summer-sale --description "Assets for the 2026 summer sale"
|
|
3159
|
+
baker videos tags delete summer-sale
|
|
3101
3160
|
```
|
|
3102
3161
|
|
|
3162
|
+
`tags create|update|delete` manage the company's custom tags, one pool shared by images, videos and testimonials. Built-in tags cannot be changed. Like every library change from a chat, they apply immediately and are listed on the chat as already applied.
|
|
3163
|
+
|
|
3103
3164
|
### `baker testimonials search <query>`
|
|
3104
3165
|
|
|
3105
3166
|
Semantic search testimonials. For landing pages, query the product/offer and USP before filtering by rating so selected quotes match what the landing sells.
|
|
@@ -3123,8 +3184,11 @@ The review pages testimonials are imported from — Google Maps, Trustpilot, Yel
|
|
|
3123
3184
|
| `baker testimonials sources list` | Every source: id, site, status, reviews imported, auto-sync. |
|
|
3124
3185
|
| `baker testimonials sources sync <id>` | Import new reviews now. |
|
|
3125
3186
|
| `baker testimonials sources schedule <id> --every <interval>` | Set the auto-sync cadence. |
|
|
3187
|
+
| `baker testimonials sources reset <id>` | Unstick a source whose import never finished, so it can import again. Keeps its reviews. |
|
|
3126
3188
|
| `baker testimonials sources remove <id>` | Remove the source and every testimonial it imported. |
|
|
3127
3189
|
|
|
3190
|
+
`baker testimonials delete <id> [--dry-run]` removes one testimonial (and the images it brought in) from the library. It applies immediately.
|
|
3191
|
+
|
|
3128
3192
|
```bash
|
|
3129
3193
|
baker testimonials sources add https://www.trustpilot.com/review/acme.com
|
|
3130
3194
|
baker testimonials sources add "Acme Bakery Madrid"
|
|
@@ -3173,7 +3237,7 @@ baker testimonials tags
|
|
|
3173
3237
|
|
|
3174
3238
|
### Avatars (`baker avatars`)
|
|
3175
3239
|
|
|
3176
|
-
Reusable AI presenters a company casts into images and video. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`,
|
|
3240
|
+
Reusable AI presenters a company casts into images and video — for an actor who has to come back across ads; a person in a single ad needs no avatar. An avatar holds one settled identity: an **identity sheet** (`sheetUrl`, four candid phone photos of the same person in their own setting) plus the **subject description** (`subjectDescription`) that person is described with. Every render grounds on the sheet via `--reference` and reuses the subject description verbatim — that pairing is what keeps the same face across a whole campaign.
|
|
3177
3241
|
|
|
3178
3242
|
`status` is `generating | ready | error | archived`; only `ready` can be cast. `create` returns immediately with `generating` because the identity sheet renders in the background (~30s). `likeness` is `synthetic` (invented from a description) or `licensed` (built from photographs of a real person, which requires `--likeness-confirmed`).
|
|
3179
3243
|
|
|
@@ -3225,6 +3289,18 @@ next one that shares her name. A `--handle` you pass yourself still conflicts: t
|
|
|
3225
3289
|
decision, and moving it quietly would point `@marta` at somebody else. The returned `handle`
|
|
3226
3290
|
is the one the avatar actually got.
|
|
3227
3291
|
|
|
3292
|
+
#### `baker avatars research "<who>"`
|
|
3293
|
+
|
|
3294
|
+
Find a real, named person — a testimonial author, the company's founder, a team member — from a description, and gather their public photos into the image library. `--create` builds their avatar from those photos right away (likeness `licensed`, no face picker), or returns the avatar they already have.
|
|
3295
|
+
|
|
3296
|
+
```bash
|
|
3297
|
+
baker avatars research "Lautaro Suarez, CTO at Vibiz"
|
|
3298
|
+
baker avatars research "the CEO of Vibiz" --create
|
|
3299
|
+
baker avatars research "Lautaro Suarez" --company Vibiz --role CTO --create --handle lautaro
|
|
3300
|
+
```
|
|
3301
|
+
|
|
3302
|
+
The answer's `status` is `found` (their `person` record, `photos`, a `subjectDescription` and, with `--create`, the `avatar`), `possible` (`candidates` that matched only in part — pick one with `--linkedin <url>`) or `not_found` (the `searched` queries; nothing was taken). A profile counts as the person only when both the name and the company match it. It checks the company's avatars first: a person who already has one (the same full name, and the same company when both name one; for a role alone, the researched avatar holding it) comes back `found` with that `avatar` (`reused: true`) — nothing searched, billed or copied again; an invented face (likeness `synthetic`) or one whose build failed never counts as them. `--linkedin` skips that check. When a lookup service does not answer (a timeout, a rate limit), the command fails with `UNAVAILABLE` rather than answering `not_found`, and nothing of that run is kept, so the next try searches again. `--name`, `--role` and `--company` override the description; `--full` adds their about text, location and every search run.
|
|
3303
|
+
|
|
3228
3304
|
#### `baker avatars update <handle>`
|
|
3229
3305
|
|
|
3230
3306
|
Edit an avatar. Profile flags are merged over the stored profile, so `--persona` alone keeps the subject description intact; an empty value clears the field it names (except `--subject`, which can only be replaced). Changing `--subject` requires `--regenerate-sheet` in the same call — otherwise the sheet still shows the person described before.
|
|
@@ -3261,6 +3337,7 @@ Connecting and removing apply immediately and show on the chat as an already-app
|
|
|
3261
3337
|
| `baker media-sources list` | Every source: key, platform, label, auto-sync, and per library `{ status, stored, pending?, failed?, error? }`. |
|
|
3262
3338
|
| `baker media-sources sync <key>` | Import new posts now. |
|
|
3263
3339
|
| `baker media-sources schedule <key> --every <interval>` | Set the auto-sync cadence. |
|
|
3340
|
+
| `baker media-sources reset <key>` | Unstick a source whose import never finished, so it can sync again. Keeps what it imported. |
|
|
3264
3341
|
| `baker media-sources remove <key>` | Disconnect; imported media stays in the library. |
|
|
3265
3342
|
|
|
3266
3343
|
```bash
|
|
@@ -3285,9 +3362,11 @@ Search the **ad-dna** corpus of scored "winning" competitor ads for reference cr
|
|
|
3285
3362
|
|
|
3286
3363
|
> Replaces the old `baker ads google library` tree, which has been removed. Competitor-by-keyword discovery still lives at `baker research advertisers`.
|
|
3287
3364
|
|
|
3365
|
+
`baker winning-ads refresh <advertiserId>` re-scrapes a followed brand so its newest ads land in the library. It applies immediately and runs at most once every 6 hours per brand; inside that window it answers `RATE_LIMITED` with an `error.fix` naming when the brand can be refreshed again. While a follow or the last refresh is still collecting the brand's ads, it starts nothing and says so.
|
|
3366
|
+
|
|
3288
3367
|
### `baker winning-ads search <query>`
|
|
3289
3368
|
|
|
3290
|
-
Semantic search (dense recall + BM25 + rerank) → `POST /api/ad-library/winners/search`. The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`, and `visual` (what the viewer sees) + `container` (a static's layout pattern) + `mechanism` (why it works) when known; plus top-level `pool_size`, `match_confidence` (`high|medium|low`), and `below_floor_count` (matches dropped under the relevance floor). The proof signals ride along (`winner_category`, `days_active`, `active`); `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + `reach` and `media_kind`. By default the search draws from the brands the company follows first (`--scope followed-first`) and tops up from the shared library; every result says `source: followed|library`. `--output json` (default) returns the lean objects; `--output md` prints a table.
|
|
3369
|
+
Semantic search (dense recall + BM25 + rerank) → `POST /api/ad-library/winners/search`. The CLI projects each result to a **lean, decision-focused** shape so the agent's context stays small — default fields: `advertiser`, `advertiser_id`, `platform`, `format`, `relevance`, `winner_score`, `summary` (what the ad is about), `media_url`, and `visual` (what the viewer sees) + `container` (a static's layout pattern) + `mechanism` (why it works) when known; plus top-level `pool_size`, `match_confidence` (`high|medium|low`), and `below_floor_count` (matches dropped under the relevance floor). The proof signals ride along (`winner_category`, `days_active`, `active`), and so does who the ad was made for when the library knows it: `audience_temperature` (`0_cold|1_warm_engaged|2_hot_visited|3_customer`), `funnel_stage`, `advertiser_size`, and `presumes_brand_knowledge` (whether the copy only works on viewers who already know the brand, with `brand_knowledge_reason` when `true`; absent while the ad is not judged yet); `--full` adds DNA detail (`angle`, `target_persona`, `hook_archetype`, `awareness_stage`, `industry`) + `reach` and `media_kind`. By default the search draws from the brands the company follows first (`--scope followed-first`) and tops up from the shared library; every result says `source: followed|library`. `--output json` (default) returns the lean objects; `--output md` prints a table.
|
|
3291
3370
|
|
|
3292
3371
|
> `media_url` is the creative itself: for `static` it's the image, for `video` it's the video file. ad-dna stores **no separate poster** for videos, so a video result has only the video URL.
|
|
3293
3372
|
|
|
@@ -3304,6 +3383,8 @@ baker winning-ads search --ref-ad-id a_12345 --first-seen-after 2026-01-01T00:00
|
|
|
3304
3383
|
baker winning-ads search "meal kit for busy parents" --scope followed-first --diverse --limit 5 --output md
|
|
3305
3384
|
# The best proven ads in one container, from any category (no query needed):
|
|
3306
3385
|
baker winning-ads search --format-pattern comparison_table --platform meta --output md
|
|
3386
|
+
# References made for strangers that don't lean on their brand being known:
|
|
3387
|
+
baker winning-ads search "delivery driver recruitment" --audience-temperature 0_cold --exclude-brand-reliant --output md
|
|
3307
3388
|
```
|
|
3308
3389
|
|
|
3309
3390
|
| Flag | Purpose |
|
|
@@ -3313,23 +3394,29 @@ baker winning-ads search --format-pattern comparison_table --platform meta --out
|
|
|
3313
3394
|
| `--scope followed-first\|followed\|all` | Which brands to draw from (default `followed-first`: the company's followed brands, topped up from the shared library; the whole library when it follows nobody) |
|
|
3314
3395
|
| `--sort winner\|longest_running\|newest` | Order of a no-query browse (default `winner`); ignored when a query is given |
|
|
3315
3396
|
| `--limit <n>` | Max results 1–100 (**default 10** — shortlist size) |
|
|
3316
|
-
| `--max-per-advertiser <n>` |
|
|
3397
|
+
| `--max-per-advertiser <n>` | Unset (default): brands spread, each brand's best 3 first and the rest after the other brands, none dropped. `1`–`50`: drop a brand's ads past N (`1` = one reference per brand). `0`: rank order. Brands the query names ("HubSpot video ads") are never capped |
|
|
3317
3398
|
| `--min-relevance <0-1>` | Relevance floor; trims weak matches |
|
|
3318
3399
|
| `--platform <list>` | `meta,linkedin,tiktok` — pass a single value to search **only** that platform |
|
|
3319
3400
|
| `--format <list>` | `video,static,carousel` |
|
|
3320
3401
|
| `--winner-category <list>` | `winner,scaled_winner,evergreen,rising,untested,dud,…` (default: all) |
|
|
3321
3402
|
| `--awareness <list>` | `unaware,problem_aware,solution_aware,product_aware,most_aware` |
|
|
3403
|
+
| `--audience-temperature <list>` | Who the ad was made for: `0_cold,1_warm_engaged,2_hot_visited,3_customer` |
|
|
3404
|
+
| `--funnel-stage <list>` | `TOF,MOF,BOF` |
|
|
3405
|
+
| `--advertiser-size <list>` | `solo,smb,mid,enterprise` |
|
|
3406
|
+
| `--exclude-brand-reliant` | Drop ads whose copy only works if the viewer already knows the brand (ads not judged yet are kept) |
|
|
3322
3407
|
| `--advertiser-id <list>` | **Restrict to** these advertiser ids (browse one brand's winners) |
|
|
3323
3408
|
| `--exclude-advertiser <list>` | **Drop** these advertiser ids, usually your own brand |
|
|
3324
|
-
| `--country <list>`
|
|
3409
|
+
| `--country <list>` | ISO country codes: ads known to run there, plus ads with no country data (every Meta ad) in that country's main language |
|
|
3410
|
+
| `--language <list>` | ISO 639-1 codes (`es,en`) for the language of the ad's copy and voiceover |
|
|
3325
3411
|
| `--first-seen-after` / `--first-seen-before` | ISO date (`2026-01-01`, read as the start of that day in UTC) or datetime bounds (recency) |
|
|
3326
3412
|
| `--hook-archetype <list>` | Narrow to ads on a proven hook **seed** (from `winning-ads hooks`) |
|
|
3327
3413
|
| `--format-pattern <list>` | Narrow to statics in these containers (from `winning-ads formats`, e.g. `comparison_table`) |
|
|
3328
3414
|
| `--diverse` | One varied page (different brands, containers, hooks) instead of rank order; ignores paging |
|
|
3329
3415
|
| `--output json\|md\|files` | Output format (default json) |
|
|
3416
|
+
| `--fields <list>` | Only these row fields (e.g. `advertiser,container,mechanism`); a `--full`-only field turns `--full` on, an unknown one is refused with `error.fix.validValues` |
|
|
3330
3417
|
| `--full` | Include DNA detail + longevity |
|
|
3331
3418
|
|
|
3332
|
-
A value outside a closed list (`--platform`, `--format`, `--winner-category`, `--awareness`, `--format-pattern`), a non-numeric `--limit`/`--max-per-advertiser`/`--min-relevance`, or a bound that is not a date is refused before the search runs; a refused list value comes with `error.fix.validValues`. References this company already adapted (`already_adapted`) never take a fresh reference's place: `--limit N` returns up to N fresh ones first. The search waits up to 120 s (`followed-first` can run two library searches).
|
|
3419
|
+
A value outside a closed list (`--platform`, `--format`, `--winner-category`, `--awareness`, `--audience-temperature`, `--funnel-stage`, `--advertiser-size`, `--format-pattern`), a non-numeric `--limit`/`--max-per-advertiser`/`--min-relevance`, or a bound that is not a date is refused before the search runs; a refused list value comes with `error.fix.validValues`. References this company already adapted (`already_adapted`) never take a fresh reference's place: `--limit N` returns up to N fresh ones first. The search waits up to 120 s (`followed-first` can run two library searches). A library that does not answer in time is `UNAVAILABLE`, with an `error.fix` to retry once narrower. An empty answer carries a hint naming the one filter to loosen.
|
|
3333
3420
|
|
|
3334
3421
|
Reading the scores: **`relevance`** (0–1) = match of the creative to your query; **`winner_score`** = how proven the ad is in-market. Pick references that are both relevant *and* proven.
|
|
3335
3422
|
|
|
@@ -3362,6 +3449,27 @@ baker winning-ads formats --awareness solution_aware --output md
|
|
|
3362
3449
|
Each key returns `count`, `mean_winner_score`, and `winner_share`. Ranking is winner-weighted volume, so a
|
|
3363
3450
|
proven grand-slam pattern floats to the top and a measured dud is floored.
|
|
3364
3451
|
|
|
3452
|
+
### `baker winning-ads fit <adId>`
|
|
3453
|
+
|
|
3454
|
+
Before building on a reference: why the ad wins, read against **this** company. One billed vision call, cached per ad
|
|
3455
|
+
under `canvas/.cache/fit/<platform>-<adId>.json` (asking again is free).
|
|
3456
|
+
|
|
3457
|
+
```bash
|
|
3458
|
+
baker winning-ads fit a_12345 --platform meta
|
|
3459
|
+
```
|
|
3460
|
+
|
|
3461
|
+
Returns `why_it_wins` in five layers (`disguise`, `voice`, `trigger`, `attention`, `logic`), the `load_bearing`
|
|
3462
|
+
elements that carry them (each with what it `needs`, whether the company has it: `have` | `adapt` | `missing`, and
|
|
3463
|
+
`ours`, what replaces it), and a `verdict`: `use`, `adapt` or `reject`. The company's Knowledge, reviews and market
|
|
3464
|
+
ground the answer. `baker canvas scaffold-static-ad --source-ad-id <adId> --source-platform <platform>` reads the cached
|
|
3465
|
+
answer, writes it into `prompt.json` as `concept`, and refuses a `reject` with `concept_does_not_fit` before anything is
|
|
3466
|
+
billed; the hint names the platform the fit was asked on, so a TikTok or LinkedIn reference is read from its own library.
|
|
3467
|
+
|
|
3468
|
+
| Flag | Default | Description |
|
|
3469
|
+
|---|---|---|
|
|
3470
|
+
| `--platform <meta\|linkedin\|tiktok>` | `meta` | Which platform the id is on |
|
|
3471
|
+
| `--full` | off | The whole answer, with the reasoning per element |
|
|
3472
|
+
|
|
3365
3473
|
### `baker winning-ads advertisers <brand>`
|
|
3366
3474
|
|
|
3367
3475
|
List corpus brands by name or domain → `GET /api/ad-library/advertisers`. Use it to find **your own** advertiser (to `--exclude-advertiser`) or a **competitor** (to `--advertiser-id` / `winners`). Lean default fields: `advertiser_id`, `label`, `platform_count`, `family_count`, `active_ad_count`, `total_ad_count`; `--full` adds `scraped_name`, `image_url`, `platforms`, `total_reach`, `last_synced_at`. Supports `--platform meta|linkedin`, `--limit`, `--offset`.
|
|
@@ -3403,7 +3511,7 @@ Winners across **every** brand you follow, in one library → `GET /api/ad-libra
|
|
|
3403
3511
|
Response `data`: `winners`, `following_count` (how many brands you follow), `advertiser_ids` (the ids actually queried = follows ∩ requested), and — when relevant — `hints` (e.g. you follow nobody yet, or a requested id isn't in your follows and was skipped). In `--output md`/`files` the table is the payload and the hints are written to **stderr** so they still surface without corrupting the output.
|
|
3404
3512
|
|
|
3405
3513
|
```bash
|
|
3406
|
-
baker winning-ads feed --
|
|
3514
|
+
baker winning-ads feed --output md
|
|
3407
3515
|
baker winning-ads feed --advertiser adv_123,adv_456 --platform meta --output md
|
|
3408
3516
|
```
|
|
3409
3517
|
|
|
@@ -3411,7 +3519,7 @@ baker winning-ads feed --advertiser adv_123,adv_456 --platform meta --output md
|
|
|
3411
3519
|
|---|---|
|
|
3412
3520
|
| `--advertiser <ids>` | Trim to a comma-separated subset of your followed advertiser ids (ids you don't follow are skipped and reported) |
|
|
3413
3521
|
| `--platform meta\|linkedin` | Filter to a single platform |
|
|
3414
|
-
| `--per-advertiser <n>` |
|
|
3522
|
+
| `--per-advertiser <n>` | Unset (default): brands spread, each brand's best 3 first and the rest after, none dropped. `1`–`50`: keep at most N per brand. `0`: rank order |
|
|
3415
3523
|
| `--limit <n>` | Total winners across all follows 1–200 (default 40) |
|
|
3416
3524
|
| `--winner-category <list>` | Restrict to specific winner categories |
|
|
3417
3525
|
| `--format <list>` | Restrict to specific formats (e.g. `static,video`) |
|
|
@@ -3461,6 +3569,7 @@ baker scheduled-agents create --name "Celebrate the first sale" --description "O
|
|
|
3461
3569
|
baker scheduled-agents update <id-or-temp_agent_id> --enabled false
|
|
3462
3570
|
baker scheduled-agents update <id-or-temp_agent_id> --run-at "2026-09-02T07:00:00Z" --prompt "Use the launch checklist"
|
|
3463
3571
|
baker scheduled-agents update <id-or-temp_agent_id> --trigger experiment.ended
|
|
3572
|
+
baker scheduled-agents update <id-or-temp_agent_id> --slack "#marketing" # each run opens a thread there; also here | me | off
|
|
3464
3573
|
baker scheduled-agents delete <id-or-temp_agent_id>
|
|
3465
3574
|
baker scheduled-agents unstage <id-or-temp_agent_id> # everything this chat staged on it
|
|
3466
3575
|
baker scheduled-agents unstage <id-or-temp_agent_id> --index 2 # one staged change only
|
|
@@ -3537,13 +3646,14 @@ baker scheduled-agents templates --delete weekly-posthog-check
|
|
|
3537
3646
|
|
|
3538
3647
|
Read the client's marketing/analytics tags (Meta pixel, ChatGPT Ads pixel, GA4, Google Ads, GTM, Clarity, Hotjar, PostHog, …) — production tags overlaid with the changes staged in this chat.
|
|
3539
3648
|
|
|
3540
|
-
**The CLI
|
|
3649
|
+
**The CLI never creates or edits a tag.** Every tag change (create, edit, delete) goes through the `request_tag_input` tool (`baker_ui` MCP server): the agent proposes one or more changes — each becomes a tab in one blocking approval form — pre-filling the non-secret fields it knows; the user reviews, edits, fills secret fields, and approves or skips each tab. Approved changes stage on the chat and apply when the chat is published; discarding the chat drops them.
|
|
3541
3650
|
|
|
3542
3651
|
`BAKER_CHAT_ID` must be set.
|
|
3543
3652
|
|
|
3544
3653
|
```bash
|
|
3545
3654
|
baker tags list # effective view: production + staged, full readable config + secret status
|
|
3546
3655
|
baker tags draft # review the staged changes awaiting publish
|
|
3656
|
+
baker tags unstage tag_temp_abc123 # take back one change this chat staged (or --all)
|
|
3547
3657
|
```
|
|
3548
3658
|
|
|
3549
3659
|
Notes:
|
|
@@ -3553,7 +3663,7 @@ Notes:
|
|
|
3553
3663
|
- **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.
|
|
3554
3664
|
- A `chatgptAds` tag takes a Pixel ID. Its optional `apiKey` is the Conversions API key and enables the server copy of page and Form events; the key stays out of the CLI and published page.
|
|
3555
3665
|
- 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.
|
|
3556
|
-
-
|
|
3666
|
+
- **`unstage` is the one write.** It takes back a change this chat staged — the same as discarding the row in the change review — by the ref `draft` prints, or `--all`. It carries no config and no secret, never touches a live tag, and refuses a ref nothing is staged under. Taking back a staged create drops the secrets entered for it with it.
|
|
3557
3667
|
- Single-instance types (`code`, `posthog`, `datafast`) reject a second instance against the chat's effective view.
|
|
3558
3668
|
- Conflicts at publish (tag deleted in the dashboard, config invalid) skip the op with a recorded reason — they never block the publish.
|
|
3559
3669
|
|
|
@@ -3671,10 +3781,17 @@ baker actions complete <action-id-or-tempId> --note "What was done" # stages
|
|
|
3671
3781
|
# --note is REQUIRED (≥20 chars): what changed, where, and the outcome. It is the durable, client-facing
|
|
3672
3782
|
# record surfaced by `baker actions log`. A missing/too-short note is rejected with a VALIDATION_ERROR.
|
|
3673
3783
|
baker actions discard <action-id> --reason "obsolete"
|
|
3784
|
+
baker actions reopen <action-id> # stages — a completed Task goes back to the backlog on publish
|
|
3785
|
+
|
|
3786
|
+
# Assigning — staged, applies on publish. --to is a userId or email from `members`, or `none`
|
|
3787
|
+
baker actions members # teammates a Task can be handed to (freelancers hidden unless you are a superadmin)
|
|
3788
|
+
baker actions assign <id-or-tempId> --to maria@empresa.com
|
|
3789
|
+
baker actions assign <id> --to none # unassign
|
|
3674
3790
|
|
|
3675
3791
|
# Tags — taxonomy is built-in defaults + company custom tags; --tags is STRICT (unknown slugs are rejected)
|
|
3676
3792
|
baker actions tags list # available tags (defaults + company custom); errors → stderr (inline-safe)
|
|
3677
3793
|
baker actions tags create --slug pmax --description "Performance Max work" # mint a custom tag
|
|
3794
|
+
baker actions tags update --slug pmax --description "Performance Max campaigns" # what it means; applies at once
|
|
3678
3795
|
baker actions tags delete --slug pmax # remove a custom tag (default tags can't be deleted)
|
|
3679
3796
|
|
|
3680
3797
|
baker actions link --blocker <id-or-tempId> --blocked <id-or-tempId>
|
|
@@ -3686,7 +3803,7 @@ baker actions draft --output md # one row per staged op (json defaul
|
|
|
3686
3803
|
baker actions draft --output md --fields kind,name,ref,tags # pick the columns
|
|
3687
3804
|
baker actions draft --output md --full # add each op's payload (description, note, reason, blocker)
|
|
3688
3805
|
baker actions draft remove temp_hero # drop a staged create (cascades its complete/link ops)
|
|
3689
|
-
baker actions draft remove <id> --op complete # drop a staged op on a real action (--op update|complete|discard)
|
|
3806
|
+
baker actions draft remove <id> --op complete # drop a staged op on a real action (--op update|complete|discard|assign|reopen)
|
|
3690
3807
|
baker actions draft clear # drop everything staged in this chat
|
|
3691
3808
|
```
|
|
3692
3809
|
|
|
@@ -3724,13 +3841,16 @@ Permissions enforced server-side:
|
|
|
3724
3841
|
- Only `claim`/`release` mutate live state.
|
|
3725
3842
|
- `claim` returns action details (`id`, `name`, `description`) plus `recommendedSkills` — a fast model reads the action's name/description/tags against the local `.claude/skills/` catalog and returns the skills (`{ name, reason }`) most worth loading for the work, echoed as `/skill — reason` lines in `hints`. It is best-effort: if the catalog is absent or the model call fails, the claim still succeeds and falls back to a generic review hint. Every claim also carries a routing hint: actions often span several surfaces, so load every skill that owns part of the work and read the tool doc (`__tooling__/docs/tools/baker/<family>.md`) for each baker CLI family the work needs.
|
|
3726
3843
|
- `complete` and `unlink` require the action to be claimed by the current chat — otherwise the API returns `FORBIDDEN` with a hint to claim first. Completing is the record that the work is done, so it stays claim-gated.
|
|
3844
|
+
- `assign` and `reopen` do not require a claim either, and are blocked the same way. `assign --to` is resolved only among the teammates the key's person can see (`members`), so a hidden freelancer cannot be named by id or email; restaging replaces the earlier assignment. At publish an assignee who lost access to the company is skipped as a conflict, never a failed publish.
|
|
3845
|
+
- `reopen` refuses a Task that is not completed, at stage time and again at publish.
|
|
3846
|
+
- `tags update` changes only what a custom tag means; the slug stays, because every Task carrying it stores it.
|
|
3727
3847
|
- `update` and `discard` do NOT require a claim — a chat may edit or retire an action it never claimed. They are blocked only when another chat currently holds the claim (`CONFLICT` with that chat's title).
|
|
3728
3848
|
- Claiming an action already claimed by another chat returns `CONFLICT` with the other chat's title — pick a different action.
|
|
3729
3849
|
- All staged ops are reverted automatically when the chat is discarded.
|
|
3730
3850
|
|
|
3731
3851
|
---
|
|
3732
3852
|
|
|
3733
|
-
### `baker mcp connected | catalog | list | add | remove`
|
|
3853
|
+
### `baker mcp connected | catalog | list | add | remove | verify | disable`
|
|
3734
3854
|
|
|
3735
3855
|
See everything a chat can reach — managed integrations, custom servers, and connected platforms — and manage **custom MCP servers** by pointing the agent at any remote HTTPS MCP endpoint. Registered tools appear as `mcp__<name>__*` on the next chat message.
|
|
3736
3856
|
|
|
@@ -3742,10 +3862,12 @@ baker mcp add --name weather --url https://mcp.example.com/mcp
|
|
|
3742
3862
|
baker mcp add --name acme --url https://acme.dev/mcp --header "Authorization: Bearer sk-…"
|
|
3743
3863
|
baker mcp add --name mine --url https://my.dev/mcp --scope user
|
|
3744
3864
|
baker mcp remove --name weather
|
|
3865
|
+
baker mcp verify --name weather # check it now (the dashboard's "Check now")
|
|
3866
|
+
baker mcp disable --name weather # stop its tools loading; only a person turns it back on (org-wide servers: a person only)
|
|
3745
3867
|
```
|
|
3746
3868
|
|
|
3747
3869
|
- `connected` returns three lists. `managed` and `custom` are MCP tools the agent calls directly; `platforms` are the accounts the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager, LinkedIn Ads), each with its login and the resources it may read. A platform has **no** `mcp__*` tools — it is read through its own command family — so it is listed here to keep the agent from concluding a working connection is missing.
|
|
3748
|
-
- `catalog --search <name> [--limit 1-20]` searches the managed apps a user can connect when a vendor has no MCP server of its own. Each app reports its slug (`app`), how the user connects it (`sign-in
|
|
3870
|
+
- `catalog --search <name> [--limit 1-20]` searches the managed apps a user can connect when a vendor has no MCP server of its own. Each app reports its slug (`app`), how the user connects it (`sign-in` or `key`; apps that need no sign-in cannot be connected and are left out) — never a managed copy of one of Baker's own connections: those come back under `platforms`, to ask for with `{ platform }` and whether this chat already reaches it (`connected`).
|
|
3749
3871
|
- `--url` must be the **full MCP endpoint** (commonly ending in `/mcp`), HTTPS and public; loopback / private / cloud-metadata hosts are rejected. A bare domain frequently accepts the OAuth sign-in and then serves no tools, so every address is handshake-checked after it changes: `connected` and `list` report `usable: false` plus a `problem` (and a `suggestedAddress` when the server advertises a better endpoint) for a server that isn't serving tools, or `lastCheck: failed` when the last check only hit an outage — that one may work anyway, and a later turn re-checks it automatically. Repointing a server at a different host clears its saved sign-in: a token belongs to the host that issued it, so the server has to be connected again.
|
|
3750
3872
|
- Scope is two independent choices — audience (just you / everyone) × level (this company / whole org):
|
|
3751
3873
|
- `--scope company` (default) — everyone in this company.
|
|
@@ -3792,7 +3914,40 @@ baker history list --category ads --full # ad writes with raw metadata
|
|
|
3792
3914
|
|
|
3793
3915
|
---
|
|
3794
3916
|
|
|
3795
|
-
### `baker
|
|
3917
|
+
### `baker credentials list | put | delete`
|
|
3918
|
+
|
|
3919
|
+
Credentials are a company's stored API keys, tokens and passwords. The CLI never returns a value: each credential has a placeholder, `{{credential:NAME}}`, that a request inside a Baker Session can carry in any header, URL or body. The Session's HTTPS proxy fills in the value only for the hosts the credential is bound to, and puts the placeholder back in responses.
|
|
3920
|
+
|
|
3921
|
+
```bash
|
|
3922
|
+
baker credentials list # name, placeholder, allowedHosts, label
|
|
3923
|
+
baker credentials list --full # + source, lastUsedAt
|
|
3924
|
+
printf %s "$KEY" | baker credentials put stripe-key --host api.stripe.com --label "Stripe key"
|
|
3925
|
+
baker credentials delete stripe-key
|
|
3926
|
+
```
|
|
3927
|
+
|
|
3928
|
+
- `put` reads the value from **stdin only** — a `--value` flag or a positional value is refused, because argv is visible to other processes. One trailing newline is trimmed; empty stdin is refused. `--host` is repeatable (or comma-separated) and accepts `*.example.com` wildcards.
|
|
3929
|
+
- `put` and `delete` act only on credentials the agent created. Credentials a person added are managed in the dashboard (Settings → Credentials).
|
|
3930
|
+
- The company must have credentials switched on; otherwise every command answers `FORBIDDEN`.
|
|
3931
|
+
|
|
3932
|
+
---
|
|
3933
|
+
|
|
3934
|
+
### `baker setup-links create | list`
|
|
3935
|
+
|
|
3936
|
+
A setup link lets whoever holds the company's accounts connect them without a Baker login. `create` makes one at once and returns its `url` (valid 30 days); the link is also listed on the chat as already made. Revoke links in the dashboard (Settings → Setup links).
|
|
3937
|
+
|
|
3938
|
+
```bash
|
|
3939
|
+
baker setup-links create --connect google-ads,meta --tags meta # sign in to accounts, add tracking tags
|
|
3940
|
+
baker setup-links create --keys chatgpt-ads --domains shop.acme.com # paste a key, set a domain's DNS
|
|
3941
|
+
baker setup-links create --apps slack --mcp crm=https://mcp.example.com/sse
|
|
3942
|
+
baker setup-links list # active links: asks, progress, url, expiresAt
|
|
3943
|
+
```
|
|
3944
|
+
|
|
3945
|
+
- Every flag is repeatable or comma-separated; at least one is required. Unknown tag, connection or key-account names are refused with the valid list. A `--domains` hostname must already be one of the company's domains.
|
|
3946
|
+
- `list` returns what each link asks for in the same words `create` takes. A read-only key sees no `url`.
|
|
3947
|
+
|
|
3948
|
+
---
|
|
3949
|
+
|
|
3950
|
+
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot companies search` · `baker hubspot deals list | properties | fields` · `baker hubspot meetings list | view`
|
|
3796
3951
|
|
|
3797
3952
|
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.
|
|
3798
3953
|
|
|
@@ -3814,6 +3969,8 @@ baker hubspot pipelines list # where a lead lands, and the st
|
|
|
3814
3969
|
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
3815
3970
|
baker hubspot contacts lookup lead@example.com # was this one lead picked up?
|
|
3816
3971
|
baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
|
|
3972
|
+
baker hubspot companies search --domains acme.com,globex.io --with-deals # CRM accounts by domain, with their deals
|
|
3973
|
+
baker hubspot deals list --pipeline default --modified-since 2026-01-01 # deals: amount, stage, closed won
|
|
3817
3974
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
3818
3975
|
baker hubspot meetings view <slug> # booking-form fields for one calendar
|
|
3819
3976
|
baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
|
|
@@ -3824,7 +3981,9 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
|
|
|
3824
3981
|
**`forms submissions`:** positional `<formId>`; `--days <1-365>` (default 30) sets the window; `--full` adds each submission's field values. The default response is counts and dates only — `total`, `inWindow`, `lastSubmittedAt`, and per-day counts — because submissions carry the lead's own contact details. `truncated: true` means the page budget ran out before the window was covered, so `inWindow` is a floor rather than an exact count.
|
|
3825
3982
|
**`workflows list` flags:** `--search <text>`, `--enabled-only`. Disabled workflows are listed too — a form wired to one that is off looks connected and does nothing.
|
|
3826
3983
|
**`workflows view`:** positional `<workflowId>` (the id from `workflows list`, and the one in HubSpot's own URL — the command translates it to HubSpot's separate internal flow id for you). Returns the enrolment criteria and whether it re-enrols (`null` = HubSpot does not report it for that criteria type, so it is unknown rather than off), plus every step with the property it writes. On a `LIST_BRANCH` step, branches come back in **evaluation order** (`order: 1` wins over `order: 2`) — a broad branch above a narrower one silently swallows the records the narrow one was written for, and HubSpot reports nothing. A `STATIC_BRANCH` matches on a value rather than top down, so its `order` is display only. `steps` is **not** in execution order; follow `nextActionId`. `--full` adds the raw payload, the only place the branch filter criteria (which property is compared to which value) can be read.
|
|
3827
|
-
**`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
|
|
3984
|
+
**`pipelines list`:** no flags; stages come back in the order they run in HubSpot, each with `isClosed` and `isWon` (closed with probability 1).
|
|
3985
|
+
**`companies search` flags:** `--domains <a.com,b.com>` (up to 100; a URL or `www.` is reduced to the bare domain), `--names <"Acme, Inc.;Globex">` (semicolon-separated), `--with-deals`, `--limit <1-500>` (default 100), `--output json|csv|jsonl|md`. Rows: `{ id, name, domain, linkedinPage, deals? }`; `notFound` lists the searched domains no CRM company carries; `truncated: true` means more matched than `--limit`.
|
|
3986
|
+
**`deals list` flags:** `--pipeline <id,id>`, `--modified-since <YYYY-MM-DD>`, `--limit <1-1000>` (default 200), `--after <cursor>`, `--output json|csv|jsonl|md`. Rows, oldest change first: `{ id, name, amount, currency, pipelineId, stageId, stageLabel, isClosed, isWon, createdAt, closedAt, modifiedAt, companyIds }` — `amount: null` is none entered. `next` is the `--after` for the following page; `restartFrom` appears when HubSpot's 10,000-result search ceiling is reached (read on with `--modified-since <restartFrom>`); `companiesReadable: false` means the connection cannot read companies, so no deal carries `companyIds`.
|
|
3828
3987
|
**`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.
|
|
3829
3988
|
**`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.
|
|
3830
3989
|
**`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.
|
|
@@ -3856,6 +4015,30 @@ No write surface against HubSpot itself — a form's fields, redirect, or consen
|
|
|
3856
4015
|
|
|
3857
4016
|
---
|
|
3858
4017
|
|
|
4018
|
+
### `baker accounts influenced | engagement | warm | company | campaigns | crm`
|
|
4019
|
+
|
|
4020
|
+
Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Reads Baker's nightly copy of LinkedIn and the CRM, so it is fast and covers up to 24 months; nothing is changed anywhere. Only for companies with Account insights switched on — otherwise every command exits 1 with `ACCOUNTS_NOT_ENABLED` and an `error.fix` pointing at the live read (`baker ads linkedin companies`, plus `baker hubspot companies search`). `--start`/`--end` must be real calendar days within 24 calendar months; a bad flag or an unsupported `--output` exits 1 with `INVALID_ARGS`.
|
|
4021
|
+
|
|
4022
|
+
```bash
|
|
4023
|
+
baker accounts influenced # influenced pipeline, last 12 calendar months
|
|
4024
|
+
baker accounts influenced --basis revenue --start 2026-01-01 --end 2026-06-30
|
|
4025
|
+
baker accounts influenced --full # every deal in the period, influenced or not
|
|
4026
|
+
baker accounts engagement --last-months 3 --limit 50 # companies reached, New/Rising, CRM match and deals
|
|
4027
|
+
baker accounts engagement --campaign-id 123456 --output csv
|
|
4028
|
+
baker accounts warm --output csv > warm.csv # companyName,domain,organizationUrn
|
|
4029
|
+
baker accounts company acme.com # or a LinkedIn company id / urn:li:organization:N
|
|
4030
|
+
baker accounts campaigns --last-months 6 --output md # per campaign: reached, engaged, influenced pipeline (--basis revenue)
|
|
4031
|
+
baker accounts influenced --model awareness --threshold 10 --output csv # another rule, this call only
|
|
4032
|
+
baker accounts warm --min 0 # every New/Rising company, below the threshold too
|
|
4033
|
+
baker accounts crm status # deals from HubSpot or sent by the client; what arrived, linked Google Sheets
|
|
4034
|
+
baker accounts crm check --file deals.csv # judge a deals or company CSV offline, nothing sent
|
|
4035
|
+
baker accounts crm contract # the send-your-CRM contract, as Markdown for a developer
|
|
4036
|
+
```
|
|
4037
|
+
|
|
4038
|
+
A deal is **influenced** when its company reached the company's influence rule — by default 3+ LinkedIn ad engagements (Awareness: 7+ impressions; Intent: 3+ clicks) — in the 12 months up to and including the month the deal was created (`--basis pipeline`, deals not won) or closed won (`--basis revenue`). It is a labelled rule, not credit: campaign rows overlap, and money is reported per currency, never summed across currencies. Deals whose company match is only a guess come back under `lowConfidence` and stay out of `summary` until the match is confirmed in Measure → Accounts. Without the deals set up there — HubSpot's pipelines and won/lost stages, or Another CRM — `influenced` and `campaigns` exit 1 with `ACCOUNTS_NOT_SET_UP`. A CRM Baker cannot read sends its companies and deals to `POST /v1/accounts/crm` (a `crm_ingest` key, `X-Baker-Mode: test` to rehearse, up to 500 records per request, records naming a person refused), is uploaded as a CSV in Measure → Accounts, or lives in a Google Sheet linked there that Baker reads every night (a row deleted from the sheet leaves Baker). A file is a deals export or a company list — a company needs only its website or LinkedIn page; `crm check` runs the same rules offline and `crm contract` prints the whole contract. The CLI never sends deals or links a sheet. `--model engagement|awareness|intent` and `--threshold N` on `influenced`, `campaigns` and `warm` read with another rule for that call only; the saved setting is unchanged. A **warm** account is New or Rising, reached the company's influence threshold this period on the rule's metric (`--min N` overrides, `--min 0` drops it), and has no open deal, no won deal and no deal lost in the last 6 months; `engagement` and `warm` rows carry `dealStatus` (`won` > `open` > `lost` > `none`), `engagement --output csv` adds `lastMonth` and `previousImpressions/Clicks/Engagements`, and `campaigns` / `company` carry each campaign's `campaignName` (null when LinkedIn could not name it). `company` returns `months` (the company's totals), `byCampaign` (the same numbers per campaign — overlapping, never add them) and its `deals` with `touchValue` and `influenced`. `warm --output csv` is the file `baker ads linkedin audiences create --type company-list --list-file` takes.
|
|
4039
|
+
|
|
4040
|
+
---
|
|
4041
|
+
|
|
3859
4042
|
### `baker chats list | view | transcript | diff`
|
|
3860
4043
|
|
|
3861
4044
|
Read-only inspection of **other Sessions (chats) on the same account** — so an agent can reuse prior work ("like we did in that other chat, but for X"). Same-account only; never crosses a company boundary and has no write surface.
|
|
@@ -4084,9 +4267,9 @@ baker canvas validate my-canvas.json
|
|
|
4084
4267
|
# legal config to review before billing — a frame that describes a recurring element
|
|
4085
4268
|
# (a pet/product) but doesn't wire its reference (VIDEO_REFERENCE_MISSING), or a scene
|
|
4086
4269
|
# span that exceeds the assigned model's max clip duration (VIDEO_SPAN_EXCEEDS_MODEL),
|
|
4087
|
-
# or the same ad rendered once per aspect ratio instead of one hero + a
|
|
4088
|
-
# image_aspect_adapt (IMAGE_PARALLEL_FORMAT_RENDERS — separate renders drift
|
|
4089
|
-
# crop, and color, so the sizes stop matching).
|
|
4270
|
+
# or the same ad rendered once per aspect ratio instead of one hero + cuts or a
|
|
4271
|
+
# terminal image_aspect_adapt (IMAGE_PARALLEL_FORMAT_RENDERS — separate renders drift
|
|
4272
|
+
# in type, crop, and color, so the sizes stop matching).
|
|
4090
4273
|
# A generator prompt is measured with its {{slots}} filled from text nodes and local
|
|
4091
4274
|
# prompt.json/text ingests: past the model's character cap it is refused as
|
|
4092
4275
|
# PROMPT_OVER_MODEL_LIMIT, naming the node and the overshoot, before anything bills.
|
|
@@ -4098,6 +4281,8 @@ baker canvas run my-canvas.json
|
|
|
4098
4281
|
# Advisory warnings print as `[warn] <CODE>: …` before anything bills.
|
|
4099
4282
|
# Only nodes reachable from the declared output execute (orphaned nodes are skipped,
|
|
4100
4283
|
# not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
|
|
4284
|
+
# The JSON ends with `verdict` (static ads), `run_id` and `files` (each delivered size's
|
|
4285
|
+
# local file by its label), so `| tail` keeps them.
|
|
4101
4286
|
# A failed run reports `error.code: "input_invalid"` when the backend refused a node's
|
|
4102
4287
|
# own input (it fails the same way until the input changes), `"runtime"` otherwise.
|
|
4103
4288
|
|
|
@@ -4188,10 +4373,11 @@ because a video model garbles a wordmark every time:
|
|
|
4188
4373
|
call to action when the spec has a `cta`; with neither a mark nor a `cta`, the last beat
|
|
4189
4374
|
keeps its footage. A typed stand-in (an SVG wordmark in live `<text>`) is still refused,
|
|
4190
4375
|
and `--no-brand` still means no brand layer at all: no mark and no closing card.
|
|
4191
|
-
- **Captions and the mark sit inside the safe area
|
|
4192
|
-
the
|
|
4193
|
-
|
|
4194
|
-
|
|
4376
|
+
- **Captions and the mark sit inside the safe area** (`videoSafeArea` in
|
|
4377
|
+
`src/engine/scaffold/platform-safe-area.ts`, the one owner of those margins). On 9:16 the
|
|
4378
|
+
caption cards end above the Reels interface at the bottom and the mark starts below the
|
|
4379
|
+
Stories and Reels header; 4:5 and 1:1 use modest feed margins. The closing card is centred
|
|
4380
|
+
in the same area. Every composition is sized to the spec's
|
|
4195
4381
|
`format.aspect_ratio`, so a 4:5 ad lays its captions out on a 4:5 frame.
|
|
4196
4382
|
- **Captions and the closing button are set in the brand's face**: a font file in
|
|
4197
4383
|
`src/brand/fonts/`, else the typeface `tokens.json` or `BRAND.md` names, loaded as a web
|
|
@@ -4209,7 +4395,7 @@ because a video model garbles a wordmark every time:
|
|
|
4209
4395
|
every beat they appear in on that avatar's identity sheet and copies its subject
|
|
4210
4396
|
description verbatim, so the ad shows the same face as the rest of the company's work.
|
|
4211
4397
|
This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
|
|
4212
|
-
- **`cast.description`** — a person described in words, when there is no avatar.
|
|
4398
|
+
- **`cast.description`** — a person described in words, when there is no avatar: a candid portrait is generated from it and every beat grounds on that one invented person.
|
|
4213
4399
|
|
|
4214
4400
|
Each of these also removes a generation: the closing beat costs no image and no clip.
|
|
4215
4401
|
|
|
@@ -4464,6 +4650,14 @@ Open record keyed by the composition's `meta.json` inputs.
|
|
|
4464
4650
|
|
|
4465
4651
|
---
|
|
4466
4652
|
|
|
4653
|
+
##### `static_prefilter`
|
|
4654
|
+
|
|
4655
|
+
Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
|
|
4656
|
+
|
|
4657
|
+
##### `static_safe_fit`
|
|
4658
|
+
|
|
4659
|
+
Local; no credits (one box-reader call, about $0.005, which rides on the run record's review spend). Brings a finished 9:16's words and logo inside the band Reels or Stories leave clear without rendering again: reads where every line and the logo sit and, when any reaches past the band, shrinks the whole picture a little (to 85% at most) and shifts it, carrying its edge into the uncovered margin, only when that edge is plain ground; where the review blocks a line under Reels' or TikTok's buttons down the right (an ad set that runs there and not in Stories), the fit clears them too (a busy edge would read as a blurred duplicate, so the picture is passed on as rendered). The image model cannot move an element by a few points on request, so this replaces re-rendering for small misses. **Inputs:** `image`. **Params:** `format`, `placements` (the ad set's, as the sizes were planned). **Outputs:** `image` (same pixel size), `fit` (`{ scale, x, y }`, or null when passed on unchanged), `reason` (`not_read`, `inside_band`, `too_far` when clearing the band needs more than the 85% shrink, or `busy_edge` when the edge it would carry is not plain ground) and `usage`. Scaffolded by `scaffold-static-ad` after every 9:16 Reels or Stories covers (`safe_<size>`, tagged `static_safe_fit`). It never sets the picture over a blurred or mirrored copy of itself: a layout `too_far` or `busy_edge` is passed on as it is, the run review names what Reels or Stories covers, and `baker canvas run` lays that size out again by itself.
|
|
4660
|
+
|
|
4467
4661
|
##### `font_specimen`
|
|
4468
4662
|
|
|
4469
4663
|
Render specimen text in a given font file to a **PNG** — black `#000` text on a white `#fff` background — via headless Chromium at 2× device-scale. Produces a **typeface reference image**: wire `image` into `image_generate`'s `reference` input so the model replicates the font when generating images that contain text.
|
|
@@ -4559,8 +4753,8 @@ None.
|
|
|
4559
4753
|
| `model` | enum | yes | one of `~google/gemini-flash-latest`, `~google/gemini-pro-latest` |
|
|
4560
4754
|
| `prompt` | string | yes | non-empty |
|
|
4561
4755
|
| `system` | string | no | system prompt |
|
|
4562
|
-
| `response_format` | enum | no | `text` (default) or `json_object`. Set `json_object` when a downstream `{{slot}}` consumes the output as JSON (e.g.
|
|
4563
|
-
| `web_search` | boolean | no | When `true`, routes through OpenRouter's `:online` web plugin so the model searches the live web before answering. Use it on
|
|
4756
|
+
| `response_format` | enum | no | `text` (default) or `json_object`. Set `json_object` when a downstream `{{slot}}` consumes the output as JSON (e.g. a hand-built transform step) so the model returns clean JSON with no markdown fences or prose. |
|
|
4757
|
+
| `web_search` | boolean | no | When `true`, routes through OpenRouter's `:online` web plugin so the model searches the live web before answering. Use it on a text step that must adapt copy to the target brand's real facts (current pricing, the trust signals it actually has) instead of guessing. Adds web-search cost. |
|
|
4564
4758
|
| `temperature` | number | no | 0–2 |
|
|
4565
4759
|
| `max_tokens` | number | no | positive int |
|
|
4566
4760
|
|
|
@@ -4598,7 +4792,7 @@ Generate images via OpenRouter. The Zod schema is a permissive union; per-model
|
|
|
4598
4792
|
| `reference` | image **or** image[] | no | `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
|
|
4599
4793
|
|
|
4600
4794
|
`reference` accepts a single image or an **array** of images. Wire several to combine references in one
|
|
4601
|
-
generation — e.g. a subject reference sheet + a font specimen
|
|
4795
|
+
generation — e.g. a subject reference sheet + a product shot + a font specimen (never someone else's ad: the model copies what it is shown). Each image is
|
|
4602
4796
|
forwarded to the model as a separate input, in array order (the provider accepts multiple). Wire an
|
|
4603
4797
|
array as a literal list: `"reference": ["$ref:subject.sheet", "$ref:type.image"]`.
|
|
4604
4798
|
|
|
@@ -4608,7 +4802,9 @@ array as a literal list: `"reference": ["$ref:subject.sheet", "$ref:type.image"]
|
|
|
4608
4802
|
|---|---|---|
|
|
4609
4803
|
| `images` | image[] | `image/png \| image/jpeg \| image/webp` (provider-determined) |
|
|
4610
4804
|
|
|
4611
|
-
**Cost** — 5 credits estimate per
|
|
4805
|
+
**Cost** — 5 credits estimate per image asked for (`n`, default 1), 20 on Gemini 3 Pro Image (as `image_aspect_adapt` quotes it) and 20 at quality `max` (a static ad's re-laid-out sizes); the backend charges the call's actual OpenRouter usage.
|
|
4806
|
+
|
|
4807
|
+
**Several candidates from one call** — `n` (integer, default 1) asks one call for several images, returned together in `images`. Each image is billed as if it were its own call (`n: 3` costs three images); what it saves is requests and waiting. Only models that declare it take it (today `openai/gpt-image-2.5-sunburst`, up to 10); any other model refuses it before anything is billed. Wire the whole array into `image_select` to keep the best.
|
|
4612
4808
|
|
|
4613
4809
|
> `seed` is not supported — OpenRouter's `image_config` schema has no seed slot for image-gen.
|
|
4614
4810
|
|
|
@@ -4779,7 +4975,7 @@ Guidance:
|
|
|
4779
4975
|
|
|
4780
4976
|
Adapt ONE creative into multiple aspect ratios in a single step — the standard fan-out when a hero creative must ship to every placement (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9…). The AI recomposes the layout per format: identical subject, faces, products, text (verbatim, same typography), logos, colors, and style; the scene is extended/restructured for the new canvas — never stretched, cropped, or letterboxed.
|
|
4781
4977
|
|
|
4782
|
-
The adaptation prompt is baked into the backend and is direction-aware (a
|
|
4978
|
+
The adaptation prompt is baked into the backend and is direction-aware by orientation: a portrait source adapted to landscape (or the reverse) is recomposed; a same-orientation change only extends the scene at the sides or at the ends, every element kept where it is. Optional `copy` (the ad's exact lines: each kept once, spelled as given) and `safe_areas` (per format, the fraction of each edge the platform's interface covers) are set by the static scaffold. Formats that already match the source ratio (within ~2%) pass the source asset through unchanged — no model call, no cost.
|
|
4783
4979
|
|
|
4784
4980
|
**Inputs**
|
|
4785
4981
|
|
|
@@ -4921,13 +5117,13 @@ Search the web for real images instead of generating them. A backend LLM agent p
|
|
|
4921
5117
|
|
|
4922
5118
|
##### `image_select`
|
|
4923
5119
|
|
|
4924
|
-
Pick the best `count` images out of
|
|
5120
|
+
Pick the best `count` images out of the candidates with a vision LLM, judged against a prompt. The output is a passthrough subset of the input refs — no new pixels are generated, so selection is cheap. Use it after an `image_generate` run with `n` > 1, several `image_generate` variants, or an `image_search` batch, to keep only the strongest before expensive downstream steps. A single candidate (a generation run at `n: 1`) passes through unjudged: no call, no charge, no `reasoning`.
|
|
4925
5121
|
|
|
4926
5122
|
**Inputs**
|
|
4927
5123
|
|
|
4928
5124
|
| Slot | Kind | Required | Accepted MIMEs |
|
|
4929
5125
|
|---|---|---|---|
|
|
4930
|
-
| `images` | image[] | yes — at least
|
|
5126
|
+
| `images` | image[] | yes — at least 1 | `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
|
|
4931
5127
|
|
|
4932
5128
|
Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list collecting refs from several nodes (`["$ref:a.images#0", "$ref:b.images#0"]`).
|
|
4933
5129
|
|
|
@@ -4938,7 +5134,7 @@ Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list colle
|
|
|
4938
5134
|
| `model` | enum | yes | one of `~google/gemini-flash-latest`, `~google/gemini-pro-latest` (`pro` for harder aesthetic judgement) |
|
|
4939
5135
|
| `prompt` | string | yes | selection criteria, e.g. `"most professional product shot, clean background"` |
|
|
4940
5136
|
| `count` | int | no | images to select, ≥ 1 (default 1); must not exceed the wired image count |
|
|
4941
|
-
| `temperature` | number | no | 0–2,
|
|
5137
|
+
| `temperature` | number | no | 0–2; unset by default, so the Gemini judge keeps its own default |
|
|
4942
5138
|
| `max_tokens` | number | no | positive int |
|
|
4943
5139
|
|
|
4944
5140
|
**Outputs**
|
|
@@ -4946,7 +5142,7 @@ Wire either a whole upstream array (`"$ref:gen.images"`) or a literal list colle
|
|
|
4946
5142
|
| Slot | Kind | MIME |
|
|
4947
5143
|
|---|---|---|
|
|
4948
5144
|
| `images` | image[] | per input — exactly `count` refs, in the model's preference order, so `images#0`…`images#count-1` are always safe to wire |
|
|
4949
|
-
| `reasoning` | text | `text/plain` — why the winners won, compared against the rejected candidates |
|
|
5145
|
+
| `reasoning` | text | `text/plain` — why the winners won, compared against the rejected candidates; absent when a single candidate passed through |
|
|
4950
5146
|
|
|
4951
5147
|
**Cost** — 1 credit estimate per call (backend charges actual OpenRouter usage).
|
|
4952
5148
|
|
|
@@ -5592,7 +5788,7 @@ Cache policies (via `--cache-policy`):
|
|
|
5592
5788
|
|
|
5593
5789
|
Parse + schema-check + per-node deep validation (composition `meta.json` is consulted for `hyperframe_*`). Prints a structured result with a per-node cost preview. Exits non-zero on issues. Never calls Convex, never spends credits.
|
|
5594
5790
|
|
|
5595
|
-
**The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's
|
|
5791
|
+
**The canvas file is brought up to date first.** Parts of a creative's canvas are projections of its blueprint: each video scene's frame and clip prompts (from `scenes/sNN.json`), a static ad's render prompts, judge criteria and type specimen (compiled from the ad spec in `prompt.json`), and the derived style file the frames read (`prompt.style.json`). `validate`, `run`, `critique` and `set-prompt` share one preflight that applies every projection and **writes the result back to the canvas file** (only when something changed; `canvas_updated` says so; `sync-definition`, which the file watcher runs over every creative, applies it without writing), so the file on disk, the graph the dashboard shows, the graph the critic scores and the graph that renders are the same. A scene edit that changes timing or the number of scenes is structural and still asks for a re-scaffold. Every preflight failure prints `{ ok: false, error: { code, message, fix? } }` and exits 2.
|
|
5596
5792
|
|
|
5597
5793
|
```bash
|
|
5598
5794
|
$ baker canvas validate my-canvas.json
|
|
@@ -5652,6 +5848,27 @@ $ baker canvas critique my-static-ad.json
|
|
|
5652
5848
|
"dimensions": [ { "dimension": "text_legibility_risk", "score": 1, "note": "2 copy block(s) — reads cleanly" }, … ] }
|
|
5653
5849
|
```
|
|
5654
5850
|
|
|
5851
|
+
#### `baker canvas viewer-check <slug>`
|
|
5852
|
+
|
|
5853
|
+
Would the person a static ad reaches understand it? A model plays that viewer — what they already know and
|
|
5854
|
+
where they scroll — and says, from `prompt.json`'s copy and scene alone, what is offered, to whom and what to do
|
|
5855
|
+
next; the answer is compared with `prompt.json`'s `message`. The ad fails when the viewer can only guess what is
|
|
5856
|
+
offered, or, for a viewer who does not know the brand, when the words say they already did something with it (a
|
|
5857
|
+
cart, a trial, a donation, a registration); who it is for is reported but never fails an ad alone. The viewer is the `viewer` recorded in
|
|
5858
|
+
`_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
|
|
5859
|
+
the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
|
|
5860
|
+
`viewer_check_failed` and a `fix` naming what the viewer missed. Answers are cached per copy, scene and viewer.
|
|
5861
|
+
`baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
|
|
5862
|
+
`--skip-viewer-check` renders without it; a model outage lets the render through with a warning). `--full` adds
|
|
5863
|
+
the viewer's whole answer and how the viewer was chosen.
|
|
5864
|
+
|
|
5865
|
+
```bash
|
|
5866
|
+
$ baker canvas viewer-check club-whiteboard
|
|
5867
|
+
{ "ok": false, "error": { "code": "viewer_check_failed",
|
|
5868
|
+
"message": "Not rendered: a cold viewer who knows nothing about the brand yet, on automatic placements would not understand this ad.",
|
|
5869
|
+
"fix": "Seen by a cold viewer …, the words and picture alone do not say what is offered. …", "missed": [ … ] } }
|
|
5870
|
+
```
|
|
5871
|
+
|
|
5655
5872
|
#### `baker canvas run <file.json> [flags]` / `baker canvas run --slug <slug> [flags]`
|
|
5656
5873
|
|
|
5657
5874
|
Validate, then execute the graph. Blocks until done. Logs one line per node. Returns `{ ok, run_id, outputs_dir, output, stats }` on stdout.
|
|
@@ -5666,7 +5883,14 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5666
5883
|
| `--force-remote` | off | With `--slug`: restore the recorded files even over local ones. |
|
|
5667
5884
|
| `--run-id <id>` | auto ULID | Override the generated run id. |
|
|
5668
5885
|
| `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
|
|
5669
|
-
| `--concurrency <n>` | `
|
|
5886
|
+
| `--concurrency <n>` / `--parallel <n>` | `8` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
|
|
5887
|
+
| `--fresh` | off | Start a new run id instead of resuming an interrupted run of this canvas. |
|
|
5888
|
+
| `--regenerate <ids\|roles>` | none | Force these nodes, and everything downstream, fresh this run only. A role names every node carrying it: on a static ad `static_variants` (or its id `gen`) renders the variants again. An unknown name exits 2 (`unknown_regenerate_node`). |
|
|
5889
|
+
| `--fix "<what to change>"` | none | Static ad only: correct a finished version with one render that changes only what you say; every size is made again from it (see below). The same fix after it left its defect in place is refused (`no_progress`). |
|
|
5890
|
+
| `--use-variant <sha256>` | none | Static ad only: make every size again from another of a version's variants, the one the person chose; the only requests re-lay it out for each other size (see below). |
|
|
5891
|
+
| `--from <run_id>` | the last render | With `--fix` or `--use-variant`: the version to start from. |
|
|
5892
|
+
| `--autofix` / `--no-autofix` | on | Static ad only: when the review finds a blocking defect, make up to 2 more renders (`AUTO_FIX_ROUNDS`) that correct it before reporting (see below). `--no-autofix` reports after the first render. |
|
|
5893
|
+
| `--skip-viewer-check` | off | Static ad only: render without the viewer check (`baker canvas viewer-check`), when the person heard what the viewer would miss and wants the copy as it is. |
|
|
5670
5894
|
| `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Operator only. Company-scoped remote cache + durable asset persistence. |
|
|
5671
5895
|
| `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
|
|
5672
5896
|
| `--until frames` | none | Stop before the clips with the frames settled; the run is recorded as `awaiting_approval` (not failed, not interrupted) and resumes with `--run-id`. |
|
|
@@ -5678,6 +5902,12 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5678
5902
|
|
|
5679
5903
|
**Failures don't abandon sibling work.** Nodes in a layer run under the concurrency cap and every one **settles** — a failed clip no longer kills its in-flight siblings, whose finished results still land in the content-addressed cache. One failure re-throws as-is; several are reported together (each failed node named). Re-running `baker canvas run` resumes from the cache and re-executes **only** the failed nodes and their descendants — never hand-orchestrate per-node renders. **One render per canvas at a time:** a `run` of a canvas that another live process in the same workspace is still rendering (a backgrounded render, or a subagent's) is refused with `CONFLICT` instead of sharing its run, `--run-id` included — wait for it and use its result. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
|
|
5680
5904
|
|
|
5905
|
+
**Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
|
|
5906
|
+
|
|
5907
|
+
**Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it; anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`), so the review's second look at the same pixels never counts as a new defect. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
|
|
5908
|
+
|
|
5909
|
+
**Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, and every size is made again from the corrected picture. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
|
|
5910
|
+
|
|
5681
5911
|
#### `baker canvas scaffold-video <video> [flags]`
|
|
5682
5912
|
|
|
5683
5913
|
Turn a reference video into a **runnable, self-validated reproduction canvas** in one command — the video counterpart of `scaffold-static-ad`. The `<video>` positional is a **local path OR an http(s) URL** (a `baker winning-ads` `media_url`, a library URL, any reel link) — a URL is downloaded for you (no manual `curl` first), so pass `--slug`/`--out` with it to give the canvas a home. It runs **billed passes** up front:
|
|
@@ -5691,7 +5921,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
|
|
|
5691
5921
|
|
|
5692
5922
|
A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
|
|
5693
5923
|
|
|
5694
|
-
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast
|
|
5924
|
+
It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are invented or cast**: a person with no avatar gets a generated candid portrait from their description (an `image_generate` node at the `el_*` slot, fused into a sheet), so every frame shows one realistic invented person; with `--avatar <handle>` each recurring person grounds on that avatar's identity sheet, used as-is. Never a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
|
|
5695
5925
|
|
|
5696
5926
|
**Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
|
|
5697
5927
|
|
|
@@ -5787,29 +6017,32 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
|
|
|
5787
6017
|
|
|
5788
6018
|
Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. It runs **billed Gemini passes** up front:
|
|
5789
6019
|
|
|
5790
|
-
1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image
|
|
5791
|
-
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`). The describe pass also records the ad's **typography**
|
|
6020
|
+
1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels; the image model renders only the screen's `photos`, each a plain phone photo of ordinary people of the market; adding the block switches an existing ad on its next run, `--fix` does not apply, an invented brand handle or a rating the Knowledge does not hold (either decimal mark) is refused as `native_screen_invalid`, and a photo of ours goes in as its `image`: a native photo takes no `avatar`, since nothing would cast it), a role on each third-party brand (`partner` | `proof` | `category_signal`), a reference's `image` (wired as a picture of ours) and a person's `avatar` (the company avatar cast), and `concept` (`why_it_wins` in five layers, `load_bearing` elements with `have`/`adapt`/`missing` and `ours`, `verdict` `use`|`adapt`|`reject`, `transfer_note`; seeded from `baker winning-ads fit`). Every render keeps the load-bearing elements; `validate`/`run` refuse a `reject` or a `missing` element with nothing named (`concept_does_not_fit`) and a rating the Knowledge does not hold (`proof_without_source`); a line citing a review platform gets its real badge wired. **You never write a model prompt**: every render prompt is compiled from this file for the model it runs on (short labelled sections for gpt-image, sentences with reasons for Gemini; each line quoted once; pictures numbered with one role each; colours from the brand; realism as capture cues; the safe area from its one owner; constraints last), on every `validate` and `run`, and shown on its node. A `prompt.json` written before the ad spec (the describe blueprint) is read through an adapter and keeps working. The source's own read (`source`, and the library's `reference`) stays as bookkeeping and is never sent to a model.
|
|
6021
|
+
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`). The describe pass also records the ad's **typography** (each typeface's classification, a best-guess family, and its weight/case), which seeds the spec's `brand.typeface`. Background extras are dropped. Each element becomes a `references` entry in the spec, named by its role and naming the canvas slot it fills, so the file says which slot grounds which subject.
|
|
5792
6022
|
|
|
5793
6023
|
It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
|
|
5794
6024
|
|
|
5795
|
-
- Up to **three
|
|
5796
|
-
-
|
|
5797
|
-
- Every
|
|
5798
|
-
-
|
|
6025
|
+
- Up to **three variants** (`--takes`, default 3) render on the model the router picks for the concept (today `openai/gpt-image-2.5-sunburst` at quality `max`; `--gen-model` overrides), each a finished ad: the picture and every word of the copy. Each variant is **its own request** (tagged `static_variant_call`), run in parallel on its own direction (the scene as written; another picture of the same message, by the scene's look; another line leading, named by number so no word changes), and a `collect` tagged `static_variants` gathers them, on every model: one request with `n` costs the same (`n` is billed per picture) but gave fewer distinct options, so the graph never uses it. The brief quotes every line of `copy` verbatim and asks for each once; it is compiled again from `prompt.json` on every `validate` and `run`, so an edit re-keys exactly the renders it touches. The brand's **typeface** is shown to the model as a `font_specimen` set on the `designed` lines: a file in `src/brand/fonts/` (via `src/brand/tokens.json` when present), else the heading family `tokens.json` or `BRAND.md` names, fetched as a web font file; an `in_scene` line keeps its own lettering only on the real packaging, signage or screen its `on` names (handwriting on post-its, whiteboards or chalkboards, and a line naming no surface, are set as designed type). Each line's `role` sets its size: a price or stat (else the headline) leads, the `cta` is a large solid button, fine print is smallest. **The logo is optional**: the real file in `src/brand/logos/` goes to the model as a reference to reproduce once, at least a fifth of the frame wide, where it contrasts or on a small brand-colour plate, only when `prompt.json` has `"logo": true` (an older blueprint: an `advertiser_brand` entry in `brands_logos`) and the file exists; no logo is never an error, never a slot to fill and never a stand-in. Setting it false drops the logo on the next run; adding one to a canvas scaffolded without the file needs the file in `src/brand/logos/` and a re-scaffold. `prompt.json` also carries a **`brand`** block (ground, ink, accent, CTA and palette, from `src/brand/tokens.json` roles or `BRAND.md`); a colour outside it never reaches the model. The brief describes a complete ad at the master's shape and asks only for its edge margin (see below). `baker canvas set-prompt` refuses these nodes and names `prompt.json`.
|
|
6026
|
+
- A **`static_prefilter`** node (tagged `variant_prefilter`) drops variants that fail the deterministic checks before the judge sees them, never all of them; its **judge** (`judge_model`, `judge_prompt`) compares the rest pairwise against the compiled brief when the checks tie (one variant passes through unjudged). A canvas scaffolded with a separate `image_select` judge (tagged `pick`) has it folded into the prefilter on its next run.
|
|
6027
|
+
- **Every placement format is made from the chosen picture, and no size is a crop or a paste of another.** The pick goes to exact pixels as the master (an `imagemagick` node tagged `static_master`) at the lead size: the 4:5 when a feed size is delivered, else the square, else the first portrait size, rendered at the model's nearest ratio (3:4 on GPT Image) and filled to Meta's pixels. It is delivered as it is. Every other size (9:16, 1:1, 16:9, …) is **re-laid out** from the master by one request (`image_generate` node `adapt_<size>`, tagged `static_adapt_format`, on the router's `relayout` model, GPT Image 2.5 at quality `max`): the master, the variants' own product and people pictures and the logo file as references and a brief compiled from `prompt.json` for that frame on every run (`static-adapt.ts`): the exact lines and their hierarchy, the same people (same face, same clothes), product and label, logo and none the chosen ad does not show, the words and the button re-scaled for the frame, the frame stated as zones inside its safe area, never the master shrunk onto a background, set as a square or cut. A `size_<size>` node (`static_size`) brings it to Meta's exact pixels (1:1 at 1080×1080; a landscape size the same way). Every 9:16 Reels or Stories covers then passes a free `static_safe_fit` node (`safe_<size>`), which shrinks and shifts the picture just enough (to 85% at most) to bring a word or the logo a few points past the band back inside it; a layout further out passes unchanged and the run lays it out again. The 9:16 is never extended instead (the 4:5 set whole in the clear band, its scene continued to the frame) nor set over its own blurred copy: rated in October 2026, the first showed the ad small inside a continued scene ("text super tiny") and the second "looks bad". The master shape is one decision in the code (`DEFAULT_MASTER_SHAPE`: `relayout`; the `9x16_band` and `4x5_adapt` shapes, kept for the bench, make the sizes by cutting or reframing the master, and the person rating them rejected every such way; a canvas from the retired band master, which extended a square master into the 9:16, is upgraded to the re-layout graph on its next run). Placing the logo file on every size instead of having the model draw it (`logo_composite` nodes) is a scaffold option, off until it is proven on the bench.
|
|
6028
|
+
- Readers go by these roles, never by node ids: the run review, `--fix`, `--use-variant`, `--regenerate static_variants`, and the dashboard. A static ad scaffolded before this graph (`ad_a`/`ad_b`/`ad_c`, `pick`, re-lays `ad_4x5`/`ad_9x16`, `fit_4x5`) is **upgraded in place** on its next `validate` or `run`: prompt.json, every asset slot, the model and the formats are kept, the render part becomes this graph, and the creative is not reported as changed by the upgrade. Its old runs stay readable.
|
|
6029
|
+
- Detected elements are **never slots to fill**: the source's people, products, animals and badges are its world. The original image stays on the canvas as the record of the source; when `prompt.json` keeps its concept (`concept.verdict` `use` or `adapt`) the first renders are shown it **for its structure only**, and otherwise it never reaches a render. Only the people cast from a company Avatar and the brand's logo become pictures on the canvas; everything else is described in words (a picture of ours is added through a reference's `image`). A **person** with no avatar needs no slot and no casting: the render draws an invented, realistic person from the element's description. With `--avatar <handle>` (for an actor who recurs across ads) the slot ingests the Avatar's identity sheet and `prompt.json` describes that person in the Avatar's own words. A reference's `avatar` names who was cast and draws no face on its own: its face reaches the render only with a picture behind it (the `--avatar` slot, or the identity sheet URL from `baker avatars get <handle>` as the reference's `image`).
|
|
5799
6030
|
|
|
5800
6031
|
The canvas is validated before it is written. stdout returns `{ ok, canvas_path, prompt_path, models, aspect_ratio, placements, output_formats, stats, checklist }`; the **checklist** carries `summary` (what a run does: candidates, judge, type, logo, formats), `font`, `logo`, `in_scene_text`, `formats` and every real asset to drop in.
|
|
5801
6032
|
|
|
5802
|
-
**Placement formats.** The canvas delivers
|
|
6033
|
+
**Placement formats.** The canvas delivers every size where the ad runs, each at its exact size, collected under `final` with the format as its label. `--platforms meta,tiktok,google_display,…` (platforms or placement ids from the registry in `@baker/canvas-contract` `AD_PLACEMENTS`: meta, google_display, google_pmax, google_demandgen, google_search, youtube, linkedin, tiktok, x, reddit, pinterest, snapchat, chatgpt) delivers every size their placements require, each kept clear of its own placement's interface (a TikTok 9:16 of TikTok's buttons and caption), and lists fixed banners (`728x90`, …) in the report as `banners`, never made from the picture (a destination of banners alone is refused with `invalid_platforms` before anything bills). A 9:16 for another platform keeps clear of its interface even when `--target-adset` narrows Meta's placements. Without it the set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or `--placements none` for a single format. The report's `aspect_ratio` is the shape the variants really render at (the model's nearest to the master's format), and `format_plan` names the master and how each size is made from it (`master`, `crop` with its rectangle, or `adapt`).
|
|
6034
|
+
|
|
6035
|
+
**Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
|
|
5803
6036
|
|
|
5804
|
-
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every
|
|
6037
|
+
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. There is no render limit. When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
|
|
5805
6038
|
|
|
5806
|
-
The render prompt is **model-aware**:
|
|
6039
|
+
The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
|
|
5807
6040
|
|
|
5808
6041
|
**A reference from the winners library goes in by id.** `--source-ad-id <ad_id> --source-platform meta|linkedin|tiktok [--source-advertiser-id <id>]` (the ids beside the ad in `baker winning-ads search`) fetches the library's structured read of that ad, the same one `baker winning-ads content <ad_id> --full` returns, and writes it into `prompt.json` as a `reference` block: `{ ad_id, platform, advertiser, advertiser_id, format_pattern, layout: { composition, focal_subject, aspect_ratio, background, palette_mood, colors, production, aesthetic, cta_in_image, markers }, message: { mechanism, awareness, sophistication, angle, hook_archetype, pain_points, desires, value_props, objections_handled, proof, risk_reversal }, offer: { offer_type, pricing_model, entry_price, no_credit_card, guarantee, guarantee_days, urgency, bonus }, text_blocks: [{ text, role, position }], copy: { primary_text, headline, cta } }` (filled fields only). The describe pass still reads the pixels but is told that read as grounding, so the two cannot disagree on the container or the mechanism; when it names no `winning_mechanisms`, the library's container + mechanism seeds the list. The source ids (`sourceAdId`, `sourcePlatform`, `sourceAdvertiserId`, and `sourceAdvertiser` from the library when `--advertiser` is absent) are written to `_definition.md` and synced to the creative at scaffold time. A library read that fails warns, records the ids anyway and scaffolds from the picture alone (`hints`: `REFERENCE NOT READ`); with no picture either, the command exits 2 (`source_unavailable`). `baker canvas sync-definition` re-reads the three ids from `_definition.md` on every sync.
|
|
5809
6042
|
|
|
5810
6043
|
```bash
|
|
5811
|
-
baker canvas scaffold-static-ad --source-ad-id adg_123 --source-platform meta --slug spring-offer --platform meta
|
|
5812
|
-
# → the library's picture and read; writes src/creatives/spring-offer/{spring-offer.canvas.json, prompt.json, _definition.md}
|
|
6044
|
+
baker canvas scaffold-static-ad --source-ad-id adg_123 --source-platform meta --slug spring-offer --platform meta --target-adset 120249211706990208
|
|
6045
|
+
# → the library's picture and read, made for the viewer that ad set reaches; writes src/creatives/spring-offer/{spring-offer.canvas.json, prompt.json, _definition.md}
|
|
5813
6046
|
baker canvas scaffold-static-ad ./reference-ad.png --context "competitor ad for <brand>, <category>, <market>"
|
|
5814
6047
|
# → writes ./static-ad.canvas.json + ./prompt.json
|
|
5815
6048
|
# edit prompt.json into your ad + replace each [TODO] ingest path with a real file, then:
|
|
@@ -5827,18 +6060,16 @@ baker canvas run ./static-ad.canvas.json
|
|
|
5827
6060
|
| `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. With a slug, the reference image is localized to durable storage and normalized to a model-safe format (SVG/AVIF/HEIC/… → PNG), so the canvas ingests a stable URL instead of an expiring one. |
|
|
5828
6061
|
| `--describe-model <id>` | registry default (`~google/gemini-pro-latest`) | Override the `image_describe` model. |
|
|
5829
6062
|
| `--select-model <id>` | registry default (`~google/gemini-flash-latest`) | Override the element-selection `text_generate` model. |
|
|
5830
|
-
| `--gen-model <id>` |
|
|
5831
|
-
| `--takes <n>` | `3` |
|
|
6063
|
+
| `--gen-model <id>` | the router's pick for the concept | Override the render model. An unknown model id in any `--*-model` flag fails the command (`unknown_model`) before anything is billed. |
|
|
6064
|
+
| `--takes <n>` | `3` | Variants of the finished ad rendered (one request each) and judged (1-3); `1` skips the judge. |
|
|
5832
6065
|
| `--platform <channel>` | `meta` | `_definition.md` channel **and** the default placement set (`meta`\|`google`\|`linkedin`\|`tiktok`\|`youtube`\|`x`\|`other`; `x`/`other` deliver one format). |
|
|
5833
|
-
| `--
|
|
6066
|
+
| `--platforms <list>` | none | Where the ad runs, comma-separated platforms or placement ids: every size they require, each kept clear of its own interface. Wins over `--placements`. Banners alone are refused. |
|
|
6067
|
+
| `--placements <preset\|none>` | derived from `--platform` | An older fixed set: `meta_feed`\|`meta_feed_stories`\|`meta_all`\|`linkedin`\|`google_demandgen`\|`tiktok`\|`youtube`, or `none` for a single base ad. |
|
|
5834
6068
|
| `--aspect <ratio>` | inferred from the image, else `9:16` | Force the lead format (it leads the placement set when the set has it). |
|
|
5835
|
-
| `--skip-actor-sheets` | off | Ground each animal hero on its lone dropped photo instead of a generated multi-view reference sheet. |
|
|
5836
6069
|
| `--avatar <handles>` | — | Cast company avatars as the image's people, comma-separated in the order they were detected (`marta` or `marta,joan`). Each person slot grounds on that avatar's identity sheet and takes its subject description. Checked before any billed pass; an unknown, not-ready or sheetless avatar fails with code `avatar`. A handle with no person to play is reported in `hints` as `NOT CAST`. |
|
|
5837
6070
|
|
|
5838
6071
|
Scaffolding runs (and bills) the two vision passes; **running** the result generates billed images. `baker canvas validate` does not check that the `[TODO]` paths exist — supply the real files before `run`.
|
|
5839
6072
|
|
|
5840
|
-
**Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. **Correct a finished static ad** with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; the other formats and the review follow, and it counts as one render of the round. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
|
|
5841
|
-
|
|
5842
6073
|
#### `baker canvas rerun <slug> [flags]` (deprecated)
|
|
5843
6074
|
|
|
5844
6075
|
Now `baker canvas run --slug <slug>`; `rerun` stays as a hidden alias for one release and prints a deprecation note. Every `canvas run` of a creative records a portable snapshot (the canvas JSON plus the local files it ingests: prompt blueprints, composition dirs, reference images) in durable storage, so `run --slug` can rebuild `src/creatives/<slug>/` in a workspace that never had it. A snapshot whose files are exactly the last one uploaded for the creative is reused without a request, and a file the store already holds is not uploaded again. Files the snapshot could not include (missing at run time, or oversized) are listed as warnings.
|
|
@@ -5850,7 +6081,7 @@ baker canvas run --slug spring-offer --force-remote --regenerate gen
|
|
|
5850
6081
|
|
|
5851
6082
|
#### `baker canvas stale`
|
|
5852
6083
|
|
|
5853
|
-
Lists the creatives edited since their last render in this workspace, each with the command to re-render it: `{ ok, data: { stale: [{ slug, canvasPath, renderedRunId }] }, hints? }`. "Edited" is one content fingerprint of the
|
|
6084
|
+
Lists the creatives edited since their last render in this workspace, each with the command to re-render it: `{ ok, data: { stale: [{ slug, canvasPath, renderedRunId, sourceSha }] }, hints? }`. "Edited" is one content fingerprint of what the models would be sent: the canvas as it renders (after the copy, logo and scene projections), `prompt.json` without its bookkeeping (`reference`, `source_context`, `notes`), every `scenes/sNN.json`, every local composition directory the canvas renders, and the `formats` in `_definition.md`, so a change counts however it was made and reformatting a JSON file is not a change. The fingerprint is prefixed `v2:`; a render fingerprinted by the older rule (a bare sha) is never reported stale, so upgrading the CLI re-renders nothing. A completed `run` records the fingerprint it drew in `canvas/.rendered/<slug>.json`, and a render attempt of the current source is not asked for again until the source moves. Creatives never rendered in this workspace, and ones with an unfilled `[TODO]` asset slot, are not listed. Read-only, offline, free. The scaffold's turn-end reminder and its `validate-creative-renders` check read this list (at Publish it is a warning: Publish ships the last version the person was shown and never renders); the same fingerprint is what `sync-definition` and `run` post as `sourceSha`.
|
|
5854
6085
|
|
|
5855
6086
|
#### `baker canvas inspect <run_id> [--thumbnails]`
|
|
5856
6087
|
|
|
@@ -5916,14 +6147,20 @@ baker canvas catalog --category audio --full
|
|
|
5916
6147
|
// Path source — an absolute path is used as-is, and so resolves only on the machine that wrote it.
|
|
5917
6148
|
// Keep canvases portable: creative canvases are validated for this (`absolute-reference`).
|
|
5918
6149
|
{ "id": "src", "type": "ingest", "params": { "source": "path", "path": "/abs/work/clip.mp4", "expect": "video" } }
|
|
6150
|
+
|
|
6151
|
+
// URL source — an earlier output, by its durable canvas-assets URL: the same bytes and sha back
|
|
6152
|
+
{ "id": "src", "type": "ingest", "params": { "source": "url", "url": "https://media.withbaker.com/canvas-assets/5b/5b50…c672.mp3", "expect": "audio" } }
|
|
5919
6153
|
```
|
|
5920
6154
|
|
|
6155
|
+
**Never point `path` into `canvas/`.** `canvas/.cache/` and `canvas/r_<id>/` are the engine's own working folder and are never committed, so a canvas that reads from them renders only where it was written. `canvas validate` and `canvas run` rewrite such a path to the durable URL of the same bytes (reported under `normalized_paths`), keeping the file first if the store does not hold it yet.
|
|
6156
|
+
|
|
5921
6157
|
**Strategy resolution** (deterministic from `source` + `expect` + URL extension for text):
|
|
5922
6158
|
|
|
5923
6159
|
| `source` | `expect` | URL / path | Strategy |
|
|
5924
6160
|
|----------|---------------|----------------------------------|-----------------|
|
|
5925
6161
|
| path | any | any | `local_file` — read from disk, sniff mime, upload to R2 via presign |
|
|
5926
|
-
| url |
|
|
6162
|
+
| url | any | `…/canvas-assets/<xx>/<sha>.<ext>` | `canvas_asset` — the stored asset, verbatim, its sha checked |
|
|
6163
|
+
| url | video / audio | any other | `yt_dlp` (works on platform URLs *and* direct file URLs via the generic extractor; verifies TLS against the system bundle) |
|
|
5927
6164
|
| url | image / json / font | any | `direct_fetch` (HTTP GET) |
|
|
5928
6165
|
| url | text | ends in `.txt` or `.md` | `direct_fetch` |
|
|
5929
6166
|
| url | text | anything else (HTML, PDF, …) | `firecrawl` `/v2/scrape` (extracted markdown — HTML/PDF made readable) |
|
|
@@ -6380,6 +6617,8 @@ baker creatives feedback --slug spring-offer --full
|
|
|
6380
6617
|
|
|
6381
6618
|
Each entry carries its `source`: `review` (the buttons in Baker), `selection` (choosing or saving the version) or `chat` (what the person wrote in a chat, recorded with `feedback record`).
|
|
6382
6619
|
|
|
6620
|
+
With `--slug`, `data.variantChoice` (`runId`, `option`, `sha256`) says which variant of a version the person chose over our pick in Baker, and the first hint is the command that makes every size from it (the only requests re-lay it out for each other size): `baker canvas run --slug <slug> --use-variant <sha256> --from <runId>`. Once a finished run made its sizes from that picture, the choice is used up and no longer returned.
|
|
6621
|
+
|
|
6383
6622
|
### `baker creatives feedback record`
|
|
6384
6623
|
|
|
6385
6624
|
Record what the person said about one version in a chat: an approval, or a rejection with reasons and their own words. It is stored like the Approve / Not this one buttons, as the verdict of the person who wrote the chat's latest message (`recordedFor: "sender"`), or of the chat itself when nobody has written in it (`"chat"`). One verdict per person per version or option: a later one replaces theirs. Must run from a chat.
|
|
@@ -6428,6 +6667,7 @@ baker landing inspiration page <source id> # a whole page as a section sequ
|
|
|
6428
6667
|
baker landing inspiration sequences "b2b saas pricing page" --scope all # what follows what, across many pages
|
|
6429
6668
|
baker landing inspiration add https://linear.app --note "client likes this density"
|
|
6430
6669
|
baker landing inspiration favorites # what this company has saved
|
|
6670
|
+
baker landing inspiration retry-failed # study again every saved page that failed
|
|
6431
6671
|
baker landing inspiration scrape <url> --out .baker/<dir> # capture any page now, synchronously
|
|
6432
6672
|
```
|
|
6433
6673
|
|
|
@@ -6467,6 +6707,21 @@ baker landing variant oferta --fork Hero.astro,Proof.astro --because "…" --cha
|
|
|
6467
6707
|
- **Errors carry the retry.** `NOT_FOUND` returns `availableSlugs` (wrong page) or `availableComponents` (wrong `--fork`); `INVALID_SLUG` fires when the slug is not a page, or is already a variant of one. Omitting `--fork` succeeds with a hint saying the page currently renders exactly the control, so a test between them cannot conclude.
|
|
6468
6708
|
- **Run `baker experiment plan` first.** Most pages never get the traffic to settle most questions.
|
|
6469
6709
|
|
|
6710
|
+
### `baker landing export`
|
|
6711
|
+
|
|
6712
|
+
A download of the company's **published** landings as a ZIP of plain HTML — the same pack Landings → Export makes. It reads what is live, so it starts at once; landings changed in the chat and not yet published are not in it.
|
|
6713
|
+
|
|
6714
|
+
```bash
|
|
6715
|
+
baker landing export start # → { exportId }, returns at once
|
|
6716
|
+
baker landing export start --profile '{"htmlOutput":"fragment"}' # download format for this export only (JSON or @file)
|
|
6717
|
+
baker landing export status <id> # progress; downloadUrl once ready
|
|
6718
|
+
baker landing export list # the last 10 downloads
|
|
6719
|
+
```
|
|
6720
|
+
|
|
6721
|
+
- **No password flag.** A protected ZIP is made from the dashboard — secrets never pass through the agent.
|
|
6722
|
+
- **Listed on the chat as already applied**, with a download button once ready.
|
|
6723
|
+
- `--profile` fields sit on top of the company's saved download format; a field that cannot be built is refused by name (`BAD_REQUEST`).
|
|
6724
|
+
|
|
6470
6725
|
### `baker landing critique`
|
|
6471
6726
|
|
|
6472
6727
|
Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.
|