@koda-sl/baker-cli 0.165.0 → 0.166.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
@@ -296,7 +296,7 @@ Read it before interpreting any null. It does not fire on the `--out` path (rows
296
296
  | Preset | Description | Default date range |
297
297
  |------------------------|------------------------------------------|--------------------|
298
298
  | `campaign-performance` | Campaign metrics overview | LAST_30_DAYS |
299
- | `keyword-analysis` | Keyword performance with match type | LAST_30_DAYS |
299
+ | `keyword-analysis` | Keyword performance per ad group | LAST_30_DAYS |
300
300
  | `positive-keywords` | Positive (targeting) keywords only | ALL_TIME |
301
301
  | `negative-keywords` | Negative (blocking) keywords only | ALL_TIME |
302
302
  | `negative-keyword-lists` | Shared negative lists + their terms | ALL_TIME |
@@ -582,6 +582,8 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
582
582
 
583
583
  **Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
584
584
 
585
+ **Pausing or removing a keyword returns a per-ad-group hint** — `keywords update --status PAUSED|REMOVED` and `keywords remove` come back with hints telling you to base the decision on `keyword_view` rows carrying `ad_group.id` + `ad_group_criterion.criterion_id`, never on campaign totals. A keyword's criterion id is derived from its text + match type, so every copy of the same keyword across ad groups shares one id and any aggregate blends the copy holding the history with the copies that never served. The `keyword-analysis` and `positive-keywords` presets select both ids so one query identifies each copy. A bid or URL edit gets no hint — only a pause/removal can strand history.
586
+
585
587
  ---
586
588
 
587
589
  ### Caching
package/dist/cli.js CHANGED
@@ -4436,6 +4436,7 @@ var FIELD_DESCRIPTIONS = {
4436
4436
  "ad_group_ad.ad.responsive_search_ad.headlines": "Ad headline variations (array)",
4437
4437
  "ad_group_ad.ad.responsive_search_ad.descriptions": "Ad description variations (array)",
4438
4438
  // Keywords
4439
+ "ad_group_criterion.criterion_id": "Keyword criterion id \u2014 derived from text + match type, so every copy of the same keyword across ad groups shares it. Pair with ad_group.id to identify one copy (writes take customers/<cid>/adGroupCriteria/<adGroupId>~<criterionId>)",
4439
4440
  "ad_group_criterion.keyword.text": "The keyword text",
4440
4441
  "ad_group_criterion.keyword.match_type": "EXACT, PHRASE, or BROAD",
4441
4442
  "ad_group_criterion.status": "ENABLED, PAUSED, or REMOVED",
@@ -7083,8 +7084,8 @@ async function stageGoogleOps(rawOps, hints) {
7083
7084
  async function stageUpdate(kind, customerId, target, payload, hints) {
7084
7085
  await stageGoogleOp({ kind, customerId, target, payload }, hints);
7085
7086
  }
7086
- async function stageTarget(kind, customerId, target) {
7087
- await stageGoogleOp({ kind, customerId, target });
7087
+ async function stageTarget(kind, customerId, target, hints) {
7088
+ await stageGoogleOp({ kind, customerId, target }, hints);
7088
7089
  }
7089
7090
  async function draftAction(path28, body) {
7090
7091
  try {
@@ -7894,7 +7895,7 @@ var PRESETS = [
7894
7895
  {
7895
7896
  name: "keyword-analysis",
7896
7897
  description: "Keyword performance with match type and quality",
7897
- 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}`,
7898
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, 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}`,
7898
7899
  defaultDateRange: "LAST_30_DAYS",
7899
7900
  defaultLimit: 200,
7900
7901
  statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
@@ -7902,7 +7903,7 @@ var PRESETS = [
7902
7903
  {
7903
7904
  name: "positive-keywords",
7904
7905
  description: "Positive (targeting) keywords only \u2014 excludes negatives",
7905
- 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}`,
7906
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, 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}`,
7906
7907
  defaultDateRange: "ALL_TIME",
7907
7908
  defaultLimit: 500,
7908
7909
  statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
@@ -8743,7 +8744,7 @@ var campaignsCommand = defineCommand30({
8743
8744
  remove: statusCommand2("google.campaign.remove", "campaign")
8744
8745
  }
8745
8746
  });
8746
- function statusCommand2(kind, entity) {
8747
+ function statusCommand2(kind, entity, hints) {
8747
8748
  return defineCommand30({
8748
8749
  meta: { name: kind.split(".")[2], description: `Stage a ${entity} ${kind.split(".")[2]}` },
8749
8750
  args: {
@@ -8752,7 +8753,7 @@ function statusCommand2(kind, entity) {
8752
8753
  },
8753
8754
  run: async ({ args }) => {
8754
8755
  const customerId = requireCustomerId(args);
8755
- await stageTarget(kind, customerId, requireTarget(args, entity));
8756
+ await stageTarget(kind, customerId, requireTarget(args, entity), hints);
8756
8757
  }
8757
8758
  });
8758
8759
  }
@@ -8807,6 +8808,14 @@ var adGroupsCommand = defineCommand30({
8807
8808
  remove: statusCommand2("google.adGroup.remove", "ad group")
8808
8809
  }
8809
8810
  });
8811
+ var KEYWORD_KILL_HINTS = [
8812
+ "Keyword metrics are per ad group. Decide this from keyword_view rows carrying ad_group.id + ad_group_criterion.criterion_id (`--preset keyword-analysis` returns both) \u2014 never from campaign totals or metrics summed across ad groups.",
8813
+ "The same keyword text in several ad groups shares ONE criterion id, so an aggregate blends the copy that holds the history with the copies that never served. Compare the copies row by row and keep the one with the impressions/conversions; if the keyword is duplicated and only one copy performs, pause the others, not it."
8814
+ ];
8815
+ function keywordKillHints(status) {
8816
+ const kills = typeof status === "string" && ["PAUSED", "REMOVED"].includes(status.toUpperCase());
8817
+ return kills ? KEYWORD_KILL_HINTS : [];
8818
+ }
8810
8819
  var keywordWriteSubcommands = {
8811
8820
  add: defineCommand30({
8812
8821
  meta: { name: "add", description: "Add keyword(s) to an ad group \u2014 one --text or a whole batch" },
@@ -8841,13 +8850,16 @@ var keywordWriteSubcommands = {
8841
8850
  args: { ...customerIdArg, "cpc-bid": { type: "string" }, status: { type: "string" } },
8842
8851
  run: async ({ args }) => {
8843
8852
  const customerId = requireCustomerId(args);
8844
- await stageUpdate("google.keyword.update", customerId, requireTarget(args, "keyword"), {
8845
- cpcBidMicros: microsFlag(args["cpc-bid"], "--cpc-bid"),
8846
- status: args.status
8847
- });
8853
+ await stageUpdate(
8854
+ "google.keyword.update",
8855
+ customerId,
8856
+ requireTarget(args, "keyword"),
8857
+ { cpcBidMicros: microsFlag(args["cpc-bid"], "--cpc-bid"), status: args.status },
8858
+ keywordKillHints(args.status)
8859
+ );
8848
8860
  }
8849
8861
  }),
8850
- remove: statusCommand2("google.keyword.remove", "keyword")
8862
+ remove: statusCommand2("google.keyword.remove", "keyword", KEYWORD_KILL_HINTS)
8851
8863
  };
8852
8864
  var NEGATIVE_COVERAGE_HINTS = [
8853
8865
  "Before publishing, confirm each term isn't already blocked: run `baker ads google query --preset negative-keyword-lists` (shared-list terms) and `--preset negative-keywords` (campaign/ad-group negatives).",