@koda-sl/baker-cli 0.382.0 → 0.383.0-dev.3b7d77b41

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.
Files changed (28) hide show
  1. package/README.md +76 -53
  2. package/dist/{chunk-XHC4LMK4.js → chunk-2QXMPTHG.js} +18 -19
  3. package/dist/chunk-2QXMPTHG.js.map +1 -0
  4. package/dist/{chunk-HJEMOSPH.js → chunk-7YNOGIWQ.js} +2 -439
  5. package/dist/chunk-7YNOGIWQ.js.map +1 -0
  6. package/dist/{chunk-VZEDAFAR.js → chunk-IZOAYU2G.js} +2 -2
  7. package/dist/{chunk-VLUGGVJL.js → chunk-PGYWZEAW.js} +4 -9
  8. package/dist/{chunk-VLUGGVJL.js.map → chunk-PGYWZEAW.js.map} +1 -1
  9. package/dist/{chunk-FGHMWBUD.js → chunk-RK44ZR7L.js} +114 -196
  10. package/dist/chunk-RK44ZR7L.js.map +1 -0
  11. package/dist/{chunk-MF7YA62W.js → chunk-UMIALY2G.js} +3 -3
  12. package/dist/cli.js +6301 -4373
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-GR6YL3YX.js → client-OTWPBCHC.js} +4 -4
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-4P5HOGNA.js → env-P7YVNPZ6.js} +2 -2
  17. package/dist/{output-NMQXZF3Z.js → output-WCDFLOHZ.js} +5 -5
  18. package/dist/{shared-J7DQZ2ED.js → shared-JZJ6PVJ7.js} +6 -8
  19. package/package.json +1 -2
  20. package/dist/chunk-FGHMWBUD.js.map +0 -1
  21. package/dist/chunk-HJEMOSPH.js.map +0 -1
  22. package/dist/chunk-XHC4LMK4.js.map +0 -1
  23. /package/dist/{chunk-VZEDAFAR.js.map → chunk-IZOAYU2G.js.map} +0 -0
  24. /package/dist/{chunk-MF7YA62W.js.map → chunk-UMIALY2G.js.map} +0 -0
  25. /package/dist/{client-GR6YL3YX.js.map → client-OTWPBCHC.js.map} +0 -0
  26. /package/dist/{env-4P5HOGNA.js.map → env-P7YVNPZ6.js.map} +0 -0
  27. /package/dist/{output-NMQXZF3Z.js.map → output-WCDFLOHZ.js.map} +0 -0
  28. /package/dist/{shared-J7DQZ2ED.js.map → shared-JZJ6PVJ7.js.map} +0 -0
package/README.md CHANGED
@@ -110,22 +110,6 @@ question — and nothing in the output said so. Both spellings of an option work
110
110
  that lists every option: `baker schema <command>` (`check_schema`) when the command
111
111
  registers a schema, `baker <command> --help` (`check_help`) when it does not.
112
112
 
113
- ### Narrowing a result: `--fields`
114
-
115
- `--fields a,b,c` works on every command that prints the JSON envelope. It keeps only
116
- those keys of each record in `data` — the rows when `data` is a list or wraps one
117
- (`baker chats list --fields id,title`), `data` itself when it is a single record
118
- (`baker experiment plan ... --fields traffic,warning`). Naming a key of the wrapper beside
119
- keys of its rows narrows both. `meta`, `hints` and warnings are
120
- kept, and a name no record carries is reported in `hints` alongside the keys that do
121
- exist. Commands that declare `--fields` themselves keep their own meaning
122
- (`ads meta insights` chooses which metrics to request; `ads linkedin creatives` takes dotted
123
- paths; `actions draft` picks the columns of `--output md|files` and leaves its JSON
124
- whole). A command whose default output is text needs `--output json` for there to be an
125
- envelope to narrow; without it a note on stderr says the flag changed nothing. A success
126
- that prints its result beside `ok` rather than under `data` (`canvas validate`,
127
- `canvas critique`) comes back whole, with a hint saying `--fields` was not applied.
128
-
129
113
  ### Fresh reads: `--no-cache`
130
114
 
131
115
  Every command that caches takes `--no-cache`, and it skips **both** cache layers — the
@@ -721,7 +705,7 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
721
705
 
722
706
  **Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
723
707
 
724
- **Reading an earlier chat's changes** — `draft list` and `draft show` take `--chat <chat-id>` to read another chat in the same company instead of the current one. Every staged-draft family accepts it: `ads google|meta|linkedin draft list|show`, `tag-manager draft list|show`, `actions draft`, `tags draft`, and `scheduled-agents list|get` (`list` leaves briefs out; `get <id>` or `list --full` returns them). A published draft is kept in full, so this returns what that chat actually wrote — every headline, bid and target — plus each op's outcome and error. Use it when asked to redo or reapply earlier work rather than reconstructing copy from a summary. Read-only: staging, `amend`, `remove` and `clear` stay bound to `BAKER_CHAT_ID`, so one chat can never edit another's draft. To re-run only the changes a publish failed to land, use the chat's **Retry** control in the dashboard — it re-sends exactly those and leaves the ones that applied untouched.
708
+ **Reading an earlier chat's changes** — `draft list` and `draft show` take `--chat <chat-id>` to read another chat in the same company instead of the current one. Every staged-draft family accepts it: `ads google|meta|linkedin draft list|show`, `tag-manager draft list|show`, `actions draft`, `tags draft`, and `scheduled-agents list|get`. A published draft is kept in full, so this returns what that chat actually wrote — every headline, bid and target — plus each op's outcome and error. Use it when asked to redo or reapply earlier work rather than reconstructing copy from a summary. Read-only: staging, `amend`, `remove` and `clear` stay bound to `BAKER_CHAT_ID`, so one chat can never edit another's draft. To re-run only the changes a publish failed to land, use the chat's **Retry** control in the dashboard — it re-sends exactly those and leaves the ones that applied untouched.
725
709
 
726
710
  **Already-satisfied writes aren't staged** — an update or pause/resume whose every field already holds the requested value on the live account is dropped instead of staged: the envelope returns `"staged": false, "noop": true` with a `reason` ("this ad is already Paused on Google Ads"), plus a hint. This is a success, not an error — there is nothing to publish. Only a positively-read live snapshot can trigger it; a failed or empty read always stages. Batch responses list the dropped ops under `skipped`, and `count` reflects what actually entered the draft.
727
711
 
@@ -905,9 +889,6 @@ baker ads meta leads --form-id 987654321 --full # raw conta
905
889
  baker ads meta campaigns --account-id act_123 # ACTIVE only by default
906
890
  baker ads meta ads --account-id act_123 --all-statuses # widen to everything
907
891
 
908
- # "Find the October ad" — Meta matches the name, so only that row is read
909
- baker ads meta ads --account-id act_123 --all-statuses --name-contains octubre --fields id,name,adset_id,creative_id
910
-
911
892
  # "Tell me about ad 9988"
912
893
  baker ads meta creatives --creative-id 9988
913
894
  baker ads meta preview --creative-id 9988 --ad-format MOBILE_FEED_STANDARD --out-file /tmp/p.html
@@ -942,8 +923,6 @@ baker ads meta custom-conversions --account-id act_123 --full # raw rule JSON,
942
923
 
943
924
  Meta writes mirror the Google/LinkedIn model: they **never hit the Graph API when you run them**. Each op is validated at stage time (field shapes, budget rules, page/Instagram identity ownership, CBO/ABO exclusivity, status-transition legality), shows in the dashboard chat as a pending Meta change, and applies only when the chat is published. Meta has no atomic batch mutate, so ops apply **sequentially** on publish — one failure skips its dependents but the publish completes.
944
925
 
945
- Staging also refuses what LinkedIn would reject at publish: a campaign group created `PAUSED` (only `DRAFT`/`ACTIVE`), a group `--total-budget` without `--end`, a `PAUSED` campaign in a `DRAFT` group, archiving a `DRAFT` ad, and a library image that is not JPG, PNG or GIF. Every campaign carries `--political-intent` (`NOT_DECLARED` by default; `NOT_POLITICAL` or `POLITICAL` are the advertiser's declaration). A write to an ad account LinkedIn has not enabled for Baker fails with a message saying so.
946
-
947
926
  **That publish reaches the real ad account for every company** — Meta writes are generally available, with no per-company flag. Discarding the chat before it completes is what undoes a staged change.
948
927
 
949
928
  Chain a full ad from scratch with `meta_temp_*` refs (each create returns one):
@@ -964,14 +943,13 @@ baker ads meta creatives create --account-id act_123 --page 555 --instagram-us
964
943
  baker ads meta ads create --account-id act_123 --name "Ad 1" --adset meta_temp_as --creative meta_temp_cr
965
944
 
966
945
  # review + edit before publish
967
- baker ads meta draft # one line per op: ref · kind · summary · after <refs>, then advisories
968
- baker ads meta draft --json # raw JSON envelope for scripting
946
+ baker ads meta draft
969
947
  baker ads meta draft show meta_temp_cr # full staged payload — the receipt before publish
970
948
  baker ads meta draft remove meta_temp_cr # removing a create cascades to dependents
971
949
  baker ads meta draft clear
972
950
  ```
973
951
 
974
- Status changes are `pause|resume|archive|delete` subcommands on `campaigns`/`adsets`/`ads`. `archive` keeps the entity readable in Ads Manager; `delete` removes it permanently (an archived entity can only be deleted). `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes — write it attached, `--replace=false` or `--no-replace`, since the spaced form `--replace false` is rejected: a boolean flag written with a space is set to true and the word dropped). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
952
+ Status changes are `pause|resume|archive` subcommands on `campaigns`/`adsets`/`ads`. `duplicate` reconstructs a paused copy from the live snapshot (`--replace` also pauses the original once the copy publishes — write it attached, `--replace=false` or `--no-replace`, since the spaced form `--replace false` is rejected: a boolean flag written with a space is set to true and the word dropped). Carousel/dynamic creatives and complex targeting go via `--file` (creativeType `carousel`/`dynamic`; full Meta targeting spec). Advantage+ Creative enhancements: `--standard-enhancements on|off`, or per-feature toggles via a `--file` `enhancements.features` map.
975
953
 
976
954
  #### Smart defaults (so agents don't enumerate the API)
977
955
 
@@ -987,10 +965,6 @@ Status changes are `pause|resume|archive|delete` subcommands on `campaigns`/`ads
987
965
 
988
966
  **Listings (`campaigns`/`adsets`/`ads`)** default to `effective_status=ACTIVE` — pass `--all-statuses` to widen, or `--effective-status ACTIVE,PAUSED` for a custom set. An empty filtered list carries a `hints` entry naming the filter and `--all-statuses` (on **stderr** for `--output csv|jsonl|md`), so "nothing active" is never read as "nothing there".
989
967
 
990
- **Listing rows (`campaigns`/`adsets`/`ads`/`activities`)** print a table of compact rows by default — the fields you decide with (id, name, `effective_status`, budgets, goal; for activities who/when/what and a one-line `change`). `--fields a,b.c` returns exactly those dotted paths, `--full` returns every field Meta sent (targeting, tracking specs, raw `extra_data`), and `--output json` returns the rows in the `{ ok, data }` envelope.
991
-
992
- To find one entity by name, pass `--name-contains <text>` to `campaigns`, `adsets` or `ads`: Meta matches it server-side (case-insensitive), so only the matching rows are read instead of the whole account.
993
-
994
968
  **Pagination** is auto-drained (no manual cursor handling).
995
969
 
996
970
  #### Full command surface
@@ -1109,7 +1083,7 @@ baker ads linkedin bid-pricing --account-id 503001492 \
1109
1083
  - URN normalization: every URN field becomes `{type, id, urn}` so rows are self-describing.
1110
1084
  - Wide windows (>180d) auto-segment into 30-day chunks and combine, since `q=analytics` caps at 15,000 rows with no pagination.
1111
1085
 
1112
- **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set. Only campaigns can be `COMPLETED`; each listing's `--help` names the statuses it accepts, and any other value is refused with `INVALID_STATUS` and that list before LinkedIn is called.
1086
+ **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set.
1113
1087
 
1114
1088
  **`creatives` is compact by default** — `id`, `urn`, `campaign`, `intendedStatus`, `isServing`, `reviewStatus`, `format`, and the resolved destination. The destination is read out of the format-specific `content` block for you: direct-content ads (text, spotlight) report a `landingUrl`; post-based ads (image, video, carousel, document, article, event, TLA) report a `postUrn`, because their destination lives in the sponsored post and is not reachable through the ads API. Pass `--fields id,content.textAd.landingPage` for specific dotted paths, or `--full` for the complete raw records.
1115
1089
 
@@ -1170,7 +1144,7 @@ Write commands never call the LinkedIn API at stage time. Each stages an op on t
1170
1144
 
1171
1145
  ```bash
1172
1146
  # Chain creates via li_temp_* refs — group → campaign → creative in one chat
1173
- baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-08-01 --end 2026-10-31 --total-budget 5000 --currency EUR # → li_temp_x1
1147
+ baker ads linkedin campaign-groups create --name "Q3 ABM" --total-budget 5000 --currency EUR # → li_temp_x1
1174
1148
  baker ads linkedin campaigns create --name "ABM Tier-1" --group li_temp_x1 \
1175
1149
  --objective WEBSITE_VISIT --type SPONSORED_UPDATES --cost-type CPC \
1176
1150
  --bid 8.50 --daily-budget 75 --currency EUR --locale en_US --targeting-file targeting.json # → li_temp_x2
@@ -1315,9 +1289,6 @@ baker analytics map --platform google --set kw=keyword # name a parameter, se
1315
1289
  baker analytics map --platform google --remove kw # take that answer back
1316
1290
  baker analytics conversions # what counts, what each produced, and which Forms count nothing
1317
1291
  baker analytics conversions --candidates --output md # everything these pages do, and which of it is counted, as tables
1318
- baker analytics conversions --candidates # the same as compact JSON: one line per page event, Form keys folded onto `flows`
1319
- baker analytics conversions --candidates --flow contact # only that Form's event keys, one line each
1320
- baker analytics conversions --candidates --full --output json # every field of every candidate, Form keys unfolded
1321
1292
  baker analytics conversions --flow contact # one Form's endings as event keys, traffic or not
1322
1293
  baker analytics conversions --event page:request_demo --name "Demo requested"
1323
1294
  baker analytics conversions --event page:a --event submit:b --name "Lead" # both, as one number
@@ -1369,7 +1340,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1369
1340
 
1370
1341
  **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did. A value that is not a whole positive number (`--page /pricing`) is refused before any request, naming `--landing` as the flag that narrows a report to one page of the site.
1371
1342
 
1372
- **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. The JSON is compact by default: each page or system event is one line (`eventKey`, `events`, `sessions`, `countedAs` when counted), and a Form's keys fold onto its `flows` row as `keys` fired and `events` — dropping that row's empty `counted` and `noConversion` — or onto a `formCandidates` row for a Form not in `flows`. `--candidates --flow <slug>` narrows the list to that Form's keys, and `--full` returns every field (`origin`, `nodeType`, `flowSlug`, `lastSeen`, `sources`, `doors`) of every key, unfolded. `--output md` always folds a Form's keys into one row per Form. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
1343
+ **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
1373
1344
 
1374
1345
  **The name is the grouping, so the CLI defends it.** Repeat `--event` to count several events under one `--name` in a single call; `--rename "<old>" --to "<new>"` renames every event under an outcome, and renaming *onto* a name that already exists **merges** them into one row and one number — the fix for a company whose Forms each invented their own word for the same thing. `--remove-name` drops an outcome entirely, `--remove` a single event key. A name that differs from one already counted only by case, accent, punctuation or a plural is **refused**: Baker groups by exact name, so "Booked a Call" beside "Booked a call" is a number split in half with both halves plausible. Every answer leads with the names already in use, and a company that has named nothing gets a standard set to start from — after which the rule is reuse, in that company's own language.
1375
1346
 
@@ -3321,10 +3292,8 @@ baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --top-n 10 --out
3321
3292
  A Scheduled agent is a **trigger** and a **prompt**. When the trigger fires, a Session opens with that prompt and does the job — there is no Task in between. `create`, `update`, and `delete` stage draft ops on `BAKER_CHAT_ID` and apply when the chat is published. `unstage` takes back what this chat staged on one item before Publish (`--index <n>` for one change only, by its index in `list` → `draft.ops`). `list` and `get` include draft state when `BAKER_CHAT_ID` is set; `get` on a published item also returns `runsAs` — who each run acts as and which tools it loads. `run` fires a published item immediately and does not require a chat id. Publishing every run unreviewed is switched on by the user on the item, never through the CLI: `--auto-publish` is refused with an `ask_user` fix.
3322
3293
 
3323
3294
  ```bash
3324
- baker scheduled-agents triggers # every trigger kind, by group, compact
3325
- baker scheduled-agents triggers --trigger experiment.decided # one kind (or a family: --trigger experiment) in full, with its payload fields
3326
- baker scheduled-agents triggers --group tasks # one group in full; --full for the whole catalog
3327
- baker scheduled-agents list # id, name, trigger, status; briefs left out (--full for everything)
3295
+ baker scheduled-agents triggers # every trigger there is
3296
+ baker scheduled-agents list
3328
3297
  baker scheduled-agents get <id-or-temp_agent_id>
3329
3298
 
3330
3299
  baker scheduled-agents create --name "Weekly report" --description "Prepare report" --prompt "Review last week's spend." --cron "0 9 * * MON" --timezone Europe/Madrid
@@ -3344,8 +3313,11 @@ baker scheduled-agents run <id>
3344
3313
  baker scheduled-agents templates --list # every recipe this company can run
3345
3314
  baker scheduled-agents templates --category measurement # one group only
3346
3315
  baker scheduled-agents create --template baker-ads --name "Weekly ad review" --description "..." --cron "0 8 * * MON"
3316
+ baker scheduled-agents create --template baker-store-out-of-stock # --name and --description default to the recipe's own
3347
3317
  ```
3348
3318
 
3319
+ With `--template`, `--name`, `--description` and `--prompt` are optional: the recipe's name, line and brief are used, and `--prompt` is appended to the brief. An item on a stock event whose name or line says it acts automatically is refused — every run's ad changes wait for review.
3320
+
3349
3321
  Triggers:
3350
3322
 
3351
3323
  | `--trigger` | Fires when | Narrow it with |
@@ -3487,8 +3459,7 @@ baker actions list # default: bucketed (claimable, myCla
3487
3459
  # then most recent. The `blocked` bucket is ordered closest-to-ready first.
3488
3460
  baker actions list --bucketed=false --status pending
3489
3461
  baker actions list --bucketed=false --sort priority # flat list, do-first order (default); use --sort recent for newest-first
3490
- baker actions list --output md # id-first Markdown; each bucket's guidance on its heading, blockers/holder under a row; flat rows add a one-line summary
3491
- baker actions list --full # whole descriptions and full records (large on a real backlog)
3462
+ baker actions list --output md # id-first Markdown; works for both the bucketed and flat shapes
3492
3463
  baker actions list --search "conversion goal" --output md # find Tasks by words in name/description/tags (flat, best match first)
3493
3464
  # With BAKER_CHAT_ID, the bucketed list folds in THIS chat's draft: staged creates appear in
3494
3465
  # `draftCreates`; published actions being completed/discarded/updated carry a `draftStatus` marker.
@@ -3524,6 +3495,8 @@ baker actions create --name "Build hero" --priority high --tags landing,creative
3524
3495
  # fuller fan-out hint: re-read the draft and merge actions that touch the same system into one.
3525
3496
  # If no --tags are passed, create emits a MISSING --tags hint — the action is invisible to the backlog's tag filter.
3526
3497
  # If no --priority is passed, create emits a MISSING --priority hint — the action ranks as normal/medium in the do-first order.
3498
+ # If the name or description holds a shell name such as /bin/bash or bash where a `$0` stood (double quotes let the
3499
+ # shell expand `$` amounts), create leads with a hint to fix it with `baker actions update <tempId>` in single quotes.
3527
3500
  baker actions update <action-id> --name "Better name" --tags google-ads,strategy # --tags REPLACES the set; '' clears
3528
3501
  baker actions update <action-id> --priority urgent # urgent|high|medium|low; 'none' clears
3529
3502
  baker actions update <tempId> --description "..." # a tempId from `create` in THIS chat edits the still-staged task in place
@@ -3690,7 +3663,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3690
3663
  **`meetings list` flags:** `--search <text>`, `--redirecting-only`.
3691
3664
  **`meetings view`:** positional `<slug>` — the only reliable source for a calendar's booking fields (HubSpot omits `customParams` from the list endpoint, so `fieldCount: null` there means unknown, not zero); `--as-node` as above.
3692
3665
 
3693
- **`HUBSPOT_RECONNECT_REQUIRED`** is returned when HubSpot is connected but that account never granted access to what the command needs — a HubSpot grant is fixed at the moment the user approves it, so an account connected before a capability shipped cannot serve it. It is not the same as `NO_HUBSPOT_CONNECTION`: the connection works, and only the user can widen it by reconnecting from Brain → Integrations → Platforms. The error carries a `fix` describing exactly that.
3666
+ **`HUBSPOT_RECONNECT_REQUIRED`** is returned when HubSpot is connected but that account never granted access to what the command needs — a HubSpot grant is fixed at the moment the user approves it, so an account connected before a capability shipped cannot serve it. It is not the same as `NO_HUBSPOT_CONNECTION`: the connection works, and only the user can widen it by reconnecting from Brain → Integrations → Tools. The error carries a `fix` describing exactly that.
3694
3667
 
3695
3668
  **`--as-node`** returns exactly the object a flow node's `form.external` expects, so an agent can wire a HubSpot step by editing the flow's `_data.json` instead of interrupting the user with the `request_flow_input` approval form. It includes two fields the dashboard picker omits and the renderer needs: `configuration.postSubmitAction` (drives post-submit redirect suppression) and `region` (selects the v4 embed host — without it a v4 form on a eu1/ap1 portal loads the wrong script host).
3696
3669
 
@@ -3709,12 +3682,69 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3709
3682
  }
3710
3683
  ```
3711
3684
 
3712
- No write surface against HubSpot itself — a form's fields, redirect, or consent text are changed inside HubSpot. Wiring one into a Form step is done with `--as-node` (above), not `request_flow_input`; the approval form is only needed when no account is connected or the right form is ambiguous. When no HubSpot account is connected, every command exits 1 with `NO_HUBSPOT_CONNECTION` and a `fix` telling the agent to route the user to **Brain → Integrations → Platforms**.
3685
+ No write surface against HubSpot itself — a form's fields, redirect, or consent text are changed inside HubSpot. Wiring one into a Form step is done with `--as-node` (above), not `request_flow_input`; the approval form is only needed when no account is connected or the right form is ambiguous. When no HubSpot account is connected, every command exits 1 with `NO_HUBSPOT_CONNECTION` and a `fix` telling the agent to route the user to **Brain → Integrations → Tools**.
3713
3686
 
3714
3687
  `meetings list` reports `redirectKnown: false` when HubSpot omitted the calendar's booking settings; `redirectUrl: null` alongside it means *unknown*, not "no redirect". `--redirecting-only` deliberately keeps those rows rather than answering "none" from missing data.
3715
3688
 
3716
3689
  ---
3717
3690
 
3691
+ ### `baker shopify store | products | product | collections | stock | sales | buyers | buyer | countries | best-sellers | cohorts | segments | promotions | stock-holds`
3692
+
3693
+ Read-only view of the company's **connected Shopify store**, from **Baker's own copy** of its catalog and the store's own orders — never a live call to Shopify. Baker keeps the copy current from the store (a change usually shows within about a minute) and checks the whole catalog daily; product photos are already in the image library.
3694
+
3695
+ ```bash
3696
+ baker shopify store # name, currency, home country, counts, prices by country, the Products page link
3697
+ baker shopify products --search "snowboard" # what the store sells: price, sale price, stock, where it can be bought
3698
+ baker shopify products --stock out_of_stock # --stock in_stock|partly_out|out_of_stock, --collection <handle|id>
3699
+ baker shopify products --limit 10 --cursor <c> # page with meta.next_cursor (25 by default, up to 100)
3700
+ baker shopify product <id|handle|link> # one product: options with buyUrl, prices by country, stock, photos, videos
3701
+ baker shopify product <handle> --content # its whole details: description, custom fields, subscriptions, translations
3702
+ baker shopify product <handle> --field "Size chart" # one custom field, whole
3703
+ baker shopify product <handle> --quantity 2 # buy links that put 2 of the option in the cart
3704
+ baker shopify product <id> --out-of-stock # only the options that are out of stock (page with --variants-cursor)
3705
+ baker shopify collections --search "summer" # collections, with the handle --collection takes (--full: descriptions, images)
3706
+ baker shopify stock --limit 20 # every product with every option out, and every product with some out
3707
+ baker shopify sales --days 7 --by campaign # what sold and which campaigns brought it (--by day|product|campaign|landing)
3708
+ baker shopify sales --all --by weekday # when buyers order, in the store's time (--by hour|month|month_of_year)
3709
+ baker shopify buyers --days 90 --by channel # who buys: new and returning, repeat rate, segments, value per acquisition
3710
+ baker shopify buyer 1042 # one numbered customer: profile, every order, what they bought
3711
+ baker shopify countries --country DE # where buyers are: by country, a country's regions (--region for cities)
3712
+ baker shopify best-sellers --view repeat # what sells (--view products|variants|trend|together|repeat|momentum)
3713
+ baker shopify segments # best, loyal, at risk, new, one-time, with this store's own rules
3714
+ baker shopify cohorts --months 12 # buyers by first-order month: who came back, spend per buyer
3715
+ baker shopify promotions <id|handle|link> # what advertises a product: ads (via page, buy link, landing), landings, Shopping
3716
+ baker shopify stock-holds --product <id> # ads Baker paused because the product ran out: what to resume, and what to tell the user
3717
+ ```
3718
+
3719
+ **`meta.store`** is on every answer: `connection` (`connected` · `needs_reconnect` · `disconnected`), `catalog` (`starting` · `importing` · `ready` · `failed`, with `importProgress` while importing), `unreadable` (products Baker couldn't read; `unreadableCapped` past 100), `checksFailing` (Baker couldn't check the store for changes lately), `storefront` (`online_store`, or `own` for a store that sells through its own site) and `lastActivity` (when Baker last heard from the store — not a promise the copy is current). `hints[]` turn each of these into what to tell the user.
3720
+ **`store`:** `--full` adds the admin link and every country. `capped: true` means a count stopped at its ceiling — `outOfStock` and `partlyOut` at 100, `collections` at 500 — so that count reads "that many or more"; a hint says which.
3721
+ **`products`:** `--search <words>`, `--collection <handle|id>`, `--stock`, `--limit <1-100>`, `--cursor`; `--full` adds vendor, type, option count, photo link, visibility and status. Every price, here and in `product` (`price`, `compareAt`, each option's price, `pricesByCountry`), is one string with its currency beside each amount: `"USD 785.95"`, `"USD 9.95 – USD 49.95"`. A row's `availability` is absent when the product is on the store, `link_only` when it is buyable only through its link, `not_for_sale` (with `status`) when it can't be bought on the online store.
3722
+ **`product`:** positional `<ref>` — the numeric id, the handle, a store product link (`…/products/<handle>`, even an all-digit handle), a Baker Products link, a Shopify admin product link (`admin.shopify.com/store/<shop>/products/<n>`), or `gid://shopify/Product/<n>`. Returns options (50 per page, `--variants-cursor` for more; 250 with `--full`), `outOptions` (up to 50, `outOptionsMore` for the rest), `pricesByCountry` when the store sells at more than one price, `photos[]` (`imageId` + a Baker-hosted `url`, never a Shopify link), `url` (the product's page in Baker) and `storeUrl` (the product's page on the store — never a buy button's link). `--full` adds the whole description, SEO, category, the admin link and each option's SKU, barcode, feed id and prices by country. `NOT_FOUND` carries a hint to search instead.
3723
+
3724
+ **Details and buy links (`product`):** every option carries `buyUrl` — the link for a page's buy button, following the store's own setting (`buyDestination`: `checkout`, `store_cart` or `product_page`) — with `buyUrlKind`, `unavailable` when it can't be bought, and `notes` for what a link can't do (`password_protected`, `subscription_only`, `subscription_optional`, `own_storefront`, `details_pending`). `--quantity <1-99>` sets how many it adds. `videos[]` lists the product's videos in the library (`videoId`, `playbackId`); `contentSummary` names what else Baker keeps. `--content` adds `content`: the formatted description, every custom field by its name (long values previewed, `truncated: true`), the rating, subscriptions, bundle, YouTube/Vimeo and 3D links, translations, and `unreadable` for content the store owner hasn't let Baker read yet. `--field <name|namespace.key>` returns one field whole. `--full` adds `--content`, all three links per option and each option's weight, unit price, shipping and tax. `store` adds `buyDestination`, `passwordProtected`, `languages`, `policies` (`--full`: their text, and the store's brand).
3725
+ **`collections`:** `--search <words>`; each row has `handle`, Shopify's product count and, on a store with an online store, `onStore`.
3726
+ **`stock`:** `--collection`, `--limit <1-200>` (default 50 per list), `--full` (SKUs and Baker links). Options that keep selling at zero or don't track stock are not out of stock.
3727
+ **`sales`:** `--days <1-395>` (default 30), `--by day|product|campaign|landing` (default `day`), `--limit <1-100>`, `--full` (each row's source, medium, campaign, click platform, landing or product id). `data.totals` (`orders`, `sales`, `added`, `refunded`, `net`) in `data.currency`; `data.rows[]` with a `label` and, by campaign or landing, a `credit` (`campaign` · `order` · `landing` · `unattributed` · `before_baker` · `other`), adding up to the totals. A sale counts when it is paid, at what was received; by day and product each fact counts when it happened, by campaign and landing each order paid in the period carries what was added and refunded on it since. `meta.salesReporting` (`meta`, `google`: `baker` · `shopify_channel` · `undecided`) says who reports the sales to the ad platforms; `meta.history` is `importing` while past orders still come in.
3728
+
3729
+ **`sales --by weekday|hour|month|month_of_year`:** orders placed in the period by when they were placed, in the store's timezone (`data.window.timezone`), from the store's whole order history — `--days` up to 3650 or `--all` (only these breakdowns take them).
3730
+
3731
+ **Buyers, countries and best sellers** (`buyers`, `buyer`, `countries`, `best-sellers`, `cohorts`, `segments`) read the store's whole order history as Baker keeps it. Every answer carries `meta.window` (local `from`/`to`, `days` or `"all"`, the store's `timezone`), `meta.history` (`state`: `importing` · `done` · `failed` · `none`; `reach`: `all` or `60_days`; the first day held) and `meta.basis` (`all_orders`, or `last_60_days` when the store lets Baker read only its last 60 days — then a buyer whose earlier orders are older counts as new). The period is orders **placed** in it, with what was refunded on them since. A buyer is a numbered customer (`Customer #1042`) with a country, region and city — never a name or an email; guest orders count in money, never as buyers. Shared flags: `--days <1-3650>` (default 365) or `--all`, `--country <ISO2>`, `--full`.
3732
+
3733
+ - **`buyers`:** `data.summary` (buyers, new and returning, guest orders, orders, net, average order, new/returning orders and net, repeat rate, median days to a second order), `data.segments`, `data.thresholds`, `data.buyers[]` (`--sort net|orders|recent|first`, `--limit`, `--offset`, `--segment best|loyal|at_risk|new|one_time`, `--channel <key>`), and with `--by campaign|channel` `data.acquisition.rows[]` (buyers whose first order is in the period, first-order net, lifetime net per buyer, average orders, repeat rate). Baker keeps no ad spend.
3734
+ - **`buyer <number>`:** `1042`, `#1042` or `Customer #1042`. A buyer erased by their store or merged into another answers `NOT_FOUND` "This buyer is no longer in Baker."
3735
+ - **`countries`:** `data.total`, `data.rows[]` (orders, buyers, new buyers, net, average order, share, growth against the period before, the currency paid in), `data.unknown` (no readable address) and `data.other` (past `--limit`), adding up to the total; `--country` for its regions, `--region` for a region's cities, `--product <id|handle>` for one product's orders and money.
3736
+ - **`best-sellers`:** `--view products` (default; units ordered beside refunded units, orders, buyers, revenue, refunds, refund rate, growth; `--sort units|revenue|orders`) · `variants` (with `--product`) · `trend` · `together` · `repeat` · `momentum`.
3737
+ - **`cohorts`:** `--months <3-36>` (default 12); retention at months 1, 2, 3, 6, 12 and net per buyer by month 12 (`--full`: every month).
3738
+ - **`segments`:** each segment's buyers, share, net, average orders, days since the last order, top countries and first products, and its rule in this store's numbers.
3739
+
3740
+ **`promotions`:** positional `<ref>` like `product`. Reads the product, scans this workspace's `src/pages/*` (import graph, ≤ 300 pages, files ≤ 256 KB) for its buy link, `/products/<handle>` or an option id, then `POST /api/shopify/promotions` with the linked landings. `data.ads[]` (`platform`, `level` ad|asset_group, `name`, `campaign`, `status` active|paused, `via` product_page|buy_link|landing), `data.landings[]` (`published`, `via` links_to_product|visitors_went_on), `data.shopping[]`, `data.catalog_ads`, `data.checked` (`not_checked` names a platform it could not read), `data.remembered` (the read carried a Session and named an ad — only then can “Restart ads when a product is back” later stage turning a pause from it back on, for review). `--full` adds ids, accounts, campaign / ad group / ad set ids, the Google `resource` and the URL.
3741
+
3742
+ **`stock-holds`:** `--product <ref>`, `--all` (resolved holds too). Compact: `data.resume[]` — the one command to stage per ad Baker may turn back on now (`{ ad, command }`); `data.tell_user[]` — one sentence per ad, and per piece of out-of-stock work still waiting for review, written for the marketer; `data.pages[]` — landings the out-of-stock run changed, with the `baker chats diff … --surface landings` read of those changes. With `--product` it speaks only of that product's ads, including the ones Baker did not pause when it ran out. `--full` adds the codes behind each sentence: `holds[]` with each `verdict` read live from the ad platform, `pending_sessions[]`, `recently_closed[]`, `not_paused[]`, `pages_changed[]`.
3743
+
3744
+ **Refusals:** `NO_SHOPIFY_CONNECTION` — no store is connected; `error.fix.action` is `request_connection` (`{ platform: "shopify" }`). `SHOPIFY_NOT_AVAILABLE` — Shopify can't be connected on this Baker yet; `error.fix.action` is `continue_without`. Both exit 1.
3745
+
3746
+ ---
3747
+
3718
3748
  ### `baker chats list | view | transcript | diff`
3719
3749
 
3720
3750
  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.
@@ -3942,19 +3972,12 @@ baker canvas validate my-canvas.json
3942
3972
  # or the same ad rendered once per aspect ratio instead of one hero + a terminal
3943
3973
  # image_aspect_adapt (IMAGE_PARALLEL_FORMAT_RENDERS — separate renders drift in type,
3944
3974
  # crop, and color, so the sizes stop matching).
3945
- # A generator prompt is measured with its {{slots}} filled from text nodes and local
3946
- # prompt.json/text ingests: past the model's character cap it is refused as
3947
- # PROMPT_OVER_MODEL_LIMIT, naming the node and the overshoot, before anything bills.
3948
- # `scaffold-static-ad` runs the same check on the prompt.json it writes and, when it is over,
3949
- # still writes the canvas and returns the overshoot in `hints[]` — trim prompt.json, then validate.
3950
3975
 
3951
3976
  # 2. Run (executes nodes, writes files to ./canvas/<run_id>/)
3952
3977
  baker canvas run my-canvas.json
3953
3978
  # Advisory warnings print as `[warn] <CODE>: …` before anything bills.
3954
3979
  # Only nodes reachable from the declared output execute (orphaned nodes are skipped,
3955
3980
  # not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
3956
- # A failed run reports `error.code: "input_invalid"` when the backend refused a node's
3957
- # own input (it fails the same way until the input changes), `"runtime"` otherwise.
3958
3981
 
3959
3982
  # 2b. Independent nodes ALREADY fan out in parallel — every node in the same
3960
3983
  # topological layer (e.g. all the per-clip video_generate nodes) runs concurrently
@@ -5459,7 +5482,7 @@ Validate, then execute the graph. Blocks until done. Logs one line per node. Ret
5459
5482
 
5460
5483
  **Run history streams live.** The run posts its plan (every node + its dependency edges) the moment validation passes, then re-posts a progress snapshot as each node starts and settles — the dashboard's creative workflow graph shows nodes flipping pending → running → done in real time, with each node's outputs attached as they land. A failed run keeps its per-node trail (what completed, what died). All best-effort: an unreachable backend never changes the run's outcome.
5461
5484
 
5462
- **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).
5485
+ **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. Long `video_generate` clips execute as **backend jobs** (the CLI polls; a CDN/proxy timeout can no longer kill a generation mid-flight).
5463
5486
 
5464
5487
  #### `baker canvas scaffold-video <video> [flags]`
5465
5488
 
@@ -5474,7 +5497,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5474
5497
 
5475
5498
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5476
5499
 
5477
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5500
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5478
5501
 
5479
5502
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5480
5503
 
@@ -5528,7 +5551,7 @@ baker canvas run ./reference-ad.video.canvas.json
5528
5551
  |---|---|---|
5529
5552
  | `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
5530
5553
  | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
5531
- | `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast) — only on a video with nobody in it; on one showing people it is refused and every frame is generated. |
5554
+ | `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast). |
5532
5555
  | `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
5533
5556
  | `--seam-dedup <mode>` | `head` | How to dedup the boundary frame two clips SHARE when a long shot is split for length (the second clip's first frame IS the first clip's last frame, so a plain concat freezes on it for a frame). `head` drops the second clip's first frame, `tail` drops the first clip's last frame, `off` keeps both. Only touches shared-frame continuation joins — a hard cut between two shots shares no frame. |
5534
5557
  | `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-MF7YA62W.js";
4
+ } from "./chunk-UMIALY2G.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-HJEMOSPH.js";
7
+ } from "./chunk-7YNOGIWQ.js";
8
8
 
9
9
  // src/error-handler.ts
10
10
  var REQUEST_CONNECTION_PLATFORM = {
@@ -26,7 +26,7 @@ var PLATFORM_NAME = {
26
26
  function connectionFixExplanation(platform) {
27
27
  const name = PLATFORM_NAME[platform];
28
28
  const slug = REQUEST_CONNECTION_PLATFORM[platform];
29
- return `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings or end the turn here \u2014 call the \`request_connection\` tool with { platform: "${slug}", reason } and they connect and pick their accounts from inside the chat. If the \`request_connection\` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Platforms \u2192 ${name}. Then carry on with the rest of the task and say what stays unavailable until it is connected.`;
29
+ return `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings or end the turn here \u2014 call the \`request_connection\` tool with { platform: "${slug}", reason } and they connect and pick their accounts from inside the chat. If the \`request_connection\` call fails, say so plainly and give the exact route: dashboard \u2192 Brain \u2192 Integrations \u2192 Tools \u2192 ${name}. Then carry on with the rest of the task and say what stays unavailable until it is connected.`;
30
30
  }
31
31
  function handleConnectionError(platform, originalMessage) {
32
32
  const platformName = PLATFORM_NAME[platform];
@@ -168,36 +168,35 @@ function flattenRow(row) {
168
168
  }
169
169
  return flat;
170
170
  }
171
- function writeWhole(lines) {
172
- process.stdout.write(`${lines.join("\n")}
173
- `);
174
- }
175
171
  function writeAdsCsv(data, fields) {
176
172
  if (data.length === 0) return;
177
173
  const cols = fields ?? Object.keys(data[0] ?? {});
178
- const lines = [toCsvRow(cols)];
174
+ process.stdout.write(`${toCsvRow(cols)}
175
+ `);
179
176
  for (const row of data) {
180
177
  const flat = flattenRow(row);
181
- lines.push(toCsvRow(cols.map((f) => flat[f] ?? "")));
178
+ process.stdout.write(`${toCsvRow(cols.map((f) => flat[f] ?? ""))}
179
+ `);
182
180
  }
183
- writeWhole(lines);
184
181
  }
185
182
  function writeAdsJsonl(data) {
186
- if (data.length === 0) return;
187
- writeWhole(data.map((row) => JSON.stringify(row)));
188
- }
189
- function mdCell(value) {
190
- return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
183
+ for (const row of data) {
184
+ process.stdout.write(`${JSON.stringify(row)}
185
+ `);
186
+ }
191
187
  }
192
188
  function writeAdsMd(data, fields) {
193
189
  if (data.length === 0) return;
194
190
  const cols = fields ?? Object.keys(data[0] ?? {});
195
- const lines = [`| ${cols.join(" | ")} |`, `| ${cols.map(() => "---").join(" | ")} |`];
191
+ process.stdout.write(`| ${cols.join(" | ")} |
192
+ `);
193
+ process.stdout.write(`| ${cols.map(() => "---").join(" | ")} |
194
+ `);
196
195
  for (const row of data) {
197
196
  const flat = flattenRow(row);
198
- lines.push(`| ${cols.map((f) => mdCell(flat[f] ?? "")).join(" | ")} |`);
197
+ process.stdout.write(`| ${cols.map((f) => flat[f] ?? "").join(" | ")} |
198
+ `);
199
199
  }
200
- writeWhole(lines);
201
200
  }
202
201
  function writeAdsOutput(data, format, fields, hints = []) {
203
202
  switch (format) {
@@ -298,4 +297,4 @@ export {
298
297
  writeAdsOutput,
299
298
  resolveCustomerId
300
299
  };
301
- //# sourceMappingURL=chunk-XHC4LMK4.js.map
300
+ //# sourceMappingURL=chunk-2QXMPTHG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/error-handler.ts","../src/commands/ads/cache.ts","../src/commands/ads/output.ts"],"sourcesContent":["import { writeJsonEnvelope } from \"./commands/ads/output.ts\";\n\ntype ConnectionPlatform = \"google_ads\" | \"ga4\" | \"gsc\" | \"x_ads\" | \"meta_ads\" | \"linkedin_ads\";\n\n/**\n * Platforms the agent can get connected without the user leaving the chat, via\n * the blocking `request_connection` tool, and the id that tool expects. Every\n * platform this CLI can talk to is here — keep it in step with\n * `CONNECTION_PLATFORMS` in `packages/bridge/src/hono/connection-input.ts`: a\n * platform the tool accepts but this map omits sends the user to Settings for a\n * door the agent could have opened itself.\n */\nconst REQUEST_CONNECTION_PLATFORM: Record<ConnectionPlatform, string> = {\n google_ads: \"google-ads\",\n ga4: \"google-analytics\",\n gsc: \"google-search-console\",\n linkedin_ads: \"linkedin\",\n meta_ads: \"meta\",\n x_ads: \"x-ads\",\n};\n\nconst PLATFORM_NAME: Record<ConnectionPlatform, string> = {\n google_ads: \"Google Ads\",\n ga4: \"Google Analytics 4\",\n gsc: \"Google Search Console\",\n x_ads: \"X Ads\",\n meta_ads: \"Meta Ads\",\n linkedin_ads: \"LinkedIn Ads\",\n};\n\n/**\n * How to tell the agent to get a platform connected. Never \"send them to\n * Settings\" for a platform `request_connection` covers — that ends the turn on a\n * wall the agent could have opened itself, and it now covers every platform this\n * CLI talks to. The fallback line matters too: if the `request_connection` call\n * fails, the agent must still be able to say where the connect button is instead\n * of stalling.\n */\nexport function connectionFixExplanation(platform: ConnectionPlatform): string {\n const name = PLATFORM_NAME[platform];\n const slug = REQUEST_CONNECTION_PLATFORM[platform];\n return (\n `${name} is not connected for this company (or no account has been picked). Don't send the user to Settings ` +\n `or end the turn here — call the \\`request_connection\\` tool with { platform: \"${slug}\", reason } and ` +\n `they connect and pick their accounts from inside the chat. If the \\`request_connection\\` call fails, say so ` +\n `plainly and give the exact route: dashboard → Brain → Integrations → Tools → ${name}. Then carry on with the ` +\n `rest of the task and say what stays unavailable until it is connected.`\n );\n}\n\n/**\n * Shared error handler for \"No Connection\" scenarios.\n * Provides a structured error for AI agents with a suggested fix and alternative commands.\n */\nexport function handleConnectionError(platform: ConnectionPlatform, originalMessage?: string): never {\n const platformName = PLATFORM_NAME[platform];\n\n const alternative = {\n google_ads: \"keywords-for-site or research advertisers\",\n ga4: \"lighthouse for page performance\",\n gsc: \"keywords-for-site or research keyword-gap\",\n x_ads: \"research advertisers\",\n meta_ads: \"research advertisers or research keyword-ideas\",\n linkedin_ads: \"research advertisers (LinkedIn Ad Library) or research keyword-ideas\",\n }[platform];\n\n const code = {\n google_ads: \"NO_GOOGLE_ADS_CONNECTION\",\n ga4: \"NO_GA4_CONNECTION\",\n gsc: \"NO_GSC_CONNECTION\",\n x_ads: \"NO_X_ADS_CONNECTION\",\n meta_ads: \"NO_META_ADS_CONNECTION\",\n linkedin_ads: \"NO_LINKEDIN_ADS_CONNECTION\",\n }[platform];\n\n const envelope = {\n ok: false,\n error: {\n code,\n message: originalMessage || `No ${platformName} connection found for this company.`,\n fix: {\n action: \"request_connection\",\n explanation: `${connectionFixExplanation(platform)} (Optional fallback for estimated external insights: 'baker research ${alternative}'.)`,\n },\n retryable: false,\n },\n };\n\n writeJsonEnvelope(envelope);\n process.exit(1);\n}\n\n/**\n * Does this API failure mean \"the platform isn't connected\"? The read commands\n * surface the backend's own 404/403 verbatim, so the connect fix has to be\n * recognised from the code the client mapped it to plus the message.\n */\nexport function isNotConnectedError(code: string, message: string): boolean {\n if (code !== \"NOT_FOUND\" && code !== \"UNAUTHORIZED\" && code !== \"FORBIDDEN\") {\n return false;\n }\n // The last alternative is the write-path 403 (\"customer X is not in this\n // company's connected Google Ads accounts\") — a different sentence for the\n // same remedy: connect, and pick the account.\n return /no .* connection found|not connected|no .* (selected|picked)|is not in .* connected/i.test(message);\n}\n\n/**\n * Does this failure warrant the connect form? A dead token (401) always does —\n * whatever the wording, the remedy is to sign in again.\n *\n * A 404 or 403 only does when the backend says so. Both codes are overloaded:\n * 403 is also an asset-level refusal on a live connection (reconnecting cannot\n * grant it), and 404 is also how \"no staged op <ref> in this draft\" and a\n * mistyped `--chat` come back. Answering either with \"ask the user to connect\n * Meta Ads\" sends the agent to fix something that was never broken, and the\n * real missing-connection 404 says \"No <platform> connection found\" anyway.\n */\nexport function needsConnectionFix(code: string, message: string): boolean {\n if (code === \"UNAUTHORIZED\") {\n return true;\n }\n return isNotConnectedError(code, message);\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { CacheEntry } from \"./types.ts\";\n\nconst CACHE_DIR = join(homedir(), \".baker\", \"cache\", \"ads\");\n\nfunction ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\nfunction hashKey(key: string): string {\n return createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16);\n}\n\nfunction cachePath(category: string, key: string): string {\n const dir = join(CACHE_DIR, category);\n ensureDir(dir);\n return join(dir, `${hashKey(key)}.json`);\n}\n\nexport function cacheGet<T>(category: string, key: string): CacheEntry<T> | null {\n const path = cachePath(category, key);\n if (!existsSync(path)) {\n return null;\n }\n try {\n const raw = readFileSync(path, \"utf-8\");\n const entry = JSON.parse(raw) as CacheEntry<T>;\n if (entry.expiresAt < Date.now()) {\n rmSync(path, { force: true });\n return null;\n }\n return entry;\n } catch {\n rmSync(path, { force: true });\n return null;\n }\n}\n\nexport function cacheSet<T>(\n category: string,\n key: string,\n data: T,\n ttlMs: number,\n fields?: Record<string, string>,\n): void {\n const path = cachePath(category, key);\n const entry: CacheEntry<T> = {\n expiresAt: Date.now() + ttlMs,\n data,\n fields,\n };\n writeFileSync(path, JSON.stringify(entry), \"utf-8\");\n}\n\nconst HOUR = 60 * 60 * 1000;\nconst MINUTE = 60 * 1000;\n\nexport function getQueryTtl(query: string): number {\n const upper = query.toUpperCase();\n if (upper.includes(\"TODAY\")) {\n return 15 * MINUTE;\n }\n if (upper.includes(\"LAST_7_DAYS\") || upper.includes(\"LAST_14_DAYS\")) {\n return 1 * HOUR;\n }\n // Historical data (BETWEEN, LAST_30_DAYS, LAST_90_DAYS, etc.)\n return 6 * HOUR;\n}\n\nexport function buildQueryCacheKey(customerId: string, query: string): string {\n const today = new Date().toISOString().slice(0, 10);\n return `${customerId}:${today}:${query.trim().replace(/\\s+/g, \" \")}`;\n}\n\ninterface CachedAccount {\n id: string;\n access_type: \"direct\" | \"managed\";\n manager_id?: string;\n}\n\nexport function getManagerIdForCustomer(customerId: string): string | undefined {\n const cached = cacheGet<CachedAccount[]>(\"accounts\", \"list\");\n if (!cached) return undefined;\n const account = cached.data.find((a) => a.id === customerId);\n if (account?.access_type === \"managed\") return account.manager_id;\n return undefined;\n}\n","import { ApiError, apiGet } from \"../../client.ts\";\nimport { getEnv } from \"../../env.ts\";\nimport { handleConnectionError, isNotConnectedError } from \"../../error-handler.ts\";\nimport type { QueryContext } from \"../../geo-context.ts\";\nimport { cacheGet, cacheSet } from \"./cache.ts\";\nimport type { AdsErrorEnvelope, AdsFileSummary, AdsSuccessEnvelope } from \"./types.ts\";\n\ntype AdsOutput =\n | AdsSuccessEnvelope<unknown>\n | AdsErrorEnvelope\n | AdsFileSummary\n | { ok: true; data: unknown; query_context?: QueryContext; cached?: true; fields?: Record<string, string> }\n | { ok: false; error: { code: string; message: string } };\n\nexport function writeAdsJson(envelope: AdsOutput): void {\n process.stdout.write(`${JSON.stringify(envelope, null, 2)}\\n`);\n}\n\nexport function writeJsonEnvelope(envelope: Record<string, unknown>): void {\n process.stdout.write(`${JSON.stringify(envelope, null, 2)}\\n`);\n}\n\nexport function toCsvRow(values: string[]): string {\n return values\n .map((v) => {\n if (v.includes(\",\") || v.includes('\"') || v.includes(\"\\n\")) {\n return `\"${v.replace(/\"/g, '\"\"')}\"`;\n }\n return v;\n })\n .join(\",\");\n}\n\nfunction flattenRow(row: Record<string, unknown>): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, val] of Object.entries(row)) {\n flat[key] = typeof val === \"object\" && val !== null ? JSON.stringify(val) : String(val ?? \"\");\n }\n return flat;\n}\n\nfunction writeAdsCsv(data: Array<Record<string, unknown>>, fields?: string[]): void {\n if (data.length === 0) return;\n const cols = fields ?? Object.keys(data[0] ?? {});\n process.stdout.write(`${toCsvRow(cols)}\\n`);\n for (const row of data) {\n const flat = flattenRow(row);\n process.stdout.write(`${toCsvRow(cols.map((f) => flat[f] ?? \"\"))}\\n`);\n }\n}\n\nfunction writeAdsJsonl(data: Array<Record<string, unknown>>): void {\n for (const row of data) {\n process.stdout.write(`${JSON.stringify(row)}\\n`);\n }\n}\n\nfunction writeAdsMd(data: Array<Record<string, unknown>>, fields?: string[]): void {\n if (data.length === 0) return;\n const cols = fields ?? Object.keys(data[0] ?? {});\n process.stdout.write(`| ${cols.join(\" | \")} |\\n`);\n process.stdout.write(`| ${cols.map(() => \"---\").join(\" | \")} |\\n`);\n for (const row of data) {\n const flat = flattenRow(row);\n process.stdout.write(`| ${cols.map((f) => flat[f] ?? \"\").join(\" | \")} |\\n`);\n }\n}\n\n/**\n * `hints` ride in the JSON envelope; csv/jsonl/md stdout has no room for them,\n * and an empty result is exactly when the hint matters, so they go to stderr.\n */\nexport function writeAdsOutput(\n data: Array<Record<string, unknown>>,\n format: string,\n fields?: string[],\n hints: string[] = [],\n): void {\n switch (format) {\n case \"csv\":\n writeAdsCsv(data, fields);\n break;\n case \"jsonl\":\n writeAdsJsonl(data);\n break;\n case \"md\":\n writeAdsMd(data, fields);\n break;\n default:\n writeJsonEnvelope(hints.length > 0 ? { ok: true, data, hints } : { ok: true, data });\n return;\n }\n for (const hint of hints) process.stderr.write(`${hint}\\n`);\n}\n\ninterface AccountInfo {\n id: string;\n name: string;\n access_type: \"direct\" | \"managed\";\n level: number;\n manager_id?: string;\n}\n\nasync function fetchAccounts(useCache = true): Promise<AccountInfo[]> {\n if (useCache) {\n const cached = cacheGet<AccountInfo[]>(\"accounts\", \"list\");\n if (cached) return cached.data;\n }\n const params = !useCache ? { \"skip-cache\": \"true\" } : undefined;\n const data = await apiGet<AccountInfo[]>(\"/api/ads/google/accounts\", params);\n if (useCache) {\n cacheSet(\"accounts\", \"list\", data, 60 * 60 * 1000);\n }\n return data;\n}\n\nexport async function resolveCustomerId(args: Record<string, unknown>): Promise<string> {\n const fromArgs = args[\"customer-id\"] as string | undefined;\n const customerId = fromArgs || getEnv().BAKER_GOOGLE_ADS_CUSTOMER_ID;\n\n if (customerId) {\n if (!/^\\d{10}$/.test(customerId)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_CUSTOMER_ID\",\n message:\n \"Customer ID must be exactly 10 digits without dashes. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID.\",\n },\n });\n process.exit(1);\n }\n return customerId;\n }\n\n const useCache = !args[\"no-cache\"];\n\n try {\n const accounts = await fetchAccounts(useCache);\n const [single] = accounts;\n if (accounts.length === 1 && single) {\n process.stderr.write(`Using account \"${single.name}\" (${single.id})\\n`);\n return single.id;\n }\n if (accounts.length === 0) {\n handleConnectionError(\"google_ads\");\n }\n const list = accounts.map((a) => ` ${a.id} ${a.name}`).join(\"\\n\");\n writeAdsJson({\n ok: false,\n error: {\n code: \"MULTIPLE_ACCOUNTS\",\n message: `Multiple accounts found. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID:\\n${list}`,\n },\n });\n process.exit(1);\n } catch (err) {\n // \"No connection\" reaches here as a 404/403 from the accounts lookup. Reporting it as\n // an unusable customer id sends the agent hunting for an id that cannot exist yet.\n if (err instanceof ApiError && isNotConnectedError(err.code, err.message)) {\n handleConnectionError(\"google_ads\", err.message);\n }\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_CUSTOMER_ID\",\n message: \"Could not auto-detect account. Pass --customer-id or set BAKER_GOOGLE_ADS_CUSTOMER_ID.\",\n },\n });\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}