@koda-sl/baker-cli 0.170.0-dev.9ace16483 → 0.173.0-dev.11e26a37c.1276eb11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -563,6 +563,13 @@ baker ads google asset-groups create --customer-id 1234567890 --campaign-ref g_t
563
563
  --descriptions "Best widgets around.,Ships tomorrow." --business-name "Acme" --final-urls https://example.com
564
564
  baker ads google asset-groups update g_temp_<assetgroup> --status ENABLED
565
565
 
566
+ # Refresh a LIVE asset group's creatives — attach the replacement first, then detach the old one
567
+ baker ads google assets create --customer-id 1234567890 --file new-image-asset.json # -> g_temp_<img>
568
+ baker ads google asset-groups attach --customer-id 1234567890 \
569
+ --asset-group-ref 6718302454 --asset-ref g_temp_<img> --field-type PORTRAIT_MARKETING_IMAGE
570
+ baker ads google asset-groups detach customers/1234567890/assetGroupAssets/6718302454~402654762568~PORTRAIT_MARKETING_IMAGE \
571
+ --customer-id 1234567890
572
+
566
573
  # Review / undo / amend staged changes
567
574
  baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
568
575
  baker ads google draft list --json # raw JSON envelope for scripting
@@ -572,7 +579,7 @@ baker ads google draft remove g_temp_ab12 # cascades to dependents
572
579
  baker ads google draft clear
573
580
  ```
574
581
 
575
- Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--max-cpc` take major units; `--target-roas` a ratio. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
582
+ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--max-cpc` take major units; `--target-roas` a ratio. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
576
583
 
577
584
  **Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
578
585
 
@@ -1047,7 +1054,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
1047
1054
 
1048
1055
  - OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
1049
1056
  - Account scoping: backend rejects calls against an account not in the picker selection.
1050
- - Pinned API version: `Linkedin-Version: 202604` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window.
1057
+ - 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`.
1051
1058
  - 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.
1052
1059
  - `--skip-cache` (server-side) and `--no-cache` (client-side, where supported) on any command.
1053
1060
 
@@ -1159,46 +1166,6 @@ baker ga4 query --dimensions "date,sessionSourceMedium" --metrics "sessions,conv
1159
1166
 
1160
1167
  ---
1161
1168
 
1162
- ### Alarms (`baker alarm`)
1163
-
1164
- How an unattended run reaches a human about something that cannot wait until tomorrow. This is the only thing that interrupts someone out of hours — anything that can wait is `baker actions create --priority urgent`.
1165
-
1166
- Every alarm must carry the evidence that contradicts everything being fine, and the backend **refuses** it when the evidence does not. An all-zeros analytics reading is not evidence on its own: broken tracking, no traffic, a dead page and a failed check all look identical. Paid clicks over the same window are what turn zeros into proof.
1167
-
1168
- ---
1169
-
1170
- ### `baker alarm raise`
1171
-
1172
- Wake a human. Reasons: `tracking_broken`, `spend_anomaly`, `access_lost`, `site_down`.
1173
-
1174
- ```bash
1175
- baker alarm raise --reason tracking_broken --url https://acme.com/demo --window-hours 24 \
1176
- --analytics-answered true --sessions 0 --clicks 47 --spend-micros 82000000
1177
-
1178
- baker alarm raise --reason site_down --url https://acme.com/demo --http-status 503 # 'none' if it never responded
1179
- baker alarm raise --reason spend_anomaly --platform google-ads --window-hours 24 \
1180
- --spend-micros 400000000 --baseline-spend-micros 50000000
1181
- baker alarm raise --reason access_lost --platform google-ads --provider-error "USER_PERMISSION_DENIED"
1182
- ```
1183
-
1184
- `--analytics-answered false` means the provider errored, timed out, or the token expired. That is **not** zero sessions, and the gate refuses it — otherwise a broken check pages somebody about the client's tracking.
1185
-
1186
- Response: `raised: true` (a human was contacted), `deduped: true` (already open — counted, not re-sent), or `raised: false` with a `refusal` explaining what the evidence failed to show. Repeats for an open condition never page twice.
1187
-
1188
- ---
1189
-
1190
- ### `baker alarm checked`
1191
-
1192
- Record that a check came back clean.
1193
-
1194
- ```bash
1195
- baker alarm checked --reason tracking_broken --scope "6 landings"
1196
- ```
1197
-
1198
- Run it whenever a check finds nothing. Without it, "nothing was wrong" and "nobody looked" are the same silence. It never closes an alarm somebody was already woken for — one clean reading does not prove a fix.
1199
-
1200
- ---
1201
-
1202
1169
  ### Google Search Console (`baker gsc`)
1203
1170
 
1204
1171
  GSC commands for PPC-SEO arbitrage, brand halo analysis, and negative keyword discovery.
@@ -2706,7 +2673,7 @@ Permissions enforced server-side:
2706
2673
 
2707
2674
  ### `baker mcp connected | list | add | remove`
2708
2675
 
2709
- See every third-party tool a chat can reach (managed integrations + custom servers) and manage **custom MCP servers** point the agent at any remote HTTPS MCP endpoint. Registered tools appear as `mcp__<name>__*` on the next chat message.
2676
+ 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.
2710
2677
 
2711
2678
  ```bash
2712
2679
  baker mcp connected
@@ -2717,6 +2684,7 @@ baker mcp add --name mine --url https://my.dev/mcp --scope user
2717
2684
  baker mcp remove --name weather
2718
2685
  ```
2719
2686
 
2687
+ - `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.
2720
2688
  - `--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.
2721
2689
  - Scope is two independent choices — audience (just you / everyone) × level (this company / whole org):
2722
2690
  - `--scope company` (default) — everyone in this company.
@@ -2727,6 +2695,52 @@ baker mcp remove --name weather
2727
2695
 
2728
2696
  ---
2729
2697
 
2698
+ ### `baker facts list | record`
2699
+
2700
+ What the company's own people said about their business, settled with them in the guided Setup: what counts as a win, who buys, where leads land, which channels have worked before, how they want to be talked to, and their red lines. Each entry carries where it came from — a machine check, a human answer, a call, or the agent.
2701
+
2702
+ Read it before writing anything customer-facing. Record what the user tells you mid-conversation so it reaches Setup instead of dying in a paragraph.
2703
+
2704
+ ```bash
2705
+ baker facts list # settled and contested entries
2706
+ baker facts list --full # plus unanswered questions and unconfirmed suggestions
2707
+ baker facts record --key crm --value salesforce --quote "we moved off HubSpot in March"
2708
+ ```
2709
+
2710
+ **`list` flags:**
2711
+
2712
+ | Flag | Description |
2713
+ |----------|-----------------------------------------------------------------|
2714
+ | `--full` | Include unanswered questions and unconfirmed suggestions |
2715
+
2716
+ **`record` flags:**
2717
+
2718
+ | Flag | Description |
2719
+ |----------------|-------------------------------------------------------------------------|
2720
+ | `--key` | Which question this answers (see `baker facts list --full`) — required |
2721
+ | `--value` | The answer, in the user's own terms — required |
2722
+ | `--quote` | What the user actually said, shown when they confirm it |
2723
+ | `--selections` | Comma-separated option values, for questions that take a set |
2724
+
2725
+ **Response:**
2726
+
2727
+ ```json
2728
+ {
2729
+ "ok": true,
2730
+ "data": {
2731
+ "facts": [
2732
+ { "key": "crm", "question": "Where do your leads land?", "value": "hubspot", "source": "answered", "settled": true }
2733
+ ],
2734
+ "missing": ["target-customer"]
2735
+ },
2736
+ "meta": { "count": 1 }
2737
+ }
2738
+ ```
2739
+
2740
+ `record` always files a **suggestion**, never a settled answer — a human confirms it in Setup. Keys that are checked against the company's own systems (conversion tracking) are refused: nothing said in a conversation can satisfy them.
2741
+
2742
+ ---
2743
+
2730
2744
  ### `baker history list`
2731
2745
 
2732
2746
  Unified account history (audit log) — everything that changed on the account, newest first: publishes, chat lifecycle, backlog actions, team changes, setup links, tag and schedule changes, executed ad-platform writes, followed advertisers, media (images, videos, testimonials), creative generations, reports, domains, integrations, and API keys. Read-only.
@@ -2763,9 +2777,9 @@ baker history list --category ads --full # ad writes with raw metadata
2763
2777
 
2764
2778
  ---
2765
2779
 
2766
- ### `baker hubspot forms list | view` · `baker hubspot meetings list | view`
2780
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup` · `baker hubspot meetings list | view`
2767
2781
 
2768
- Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, plus the settings inside HubSpot that decide how each behaves once placed in a Form. Fetched from HubSpot live on every call; nothing is cached.
2782
+ 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.
2769
2783
 
2770
2784
  The reason this exists: a form's **post-submit action** (inline thank-you vs redirect to a URL), its embed version, its consent mode, and a calendar's booking fields are *not* persisted onto the flow node when a form is picked. They cannot be read from `_data.json` or `baker flows show` — only from here.
2771
2785
 
@@ -2776,6 +2790,12 @@ baker hubspot forms list --embed-type legacy # only legacy-embed forms
2776
2790
  baker hubspot forms view <formId> # fields, consent mode, full configuration
2777
2791
  baker hubspot forms view <formId> --full # + the raw HubSpot payload
2778
2792
  baker hubspot forms view <formId> --as-node # the form.external blob for a flow node
2793
+ baker hubspot forms submissions <formId> # how many leads it received, and when
2794
+ baker hubspot forms submissions <formId> --days 90 --full
2795
+ baker hubspot workflows list --enabled-only # what runs after a lead is captured
2796
+ baker hubspot pipelines list # where a lead lands, and the stages after it
2797
+ baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
2798
+ baker hubspot contacts lookup lead@example.com # was this one lead picked up?
2779
2799
  baker hubspot meetings list # every calendar + whether booking redirects
2780
2800
  baker hubspot meetings view <slug> # booking-form fields for one calendar
2781
2801
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -2783,9 +2803,16 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
2783
2803
 
2784
2804
  **`forms list` flags:** `--search <text>` (name substring), `--redirecting-only`, `--embed-type legacy|v4|unknown`. Rows carry only what it takes to choose; `portalId` is returned once on `data`, not per row.
2785
2805
  **`forms view`:** positional `<formId>`; `--full` adds the untouched HubSpot payload; `--as-node` returns the resource blob instead of the readable view.
2806
+ **`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.
2807
+ **`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.
2808
+ **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
2809
+ **`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.
2810
+ **`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.
2786
2811
  **`meetings list` flags:** `--search <text>`, `--redirecting-only`.
2787
2812
  **`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.
2788
2813
 
2814
+ **`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.
2815
+
2789
2816
  **`--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).
2790
2817
 
2791
2818
  **Response:**
@@ -4939,6 +4966,10 @@ This CLI is designed for AI agent consumption. Key patterns:
4939
4966
 
4940
4967
  - **0.168.0**: `baker ads google conversions goal` sets the **account-level** conversion goal (`CustomerConversionGoal`) — Google Ads' "Included in account-level goals" column, keyed by `(category, origin)`. `goal --category PURCHASE --origin WEBSITE --biddable` includes a conversion category in what the account bids toward; `--no-biddable` measures it only. This is the half the surface was missing: a conversion drives automated bidding only when BOTH its action is primary AND its category's account-level goal is biddable, which is why an account could show lead-form conversions as Primary and still bid toward nothing. Promoting an action whose goal is off now stages with a warning saying the promotion alone changes no spend, and names the command for the other half. One staged op per pair — a second op for the same `(category, origin)` is refused rather than silently overwriting the first at publish. Switching off the last goal that still has a primary action behind it is refused like a last demotion, and the "no primary left" guard now discounts actions whose goal is already off (they were never steering bidding), while still treating an unread goal as bidding. A goal staged against an account that does not own conversion tracking is refused up front with the id that does, instead of failing the whole atomic batch at publish. The staged card gains "Included in account-level goals", "Conversion category" and "Where it happens" rows.
4941
4968
 
4969
+ - **0.170.0**: `baker ads google asset-groups attach|detach` — link and unlink one asset on a Performance Max asset group that **already exists**, which is what refreshing a live group's creatives actually is. Until now nothing could do it: `asset-groups create` links a group's opening assets inline, `asset-groups update` takes only `--name --final-urls --status`, and `assets attach` reaches campaign/adGroup/customer but not an asset group — so replacing a low-performing PMax image had no route through Baker at all and ended in the Google Ads interface. `create` also gains `--portrait-image-assets`; the 4:5 slot was missing from the asset group contract entirely, so a portrait image could not be staged even at creation. Ordering is the agent's job and is now stated everywhere it matters: operations publish in **stage order**, Google Ads removals carry no dependency edge and are not reordered to run last (`docs/publishing.md` → Known gap), so a detach staged before its replacement reaches Google first and is rejected when it was the last asset of that ratio — attach first, always.
4970
+
4971
+ Shipped with it: **staging a second removal of the same shape silently did nothing.** Amend-matching reduced any composite ref to the segment after its last `~` — built for the two-segment `adGroupId~adId` refs that ad and keyword targets heal to, where that segment really is the entity's own id. It is not, in three other places: an asset-group asset (`{assetGroup}~{asset}~{FIELD_TYPE}`) and a customer-level asset link (`{asset}~{FIELD_TYPE}`) both end in a field type every asset of that ratio shares, and a campaign shared set (`{campaign}~{sharedSet}`) ends in the list's id, the same in every campaign it is attached to. So the second detach amended the first instead of staging, returned the first's ref and reported success: a PMax creative swap could only ever remove one image per ratio, and detaching one negative list from three campaigns removed it from one. Nothing in the output said the rest had been dropped. Refs now compare whole, with one one-sided exception — a **bare** id still collapses onto the two-segment composite whose child it names, which is the only case the reduction ever existed for. The duplicate copy of the same helper in the staging path is gone rather than fixed twice. Also: linking an asset Google created automatically is refused at stage time on an asset group, not just on a campaign/ad-group/customer link — a Performance Max group is where auto-created creative is most likely to be picked up. And the live-context resolver now reads the asset group and asset an attach/detach names, so the review of a swap shows names and the images themselves rather than the ids inside a resource name.
4972
+
4942
4973
  - **0.169.0**: `baker capabilities` — one call, before any platform work, that answers what Baker **cannot** do. Everything else (`baker schema`, the family docs, `--help`) describes what it can, so the only way to learn a capability was absent was to try it, usually after telling the user it would happen. Per surface (`google-ads`, `meta-ads`, `linkedin-ads`, `x-ads`, `tag-manager`, `ga4`, `gsc`) it reports: what is connected and which accounts/properties/containers were picked; whether writes are **live** or **simulated** for this company, and that Tag Manager has neither mode — completing the chat changes the real container; and a `limits` list separating *not built yet* from *the platform forbids it* from *we hold no permission for it*, because each leads to something different to tell the user. Naming one surface (`baker capabilities google-ads`) also returns `ops`: every change with its required and optional fields and the exact values each accepts — so "is there a field to demote a conversion action, and how is it spelled?" is one call instead of four guesses. Almost everything is derived rather than written down: the accounts and permissions come from the connection, the write mode from the company's gate, the writable entities and field contracts from the same schemas the backend parses with, and read-only vs read-write from the OAuth scopes the app actually requests (which is why "GA4 is read-only" is a fact, not a sentence). The handful of hand-written entries each carry a machine-checkable claim about the code behind them, and a coverage test fails when one stops holding. A rejected staged write on Google, Meta or LinkedIn now points at this command instead of leaving the agent to guess another field name.
4943
4974
 
4944
4975
  ## Publishing