@koda-sl/baker-cli 0.430.0-dev.8c14009f3 → 0.431.0

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 (29) hide show
  1. package/README.md +57 -32
  2. package/dist/{chunk-Q6F5UNOU.js → chunk-6Z4ETVEZ.js} +3 -3
  3. package/dist/{chunk-GTHLEQHU.js → chunk-DL7VXQEI.js} +6 -27
  4. package/dist/chunk-DL7VXQEI.js.map +1 -0
  5. package/dist/{chunk-PHLFNYGQ.js → chunk-RM2TTRYQ.js} +4 -4
  6. package/dist/{chunk-RFZYRG2Q.js → chunk-UKGTWBKW.js} +11 -3
  7. package/dist/chunk-UKGTWBKW.js.map +1 -0
  8. package/dist/{chunk-FQOKJYKN.js → chunk-WJIYNKLI.js} +12 -4
  9. package/dist/chunk-WJIYNKLI.js.map +1 -0
  10. package/dist/{chunk-GCTR4MDE.js → chunk-Y77DA7EO.js} +2 -2
  11. package/dist/cli.js +43681 -43348
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-GS7T7ETG.js → client-UUKOCY3Q.js} +4 -4
  14. package/dist/engine/index.js +3 -3
  15. package/dist/{env-SL22S6CM.js → env-Q3KQLCU5.js} +4 -2
  16. package/dist/output-V44OD5O3.js +22 -0
  17. package/dist/{shared-XR54KFMH.js → shared-3THDUK2O.js} +6 -6
  18. package/package.json +1 -1
  19. package/dist/chunk-FQOKJYKN.js.map +0 -1
  20. package/dist/chunk-GTHLEQHU.js.map +0 -1
  21. package/dist/chunk-RFZYRG2Q.js.map +0 -1
  22. package/dist/output-OFDKEPPJ.js +0 -18
  23. /package/dist/{chunk-Q6F5UNOU.js.map → chunk-6Z4ETVEZ.js.map} +0 -0
  24. /package/dist/{chunk-PHLFNYGQ.js.map → chunk-RM2TTRYQ.js.map} +0 -0
  25. /package/dist/{chunk-GCTR4MDE.js.map → chunk-Y77DA7EO.js.map} +0 -0
  26. /package/dist/{client-GS7T7ETG.js.map → client-UUKOCY3Q.js.map} +0 -0
  27. /package/dist/{env-SL22S6CM.js.map → env-Q3KQLCU5.js.map} +0 -0
  28. /package/dist/{output-OFDKEPPJ.js.map → output-V44OD5O3.js.map} +0 -0
  29. /package/dist/{shared-XR54KFMH.js.map → shared-3THDUK2O.js.map} +0 -0
package/README.md CHANGED
@@ -1116,10 +1116,23 @@ baker ads linkedin analytics --level campaign --campaign-id 1234 \
1116
1116
  # "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
1117
1117
  baker ads linkedin top-companies --campaign-id 1234 --last-days 30
1118
1118
 
1119
+ # "Which companies are our ads reaching?" — one row per company over the whole period,
1120
+ # most engaged first ({org, name, impressions, clicks, engagements, costLocal}).
1121
+ # Defaults: last 12 calendar months, the only selected ad account, top 100 (--full for all).
1122
+ baker ads linkedin companies --account-id 1234
1123
+ baker ads linkedin companies --by month --min-impressions 50 # months[] per company
1124
+ baker ads linkedin companies --campaign-id 1,2 --by campaign # campaigns[] per company
1125
+ baker ads linkedin companies --with-domains --stats --output csv # + domain/domainSource; read-quality stats
1126
+
1119
1127
  # "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
1120
1128
  baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
1121
1129
  baker ads linkedin resolve --ids 17719,19022 --output csv
1122
1130
 
1131
+ # "What are these companies' website domains?" — for matching against CRM companies.
1132
+ # Per org: vanityName (LinkedIn page), website, domain, domainSource (website | subdomain | none);
1133
+ # ids LinkedIn doesn't know land in `errors`. Shortlinks are never followed.
1134
+ baker ads linkedin resolve --ids 1035,1337 --with-domains
1135
+
1123
1136
  # "Who is in our audience by industry/seniority/function/title — all at once?"
1124
1137
  baker ads linkedin demographics --campaign-id 1234 --last-days 30
1125
1138
 
@@ -1188,6 +1201,7 @@ creatives # default ACTIVE intended-status
1188
1201
 
1189
1202
  analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
1190
1203
  demographics # sweep all firmographic pivots in one call
1204
+ companies # which companies the ads reached, one row per company (--by month|campaign, --with-domains)
1191
1205
  top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
1192
1206
  conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
1193
1207
 
@@ -1207,6 +1221,7 @@ audit --format md # deliverable-ready markdown table
1207
1221
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1208
1222
  campaigns create|update|pause|resume|archive|duplicate
1209
1223
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1224
+ campaigns exclude-companies <ad-set-id> --org <ids> # stop an ad set reaching these companies
1210
1225
  account url-params # UTMs for every ad in the whole account
1211
1226
  url-params [--ad-sets] # read what is tagged now, and what lands twice
1212
1227
  creatives create|update|pause|resume|duplicate
@@ -1247,9 +1262,16 @@ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=pa
1247
1262
  baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
1248
1263
  baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
1249
1264
 
1250
- # Matched audiences (CSV with header; email columns SHA-256 hashed locally)
1265
+ # Matched audiences (CSV with header; email columns SHA-256 hashed locally).
1266
+ # Company lists take companyName,domain,organizationUrn — organizationUrn matches the company page exactly
1251
1267
  baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
1252
1268
 
1269
+ # Stop an ad set reaching customers / open deals — add-only; at publish Baker re-reads the ad set
1270
+ # and merges into its targeting as it stands then. --org takes company ids, ≤200 per call;
1271
+ # a longer list goes into a company-list audience passed as --segment
1272
+ baker ads linkedin campaigns exclude-companies 123456 --org 1035,1441
1273
+ baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegment:42
1274
+
1253
1275
  # Conversions with playbook-default windows
1254
1276
  baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
1255
1277
  --post-click-window 30 --view-window 7
@@ -1311,7 +1333,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
1311
1333
  - OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
1312
1334
  - Account scoping: backend rejects calls against an account not in the picker selection.
1313
1335
  - Pinned API version: `Linkedin-Version: 202606` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. The floor is `TRACKING_PARAM_DYNAMIC_MIN_API_VERSION` (`CREATIVE_NAME` needs 202606), asserted in `ads/linkedin/utils.test.ts`.
1314
- - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
1336
+ - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, org domains 7d, bid-pricing/forecast 6h, audience-size 1h.
1315
1337
  - `--no-cache` on any command skips both cache layers (the CLI's own and the server's).
1316
1338
 
1317
1339
  #### LinkedIn-specific gotchas
@@ -1440,7 +1462,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1440
1462
 
1441
1463
  **`tracking` answers "can this account be reported on at all", and it reads the ad account to do it.** `analytics ads` can only describe parameters that have already arrived, which leaves two blind spots that mislead in the expensive direction: a platform with no traffic in the window looks exactly like a platform whose ads carry nothing, and a dimension arriving under a name nobody has mapped looks exactly like one nothing carries. `baker analytics tracking` reads the mapping, the traffic **and** the client's own Google tracking templates / Meta URL parameters, so it can answer for a platform that has not had a click yet. Every dimension comes back in one of four states, and they are four different jobs: `covered` is done; `unmapped` means the URLs already carry it under a name Baker does not read, so run the `mapCommand` in the response — free, and it repairs the traffic already collected; `missing` means nothing carries it; `unknown` means no visit arrived *and* the account could not be read, which is not a finding and must never be reported as untagged.
1442
1464
 
1443
- **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write.
1465
+ **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write. ChatGPT Ads is one Baker can write: its campaign and ad group `--tracking-template` (`baker ads chatgpt campaigns|ad-groups update`), which should carry `campaign_id={campaign_id}&ad_group_id={ad_group_id}&ad_id={ad_id}` — an id that only rides in `utm_campaign` or `utm_content` is reported as that UTM and never as the campaign or the ad, so `--platform chatgpt --role adgroup_id` has nothing to split by.
1444
1466
 
1445
1467
  **The one recommendation about the ad account itself.** `baker analytics ads --platform <p>` warns when that platform's traffic carries no campaign id, ad group id or ad id — the three tiers every "which ad worked" question is grouped by — and returns the exact URL suffix, in the platform's own macro language, that would start sending the missing ones. It is the only mapping gap that cannot be fixed afterwards: naming a parameter Baker already captures corrects the history, because roles are resolved when a report is read, but a parameter the ads never sent has no stored values for a meaning to apply to. The hint stays quiet for a platform with little traffic, and defers to the untagged-account finding rather than repeating it.
1446
1468
 
@@ -1803,22 +1825,6 @@ baker gsc sitemaps --site-url "https://example.com/"
1803
1825
 
1804
1826
  ---
1805
1827
 
1806
- ### Making ads (`baker creatives brief` / `ideas` / `make`)
1807
-
1808
- Ads start from what customers said. Three commands carry the process; the agent writes the ideas and the copy.
1809
-
1810
- ```bash
1811
- baker creatives brief [--adset <id>] [--account-id act_<id>] # one read: viewer, account bottleneck, what works, taste, customer quotes, ideas on file
1812
- baker creatives ideas [--full] # check and rank src/creatives/_ideas/*.md; build_next
1813
- baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId>] [--spec <beats.json>] [--from-idea]
1814
- baker creatives learn [--days <n>] [--dry-run] # what each idea's ads did, written back onto the idea
1815
- ```
1816
-
1817
- - `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
1818
- - `ideas` is free and local. An idea's `evidence` resolves to `src/content/insights/` entries, or it is an `explore` bet. Numbers must be in the Knowledge or our own customers' quotes (a Reddit, forum or web quote never sources a claim), no two ideas may share an angle and format, and a review or testimonial format needs a quote from our own customer.
1819
- - `learn` pools each built idea's creatives, judges them against the rest of the account's cost per result and writes `status` (`winner`, `failed`, `inconclusive`, `testing`) and a frozen `result` onto the idea. `ideas` then warns when a new idea repeats a failed one.
1820
- - `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1821
-
1822
1828
  ### Competitive Intelligence (`baker research`)
1823
1829
 
1824
1830
  Market and competitor research powered by DataForSEO and AI-powered Google Search grounding. Shows who's competing for keywords, search intent classification, competitor keyword strategies, keyword gaps, landing page performance, and open-ended AI research.
@@ -1950,16 +1956,6 @@ Every search or thread item carries verbatim `text`, `author`, `score`, `created
1950
1956
 
1951
1957
  ---
1952
1958
 
1953
- ### `baker research customers`
1954
-
1955
- Finds what real customers say and writes each quote as an insight in `src/content/insights/` (type, quote, source, date, `status: observed`). Sources: the company's reviews library, Reddit, and with `--web` pages from web research. A quote is kept only when it appears word for word in the text that was fetched; the model classifies quotes and never writes them.
1956
-
1957
- ```bash
1958
- baker research customers --focus "solar panels" # reviews + Reddit
1959
- baker research customers --query "payroll mistakes" --web --limit 15
1960
- baker research customers --dry-run # print, write nothing
1961
- ```
1962
-
1963
1959
  ### `baker research advertisers "keyword"`
1964
1960
 
1965
1961
  Returns a ranked list of **domains** competing for one keyword in Google, with average SERP position, relevance rating, estimated traffic value and visibility — domain economics, **NOT ad copy**. Despite the name it returns no headlines, descriptions or creative of any kind; for a competitor's actual ad copy use `baker winning-ads content <adId>` (Meta/LinkedIn only — Google SERP ad copy is not available anywhere in Baker).
@@ -3573,6 +3569,7 @@ baker scheduled-agents create --name "Celebrate the first sale" --description "O
3573
3569
  baker scheduled-agents update <id-or-temp_agent_id> --enabled false
3574
3570
  baker scheduled-agents update <id-or-temp_agent_id> --run-at "2026-09-02T07:00:00Z" --prompt "Use the launch checklist"
3575
3571
  baker scheduled-agents update <id-or-temp_agent_id> --trigger experiment.ended
3572
+ baker scheduled-agents update <id-or-temp_agent_id> --slack "#marketing" # each run opens a thread there; also here | me | off
3576
3573
  baker scheduled-agents delete <id-or-temp_agent_id>
3577
3574
  baker scheduled-agents unstage <id-or-temp_agent_id> # everything this chat staged on it
3578
3575
  baker scheduled-agents unstage <id-or-temp_agent_id> --index 2 # one staged change only
@@ -3950,7 +3947,7 @@ baker setup-links list # active lin
3950
3947
 
3951
3948
  ---
3952
3949
 
3953
- ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
3950
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot companies search` · `baker hubspot deals list | properties | fields` · `baker hubspot meetings list | view`
3954
3951
 
3955
3952
  Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
3956
3953
 
@@ -3972,6 +3969,8 @@ baker hubspot pipelines list # where a lead lands, and the st
3972
3969
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
3973
3970
  baker hubspot contacts lookup lead@example.com # was this one lead picked up?
3974
3971
  baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
3972
+ baker hubspot companies search --domains acme.com,globex.io --with-deals # CRM accounts by domain, with their deals
3973
+ baker hubspot deals list --pipeline default --modified-since 2026-01-01 # deals: amount, stage, closed won
3975
3974
  baker hubspot meetings list # every calendar + whether booking redirects
3976
3975
  baker hubspot meetings view <slug> # booking-form fields for one calendar
3977
3976
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -3982,7 +3981,9 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3982
3981
  **`forms submissions`:** positional `<formId>`; `--days <1-365>` (default 30) sets the window; `--full` adds each submission's field values. The default response is counts and dates only — `total`, `inWindow`, `lastSubmittedAt`, and per-day counts — because submissions carry the lead's own contact details. `truncated: true` means the page budget ran out before the window was covered, so `inWindow` is a floor rather than an exact count.
3983
3982
  **`workflows list` flags:** `--search <text>`, `--enabled-only`. Disabled workflows are listed too — a form wired to one that is off looks connected and does nothing.
3984
3983
  **`workflows view`:** positional `<workflowId>` (the id from `workflows list`, and the one in HubSpot's own URL — the command translates it to HubSpot's separate internal flow id for you). Returns the enrolment criteria and whether it re-enrols (`null` = HubSpot does not report it for that criteria type, so it is unknown rather than off), plus every step with the property it writes. On a `LIST_BRANCH` step, branches come back in **evaluation order** (`order: 1` wins over `order: 2`) — a broad branch above a narrower one silently swallows the records the narrow one was written for, and HubSpot reports nothing. A `STATIC_BRANCH` matches on a value rather than top down, so its `order` is display only. `steps` is **not** in execution order; follow `nextActionId`. `--full` adds the raw payload, the only place the branch filter criteria (which property is compared to which value) can be read.
3985
- **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
3984
+ **`pipelines list`:** no flags; stages come back in the order they run in HubSpot, each with `isClosed` and `isWon` (closed with probability 1).
3985
+ **`companies search` flags:** `--domains <a.com,b.com>` (up to 100; a URL or `www.` is reduced to the bare domain), `--names <"Acme, Inc.;Globex">` (semicolon-separated), `--with-deals`, `--limit <1-500>` (default 100), `--output json|csv|jsonl|md`. Rows: `{ id, name, domain, linkedinPage, deals? }`; `notFound` lists the searched domains no CRM company carries; `truncated: true` means more matched than `--limit`.
3986
+ **`deals list` flags:** `--pipeline <id,id>`, `--modified-since <YYYY-MM-DD>`, `--limit <1-1000>` (default 200), `--after <cursor>`, `--output json|csv|jsonl|md`. Rows, oldest change first: `{ id, name, amount, currency, pipelineId, stageId, stageLabel, isClosed, isWon, createdAt, closedAt, modifiedAt, companyIds }` — `amount: null` is none entered. `next` is the `--after` for the following page; `restartFrom` appears when HubSpot's 10,000-result search ceiling is reached (read on with `--modified-since <restartFrom>`); `companiesReadable: false` means the connection cannot read companies, so no deal carries `companyIds`.
3986
3987
  **`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
3987
3988
  **`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
3988
3989
  **`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
@@ -4014,6 +4015,30 @@ No write surface against HubSpot itself — a form's fields, redirect, or consen
4014
4015
 
4015
4016
  ---
4016
4017
 
4018
+ ### `baker accounts influenced | engagement | warm | company | campaigns | crm`
4019
+
4020
+ Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Reads Baker's nightly copy of LinkedIn and the CRM, so it is fast and covers up to 24 months; nothing is changed anywhere. Only for companies with Account insights switched on — otherwise every command exits 1 with `ACCOUNTS_NOT_ENABLED` and an `error.fix` pointing at the live read (`baker ads linkedin companies`, plus `baker hubspot companies search`). `--start`/`--end` must be real calendar days within 24 calendar months; a bad flag or an unsupported `--output` exits 1 with `INVALID_ARGS`.
4021
+
4022
+ ```bash
4023
+ baker accounts influenced # influenced pipeline, last 12 calendar months
4024
+ baker accounts influenced --basis revenue --start 2026-01-01 --end 2026-06-30
4025
+ baker accounts influenced --full # every deal in the period, influenced or not
4026
+ baker accounts engagement --last-months 3 --limit 50 # companies reached, New/Rising, CRM match and deals
4027
+ baker accounts engagement --campaign-id 123456 --output csv
4028
+ baker accounts warm --output csv > warm.csv # companyName,domain,organizationUrn
4029
+ baker accounts company acme.com # or a LinkedIn company id / urn:li:organization:N
4030
+ baker accounts campaigns --last-months 6 --output md # per campaign: reached, engaged, influenced pipeline (--basis revenue)
4031
+ baker accounts influenced --model awareness --threshold 10 --output csv # another rule, this call only
4032
+ baker accounts warm --min 0 # every New/Rising company, below the threshold too
4033
+ baker accounts crm status # deals from HubSpot or sent by the client; what arrived, linked Google Sheets
4034
+ baker accounts crm check --file deals.csv # judge a deals or company CSV offline, nothing sent
4035
+ baker accounts crm contract # the send-your-CRM contract, as Markdown for a developer
4036
+ ```
4037
+
4038
+ A deal is **influenced** when its company reached the company's influence rule — by default 3+ LinkedIn ad engagements (Awareness: 7+ impressions; Intent: 3+ clicks) — in the 12 months up to and including the month the deal was created (`--basis pipeline`, deals not won) or closed won (`--basis revenue`). It is a labelled rule, not credit: campaign rows overlap, and money is reported per currency, never summed across currencies. Deals whose company match is only a guess come back under `lowConfidence` and stay out of `summary` until the match is confirmed in Measure → Accounts. Without the deals set up there — HubSpot's pipelines and won/lost stages, or Another CRM — `influenced` and `campaigns` exit 1 with `ACCOUNTS_NOT_SET_UP`. A CRM Baker cannot read sends its companies and deals to `POST /v1/accounts/crm` (a `crm_ingest` key, `X-Baker-Mode: test` to rehearse, up to 500 records per request, records naming a person refused), is uploaded as a CSV in Measure → Accounts, or lives in a Google Sheet linked there that Baker reads every night (a row deleted from the sheet leaves Baker). A file is a deals export or a company list — a company needs only its website or LinkedIn page; `crm check` runs the same rules offline and `crm contract` prints the whole contract. The CLI never sends deals or links a sheet. `--model engagement|awareness|intent` and `--threshold N` on `influenced`, `campaigns` and `warm` read with another rule for that call only; the saved setting is unchanged. A **warm** account is New or Rising, reached the company's influence threshold this period on the rule's metric (`--min N` overrides, `--min 0` drops it), and has no open deal, no won deal and no deal lost in the last 6 months; `engagement` and `warm` rows carry `dealStatus` (`won` > `open` > `lost` > `none`), `engagement --output csv` adds `lastMonth` and `previousImpressions/Clicks/Engagements`, and `campaigns` / `company` carry each campaign's `campaignName` (null when LinkedIn could not name it). `company` returns `months` (the company's totals), `byCampaign` (the same numbers per campaign — overlapping, never add them) and its `deals` with `touchValue` and `influenced`. `warm --output csv` is the file `baker ads linkedin audiences create --type company-list --list-file` takes.
4039
+
4040
+ ---
4041
+
4017
4042
  ### `baker chats list | view | transcript | diff`
4018
4043
 
4019
4044
  Read-only inspection of **other Sessions (chats) on the same account** — so an agent can reuse prior work ("like we did in that other chat, but for X"). Same-account only; never crosses a company boundary and has no write surface.
@@ -5992,7 +6017,7 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
5992
6017
 
5993
6018
  Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. It runs **billed Gemini passes** up front:
5994
6019
 
5995
- 1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels; the image model renders only the screen's `photos`, each a plain phone photo of ordinary people of the market; adding the block switches an existing ad on its next run, `--fix` does not apply, an invented brand handle or a rating the Knowledge does not hold (either decimal mark) is refused as `native_screen_invalid`, and a photo of ours goes in as its `image`: a native photo takes no `avatar`, since nothing would cast it), a role on each third-party brand (`partner` | `proof` | `category_signal`), a reference's `image` (wired as a picture of ours) and a person's `avatar` (the company avatar cast), and `concept` (`why_it_wins` in five layers, `load_bearing` elements with `have`/`adapt`/`missing` and `ours`, `verdict` `use`|`adapt`|`reject`, `transfer_note`; seeded from `baker winning-ads fit`). Every render keeps the load-bearing elements; `validate`/`run` refuse a `reject` or a `missing` element with nothing named (`concept_does_not_fit`) and a rating, or a promised price, share, multiple or time, the Knowledge does not hold with that unit (`proof_without_source`); a line citing a review platform gets its real badge wired. **You never write a model prompt**: every render prompt is compiled from this file for the model it runs on (short labelled sections for gpt-image, sentences with reasons for Gemini; each line quoted once; pictures numbered with one role each; colours from the brand; realism as capture cues; the safe area from its one owner; constraints last), on every `validate` and `run`, and shown on its node. A `prompt.json` written before the ad spec (the describe blueprint) is read through an adapter and keeps working. The source's own read (`source`, and the library's `reference`) stays as bookkeeping and is never sent to a model.
6020
+ 1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels; the image model renders only the screen's `photos`, each a plain phone photo of ordinary people of the market; adding the block switches an existing ad on its next run, `--fix` does not apply, an invented brand handle or a rating the Knowledge does not hold (either decimal mark) is refused as `native_screen_invalid`, and a photo of ours goes in as its `image`: a native photo takes no `avatar`, since nothing would cast it), a role on each third-party brand (`partner` | `proof` | `category_signal`), a reference's `image` (wired as a picture of ours) and a person's `avatar` (the company avatar cast), and `concept` (`why_it_wins` in five layers, `load_bearing` elements with `have`/`adapt`/`missing` and `ours`, `verdict` `use`|`adapt`|`reject`, `transfer_note`; seeded from `baker winning-ads fit`). Every render keeps the load-bearing elements; `validate`/`run` refuse a `reject` or a `missing` element with nothing named (`concept_does_not_fit`) and a rating the Knowledge does not hold (`proof_without_source`); a line citing a review platform gets its real badge wired. **You never write a model prompt**: every render prompt is compiled from this file for the model it runs on (short labelled sections for gpt-image, sentences with reasons for Gemini; each line quoted once; pictures numbered with one role each; colours from the brand; realism as capture cues; the safe area from its one owner; constraints last), on every `validate` and `run`, and shown on its node. A `prompt.json` written before the ad spec (the describe blueprint) is read through an adapter and keeps working. The source's own read (`source`, and the library's `reference`) stays as bookkeeping and is never sent to a model.
5996
6021
  2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`). The describe pass also records the ad's **typography** (each typeface's classification, a best-guess family, and its weight/case), which seeds the spec's `brand.typeface`. Background extras are dropped. Each element becomes a `references` entry in the spec, named by its role and naming the canvas slot it fills, so the file says which slot grounds which subject.
5997
6022
 
5998
6023
  It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GCTR4MDE.js";
4
+ } from "./chunk-Y77DA7EO.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-FQOKJYKN.js";
7
+ } from "./chunk-WJIYNKLI.js";
8
8
 
9
9
  // src/client.ts
10
10
  var MAX_RATE_LIMIT_RETRIES = 3;
@@ -208,4 +208,4 @@ export {
208
208
  apiGet,
209
209
  apiPost
210
210
  };
211
- //# sourceMappingURL=chunk-Q6F5UNOU.js.map
211
+ //# sourceMappingURL=chunk-6Z4ETVEZ.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GCTR4MDE.js";
4
+ } from "./chunk-Y77DA7EO.js";
5
5
  import {
6
6
  API_KEY_FORMAT_MESSAGE,
7
7
  DIRECT_ROUTE,
@@ -22,11 +22,11 @@ import {
22
22
  refuseNonPublicUrl,
23
23
  requireConnection,
24
24
  shouldEscalate
25
- } from "./chunk-FQOKJYKN.js";
25
+ } from "./chunk-WJIYNKLI.js";
26
26
 
27
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js
27
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
30
30
  "use strict";
31
31
  var { hasOwnProperty } = Object.prototype;
32
32
  var stringify = configure2();
@@ -1408,7 +1408,7 @@ function resolveAdaptFormats(params) {
1408
1408
  return params.formats ?? [];
1409
1409
  }
1410
1410
 
1411
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/766ad107c42df28eeadaf0ac2f9089c8a9e200f6ba0b2b1a817b8d536cbdcc49/node_modules/safe-stable-stringify/esm/wrapper.js
1411
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1412
1412
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1413
1413
  var configure = import__.default.configure;
1414
1414
  var wrapper_default = import__.default;
@@ -2826,12 +2826,6 @@ var MODEL_ROLES = {
2826
2826
  * only and asked once per ad and viewer, so Flash; the calibration set (E8) decides.
2827
2827
  */
2828
2828
  viewerCheck: { kind: "text_generate", model: "~google/gemini-flash-latest" },
2829
- /**
2830
- * Picks the customer truths out of reviews, testimonials and threads (`baker research
2831
- * customers`): returns exact spans the code then checks against the source. Text only and
2832
- * one call per batch, so Flash.
2833
- */
2834
- customerInsights: { kind: "text_generate", model: "~google/gemini-flash-latest" },
2835
2829
  /**
2836
2830
  * The same viewer shown the FINISHED static ad at phone size for about three seconds, with no
2837
2831
  * brand and no brief: what is sold, whether it is for them, what to do next, what stopped them
@@ -2873,7 +2867,6 @@ var PLATE_JUDGE_MODEL = MODEL_ROLES.plateJudge.model;
2873
2867
  var AD_DESCRIBE_MODEL = MODEL_ROLES.adDescribe.model;
2874
2868
  var BLUEPRINT_ANALYSIS_MODEL = MODEL_ROLES.blueprintAnalysis.model;
2875
2869
  var VIEWER_CHECK_MODEL = MODEL_ROLES.viewerCheck.model;
2876
- var CUSTOMER_INSIGHTS_MODEL = MODEL_ROLES.customerInsights.model;
2877
2870
  var GLANCE_CHECK_MODEL = MODEL_ROLES.glanceCheck.model;
2878
2871
  var TEXT_BOX_READER_MODEL = MODEL_ROLES.textBoxReader.model;
2879
2872
  var VIDEO_DECONSTRUCT_MODEL = MODEL_ROLES.videoDeconstruct.model;
@@ -6919,18 +6912,6 @@ function conceptProblems(spec) {
6919
6912
  }
6920
6913
  return problems;
6921
6914
  }
6922
- var PLACEHOLDER = "[TODO";
6923
- function unwrittenProblems(spec) {
6924
- const problems = [];
6925
- if (spec.scene.description.includes(PLACEHOLDER)) {
6926
- problems.push("The scene is still a placeholder: write what the viewer sees in `scene.description`.");
6927
- }
6928
- const lines = spec.copy.filter((line) => line.text.includes(PLACEHOLDER));
6929
- if (lines.length > 0) {
6930
- problems.push(`${lines.length} line(s) of copy are still placeholders: write each as it prints, or drop it.`);
6931
- }
6932
- return problems;
6933
- }
6934
6915
  function readAdSpec(raw) {
6935
6916
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6936
6917
  const advertiser = raw?.reference?.advertiser;
@@ -18404,7 +18385,6 @@ export {
18404
18385
  AD_DESCRIBE_MODEL,
18405
18386
  BLUEPRINT_ANALYSIS_MODEL,
18406
18387
  VIEWER_CHECK_MODEL,
18407
- CUSTOMER_INSIGHTS_MODEL,
18408
18388
  GLANCE_CHECK_MODEL,
18409
18389
  VIDEO_DECONSTRUCT_MODEL,
18410
18390
  VOICEOVER_MODEL,
@@ -18476,7 +18456,6 @@ export {
18476
18456
  isAdSpec,
18477
18457
  conceptSummary,
18478
18458
  conceptProblems,
18479
- unwrittenProblems,
18480
18459
  readAdSpec,
18481
18460
  withRenderedLettering,
18482
18461
  conceptClassOf,
@@ -18564,4 +18543,4 @@ export {
18564
18543
  defaultRegistry,
18565
18544
  createEngineFromEnv
18566
18545
  };
18567
- //# sourceMappingURL=chunk-GTHLEQHU.js.map
18546
+ //# sourceMappingURL=chunk-DL7VXQEI.js.map