@koda-sl/baker-cli 0.150.0 → 0.151.0-dev.1a051172a

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/dist/cli.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  toModelSafeImage,
37
37
  ulid,
38
38
  validateCanvasDeep
39
- } from "./chunk-OO5BDH3J.js";
39
+ } from "./chunk-JBDSJZBZ.js";
40
40
  import {
41
41
  csvOrJson,
42
42
  daysAgoIso,
@@ -4042,11 +4042,21 @@ var FIELD_DESCRIPTIONS = {
4042
4042
  "asset_group.status": "ENABLED, PAUSED, or REMOVED",
4043
4043
  "asset_group_asset.field_type": "Where the asset is used (HEADLINE, DESCRIPTION, LOGO, etc.)",
4044
4044
  "asset_group_asset.performance_label": "Performance rating: BEST, GOOD, LOW, LEARNING",
4045
- // Change events
4045
+ // Change events (field-level detail, last 30 days)
4046
4046
  "change_event.change_date_time": "When the change occurred (ISO 8601)",
4047
4047
  "change_event.change_resource_type": "What was changed: CAMPAIGN, AD_GROUP, AD, etc.",
4048
4048
  "change_event.resource_change_operation": "CREATE, UPDATE, or REMOVE",
4049
4049
  "change_event.user_email": "Email of user who made the change",
4050
+ // Change status (which resources changed, last 90 days — no values, no user)
4051
+ "change_status.last_change_date_time": "When this resource last changed (ISO 8601)",
4052
+ "change_status.resource_type": "What changed: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION, etc.",
4053
+ "change_status.resource_status": "ADDED, CHANGED, or REMOVED \u2014 not the old or new value",
4054
+ "change_status.resource_name": "Identifier of the changed resource; query it to read its current values",
4055
+ "change_status.campaign": "Resource name of the campaign the change belongs to",
4056
+ "change_status.ad_group": "Resource name of the ad group the change belongs to",
4057
+ "change_status.ad_group_ad": "Resource name of the changed ad",
4058
+ "change_status.ad_group_criterion": "Resource name of the changed keyword or criterion",
4059
+ "change_status.campaign_criterion": "Resource name of the changed campaign-level criterion",
4050
4060
  // Landing pages
4051
4061
  "landing_page_view.unexpanded_final_url": "Landing page URL",
4052
4062
  // Geo
@@ -4064,6 +4074,89 @@ function getFieldDescriptions(fields) {
4064
4074
  return result;
4065
4075
  }
4066
4076
 
4077
+ // src/commands/ads/google/changes-window.ts
4078
+ var CHANGE_SCOPE_MAX_DAYS = {
4079
+ detail: 30,
4080
+ summary: 90
4081
+ };
4082
+ var DEFAULT_CHANGE_DAYS = 7;
4083
+ function isChangeScope(value) {
4084
+ return value === "detail" || value === "summary";
4085
+ }
4086
+ function resolveChangesWindow(input) {
4087
+ const scope = input.scope ?? "detail";
4088
+ if (!isChangeScope(scope)) {
4089
+ return {
4090
+ ok: false,
4091
+ error: {
4092
+ code: "INVALID_SCOPE",
4093
+ message: `Unknown scope '${scope}'. Use 'detail' (field-level changes, 30 days) or 'summary' (which resources changed, 90 days).`,
4094
+ fix: {
4095
+ action: "retry_with_flag",
4096
+ explanation: "Pass --scope detail or --scope summary."
4097
+ }
4098
+ }
4099
+ };
4100
+ }
4101
+ const days = input.days ?? DEFAULT_CHANGE_DAYS;
4102
+ if (!Number.isInteger(days) || days < 1) {
4103
+ return {
4104
+ ok: false,
4105
+ error: {
4106
+ code: "INVALID_LOOKBACK",
4107
+ message: `--days must be a whole number of days, got '${days}'.`,
4108
+ fix: {
4109
+ action: "retry_with_flag",
4110
+ explanation: `Pass --days as a positive integer, e.g. --days ${DEFAULT_CHANGE_DAYS}.`
4111
+ }
4112
+ }
4113
+ };
4114
+ }
4115
+ const max = CHANGE_SCOPE_MAX_DAYS[scope];
4116
+ if (days > max) {
4117
+ return { ok: false, error: lookbackTooLong(scope, days, max) };
4118
+ }
4119
+ return { ok: true, scope, days, hints: hintsFor(scope, days) };
4120
+ }
4121
+ function lookbackTooLong(scope, days, max) {
4122
+ if (scope === "detail" && days <= CHANGE_SCOPE_MAX_DAYS.summary) {
4123
+ return {
4124
+ code: "LOOKBACK_TOO_LONG",
4125
+ message: `Google keeps field-level change detail for ${max} days only; ${days} days is outside that window.`,
4126
+ fix: {
4127
+ action: "narrow_date_range",
4128
+ explanation: `Re-run with --scope summary --days ${days} to reach back ${CHANGE_SCOPE_MAX_DAYS.summary} days. Summary tells you which campaigns, ad groups, ads and criteria changed and when, but not the old and new values \u2014 read the resource itself for current values.`
4129
+ }
4130
+ };
4131
+ }
4132
+ return {
4133
+ code: "LOOKBACK_TOO_LONG",
4134
+ message: `Google's account history API reaches back ${CHANGE_SCOPE_MAX_DAYS.summary} days at most; ${days} days is outside that window.`,
4135
+ fix: {
4136
+ action: "use_different_resource",
4137
+ explanation: "Do not abort \u2014 infer the change from performance instead. Query monthly metrics per campaign (SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS) to see which campaigns stopped or started spending. That shows what changed and when, though not who changed it. Older history exists only in the Google Ads web UI, which has no API \u2014 ask the user to check it if the 'who' matters."
4138
+ }
4139
+ };
4140
+ }
4141
+ function hintsFor(scope, days) {
4142
+ const hints = [];
4143
+ if (scope === "detail") {
4144
+ hints.push(
4145
+ `Detailed history covers the last ${CHANGE_SCOPE_MAX_DAYS.detail} days. For anything older use --scope summary (reaches ${CHANGE_SCOPE_MAX_DAYS.summary} days, reports which resources changed but not their values).`
4146
+ );
4147
+ } else {
4148
+ hints.push(
4149
+ "Summary history reports which resources changed and when, not old and new values. To see what a changed resource looks like now, query it by resource_name."
4150
+ );
4151
+ if (days > CHANGE_SCOPE_MAX_DAYS.detail) {
4152
+ hints.push(
4153
+ `Changes inside the last ${CHANGE_SCOPE_MAX_DAYS.detail} days also have field-level detail \u2014 re-run with --scope detail for those.`
4154
+ );
4155
+ }
4156
+ }
4157
+ return hints;
4158
+ }
4159
+
4067
4160
  // src/commands/ads/google/correction-table.ts
4068
4161
  function buildCommand(query, ctx) {
4069
4162
  return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
@@ -4392,6 +4485,35 @@ var CORRECTION_RULES = [
4392
4485
  };
4393
4486
  }
4394
4487
  },
4488
+ // 20a. change_event past its 30-day reach → change_status, which reaches 90 days.
4489
+ // The enum prefix in the API message is what separates this from the change_status
4490
+ // case below; a bare START_DATE_TOO_OLD falls through to the query-pattern pass,
4491
+ // which routes on the resource the caller actually queried.
4492
+ {
4493
+ matchQuery: /FROM\s+change_event\b/i,
4494
+ matchApiError: /ChangeEventError\.START_DATE_TOO_OLD/i,
4495
+ fix: (ctx) => {
4496
+ const limit = ctx.originalQuery.match(/LIMIT\s+(\d+)/i)?.[1] ?? "50";
4497
+ const requested = ctx.originalQuery.match(/change_date_time\s*>=\s*'([^']+)'/i)?.[1]?.slice(0, 10);
4498
+ const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
4499
+ const start = requested && requested > ninetyDaysAgo ? requested : ninetyDaysAgo;
4500
+ const corrected = `SELECT change_status.last_change_date_time, change_status.resource_type, change_status.resource_status, change_status.resource_name, change_status.campaign, change_status.ad_group FROM change_status WHERE change_status.last_change_date_time >= '${start}' ORDER BY change_status.last_change_date_time DESC LIMIT ${limit}`;
4501
+ return {
4502
+ action: "use_different_resource",
4503
+ correctedCommand: buildCommand(corrected, ctx),
4504
+ explanation: `change_event only reaches back 30 days. change_status reaches 90 (from ${start}) but reports only which resources changed and whether they were added, changed or removed \u2014 no old/new values and no user email. Read a changed resource by its resource_name to see its current values.`
4505
+ };
4506
+ }
4507
+ },
4508
+ // 20b. Past 90 days no history resource reaches — infer the change from spend instead.
4509
+ {
4510
+ matchQuery: /FROM\s+change_status\b/i,
4511
+ matchApiError: /ChangeStatusError\.START_DATE_TOO_OLD/i,
4512
+ fix: () => ({
4513
+ action: "use_different_resource",
4514
+ explanation: "Google's account history reaches back 90 days at most, and this query asks for more. Do not abort \u2014 infer the change from performance: SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_12_MONTHS shows which campaigns started or stopped spending and when, though not who changed them. Anything older with attribution exists only in the Google Ads web UI, which has no API."
4515
+ })
4516
+ },
4395
4517
  // 20. change_event without date constraint
4396
4518
  {
4397
4519
  matchQuery: /FROM\s+change_event\b(?!.*change_date_time)/i,
@@ -4545,14 +4667,25 @@ function parseApiError(errorMessage, originalQuery, customerId) {
4545
4667
  // src/commands/ads/google/changes.ts
4546
4668
  registerSchema({
4547
4669
  command: "ads.google.changes",
4548
- description: "Get recent change logs with performance impact. Shows what changed and how it affected metrics.",
4670
+ description: "Read the account's change history. --scope detail (default) shows what changed and to what value, back 30 days; --scope summary shows which resources changed, back 90 days.",
4549
4671
  args: {
4550
4672
  "customer-id": {
4551
4673
  type: "string",
4552
4674
  description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
4553
4675
  required: false
4554
4676
  },
4555
- days: { type: "string", description: "Lookback days (default: 7, max: 90)", required: false, default: 7 },
4677
+ days: {
4678
+ type: "string",
4679
+ description: `Lookback days (default: ${DEFAULT_CHANGE_DAYS}). Max ${CHANGE_SCOPE_MAX_DAYS.detail} with --scope detail, ${CHANGE_SCOPE_MAX_DAYS.summary} with --scope summary. Google keeps nothing older behind any API.`,
4680
+ required: false,
4681
+ default: DEFAULT_CHANGE_DAYS
4682
+ },
4683
+ scope: {
4684
+ type: "string",
4685
+ description: `detail|summary (default: detail). detail = old/new values plus who made the change, ${CHANGE_SCOPE_MAX_DAYS.detail} days. summary = which resources were added, changed or removed, ${CHANGE_SCOPE_MAX_DAYS.summary} days, without values.`,
4686
+ required: false,
4687
+ default: "detail"
4688
+ },
4556
4689
  "resource-type": {
4557
4690
  type: "string",
4558
4691
  description: "Filter by type: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION",
@@ -4565,15 +4698,24 @@ registerSchema({
4565
4698
  var changesCommand = defineCommand20({
4566
4699
  meta: {
4567
4700
  name: "changes",
4568
- description: `Get recent changes in a Google Ads account with performance data.
4701
+ description: `Read the change history of a Google Ads account.
4702
+
4703
+ Google keeps account history in two layers, and neither reaches past 90 days:
4704
+ --scope detail (default) what changed, old \u2192 new value, who changed it \u2014 last ${CHANGE_SCOPE_MAX_DAYS.detail} days
4705
+ --scope summary which resources were added, changed or removed \u2014 last ${CHANGE_SCOPE_MAX_DAYS.summary} days
4706
+
4707
+ For anything older, infer it from performance instead \u2014 a monthly spend trend per
4708
+ campaign shows what started or stopped, just not who did it.
4569
4709
 
4570
4710
  Examples:
4571
4711
  baker ads google changes --customer-id 1234567890
4572
- baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN`
4712
+ baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN
4713
+ baker ads google changes --customer-id 1234567890 --days 60 --scope summary`
4573
4714
  },
4574
4715
  args: {
4575
4716
  "customer-id": { type: "string", description: "Google Ads customer ID", required: false },
4576
- days: { type: "string", description: "Lookback days (default 7)", required: false },
4717
+ days: { type: "string", description: `Lookback days (default ${DEFAULT_CHANGE_DAYS})`, required: false },
4718
+ scope: { type: "string", description: "detail (30 days) or summary (90 days)", required: false },
4577
4719
  "resource-type": { type: "string", description: "Filter by resource type", required: false },
4578
4720
  limit: { type: "string", description: "Max results (default 50)", required: false },
4579
4721
  "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false },
@@ -4581,9 +4723,19 @@ Examples:
4581
4723
  },
4582
4724
  run: async ({ args }) => {
4583
4725
  const customerId = await resolveCustomerId(args);
4726
+ const window = resolveChangesWindow({
4727
+ days: args.days ? Number(args.days) : void 0,
4728
+ scope: args.scope
4729
+ });
4730
+ if (!window.ok) {
4731
+ writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
4732
+ process.exit(1);
4733
+ return;
4734
+ }
4584
4735
  const body = {
4585
4736
  customerId,
4586
- days: args.days ? Number(args.days) : 7,
4737
+ days: window.days,
4738
+ scope: window.scope,
4587
4739
  limit: args.limit ? Number(args.limit) : 50
4588
4740
  };
4589
4741
  const managerId = getManagerIdForCustomer(customerId);
@@ -4600,7 +4752,7 @@ Examples:
4600
4752
  const first = data[0];
4601
4753
  const fields = first ? Object.keys(first) : [];
4602
4754
  const fieldDescs = getFieldDescriptions(fields);
4603
- writeAdsJson({ ok: true, data, fields: fieldDescs });
4755
+ writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
4604
4756
  } catch (err) {
4605
4757
  if (err instanceof ApiError) {
4606
4758
  writeAdsJson(parseApiError(err.message, "", customerId));