@koda-sl/baker-cli 0.137.0 → 0.138.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
@@ -168,9 +168,12 @@ Run arbitrary GAQL queries. The most powerful command.
168
168
  # Raw GAQL
169
169
  baker ads google query "SELECT campaign.name, metrics.clicks, metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status = 'ENABLED' ORDER BY metrics.cost_micros DESC" --customer-id 1234567890
170
170
 
171
- # Use a preset (saves tokens)
171
+ # Use a preset (saves tokens) — returns only actually-serving entities by default
172
172
  baker ads google query --preset campaign-performance --customer-id 1234567890
173
173
 
174
+ # Include paused entities too (default is serving-only)
175
+ baker ads google query --preset ad-copy-performance --customer-id 1234567890 --include-paused
176
+
174
177
  # Export to CSV
175
178
  baker ads google query --preset search-terms --customer-id 1234567890 --out results.csv
176
179
 
@@ -226,6 +229,7 @@ baker ads google query --list-presets
226
229
  | `--customer-id` | Google Ads customer ID (10 digits, no dashes). Falls back to `BAKER_GOOGLE_ADS_CUSTOMER_ID` env var. |
227
230
  | `--preset` | Named query template (see presets below) |
228
231
  | `--date-range` | Override preset date range (LAST_7_DAYS, LAST_30_DAYS, etc.) |
232
+ | `--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. |
229
233
  | `--limit` | Max rows per page (default 200) |
230
234
  | `--cursor` | Pagination cursor from previous response |
231
235
  | `--all` | Auto-paginate all results |
@@ -251,6 +255,8 @@ baker ads google query --list-presets
251
255
  | `shopping-products` | Product-level shopping metrics | LAST_30_DAYS |
252
256
  | `account-summary` | Account-level totals | LAST_30_DAYS |
253
257
 
258
+ 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.
259
+
254
260
  **Pre-flight checks and auto-fixes:**
255
261
 
256
262
  The CLI rejects or automatically corrects common GAQL mistakes before hitting the API:
package/dist/cli.js CHANGED
@@ -3814,6 +3814,7 @@ var FIELD_DESCRIPTIONS = {
3814
3814
  // Keywords
3815
3815
  "ad_group_criterion.keyword.text": "The keyword text",
3816
3816
  "ad_group_criterion.keyword.match_type": "EXACT, PHRASE, or BROAD",
3817
+ "ad_group_criterion.status": "ENABLED, PAUSED, or REMOVED",
3817
3818
  "ad_group_criterion.quality_info.quality_score": "Quality score 1-10 (higher is better)",
3818
3819
  // Search terms
3819
3820
  "search_term_view.search_term": "Actual user search query that triggered the ad",
@@ -3850,6 +3851,7 @@ var FIELD_DESCRIPTIONS = {
3850
3851
  "asset.youtube_video_asset.youtube_video_id": "YouTube video ID",
3851
3852
  "asset.youtube_video_asset.youtube_video_title": "YouTube video title",
3852
3853
  "asset_group.name": "Asset group display name",
3854
+ "asset_group.status": "ENABLED, PAUSED, or REMOVED",
3853
3855
  "asset_group_asset.field_type": "Where the asset is used (HEADLINE, DESCRIPTION, LOGO, etc.)",
3854
3856
  "asset_group_asset.performance_label": "Performance rating: BEST, GOOD, LOW, LEARNING",
3855
3857
  // Change events
@@ -6267,10 +6269,10 @@ function applyAutoFixes(query, limit) {
6267
6269
  warnings.push({ code: "FIELD_RENAMED", message: "asset.<type>_asset.final_urls \u2192 asset.final_urls" });
6268
6270
  }
6269
6271
  if (/FROM\s+campaign_budget\b/i.test(corrected)) {
6270
- const whereClause = corrected.split(/\bWHERE\b/i)[1] ?? "";
6272
+ const whereClause2 = corrected.split(/\bWHERE\b/i)[1] ?? "";
6271
6273
  const selectClause = corrected.split(/\bFROM\b/i)[0] ?? "";
6272
6274
  const selectFields = new Set(selectClause.match(/campaign\.[\w.]+/g) ?? []);
6273
- const missing = [...new Set(whereClause.match(/campaign\.[\w.]+/g) ?? [])].filter(
6275
+ const missing = [...new Set(whereClause2.match(/campaign\.[\w.]+/g) ?? [])].filter(
6274
6276
  (field) => !selectFields.has(field)
6275
6277
  );
6276
6278
  if (missing.length > 0) {
@@ -6405,23 +6407,26 @@ var PRESETS = [
6405
6407
  {
6406
6408
  name: "campaign-performance",
6407
6409
  description: "Campaign-level metrics overview",
6408
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange} AND campaign.status != 'REMOVED' ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
6410
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
6409
6411
  defaultDateRange: "LAST_30_DAYS",
6410
- defaultLimit: 200
6412
+ defaultLimit: 200,
6413
+ statusFields: ["campaign.status"]
6411
6414
  },
6412
6415
  {
6413
6416
  name: "keyword-analysis",
6414
6417
  description: "Keyword performance with match type and quality",
6415
- gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange} AND campaign.status = 'ENABLED' ORDER BY metrics.impressions DESC LIMIT {limit}`,
6418
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
6416
6419
  defaultDateRange: "LAST_30_DAYS",
6417
- defaultLimit: 200
6420
+ defaultLimit: 200,
6421
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
6418
6422
  },
6419
6423
  {
6420
6424
  name: "positive-keywords",
6421
6425
  description: "Positive (targeting) keywords only \u2014 excludes negatives",
6422
- gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE AND campaign.status != 'REMOVED' AND ad_group_criterion.status != 'REMOVED' ORDER BY campaign.name LIMIT {limit}`,
6426
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
6423
6427
  defaultDateRange: "ALL_TIME",
6424
- defaultLimit: 500
6428
+ defaultLimit: 500,
6429
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
6425
6430
  },
6426
6431
  {
6427
6432
  name: "negative-keywords",
@@ -6447,30 +6452,34 @@ var PRESETS = [
6447
6452
  {
6448
6453
  name: "search-terms",
6449
6454
  description: "Actual user search queries triggering ads",
6450
- gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange} AND campaign.status = 'ENABLED' ORDER BY metrics.impressions DESC LIMIT {limit}`,
6455
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
6451
6456
  defaultDateRange: "LAST_7_DAYS",
6452
- defaultLimit: 200
6457
+ defaultLimit: 200,
6458
+ statusFields: ["campaign.status", "ad_group.status"]
6453
6459
  },
6454
6460
  {
6455
6461
  name: "ad-copy-performance",
6456
6462
  description: "Ad headline and description effectiveness",
6457
- gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.ad.final_urls, metrics.impressions, metrics.clicks, metrics.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange} AND ad_group_ad.status = 'ENABLED' ORDER BY metrics.impressions DESC LIMIT {limit}`,
6463
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.status, ad_group_ad.status, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.ad.final_urls, metrics.impressions, metrics.clicks, metrics.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
6458
6464
  defaultDateRange: "LAST_30_DAYS",
6459
- defaultLimit: 200
6465
+ defaultLimit: 200,
6466
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_ad.status"]
6460
6467
  },
6461
6468
  {
6462
6469
  name: "asset-performance",
6463
6470
  description: "Performance Max asset performance labels",
6464
- gaqlTemplate: `SELECT campaign.id, campaign.name, asset_group.name, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange} LIMIT {limit}`,
6471
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} LIMIT {limit}`,
6465
6472
  defaultDateRange: "LAST_30_DAYS",
6466
- defaultLimit: 200
6473
+ defaultLimit: 200,
6474
+ statusFields: ["campaign.status", "asset_group.status"]
6467
6475
  },
6468
6476
  {
6469
6477
  name: "shopping-products",
6470
6478
  description: "Product-level shopping performance metrics",
6471
- gaqlTemplate: `SELECT campaign.id, campaign.name, segments.product_title, segments.product_item_id, segments.product_brand, segments.product_type_l1, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM shopping_performance_view WHERE segments.date DURING {dateRange} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
6479
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, segments.product_title, segments.product_item_id, segments.product_brand, segments.product_type_l1, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM shopping_performance_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
6472
6480
  defaultDateRange: "LAST_30_DAYS",
6473
- defaultLimit: 200
6481
+ defaultLimit: 200,
6482
+ statusFields: ["campaign.status"]
6474
6483
  },
6475
6484
  {
6476
6485
  name: "account-summary",
@@ -6483,6 +6492,11 @@ var PRESETS = [
6483
6492
  function getPreset(name) {
6484
6493
  return PRESETS.find((p) => p.name === name);
6485
6494
  }
6495
+ function buildStatusScope(statusFields, includePaused) {
6496
+ if (!statusFields || statusFields.length === 0) return "";
6497
+ const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
6498
+ return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
6499
+ }
6486
6500
  function expandPreset(preset, params) {
6487
6501
  const dateRange = params.dateRange ?? preset.defaultDateRange;
6488
6502
  const limit = params.limit ?? preset.defaultLimit;
@@ -6492,7 +6506,8 @@ function expandPreset(preset, params) {
6492
6506
  dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
6493
6507
  };
6494
6508
  }
6495
- const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit));
6509
+ const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
6510
+ const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
6496
6511
  return { query };
6497
6512
  }
6498
6513
 
@@ -6509,7 +6524,7 @@ registerSchema({
6509
6524
  },
6510
6525
  preset: {
6511
6526
  type: "string",
6512
- description: "Named query preset (campaign-performance, keyword-analysis, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary)",
6527
+ description: "Named query preset. Reads (campaign-performance, keyword-analysis, positive-keywords, negative-keywords, negative-keyword-lists, negative-list-attachments, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary). Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default \u2014 pass --include-paused to include paused ones.",
6513
6528
  required: false
6514
6529
  },
6515
6530
  "date-range": {
@@ -6517,6 +6532,11 @@ registerSchema({
6517
6532
  description: "Date range for presets (LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, or BETWEEN 'X' AND 'Y')",
6518
6533
  required: false
6519
6534
  },
6535
+ "include-paused": {
6536
+ type: "boolean",
6537
+ description: "Include paused entities in preset results. Off by default: presets return only actually-serving entities. Google Ads has no single serving flag \u2014 an entity serves only when its whole chain (campaign, ad group, ad) is ENABLED, so recommendations should target serving entities unless the user asks about paused ones.",
6538
+ required: false
6539
+ },
6520
6540
  limit: { type: "number", description: "Max rows (default 200)", required: false, default: 200 },
6521
6541
  "list-presets": { type: "boolean", description: "List all available query presets", required: false },
6522
6542
  cursor: { type: "string", description: "Pagination cursor from previous response", required: false },
@@ -6625,7 +6645,11 @@ function resolveGaql(args, limit) {
6625
6645
  });
6626
6646
  return null;
6627
6647
  }
6628
- const expanded = expandPreset(preset, { dateRange: args["date-range"], limit });
6648
+ const expanded = expandPreset(preset, {
6649
+ dateRange: args["date-range"],
6650
+ limit,
6651
+ includePaused: Boolean(args["include-paused"])
6652
+ });
6629
6653
  if (expanded.dateRangeError) {
6630
6654
  writeAdsJson({
6631
6655
  ok: false,
@@ -6648,6 +6672,51 @@ function resolveGaql(args, limit) {
6648
6672
  }
6649
6673
  return gaql;
6650
6674
  }
6675
+ var SERVING_HIERARCHY_RESOURCES = [
6676
+ "campaign",
6677
+ "ad_group",
6678
+ "ad_group_ad",
6679
+ "ad_group_criterion",
6680
+ "keyword_view",
6681
+ "search_term_view",
6682
+ "shopping_performance_view",
6683
+ "asset_group_asset"
6684
+ ];
6685
+ function whereClause(upperQuery) {
6686
+ const whereIdx = upperQuery.indexOf(" WHERE ");
6687
+ if (whereIdx === -1) return "";
6688
+ let rest = upperQuery.slice(whereIdx + 7);
6689
+ for (const trailing of [" ORDER BY ", " GROUP BY ", " LIMIT ", " PARAMETERS "]) {
6690
+ const idx = rest.indexOf(trailing);
6691
+ if (idx !== -1) rest = rest.slice(0, idx);
6692
+ }
6693
+ return rest;
6694
+ }
6695
+ function servingScopeWarnings(args, finalQuery) {
6696
+ if (args.preset) {
6697
+ if (!args["include-paused"]) return [];
6698
+ return [
6699
+ {
6700
+ code: "SERVING_SCOPE",
6701
+ message: "Paused entities are included (--include-paused). Only recommend changes for actually-serving entities (enabled campaign, ad group, and ad) unless the user asked about paused ones."
6702
+ }
6703
+ ];
6704
+ }
6705
+ const upper2 = finalQuery.toUpperCase();
6706
+ const fromMatch = upper2.match(/\bFROM\s+([A-Z_]+)/);
6707
+ const resource = fromMatch?.[1]?.toLowerCase();
6708
+ const hierarchical = resource ? SERVING_HIERARCHY_RESOURCES.includes(resource) : false;
6709
+ const filtersStatus = whereClause(upper2).includes(".STATUS");
6710
+ if (hierarchical && !filtersStatus) {
6711
+ return [
6712
+ {
6713
+ code: "SERVING_SCOPE",
6714
+ message: "This query has no status filter, so it may return paused entities that aren't actually serving. Google Ads has no single serving flag \u2014 an entity serves only when its whole chain is ENABLED. Add campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED' (as applicable), or use a --preset, unless you intend to include paused entities."
6715
+ }
6716
+ ];
6717
+ }
6718
+ return [];
6719
+ }
6651
6720
  async function fetchAllPages(body, args) {
6652
6721
  let allRows = [];
6653
6722
  let pageToken = args.cursor;
@@ -6706,7 +6775,7 @@ async function validateArgs(args) {
6706
6775
  customerId,
6707
6776
  limit,
6708
6777
  finalQuery,
6709
- warnings: preflight.warnings,
6778
+ warnings: [...preflight.warnings, ...servingScopeWarnings(args, finalQuery)],
6710
6779
  useCache: !args["no-cache"],
6711
6780
  cacheKey: buildQueryCacheKey(customerId, finalQuery)
6712
6781
  };
@@ -6746,6 +6815,11 @@ Examples:
6746
6815
  "customer-id": { type: "string", description: "Google Ads customer ID (10 digits, no dashes)", required: false },
6747
6816
  preset: { type: "string", description: "Named query preset", required: false },
6748
6817
  "date-range": { type: "string", description: "Date range override for presets", required: false },
6818
+ "include-paused": {
6819
+ type: "boolean",
6820
+ description: "Include paused entities in preset results (default: serving entities only)",
6821
+ required: false
6822
+ },
6749
6823
  limit: { type: "string", description: "Max rows (default 200)", required: false },
6750
6824
  "list-presets": { type: "boolean", description: "List available presets", required: false },
6751
6825
  cursor: { type: "string", description: "Pagination cursor", required: false },
@@ -6763,7 +6837,8 @@ Examples:
6763
6837
  name: p.name,
6764
6838
  description: p.description,
6765
6839
  defaultDateRange: p.defaultDateRange,
6766
- gaql: p.gaqlTemplate
6840
+ servingOnly: (p.statusFields?.length ?? 0) > 0,
6841
+ gaql: expandPreset(p, {}).query
6767
6842
  }))
6768
6843
  });
6769
6844
  return;