@koda-sl/baker-cli 0.243.0 → 0.245.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.
package/README.md CHANGED
@@ -278,7 +278,31 @@ baker ads google query --list-presets
278
278
  }
279
279
  ```
280
280
 
281
- Read it before interpreting any null. It does not fire on the `--out` path (rows stream to disk), and `.resource_name` is never reported (the server strips it from every row).
281
+ Read it before interpreting any null. Field coverage is folded page by page on the `--out` path too, so the file summary reports it; `.resource_name` is never reported (the server strips it from every row).
282
+
283
+ **A page is not the data set.** Without `--all`, a query returns one page capped by `--limit` (default 200, or the `LIMIT` in the statement, whichever is smaller). With `--all` every page is followed, but the statement's `LIMIT` still caps the read — and preflight adds one at `--limit` to any query that arrives without it, so `--all` on its own still stops at 200 rows. When the rows fill whichever cap is in force, or the API returns a cursor the run did not follow, the response carries a `TRUNCATED_RESULTS` warning:
284
+
285
+ ```json
286
+ {
287
+ "ok": true,
288
+ "warnings": [{ "code": "TRUNCATED_RESULTS", "message": "INCOMPLETE READ — ..." }],
289
+ "pagination": { "hasMore": true, "cursor": "eyJwYWdl..." },
290
+ "data": [...],
291
+ "notice": "Read the \"warnings\" above before using these rows: TRUNCATED_RESULTS."
292
+ }
293
+ ```
294
+
295
+ Never total or count from a response carrying it — re-run with `--all --limit 10000 --out data.csv` (the API accepts page sizes up to 10,000) and check the re-run came back without the warning. Warnings reach every output format, so no `--output` choice can hide one:
296
+
297
+ | `--output` | Where warnings land |
298
+ |------------|---------------------|
299
+ | `json` | `warnings` and `pagination` **before** `data`, one-line `notice` last |
300
+ | `csv` | `# baker <CODE>: …` comment lines under the header and after the last row |
301
+ | `md` | `> **<CODE>** — …` blockquote above and below the table |
302
+ | `jsonl` | a `{"_baker_warning":{…}}` line first and last |
303
+ | `--out` | the file summary carries `complete: true|false`, every warning, and the `notice` |
304
+
305
+ The same lines also go to stderr. A read with nothing to report prints none of it.
282
306
 
283
307
  **Response (with pagination):**
284
308
 
@@ -311,7 +335,7 @@ Read it before interpreting any null. It does not fire on the `--out` path (rows
311
335
  | `--include-paused` | Include paused entities in preset results. Off by default: presets return only actually-serving entities (enabled campaign **and** ad group **and** ad). Google Ads has no single serving flag, so the whole chain must be `ENABLED` for an entity to serve. |
312
336
  | `--limit` | Max rows per page (default 200) |
313
337
  | `--cursor` | Pagination cursor from previous response |
314
- | `--all` | Auto-paginate all results |
338
+ | `--all` | Auto-paginate all results. Exhausts the pages, not the statement's `LIMIT` — pair it with a `--limit` above the expected row count, and a read that still fills its cap is flagged `TRUNCATED_RESULTS` either way |
315
339
  | `--out` | Write data to file (format from extension: .csv, .jsonl, .json) |
316
340
  | `--append` | Append to existing file (skip CSV headers) |
317
341
  | `--output` | Output format: `json` \| `csv` \| `jsonl` \| `md` |
@@ -337,7 +361,7 @@ Read it before interpreting any null. It does not fire on the `--out` path (rows
337
361
 
338
362
  Every preset except the negatives lists and `account-summary` filters to **actually-serving entities** by default — the full status chain (`campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED'`, as applicable) — and selects the status columns so you can see them. Pass `--include-paused` to widen to `!= 'REMOVED'` (keeps paused, still drops removed). Raw GAQL (no `--preset`) has no such default: a query over a serving-hierarchy resource without a status filter emits a `SERVING_SCOPE` warning reminding you to add the chain, because Google Ads has no single serving flag.
339
363
 
340
- **"Eligible (Limited)" keywords:** an enabled keyword that barely shows is invisible in the performance presets — near-zero impressions look the same whether it is outbid, low quality, or rarely searched. `--preset keyword-serving` reads the signals behind Google Ads' status column: `ad_group_criterion.primary_status` and `primary_status_reasons` (the status enum has no `LIMITED` value — "(Limited)" lives in the reasons while the status stays `ELIGIBLE`), `position_estimates.first_page_cpc_micros` (the number in "Below first page bid (€3.63)"), the max CPC actually in force and where it comes from, quality score split into ad relevance / landing page / expected CTR, approval status, and `system_serving_status`. Any read whose rows carry those fields — preset or raw GAQL — comes back with a `KEYWORD_SERVING_LIMITED` warning naming the affected keywords (each by its ad group, since one keyword's copies share a criterion id) and the bid each would need. Below-first-page-bid and low-quality want opposite fixes (raise the bid vs. improve relevance), so the warning reports them separately. On `--out` the verdict is folded across pages as they stream to disk and returned on the file summary; as with `MISSING_FIELDS`, `--output csv|jsonl|md` emits rows only and carries no warnings.
364
+ **"Eligible (Limited)" keywords:** an enabled keyword that barely shows is invisible in the performance presets — near-zero impressions look the same whether it is outbid, low quality, or rarely searched. `--preset keyword-serving` reads the signals behind Google Ads' status column: `ad_group_criterion.primary_status` and `primary_status_reasons` (the status enum has no `LIMITED` value — "(Limited)" lives in the reasons while the status stays `ELIGIBLE`), `position_estimates.first_page_cpc_micros` (the number in "Below first page bid (€3.63)"), the max CPC actually in force and where it comes from, quality score split into ad relevance / landing page / expected CTR, approval status, and `system_serving_status`. Any read whose rows carry those fields — preset or raw GAQL — comes back with a `KEYWORD_SERVING_LIMITED` warning naming the affected keywords (each by its ad group, since one keyword's copies share a criterion id) and the bid each would need. Below-first-page-bid and low-quality want opposite fixes (raise the bid vs. improve relevance), so the warning reports them separately. On `--out` the verdict is folded across pages as they stream to disk and returned on the file summary; like every other warning it also travels in `--output csv|jsonl|md`.
341
365
 
342
366
  The warning defers to Google: when the query selects `primary_status_reasons`, a keyword Google did not flag is not reported, even if its bid sits under the first-page estimate — the estimate is advisory and the bid may not be the criterion's to set under a portfolio strategy. Only an estimates-only read (no reasons selected) falls back to inferring the limit from the bid gap.
343
367
 
@@ -432,13 +456,14 @@ baker ads google keywords discover --customer-id 1234567890 --url "https://compe
432
456
  "data": {
433
457
  "keywords": [
434
458
  {
459
+ "source": "keyword_planner_estimate",
435
460
  "keyword": "student services",
436
- "avg_monthly_searches": "1000",
437
- "competition": "LOW",
438
- "competition_index": "4",
439
- "low_top_of_page_bid_micros": 0,
440
- "high_top_of_page_bid_micros": 0,
441
- "monthly_search_volumes": [
461
+ "planner_avg_monthly_searches": "1000",
462
+ "planner_competition": "LOW",
463
+ "planner_competition_index": "4",
464
+ "planner_low_top_of_page_bid_micros": 0,
465
+ "planner_high_top_of_page_bid_micros": 0,
466
+ "planner_monthly_search_volumes": [
442
467
  { "year": "2025", "month": "APRIL", "monthly_searches": "590" }
443
468
  ]
444
469
  }
@@ -447,12 +472,13 @@ baker ads google keywords discover --customer-id 1234567890 --url "https://compe
447
472
  "next_page_token": "..."
448
473
  },
449
474
  "fields": {
475
+ "source": "Always \"keyword_planner_estimate\" — these rows are Keyword Planner ESTIMATES of total market search volume, NOT this account's data",
450
476
  "keyword": "Suggested keyword text",
451
- "avg_monthly_searches": "Average monthly search volume",
452
- "competition": "Competition level: LOW, MEDIUM, HIGH, UNKNOWN",
453
- "competition_index": "Competition index 0-100 (higher = more competitive)",
454
- "low_top_of_page_bid_micros": "Low-range CPC bid in micros (÷ 1,000,000 for currency)",
455
- "high_top_of_page_bid_micros": "High-range CPC bid in micros (÷ 1,000,000 for currency)"
477
+ "planner_avg_monthly_searches": "ESTIMATE: average monthly searches across the whole market",
478
+ "planner_competition": "ESTIMATE: competition level LOW, MEDIUM, HIGH, UNKNOWN",
479
+ "planner_competition_index": "ESTIMATE: competition index 0-100 (higher = more competitive)",
480
+ "planner_low_top_of_page_bid_micros": "ESTIMATE: low-range CPC bid in micros (÷ 1,000,000 for currency)",
481
+ "planner_high_top_of_page_bid_micros": "ESTIMATE: high-range CPC bid in micros (÷ 1,000,000 for currency)"
456
482
  }
457
483
  }
458
484
  ```
@@ -491,13 +517,14 @@ baker ads google keywords metrics --customer-id 1234567890 --keywords "running s
491
517
  "data": {
492
518
  "historical_metrics": [
493
519
  {
520
+ "source": "keyword_planner_estimate",
494
521
  "keyword": "running shoes",
495
- "avg_monthly_searches": "1000",
496
- "competition": "LOW",
497
- "competition_index": "4",
498
- "low_top_of_page_bid_micros": 0,
499
- "high_top_of_page_bid_micros": 0,
500
- "monthly_search_volumes": [
522
+ "planner_avg_monthly_searches": "1000",
523
+ "planner_competition": "LOW",
524
+ "planner_competition_index": "4",
525
+ "planner_low_top_of_page_bid_micros": 0,
526
+ "planner_high_top_of_page_bid_micros": 0,
527
+ "planner_monthly_search_volumes": [
501
528
  { "year": "2025", "month": "APRIL", "monthly_searches": "590" }
502
529
  ]
503
530
  }
@@ -1186,6 +1213,8 @@ baker analytics geo --grain postal_code --country ES
1186
1213
  baker analytics bots # which crawlers fetch the site
1187
1214
  baker analytics presets # what each report answers
1188
1215
  baker analytics ads --platform google # what Google's traffic carries, in Google's vocabulary
1216
+ baker analytics tracking # do the ad URLs carry the campaign, ad group and ad at all
1217
+ baker analytics tracking --platform meta # the same for one platform, read from their own ad account
1189
1218
  baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
1190
1219
  baker analytics map --platform google --remove kw # take that answer back
1191
1220
  baker analytics delivery --page 2 # the next page of a long list
@@ -1208,6 +1237,10 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1208
1237
 
1209
1238
  **And each `--set` is checked against the client's own ad account.** Baker reads their Google Ads tracking templates and final URL suffixes (or their Meta ads' URL parameters) and returns `evidence` per parameter: `confirmed` (their template really does fill that name with what you claimed), `contradicted` (it fills it with something else — `suggestedRole` says what), `unseen` (no template Baker can read mentions it, which is normal), `not_read` (no connection, or a platform with no template to read, with the reason). **`contradicted` is the one to stop for**: mapping a name your way when the account fills it another way produces a report that is wrong and looks entirely plausible.
1210
1239
 
1240
+ **`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.
1241
+
1242
+ **`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.
1243
+
1211
1244
  **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.
1212
1245
 
1213
1246
  An unrecognised `--timezone` is refused with a `VALIDATION_ERROR` rather than defaulting to UTC, and `--output` accepts only `json` or `md`. Pass `--timezone` whenever the client is not in UTC. Day boundaries are drawn in UTC by default, which moves two hours of every Spanish evening onto the following day — not a rounding error on a day-of-week comparison, but the wrong day.