@koda-sl/baker-cli 0.286.0-dev.93be96120 → 0.287.0-dev.4a5d811dc

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
@@ -1260,10 +1260,21 @@ baker analytics conversions # what counts as a conversion, and wha
1260
1260
  baker analytics conversions --candidates # everything these pages do, and which of it is counted
1261
1261
  baker analytics conversions --event page:request_demo --name "Demo requested"
1262
1262
  baker analytics conversions --remove page:request_demo
1263
+ baker analytics sends # where each conversion is sent, and what happened to each upload
1264
+ baker analytics sends --set --conversion "Deal won" --platform meta --tag <tagId> --event Purchase # staged; spends budget once published
1265
+ baker analytics sends --remove s1 # stop one, when the chat is published
1263
1266
  baker analytics delivery --page 2 # the next page of a long list
1264
1267
  baker analytics submissions --flow contact # every attempt at a Form, delivered or not
1268
+ baker analytics sources # the addresses the client's own tools send events to
1269
+ baker analytics sources --create --name HubSpot # a new address, learning; hand the URL to the user
1270
+ baker analytics sources --map --source <id> --field email=contact.email --field value=deal.amount
1271
+ baker analytics sources --create-hubspot --property lifecyclestage --value customer # a source Baker reads from HubSpot every five minutes
1265
1272
  ```
1266
1273
 
1274
+ **Sources: a URL for a tool that cannot be told what to post.** `sending-events` is the contract a developer writes to; `sources` is for a CRM webhook, an automation step or a billing notification that posts whatever it posts. A new source is `learning`: its first send is kept as a masked sample and counted as nothing until `--map` confirms which path holds which field (`--field` repeats; a body already in Baker's own format needs none). From then on every arrival is an ordinary event carrying `baker_source: <name>`, and `--pause`, `--resume` and `--remove` are by `--source <id>`.
1275
+
1276
+ **`--create-hubspot --property <name> --value <value> [--name <name>]`: a source Baker reads, for a HubSpot plan that cannot send a webhook.** Every five minutes the connected account is checked and every contact whose property becomes that value is one event named after the source (default: the value's label), carrying `baker_source`, the contact's email, phone, id, name, address and HubSpot's `gclid` / `fbclid`. Names come from `baker hubspot contacts properties`. Counts from creation on, never earlier contacts; a contact already at the value counts only when it leaves and re-enters. `counting` at once, nothing to map, no URL; the source carries `hubspot { property, propertyLabel, value, valueLabel }`, `countingSince`, `lastPolledAt` and `lastError`. Deals are not watched — sync the outcome onto the contact and read it there.
1277
+
1267
1278
  Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-MM-DD`, end inclusive); `--full`; `--compare` to also return the preceding window of the same length; `--timezone <IANA>` and `--granularity hour|day|week|month` to shape a series. Every call reads live — there is no cache layer, so there is nothing to bypass.
1268
1279
 
1269
1280
  **`--output md` when the numbers are going to be read.** Every report is emitted as compact JSON by default, and `--output md` renders each block as a Markdown table instead — one header rather than the keys repeated on every row, with the identical `warnings` and `hints`. Measured across all sixteen presets against a real deployment, the two changes together take the family from 110 KB to 44 KB — **about 60% smaller overall**, 38% at worst (`ads`, which is dense numeric data with nothing to prune) and 77% at best (`landings`). Three further economies apply to both formats, because the reader is usually an LLM with a context window rather than a browser:
@@ -1276,6 +1287,8 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1276
1287
 
1277
1288
  **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. One outcome per call — repeating `--event` is refused rather than silently keeping the last one.
1278
1289
 
1290
+ **A send belongs to the conversion, and `baker analytics sends` is where it is set.** "Deal won" → Meta Purchase means every event counted as "Deal won" — from any Form, page or the client's own systems — is uploaded to Meta with who the person is, the click that brought them and the amount forwarded automatically; there is nothing to map, and a send is never set on a funnel step (the step reads its conversions' sends back as `sends[]`). With no flags it lists the sends in effect with `sends[].outcome` ("12 sent · 1 failed", `lastError` in the platform's words), `platforms[]` with the tags on Tracking that can send, and `conversions[]`, the names that can be sent. `--set --conversion <name> --platform <p> --tag <tagId> --event <e>` **stages** a send: it lands when the chat is published, and from then on it **spends ad budget** on every event under the name — say so plainly; never call a published send a preview. `--tag` is copied from `platforms[].tags[].tagId`, which lists only the tags that hold their server credential — one typed from memory is refused, and a platform with none says what to add on Tracking. A name nothing is counted as is refused too. `--remove <id>` stops one, staged the same way. `warnings[]` names a Form that already sends the same thing itself, so the platform would count it twice.
1291
+
1279
1292
  **Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
1280
1293
 
1281
1294
  **Mapping is staged on the chat, not applied.** Answers appear under **Campaign parameters** in the dashboard and take effect when the chat is published; a discard takes them all back, and a report run in the same turn still reads the old vocabulary. Reading the mapping back inside the chat *does* include what it has staged, so a second turn never re-maps the same parameter.
@@ -1309,47 +1322,38 @@ Data is retained for 400 days; a window reaching further back comes back with a
1309
1322
 
1310
1323
  ---
1311
1324
 
1312
- ### A/B tests (`baker experiment`)
1325
+ ### The company's funnel (`baker funnel`)
1313
1326
 
1314
- Two versions of one landing page, split 50/50 at the edge, decided by a verdict rather than by two rates somebody reads.
1327
+ The steps a company's business actually has, wired as a graph, joined to the moments Baker already records — and counted in PEOPLE on every read. It is what makes "how far did the people from this campaign get" answerable: a conversion count can say how many converted, never how many reached Meeting booked and where the rest stopped.
1315
1328
 
1316
1329
  ```bash
1317
- baker experiment plan --landing oferta --variant oferta--1 # FIRST can this page settle the question at all?
1318
- baker experiment plan --landing oferta --variant oferta--1 --goal event:request_demo
1319
- baker experiment plan --landing oferta --variant oferta--1 --days 21 # what could 21 days settle?
1320
- baker experiment start --landing oferta --variant oferta--1 --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
1321
- baker experiment status # the verdict on every test
1322
- baker experiment status --id a3f91c2b --full # one test, with the posteriors behind it
1323
- baker experiment finish --id a3f91c2b # send all the traffic to whichever won
1324
- baker experiment finish --id a3f91c2b --abandon # stop it, keep the page as it is, record no result
1325
- baker experiment fold # make the page's own content the version that won
1330
+ baker funnel # the funnel this company has, with 30 days of people per step
1331
+ baker funnel --days 90 # a longer window
1332
+ baker funnel --moments # everything a trigger can name read before writing any trigger
1333
+ baker funnel --templates # the step library and the presets
1334
+ baker funnel --preset b2b_sales # draw the closest graph
1335
+ baker funnel --steps '[{"key":"lead","displayName":"Enquiry","stepClass":"lead","after":[],"triggers":[{"id":"t1","kind":"conversion","eventKey":"submit:contact"}]}]'
1326
1336
  ```
1327
1337
 
1328
- **A page has variants. A variant is never a page.** The variant is a real built folder at `src/pages/oferta--1/`, so it gets every quality gate a page gets — which matters, because it is about to be shown to half the client's visitors. What it does not get is a public address: it is absent from the Landings list and the sitemap, carries `noindex`, and `/oferta--1/` returns **404** at the edge. Both versions answer at `/oferta/`. Assignment happens while the HTML is served, so there is no flicker, no client JavaScript and nothing for an ad blocker to suppress, and the split is a path rewrite and never a redirect. To look at one side deliberately, `/oferta/?_bkr_preview=a` and `?_bkr_preview=b` serve it exactly, with no assignment and no cookie.
1338
+ **Every step comes back counted.** `people` reached it, `atLeast` got at least that far (the step or anything after it), `before` got as far as the step before, `share` is `atLeast / before`, and `money` is what its events said they were worth, by currency. `window` says which days, `worstStep` names where the most people are lost, and the hints say it in a sentence. Counted in people across visits and devices a landing viewed on a phone, a Form completed on a laptop and a deal a CRM posted under its own id are one person when a contact digest joins them and unordered, so a deal the CRM posted before the site's submit landed still counts. Never quote these as visits.
1329
1339
 
1330
- **Finishing takes two beats, and `fold` is the second.** `finish` records which version won, and the edge serves it to everyone within the hour that is all a decision made with nobody present can do, because it has no workspace and cannot move a file. `baker experiment fold` moves the winning version into the page's own folder, archives the one that lost as a self-contained snapshot under `src/_variants/<page id>/<n>/`, and deletes the variant folder. Until it runs, the page is served by a rewrite while its own source renders the version that lost, and it cannot be tested again. Run `fold` with no arguments after any test ends, then publish; Baker drops the rewrite by itself once the new pages are live.
1340
+ **The vocabulary is the company's; the stage is closed.** A company invents its own steps and names them whatever it calls them`displayName` is unconstrained. `key` is the stable identifier (lowercase letters, digits, underscores) and never changes when the name does. Every step also carries one of six stages `visited`, `lead`, `engaged`, `customer`, `repeat`, `lost` plain words every business has, used to compare one company's funnel with another's.
1331
1341
 
1332
- **`--days` answers the other question.** You cannot set how long a test runs — the finish line is a visitor count fixed before it starts, and a horizon that moves is not a horizon. But `--days 21` says what a window that long *could* settle on this page's traffic: *"in 21 days this page could only settle a change of +31% or more"*. That turns a refusal into a brief, because the lever is the size of the change, not the calendar.
1342
+ **It is a graph, not a ladder.** `after` is a list of predecessors, so two ways into one step is ordinary rather than an anomaly people arrive by seeing the landing *or* by asking for a demo. An empty `after` means people come in there. A cycle or a predecessor that does not exist is refused with the step named.
1333
1343
 
1334
- **`plan` before building anything.** At a 3% conversion rate a +20% lift needs roughly 14,000 visitors *per variant*; most landing pages will never get there, and `plan` refuses rather than letting a test run for three weeks and report a conclusion made on noise. `--lift` sets the smallest lift worth detecting (default `0.2`), and it is the most consequential flag here halving it roughly quadruples the traffic needed.
1344
+ **`--steps` replaces the whole funnel.** Read first and restate everything you want to keep. `--preset` is the shortcut for a company that has none.
1335
1345
 
1336
- **`--goal` decides what "better" means, and it is fixed for the life of the test.**
1346
+ **A step says what triggers it, and every trigger is PICKED from `--moments`.** `triggers` is a list and any one of them firing is enough. Two kinds and no third: `{"kind":"conversion","eventKey":"submit:contact"}` for an outcome the company counts (`moments.conversions`), and `{"kind":"landing","landingId":"<id>"}` for a landing being viewed (`moments.landings`). A moment nobody has named yet — a Form step, a `data-baker-*` event, an event a client's system posts (`moments.events`, `moments.sources`) — is named as a conversion first with `baker analytics conversions --event <key> --name <name>` and then used; the funnel has no vocabulary of its own for an unnamed moment, which is what keeps it and every conversion report agreeing. `moments.events[].origin` tells a page's event (`site`) from one the client's own systems sent (`systems`); `moments.sources` lists the client's systems by the `page:<name>` key each raises, pickable before the first send arrives. A key typed from memory saves, publishes and counts nobody forever — the hint "has a trigger and nobody reached it while N people entered" is that mistake caught after the fact. A step written with `offlineConversions` or an `analytics` trigger is refused before any round trip, with where each now lives.
1337
1347
 
1338
- | `--goal` | Counts |
1339
- |---|---|
1340
- | `leads` (default) | Whatever this company already marks as a conversion |
1341
- | `leads:<form>` | The same, one Form only |
1342
- | `event:<name>` | An event the page declares for itself (`data-baker-click="request_demo"`) |
1343
- | `event:<name>/<key>=<value>` | The same, narrowed by one of its own properties |
1344
- | `click` / `click:<host>` | Leaving the site, optionally for one destination |
1348
+ **A landing being viewed can be a step** — where most funnels start — and never a conversion: `baker analytics conversions` refuses a `view:` key.
1345
1349
 
1346
- `baker analytics events` lists what a page already reports. A goal that has never fired on the page is **refused**, not run it would return zero on both sides forever, and that reads as a tie rather than as "not measured".
1350
+ **A step nothing enters advances nobody, silently.** No `triggers` or one whose narrowing was never filled in — means the report shows zero, nothing errors, and it looks exactly like a stage people never reach. Every response names these in `hints`. **There is no default door either:** a Form is counted only where a trigger names it, and `hints` names every Form no step counts.
1347
1351
 
1348
- **Read `verdict` and nothing else** to decide: `keep_running` | `winner` | `no_difference` | `invalid` | `stopped_early_harmful`. `summary` is one sentence fit to show a client. The numbers under `--full` are for printing a test that says `keep_running` has not finished however good they look, and only `finish` on a concluded test can record a result.
1352
+ **Where a step sends is read here and set on the conversion.** Every step comes back with `sends[]` one entry per send of every conversion among its triggers (`id`, `conversionName`, `platform`, `event`, and `outcome`: "12 sent · 1 failed", or "Nothing sent yet"). It is read-only: a send belongs to the conversion by name, so it is set with `baker analytics sends --set --conversion <name> --platform <p> --tag <tagId> --event <e>`, never on a step, and a step written with `offlineConversions` is refused with that command named. A send that failed is named in the hints (`SEND FAILED on step <key>: …`) with the platform's own error.
1349
1353
 
1350
- **`--abandon` overrides the verdict.** It stops the test, keeps the original page and records no result — the only way to end one that has not concluded, and it applies even when the test *has*. That is deliberate: the only outcome it can disagree with is a variant win, so deferring to the verdict would mean a flag asking to keep the original page handed all the traffic to the alternative instead. The response reports `discardedVerdict` when a real finding was thrown away.
1354
+ **Staged, and published in one piece.** The funnel becomes the company's when the chat is published; discarding takes it back. The client sees what you staged on the funnel canvas inside the chat and can edit it there the same canvas, the same publish so read the funnel back before restating it. The version it replaces is kept, so removing a step does not remove what was measured under it.
1351
1355
 
1352
- Nothing goes live until the session is published: starting stages onto the session, and the person publishing is the review of the variant page.
1356
+ ---
1353
1357
 
1354
1358
  ### Google Analytics 4 (`baker ga4`)
1355
1359
 
@@ -3438,7 +3442,7 @@ baker history list --category ads --full # ad writes with raw metadata
3438
3442
 
3439
3443
  ---
3440
3444
 
3441
- ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup` · `baker hubspot meetings list | view`
3445
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties` · `baker hubspot meetings list | view`
3442
3446
 
3443
3447
  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.
3444
3448
 
@@ -3459,6 +3463,7 @@ baker hubspot workflows view <workflowId> --full # + the values each branch com
3459
3463
  baker hubspot pipelines list # where a lead lands, and the stages after it
3460
3464
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
3461
3465
  baker hubspot contacts lookup lead@example.com # was this one lead picked up?
3466
+ baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
3462
3467
  baker hubspot meetings list # every calendar + whether booking redirects
3463
3468
  baker hubspot meetings view <slug> # booking-form fields for one calendar
3464
3469
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -3472,6 +3477,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3472
3477
  **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
3473
3478
  **`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.
3474
3479
  **`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.
3480
+ **`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.
3475
3481
  **`meetings list` flags:** `--search <text>`, `--redirecting-only`.
3476
3482
  **`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.
3477
3483
 
@@ -5828,23 +5834,6 @@ baker landing inspiration scrape <url> --out .baker/<dir> # capture any page n
5828
5834
  - **Inspiration, never a clipboard.** A reference's structure and mechanism are the lesson; write the client's own words.
5829
5835
  - **A capture has to land in `.baker/`, and `scrape` refuses anywhere else in a workspace.** `--out` is free-form and the directory is the agent's choice, but a capture is tens of MB of lossless PNG whose individual files clear every size gate — written to a tracked directory it becomes a permanent part of the client's repo, force-cloned into every later session. `.baker/` is gitignored precisely so a capture costs nothing. The refusal is scoped to the workspace: outside a git work tree — a local run against `/tmp` — any path is allowed, because there is no history to bloat.
5830
5836
 
5831
- ### `baker landing variant`
5832
-
5833
- Creates a new **version of a page** for an A/B test. It **imports the page's sections** and owns its own copy of only what you fork, so the two versions differ by exactly the hypothesis.
5834
-
5835
- ```bash
5836
- baker landing variant oferta --fork Hero.astro
5837
- baker landing variant oferta --fork Hero.astro,Proof.astro --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
5838
- ```
5839
-
5840
- - **You do not choose the slug.** It is `<page>--<n>`, numbered per page and never reused — so it cannot collide with an archived version, and it cannot merge with an older variant's analytics history. `--` is also what every tool decides by: which analytics id the page stamps, whether the sitemap lists it, and whether the edge answers the URL. It replaced a `<page>-b` convention that nine live pages across the fleet would have been misread by.
5841
-
5842
- - **Sharing, not copying, is what keeps the test honest.** A duplicated page drifts from its control on every later edit — a price, a typo, a new testimonial — and the test then reports that drift plus the hypothesis as one result. A shared section is one file, so an edit reaches both arms at once. Baker follows the sharing too: both pages redeploy together, and both are re-critiqued when a shared section changes.
5843
- - **Relative paths are re-aimed, not string-patched.** Every specifier in the copied files is resolved and re-relativised, so `index.astro` and a forked `_components/Hero.astro` — which sit at different depths — both keep pointing at the right thing, including the control's `_images/` and shared wrappers like `src/components/Video.astro`.
5844
- - **`_definition.md` is the page's**, with a fresh quoted `internalId`, a `(variant)` suffix on `internalTitle`, and a note recording what differs, the observation behind it and the change. `metaTitle`/`metaDescription` stay identical on purpose: the variant is served under the control's URL.
5845
- - **Errors carry the retry.** `NOT_FOUND` returns `availableSlugs` (wrong page) or `availableComponents` (wrong `--fork`); `INVALID_SLUG` fires when the slug is not a page, or is already a variant of one. Omitting `--fork` succeeds with a hint saying the page currently renders exactly the control, so a test between them cannot conclude.
5846
- - **Run `baker experiment plan` first.** Most pages never get the traffic to settle most questions.
5847
-
5848
5837
  ### `baker landing critique`
5849
5838
 
5850
5839
  Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.