@koda-sl/baker-cli 0.151.0-dev.1a051172a → 0.151.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/dist/cli.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  toModelSafeImage,
37
37
  ulid,
38
38
  validateCanvasDeep
39
- } from "./chunk-JBDSJZBZ.js";
39
+ } from "./chunk-OO5BDH3J.js";
40
40
  import {
41
41
  csvOrJson,
42
42
  daysAgoIso,
@@ -4074,1041 +4074,296 @@ function getFieldDescriptions(fields) {
4074
4074
  return result;
4075
4075
  }
4076
4076
 
4077
- // src/commands/ads/google/changes-window.ts
4077
+ // ../api/src/ads-google/change-history.ts
4078
+ var CHANGE_SCOPES = ["detail", "summary"];
4078
4079
  var CHANGE_SCOPE_MAX_DAYS = {
4079
4080
  detail: 30,
4080
4081
  summary: 90
4081
4082
  };
4082
4083
  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;
4084
+ var MAX_CHANGE_DAYS = CHANGE_SCOPE_MAX_DAYS.summary;
4085
+ function isChangeLookbackWithinReach(days, scope) {
4086
+ return days <= CHANGE_SCOPE_MAX_DAYS[scope];
4158
4087
  }
4159
4088
 
4160
- // src/commands/ads/google/correction-table.ts
4161
- function buildCommand(query, ctx) {
4162
- return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
4163
- }
4164
- var CORRECTION_RULES = [
4165
- // 0. EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE — some resources (e.g. campaign_budget)
4166
- // require fields filtered in WHERE to also be selected. The error names the exact field,
4167
- // so add it to SELECT and retry. Kept first: field-specific rules below must not shadow
4168
- // this with an unrelated rewrite when the named field happens to match their pattern.
4169
- {
4170
- matchApiError: /must be present in SELECT clause:\s*'([\w.]+)'/i,
4171
- fix: (ctx) => {
4172
- const field = ctx.apiErrorMessage?.match(/must be present in SELECT clause:\s*'([\w.]+)'/i)?.[1];
4173
- if (!field) {
4174
- return {
4175
- action: "add_required_field",
4176
- explanation: "Add the field named in the error to the SELECT clause and retry"
4177
- };
4178
- }
4179
- const corrected = ctx.originalQuery.replace(/SELECT\s+/i, `SELECT ${field}, `);
4180
- return {
4181
- action: "add_required_field",
4182
- correctedCommand: buildCommand(corrected, ctx),
4183
- explanation: `${field} is used in WHERE but this resource requires it in SELECT too \u2014 added it`
4184
- };
4185
- }
4089
+ // ../api/src/ads-google/limits.ts
4090
+ var GOOGLE_ADS_LIMITS = {
4091
+ budget: {
4092
+ nameMax: 255,
4093
+ amountMicrosMin: 1
4186
4094
  },
4187
- // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
4188
- {
4189
- matchQuery: /(?<![\w.])keyword\.text\b/,
4190
- matchApiError: /keyword\.text/i,
4191
- fix: (ctx) => {
4192
- const corrected = ctx.originalQuery.replace(/(?<![\w.])keyword\.text\b/g, "ad_group_criterion.keyword.text");
4193
- return {
4194
- action: "retry_with_modified_query",
4195
- correctedCommand: buildCommand(corrected, ctx),
4196
- explanation: "Use ad_group_criterion.keyword.text \u2014 keywords are accessed through the criterion resource"
4197
- };
4198
- }
4095
+ campaign: {
4096
+ nameMax: 255
4199
4097
  },
4200
- // 2. bare keyword.match_type → ad_group_criterion.keyword.match_type (leaves <resource>.keyword.match_type untouched)
4201
- {
4202
- matchQuery: /(?<![\w.])keyword\.match_type\b/,
4203
- matchApiError: /keyword\.match_type/i,
4204
- fix: (ctx) => {
4205
- const corrected = ctx.originalQuery.replace(
4206
- /(?<![\w.])keyword\.match_type\b/g,
4207
- "ad_group_criterion.keyword.match_type"
4208
- );
4209
- return {
4210
- action: "retry_with_modified_query",
4211
- correctedCommand: buildCommand(corrected, ctx),
4212
- explanation: "Use ad_group_criterion.keyword.match_type for keyword match type"
4213
- };
4214
- }
4098
+ adGroup: {
4099
+ nameMax: 255
4215
4100
  },
4216
- // 3. campaign_budget.* queried FROM campaign
4217
- {
4218
- matchQuery: /campaign_budget\.\w+.*FROM\s+campaign\b/i,
4219
- matchApiError: /campaign_budget.*not.*valid.*campaign|cannot.*select.*campaign_budget/i,
4220
- fix: (ctx) => {
4221
- const corrected = ctx.originalQuery.replace(/FROM\s+campaign\b/i, "FROM campaign_budget");
4222
- return {
4223
- action: "use_different_resource",
4224
- correctedCommand: buildCommand(corrected, ctx),
4225
- explanation: "campaign_budget fields must be queried FROM campaign_budget, not FROM campaign"
4226
- };
4227
- }
4101
+ keyword: {
4102
+ textMax: 80,
4103
+ wordsMax: 10
4228
4104
  },
4229
- // 4. CONTAINS → LIKE
4230
- {
4231
- matchQuery: /CONTAINS\s*\(/i,
4232
- matchApiError: /CONTAINS.*not.*supported|invalid.*operator.*CONTAINS/i,
4233
- fix: (ctx) => {
4234
- const match = ctx.originalQuery.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
4235
- if (match) {
4236
- const field = match[1]?.trim();
4237
- const value = match[2] ?? "";
4238
- const corrected = ctx.originalQuery.replace(
4239
- /CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i,
4240
- `${field} LIKE '%${value}%'`
4241
- );
4242
- return {
4243
- action: "change_operator",
4244
- correctedCommand: buildCommand(corrected, ctx),
4245
- explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
4246
- };
4247
- }
4248
- return {
4249
- action: "change_operator",
4250
- explanation: "Replace CONTAINS(field, 'value') with field LIKE '%value%'"
4251
- };
4252
- }
4105
+ responsiveSearchAd: {
4106
+ headlinesMin: 3,
4107
+ headlinesMax: 15,
4108
+ headlineTextMax: 30,
4109
+ descriptionsMin: 2,
4110
+ descriptionsMax: 4,
4111
+ descriptionTextMax: 90,
4112
+ pathMax: 15
4253
4113
  },
4254
- // 5. ad_group_criterion query without negative filter — auto-add the field
4255
- {
4256
- matchQuery: /FROM\s+ad_group_criterion\b(?!.*ad_group_criterion\.negative)/i,
4257
- fix: (ctx) => {
4258
- const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT ad_group_criterion.negative, ");
4259
- return {
4260
- action: "add_required_field",
4261
- correctedCommand: buildCommand(corrected, ctx),
4262
- explanation: "Added ad_group_criterion.negative to distinguish positive (targeting) from negative (blocking) keywords. Filter with WHERE ad_group_criterion.negative = FALSE for positives only."
4263
- };
4264
- }
4114
+ responsiveDisplayAd: {
4115
+ headlinesMin: 1,
4116
+ headlinesMax: 5,
4117
+ headlineTextMax: 30,
4118
+ longHeadlineTextMax: 90,
4119
+ descriptionsMin: 1,
4120
+ descriptionsMax: 5,
4121
+ descriptionTextMax: 90,
4122
+ businessNameMax: 25
4265
4123
  },
4266
- // 6. Missing campaign.id in ad_group queries
4267
- {
4268
- matchQuery: /FROM\s+ad_group\b/i,
4269
- matchApiError: /campaign\.id.*required|must.*include.*campaign\.id/i,
4270
- fix: (ctx) => {
4271
- const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT campaign.id, ");
4272
- return {
4273
- action: "add_required_field",
4274
- correctedCommand: buildCommand(corrected, ctx),
4275
- explanation: "Ad group queries require campaign.id in SELECT for parent context"
4276
- };
4277
- }
4124
+ sharedSet: {
4125
+ nameMax: 255
4278
4126
  },
4279
- // 6. Missing campaign.id in keyword_view queries
4280
- {
4281
- matchQuery: /FROM\s+keyword_view\b/i,
4282
- matchApiError: /campaign\.id.*required.*keyword/i,
4283
- fix: (ctx) => {
4284
- const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT campaign.id, ");
4285
- return {
4286
- action: "add_required_field",
4287
- correctedCommand: buildCommand(corrected, ctx),
4288
- explanation: "Keyword view queries require campaign.id in SELECT"
4289
- };
4290
- }
4127
+ asset: {
4128
+ sitelinkLinkTextMax: 25,
4129
+ sitelinkDescriptionMax: 35,
4130
+ calloutTextMax: 25,
4131
+ structuredSnippetHeaderMax: 25,
4132
+ structuredSnippetValuesMin: 3,
4133
+ structuredSnippetValuesMax: 10
4291
4134
  },
4292
- // 7. shopping_performance_view.product_* → segments.product_*
4293
- {
4294
- matchQuery: /shopping_performance_view\.product_\w+/,
4295
- matchApiError: /shopping_performance_view\.product/i,
4296
- fix: (ctx) => {
4297
- const corrected = ctx.originalQuery.replace(/shopping_performance_view\.product_(\w+)/g, "segments.product_$1");
4298
- return {
4299
- action: "retry_with_modified_query",
4300
- correctedCommand: buildCommand(corrected, ctx),
4301
- explanation: "Product fields are segments (segments.product_*), not fields on shopping_performance_view"
4302
- };
4303
- }
4135
+ conversionAction: {
4136
+ nameMax: 255
4304
4137
  },
4305
- // 8. Open-ended date range
4306
- {
4307
- matchQuery: /segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i,
4308
- matchApiError: /date.*range.*must.*finite|open.*ended/i,
4309
- fix: (ctx) => {
4310
- const match = ctx.originalQuery.match(/segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i);
4311
- const startDate = match?.[1] ?? "2024-01-01";
4312
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4313
- const corrected = ctx.originalQuery.replace(
4314
- /segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i,
4315
- `segments.date BETWEEN '${startDate}' AND '${today}'`
4316
- );
4317
- return {
4318
- action: "narrow_date_range",
4319
- correctedCommand: buildCommand(corrected, ctx),
4320
- explanation: "Use BETWEEN with explicit end date \u2014 open-ended ranges are not supported"
4321
- };
4322
- }
4323
- },
4324
- // 9. Missing LIMIT
4325
- {
4326
- matchQuery: /^(?!.*\bLIMIT\b)/is,
4327
- fix: (ctx) => {
4328
- const corrected = `${ctx.originalQuery.trimEnd()} LIMIT 200`;
4329
- return {
4330
- action: "retry_with_modified_query",
4331
- correctedCommand: buildCommand(corrected, ctx),
4332
- explanation: "Added LIMIT 200 to prevent excessive data transfer"
4333
- };
4334
- }
4335
- },
4336
- // 10a. Customer not found (wrong customer ID)
4337
- {
4338
- matchApiError: /CUSTOMER_NOT_FOUND|not.*found.*customer/i,
4339
- fix: () => ({
4340
- action: "reject",
4341
- explanation: "Customer ID not found. Run 'baker ads google accounts' to list valid customer IDs, then retry with a valid --customer-id."
4342
- })
4343
- },
4344
- // 10b. Not accessible / login-customer-id required
4345
- {
4346
- matchApiError: /not.*accessible|login.customer.id/i,
4347
- fix: (ctx) => ({
4348
- action: "reject",
4349
- correctedCommand: buildCommand(ctx.originalQuery, ctx),
4350
- explanation: "Account not accessible \u2014 may need a manager (MCC) login-customer-id. Run 'baker ads google accounts' to refresh the accounts cache, then retry."
4351
- })
4352
- },
4353
- // 11. Campaign type by name matching
4354
- {
4355
- matchQuery: /campaign\.name\s*LIKE\s*'%\s*(shopping|pmax|search|display|video)\s*%'/i,
4356
- fix: (ctx) => {
4357
- const match = ctx.originalQuery.match(/campaign\.name\s*LIKE\s*'%\s*(shopping|pmax|search|display|video)\s*%'/i);
4358
- const typeMap = {
4359
- shopping: "SHOPPING",
4360
- pmax: "PERFORMANCE_MAX",
4361
- search: "SEARCH",
4362
- display: "DISPLAY",
4363
- video: "VIDEO"
4364
- };
4365
- const channelType = typeMap[match?.[1]?.toLowerCase() ?? ""] ?? "SEARCH";
4366
- const corrected = ctx.originalQuery.replace(
4367
- /campaign\.name\s*LIKE\s*'%[^']*%'/i,
4368
- `campaign.advertising_channel_type = '${channelType}'`
4369
- );
4370
- return {
4371
- action: "retry_with_modified_query",
4372
- correctedCommand: buildCommand(corrected, ctx),
4373
- explanation: "Filter by campaign.advertising_channel_type enum, not by name pattern"
4374
- };
4375
- }
4376
- },
4377
- // 12. Incompatible fields
4378
- {
4379
- matchApiError: /incompatible|mutually.*exclusive|cannot.*select.*together/i,
4380
- fix: () => ({
4381
- action: "split_query",
4382
- explanation: "These fields cannot be in the same query \u2014 split into separate queries and join client-side"
4383
- })
4384
- },
4385
- // 13. campaign.status = 'ACTIVE' → 'ENABLED'
4386
- {
4387
- matchQuery: /campaign\.status\s*=\s*'ACTIVE'/i,
4388
- fix: (ctx) => {
4389
- const corrected = ctx.originalQuery.replace(/campaign\.status\s*=\s*'ACTIVE'/gi, "campaign.status = 'ENABLED'");
4390
- return {
4391
- action: "retry_with_modified_query",
4392
- correctedCommand: buildCommand(corrected, ctx),
4393
- explanation: "Campaign status uses ENABLED, not ACTIVE"
4394
- };
4395
- }
4396
- },
4397
- // 14. ad_group.status = 'ACTIVE' → 'ENABLED'
4398
- {
4399
- matchQuery: /ad_group\.status\s*=\s*'ACTIVE'/i,
4400
- fix: (ctx) => {
4401
- const corrected = ctx.originalQuery.replace(/ad_group\.status\s*=\s*'ACTIVE'/gi, "ad_group.status = 'ENABLED'");
4402
- return {
4403
- action: "retry_with_modified_query",
4404
- correctedCommand: buildCommand(corrected, ctx),
4405
- explanation: "Ad group status uses ENABLED, not ACTIVE"
4406
- };
4407
- }
4408
- },
4409
- // 15. ad.final_urls → ad_group_ad.ad.final_urls
4410
- {
4411
- matchQuery: /\bad\.final_urls\b(?!.*ad_group_ad)/,
4412
- matchApiError: /ad\.final_urls/i,
4413
- fix: (ctx) => {
4414
- const corrected = ctx.originalQuery.replace(/\bad\.final_urls\b/g, "ad_group_ad.ad.final_urls");
4415
- return {
4416
- action: "retry_with_modified_query",
4417
- correctedCommand: buildCommand(corrected, ctx),
4418
- explanation: "Use the full path: ad_group_ad.ad.final_urls"
4419
- };
4420
- }
4421
- },
4422
- // 15b. asset.<type>_asset.final_urls → asset.final_urls (final URLs live on the asset
4423
- // itself; the typed sub-message only carries type-specific fields like link_text)
4424
- {
4425
- matchQuery: /asset\.\w+_asset\.final_(mobile_)?urls\b/,
4426
- matchApiError: /asset\.\w+_asset\.final_(mobile_)?urls/i,
4427
- fix: (ctx) => {
4428
- const corrected = ctx.originalQuery.replace(/asset\.\w+_asset\.final_(mobile_)?urls\b/g, "asset.final_$1urls");
4429
- return {
4430
- action: "retry_with_modified_query",
4431
- correctedCommand: buildCommand(corrected, ctx),
4432
- explanation: "Final URLs are on the asset itself \u2014 use asset.final_urls, not asset.<type>_asset.final_urls"
4433
- };
4434
- }
4435
- },
4436
- // 16. Rate limit / quota
4437
- {
4438
- matchApiError: /RESOURCE_EXHAUSTED|quota.*exceeded|rate.*limit/i,
4439
- fix: () => ({
4440
- action: "wait_and_retry",
4441
- explanation: "API quota exceeded \u2014 wait 30 seconds before retrying"
4442
- })
4443
- },
4444
- // 17. WHERE on metrics
4445
- {
4446
- matchApiError: /cannot.*filter.*metric|WHERE.*metrics|prohibited.*where/i,
4447
- fix: () => ({
4448
- action: "reject",
4449
- explanation: "Cannot filter on metrics in WHERE clause \u2014 remove the metrics filter and filter results client-side"
4450
- })
4451
- },
4452
- // 18. ORDER BY field not in SELECT
4453
- {
4454
- matchApiError: /order.*by.*field.*not.*selected|must.*select.*field.*order/i,
4455
- fix: (ctx) => {
4456
- const orderMatch = ctx.originalQuery.match(/ORDER\s+BY\s+([\w.]+)/i);
4457
- const field = orderMatch?.[1] ?? "field";
4458
- const corrected = ctx.originalQuery.replace(/SELECT\s+/i, `SELECT ${field}, `);
4459
- return {
4460
- action: "add_required_field",
4461
- correctedCommand: buildCommand(corrected, ctx),
4462
- explanation: `Add ${field} to SELECT \u2014 ORDER BY fields must be selected`
4463
- };
4464
- }
4465
- },
4466
- // 19. Results include REMOVED entities
4467
- {
4468
- matchApiError: /REMOVED.*entities|status.*filter/i,
4469
- fix: (ctx) => {
4470
- const resourceMatch = ctx.originalQuery.match(/FROM\s+(\w+)/i);
4471
- const resource = resourceMatch?.[1] ?? "campaign";
4472
- const hasWhere = /WHERE/i.test(ctx.originalQuery);
4473
- const suffix = hasWhere ? ` AND ${resource}.status != 'REMOVED'` : ` WHERE ${resource}.status != 'REMOVED'`;
4474
- const limitMatch = ctx.originalQuery.match(/(\s+LIMIT\s+\d+)/i);
4475
- let corrected;
4476
- if (limitMatch) {
4477
- corrected = ctx.originalQuery.replace(/(\s+LIMIT\s+\d+)/i, `${suffix}$1`);
4478
- } else {
4479
- corrected = ctx.originalQuery.trimEnd() + suffix;
4480
- }
4481
- return {
4482
- action: "retry_with_modified_query",
4483
- correctedCommand: buildCommand(corrected, ctx),
4484
- explanation: `Add status filter to exclude REMOVED ${resource}s`
4485
- };
4486
- }
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
- },
4517
- // 20. change_event without date constraint
4518
- {
4519
- matchQuery: /FROM\s+change_event\b(?!.*change_date_time)/i,
4520
- matchApiError: /change_event.*date|date.*required.*change/i,
4521
- fix: (ctx) => {
4522
- const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
4523
- const hasWhere = /WHERE/i.test(ctx.originalQuery);
4524
- const suffix = hasWhere ? ` AND change_event.change_date_time >= '${sevenDaysAgo}'` : ` WHERE change_event.change_date_time >= '${sevenDaysAgo}'`;
4525
- const corrected = ctx.originalQuery.trimEnd() + suffix;
4526
- return {
4527
- action: "add_required_field",
4528
- correctedCommand: buildCommand(corrected, ctx),
4529
- explanation: "change_event queries require a date constraint on change_date_time"
4530
- };
4531
- }
4532
- },
4533
- // 21. resource_name = 'customers/X/Y/Z' → resource.id = Z
4534
- {
4535
- matchQuery: /(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i,
4536
- fix: (ctx) => {
4537
- const match = ctx.originalQuery.match(/(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i);
4538
- const resource = match?.[1] ?? "campaign";
4539
- const id = match?.[2] ?? "ID";
4540
- const corrected = ctx.originalQuery.replace(
4541
- /(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i,
4542
- `${resource}.id = ${id}`
4543
- );
4544
- return {
4545
- action: "retry_with_modified_query",
4546
- correctedCommand: buildCommand(corrected, ctx),
4547
- explanation: `Simpler: use ${resource}.id = ${id} instead of resource_name path`
4548
- };
4549
- }
4138
+ audience: {
4139
+ nameMax: 255
4550
4140
  },
4551
- // 22. segments.click_type incompatibility
4552
- {
4553
- matchApiError: /click_type.*incompatible|cannot.*click_type/i,
4554
- fix: (ctx) => {
4555
- const corrected = ctx.originalQuery.replace(/,?\s*segments\.click_type/g, "");
4556
- return {
4557
- action: "split_query",
4558
- correctedCommand: buildCommand(corrected, ctx),
4559
- explanation: "segments.click_type is incompatible with other segments \u2014 remove it or query separately"
4560
- };
4561
- }
4141
+ biddingStrategy: {
4142
+ nameMax: 255
4562
4143
  },
4563
- // 23. Bare resource in SELECT
4564
- {
4565
- matchApiError: /cannot.*select.*bare.*resource|invalid.*field/i,
4566
- fix: () => ({
4567
- action: "retry_with_modified_query",
4568
- explanation: "Cannot SELECT a bare resource name \u2014 use resource.id, resource.name, or resource.resource_name"
4569
- })
4144
+ label: {
4145
+ nameMax: 255
4570
4146
  },
4571
- // 24. Authentication error
4572
- {
4573
- matchApiError: /unauthenticated|authentication.*required|invalid.*credentials/i,
4574
- fix: () => ({
4575
- action: "authenticate",
4576
- explanation: "Google Ads authentication failed \u2014 reconnect Google Ads in dashboard settings"
4577
- })
4578
- },
4579
- // 25. Timeout / deadline exceeded
4580
- {
4581
- matchApiError: /timeout|deadline.*exceeded|DEADLINE_EXCEEDED/i,
4582
- fix: (ctx) => ({
4583
- action: "retry_with_modified_query",
4584
- correctedCommand: buildCommand(ctx.originalQuery, ctx),
4585
- explanation: "Query timed out \u2014 narrow the date range, add more WHERE filters, or reduce LIMIT"
4586
- })
4147
+ assetGroup: {
4148
+ nameMax: 255,
4149
+ headlinesMin: 3,
4150
+ headlinesMax: 15,
4151
+ headlineTextMax: 30,
4152
+ longHeadlinesMin: 1,
4153
+ longHeadlinesMax: 5,
4154
+ longHeadlineTextMax: 90,
4155
+ descriptionsMin: 2,
4156
+ descriptionsMax: 5,
4157
+ descriptionTextMax: 90,
4158
+ businessNameMax: 25
4587
4159
  }
4160
+ };
4161
+ var KEYWORD_MATCH_TYPES = ["EXACT", "PHRASE", "BROAD"];
4162
+ var ADVERTISING_CHANNEL_TYPES = [
4163
+ "SEARCH",
4164
+ "DISPLAY",
4165
+ "SHOPPING",
4166
+ "VIDEO",
4167
+ "PERFORMANCE_MAX",
4168
+ "DEMAND_GEN",
4169
+ "MULTI_CHANNEL",
4170
+ "LOCAL",
4171
+ "SMART",
4172
+ "TRAVEL",
4173
+ "HOTEL",
4174
+ "DISCOVERY"
4588
4175
  ];
4589
-
4590
- // src/commands/ads/google/error-parser.ts
4591
- function mapErrorCode(message) {
4592
- if (/must be present in SELECT clause/i.test(message)) return "MISSING_SELECT_FIELD";
4593
- if (/field.*not.*found|unrecognized.*field|not.*valid.*field/i.test(message)) return "FIELD_NOT_FOUND";
4594
- if (/not.*valid.*resource|cannot.*select.*from/i.test(message)) return "WRONG_RESOURCE";
4595
- if (/operator|CONTAINS|LIKE/i.test(message)) return "INVALID_OPERATOR";
4596
- if (/CUSTOMER_NOT_FOUND/i.test(message)) return "CUSTOMER_NOT_FOUND";
4597
- if (/login.customer.id|not.*accessible/i.test(message)) return "MISSING_MANAGER_ID";
4598
- if (/incompatible|mutually.*exclusive|PROHIBITED_SEGMENT_WITH_METRIC/i.test(message)) return "INCOMPATIBLE_FIELDS";
4599
- if (/unauthenticated|authentication/i.test(message)) return "AUTH_ERROR";
4600
- if (/permission/i.test(message)) return "PERMISSION_DENIED";
4601
- if (/RESOURCE_EXHAUSTED|quota|rate.*limit/i.test(message)) return "QUOTA_EXCEEDED";
4602
- if (/timeout|DEADLINE_EXCEEDED/i.test(message)) return "TIMEOUT";
4603
- return "API_ERROR";
4604
- }
4605
- function isRetryable(code) {
4606
- return code === "QUOTA_EXCEEDED" || code === "TIMEOUT";
4607
- }
4608
- function getRetryDelay(code) {
4609
- if (code === "QUOTA_EXCEEDED") return 3e4;
4610
- if (code === "TIMEOUT") return 5e3;
4611
- return void 0;
4176
+ var ADVERTISING_CHANNEL_SUB_TYPES = [
4177
+ "SEARCH_MOBILE_APP",
4178
+ "DISPLAY_MOBILE_APP",
4179
+ "SEARCH_EXPRESS",
4180
+ "DISPLAY_EXPRESS",
4181
+ "SHOPPING_SMART_ADS",
4182
+ "DISPLAY_GMAIL_AD",
4183
+ "DISPLAY_SMART_CAMPAIGN",
4184
+ "VIDEO_OUTSTREAM",
4185
+ "VIDEO_ACTION",
4186
+ "VIDEO_NON_SKIPPABLE",
4187
+ "APP_CAMPAIGN",
4188
+ "APP_CAMPAIGN_FOR_ENGAGEMENT",
4189
+ "LOCAL_CAMPAIGN",
4190
+ "SHOPPING_COMPARISON_LISTING_ADS",
4191
+ "SEARCH_MOBILE_APP_ENGAGEMENT",
4192
+ "TRAVEL_ACTIVITIES"
4193
+ ];
4194
+ var BIDDING_STRATEGY_TYPES = [
4195
+ "MANUAL_CPC",
4196
+ "MAXIMIZE_CONVERSIONS",
4197
+ "MAXIMIZE_CONVERSION_VALUE",
4198
+ "TARGET_SPEND",
4199
+ "TARGET_CPA",
4200
+ "TARGET_ROAS",
4201
+ "TARGET_IMPRESSION_SHARE",
4202
+ "MANUAL_CPM",
4203
+ "MANUAL_CPV",
4204
+ "PERCENT_CPC"
4205
+ ];
4206
+ var BUDGET_DELIVERY_METHODS = ["STANDARD", "ACCELERATED"];
4207
+ var AD_GROUP_TYPES = [
4208
+ "SEARCH_STANDARD",
4209
+ "DISPLAY_STANDARD",
4210
+ "SHOPPING_PRODUCT_ADS",
4211
+ "VIDEO_BUMPER",
4212
+ "VIDEO_TRUE_VIEW_IN_STREAM",
4213
+ "VIDEO_TRUE_VIEW_IN_DISPLAY",
4214
+ "VIDEO_NON_SKIPPABLE_IN_STREAM",
4215
+ "VIDEO_OUTSTREAM",
4216
+ "DEMAND_GEN_AD"
4217
+ ];
4218
+ var SHARED_SET_TYPES = [
4219
+ "NEGATIVE_KEYWORDS",
4220
+ "NEGATIVE_PLACEMENTS",
4221
+ "ACCOUNT_LEVEL_NEGATIVE_KEYWORDS"
4222
+ ];
4223
+ var STAGEABLE_CREATE_STATUSES2 = ["ENABLED", "PAUSED"];
4224
+ var PINNED_FIELDS = ["HEADLINE_1", "HEADLINE_2", "HEADLINE_3", "DESCRIPTION_1", "DESCRIPTION_2"];
4225
+ var CONVERSION_ACTION_CATEGORIES = [
4226
+ "DEFAULT",
4227
+ "PAGE_VIEW",
4228
+ "PURCHASE",
4229
+ "SIGNUP",
4230
+ "LEAD",
4231
+ "DOWNLOAD",
4232
+ "ADD_TO_CART",
4233
+ "BEGIN_CHECKOUT",
4234
+ "SUBSCRIBE_PAID",
4235
+ "PHONE_CALL_LEAD",
4236
+ "SUBMIT_LEAD_FORM",
4237
+ "BOOK_APPOINTMENT",
4238
+ "REQUEST_QUOTE",
4239
+ "CONTACT"
4240
+ ];
4241
+ var CONVERSION_ACTION_TYPES = [
4242
+ "WEBPAGE",
4243
+ "UPLOAD_CLICKS",
4244
+ "UPLOAD_CALLS",
4245
+ "WEBSITE_CALL",
4246
+ "GOOGLE_ANALYTICS_4_CUSTOM"
4247
+ ];
4248
+ var CONVERSION_COUNTING_TYPES = ["ONE_PER_CLICK", "MANY_PER_CLICK"];
4249
+ var USER_LIST_TYPES = ["CRM_BASED", "RULE_BASED", "LOGICAL", "BASIC", "LOOKALIKE"];
4250
+ var ASSET_FIELD_TYPES = [
4251
+ "SITELINK",
4252
+ "CALLOUT",
4253
+ "STRUCTURED_SNIPPET",
4254
+ "CALL",
4255
+ "PRICE",
4256
+ "PROMOTION",
4257
+ "MOBILE_APP",
4258
+ "HEADLINE",
4259
+ "DESCRIPTION",
4260
+ "LOGO",
4261
+ "MARKETING_IMAGE",
4262
+ "BUSINESS_NAME"
4263
+ ];
4264
+ var DEVICE_TYPES = ["MOBILE", "TABLET", "DESKTOP", "CONNECTED_TV", "OTHER"];
4265
+ var DAYS_OF_WEEK = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"];
4266
+ var CAMPAIGN_OBJECTIVES = [
4267
+ "SALES",
4268
+ "LEADS",
4269
+ "WEBSITE_TRAFFIC",
4270
+ "PRODUCT_BRAND_CONSIDERATION",
4271
+ "BRAND_AWARENESS_REACH",
4272
+ "APP_PROMOTION",
4273
+ "LOCAL_STORE_VISITS"
4274
+ ];
4275
+ var MICROS_PER_UNIT = 1e6;
4276
+ function toMicros(amount) {
4277
+ return Math.round(amount * MICROS_PER_UNIT);
4612
4278
  }
4613
- function parseApiError(errorMessage, originalQuery, customerId) {
4614
- const ctx = {
4615
- originalQuery,
4616
- customerId,
4617
- apiErrorMessage: errorMessage
4618
- };
4619
- for (const rule of CORRECTION_RULES) {
4620
- if (rule.matchApiError?.test(errorMessage)) {
4621
- const fix = rule.fix(ctx);
4622
- const code2 = mapErrorCode(errorMessage);
4623
- return {
4624
- ok: false,
4625
- error: {
4626
- code: code2,
4627
- message: errorMessage,
4628
- fix,
4629
- retryable: isRetryable(code2),
4630
- retryAfterMs: getRetryDelay(code2)
4631
- }
4632
- };
4633
- }
4279
+ var PLAYBOOK_DAILY_BUDGET_FLOOR_MICROS = 10 * MICROS_PER_UNIT;
4280
+ var TEMP_REF_REGEX2 = /^g_temp_[A-Za-z0-9_-]{2,}$/;
4281
+ var NUMERIC_ID_REGEX2 = /^\d+$/;
4282
+ var RESOURCE_NAME_REGEX = /^customers\/\d+\/[A-Za-z]+\/[-\w~]+$/;
4283
+ var GEO_TARGET_CONSTANT_REGEX = /^geoTargetConstants\/\d+$/;
4284
+ var LANGUAGE_CONSTANT_REGEX = /^languageConstants\/\d+$/;
4285
+
4286
+ // ../api/src/ads-google/ops.ts
4287
+ import { z as z12 } from "zod";
4288
+ var tempRefSchema2 = z12.string().regex(TEMP_REF_REGEX2, "expected a g_temp_* reference");
4289
+ var refSchema = z12.union([
4290
+ z12.string().regex(RESOURCE_NAME_REGEX, "expected a customers/\u2026/\u2026/\u2026 resource name"),
4291
+ z12.string().regex(NUMERIC_ID_REGEX2, "expected a numeric id"),
4292
+ tempRefSchema2
4293
+ ]);
4294
+ var targetRefSchema = refSchema;
4295
+ var microsSchema = z12.number().int().positive("expected a positive micros amount");
4296
+ var httpsUrlSchema2 = z12.string().url().refine((u) => u.startsWith("https://"), "final URLs must be https");
4297
+ var customerIdSchema = z12.string().regex(NUMERIC_ID_REGEX2, "customerId must be the bare numeric customer id");
4298
+ var stageableStatusSchema2 = z12.enum(STAGEABLE_CREATE_STATUSES2);
4299
+ var matchTypeSchema = z12.enum(KEYWORD_MATCH_TYPES);
4300
+ var keywordTextSchema = z12.string().min(1).max(GOOGLE_ADS_LIMITS.keyword.textMax).refine((t) => t.trim().split(/\s+/).length <= GOOGLE_ADS_LIMITS.keyword.wordsMax, "keyword exceeds 10 words");
4301
+ var budgetCreateSchema = z12.object({
4302
+ name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.budget.nameMax),
4303
+ amountMicros: microsSchema,
4304
+ deliveryMethod: z12.enum(BUDGET_DELIVERY_METHODS).default("STANDARD"),
4305
+ explicitlyShared: z12.boolean().default(false)
4306
+ });
4307
+ var budgetUpdateSchema = z12.object({
4308
+ name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.budget.nameMax).optional(),
4309
+ amountMicros: microsSchema.optional(),
4310
+ deliveryMethod: z12.enum(BUDGET_DELIVERY_METHODS).optional()
4311
+ }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
4312
+ var biddingConfigSchema = z12.object({
4313
+ type: z12.enum(BIDDING_STRATEGY_TYPES),
4314
+ targetCpaMicros: microsSchema.optional(),
4315
+ targetRoas: z12.number().positive().optional(),
4316
+ cpcBidCeilingMicros: microsSchema.optional(),
4317
+ enhancedCpcEnabled: z12.boolean().optional()
4318
+ }).superRefine((p, ctx) => {
4319
+ if (p.type === "TARGET_CPA" && p.targetCpaMicros === void 0) {
4320
+ ctx.addIssue({ code: "custom", path: ["targetCpaMicros"], message: "TARGET_CPA needs targetCpaMicros" });
4634
4321
  }
4635
- for (const rule of CORRECTION_RULES) {
4636
- if (rule.matchQuery?.test(originalQuery)) {
4637
- const fix = rule.fix(ctx);
4638
- const code2 = mapErrorCode(errorMessage);
4639
- return {
4640
- ok: false,
4641
- error: {
4642
- code: code2,
4643
- message: errorMessage,
4644
- fix,
4645
- retryable: isRetryable(code2),
4646
- retryAfterMs: getRetryDelay(code2)
4647
- }
4648
- };
4649
- }
4650
- }
4651
- const code = mapErrorCode(errorMessage);
4652
- return {
4653
- ok: false,
4654
- error: {
4655
- code,
4656
- message: errorMessage,
4657
- fix: {
4658
- action: "reject",
4659
- explanation: "Unrecognized error \u2014 verify query syntax against Google Ads GAQL reference"
4660
- },
4661
- retryable: isRetryable(code),
4662
- retryAfterMs: getRetryDelay(code)
4663
- }
4664
- };
4665
- }
4666
-
4667
- // src/commands/ads/google/changes.ts
4668
- registerSchema({
4669
- command: "ads.google.changes",
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.",
4671
- args: {
4672
- "customer-id": {
4673
- type: "string",
4674
- description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
4675
- required: false
4676
- },
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
- },
4689
- "resource-type": {
4690
- type: "string",
4691
- description: "Filter by type: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION",
4692
- required: false
4693
- },
4694
- limit: { type: "string", description: "Max change events (default: 50, max: 1000)", required: false, default: 50 },
4695
- output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" }
4322
+ if (p.type === "TARGET_ROAS" && p.targetRoas === void 0) {
4323
+ ctx.addIssue({ code: "custom", path: ["targetRoas"], message: "TARGET_ROAS needs targetRoas" });
4696
4324
  }
4697
4325
  });
4698
- var changesCommand = defineCommand20({
4699
- meta: {
4700
- name: "changes",
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.
4709
-
4710
- Examples:
4711
- baker ads google changes --customer-id 1234567890
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`
4714
- },
4715
- args: {
4716
- "customer-id": { type: "string", description: "Google Ads customer ID", 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 },
4719
- "resource-type": { type: "string", description: "Filter by resource type", required: false },
4720
- limit: { type: "string", description: "Max results (default 50)", required: false },
4721
- "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false },
4722
- output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" }
4723
- },
4724
- run: async ({ args }) => {
4725
- const customerId = await resolveCustomerId(args);
4726
- const window = resolveChangesWindow({
4727
- days: args.days ? Number(args.days) : void 0,
4728
- scope: args.scope
4326
+ var networkSettingsSchema = z12.object({
4327
+ targetGoogleSearch: z12.boolean().optional(),
4328
+ targetSearchNetwork: z12.boolean().optional(),
4329
+ targetContentNetwork: z12.boolean().optional(),
4330
+ targetPartnerSearchNetwork: z12.boolean().optional()
4331
+ });
4332
+ var dateSchema = z12.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected a YYYY-MM-DD date");
4333
+ var campaignCreateSchema2 = z12.object({
4334
+ name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.campaign.nameMax),
4335
+ channelType: z12.enum(ADVERTISING_CHANNEL_TYPES),
4336
+ channelSubType: z12.enum(ADVERTISING_CHANNEL_SUB_TYPES).optional(),
4337
+ budget: refSchema,
4338
+ /** Inline standard bidding, or a portfolio strategy ref via biddingStrategy. */
4339
+ bidding: biddingConfigSchema.optional(),
4340
+ biddingStrategy: refSchema.optional(),
4341
+ networkSettings: networkSettingsSchema.optional(),
4342
+ startDate: dateSchema.optional(),
4343
+ endDate: dateSchema.optional(),
4344
+ /** Advisory Google Ads UI objective drives warnings, not sent to the API. */
4345
+ objective: z12.enum(CAMPAIGN_OBJECTIVES).optional(),
4346
+ status: stageableStatusSchema2.default("PAUSED")
4347
+ }).superRefine((p, ctx) => {
4348
+ if (!p.bidding && !p.biddingStrategy) {
4349
+ ctx.addIssue({
4350
+ code: "custom",
4351
+ path: ["bidding"],
4352
+ message: "set an inline bidding strategy or reference a portfolio biddingStrategy"
4729
4353
  });
4730
- if (!window.ok) {
4731
- writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
4732
- process.exit(1);
4733
- return;
4734
- }
4735
- const body = {
4736
- customerId,
4737
- days: window.days,
4738
- scope: window.scope,
4739
- limit: args.limit ? Number(args.limit) : 50
4740
- };
4741
- const managerId = getManagerIdForCustomer(customerId);
4742
- if (managerId) body.managerId = managerId;
4743
- if (args["resource-type"]) body.resourceType = args["resource-type"];
4744
- if (args["no-cache"]) body.skipCache = true;
4745
- try {
4746
- const data = await apiPost("/api/ads/google/changes", body);
4747
- const format = args.output || "json";
4748
- if (format !== "json") {
4749
- writeAdsOutput(data, format);
4750
- return;
4751
- }
4752
- const first = data[0];
4753
- const fields = first ? Object.keys(first) : [];
4754
- const fieldDescs = getFieldDescriptions(fields);
4755
- writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
4756
- } catch (err) {
4757
- if (err instanceof ApiError) {
4758
- writeAdsJson(parseApiError(err.message, "", customerId));
4759
- process.exit(1);
4760
- }
4761
- writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
4762
- process.exit(1);
4763
- }
4764
4354
  }
4765
- });
4766
-
4767
- // src/commands/ads/google/currency.ts
4768
- import { defineCommand as defineCommand21 } from "citty";
4769
- registerSchema({
4770
- command: "ads.google.currency",
4771
- description: "Get the currency code for a Google Ads account. Returns currency_code, customer_id, account_name, and access_type. Call this before interpreting cost_micros values.",
4772
- args: {
4773
- "customer-id": {
4774
- type: "string",
4775
- description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
4776
- required: false
4777
- }
4355
+ if (p.bidding && p.biddingStrategy) {
4356
+ ctx.addIssue({
4357
+ code: "custom",
4358
+ path: ["bidding"],
4359
+ message: "use inline bidding OR a portfolio biddingStrategy, not both"
4360
+ });
4778
4361
  }
4779
- });
4780
- var currencyCommand = defineCommand21({
4781
- meta: {
4782
- name: "currency",
4783
- description: `Get account currency code. Use this to interpret metrics.cost_micros values.
4784
-
4785
- Examples:
4786
- baker ads google currency --customer-id 1234567890`
4787
- },
4788
- args: {
4789
- "customer-id": { type: "string", description: "Google Ads customer ID (10 digits)", required: false },
4790
- "no-cache": { type: "boolean", description: "Skip cache", required: false }
4791
- },
4792
- run: async ({ args }) => {
4793
- const customerId = await resolveCustomerId(args);
4794
- const useCache = !args["no-cache"];
4795
- const cacheKey = `currency:${customerId}`;
4796
- if (useCache) {
4797
- const cached = cacheGet("accounts", cacheKey);
4798
- if (cached) {
4799
- writeAdsJson({ ok: true, data: cached.data, cached: true });
4800
- return;
4801
- }
4802
- }
4803
- try {
4804
- const params = { "customer-id": customerId };
4805
- const managerId = getManagerIdForCustomer(customerId);
4806
- if (managerId) params["manager-id"] = managerId;
4807
- if (!useCache) params["skip-cache"] = "true";
4808
- const raw = await apiGet("/api/ads/google/currency", params);
4809
- const data = {
4810
- currency_code: raw.currencyCode,
4811
- customer_id: raw.customerId
4812
- };
4813
- if (useCache) {
4814
- cacheSet("accounts", cacheKey, data, 24 * 60 * 60 * 1e3);
4815
- }
4816
- writeAdsJson({ ok: true, data });
4817
- } catch (err) {
4818
- if (err instanceof ApiError) {
4819
- writeAdsJson(parseApiError(err.message, "", customerId));
4820
- process.exit(1);
4821
- }
4822
- writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
4823
- process.exit(1);
4824
- }
4825
- }
4826
- });
4827
-
4828
- // src/commands/ads/google/draft.ts
4829
- import { defineCommand as defineCommand22 } from "citty";
4830
-
4831
- // src/commands/ads/google/write-shared.ts
4832
- import { readFileSync as readFileSync2 } from "fs";
4833
-
4834
- // ../api/src/ads-google/limits.ts
4835
- var GOOGLE_ADS_LIMITS = {
4836
- budget: {
4837
- nameMax: 255,
4838
- amountMicrosMin: 1
4839
- },
4840
- campaign: {
4841
- nameMax: 255
4842
- },
4843
- adGroup: {
4844
- nameMax: 255
4845
- },
4846
- keyword: {
4847
- textMax: 80,
4848
- wordsMax: 10
4849
- },
4850
- responsiveSearchAd: {
4851
- headlinesMin: 3,
4852
- headlinesMax: 15,
4853
- headlineTextMax: 30,
4854
- descriptionsMin: 2,
4855
- descriptionsMax: 4,
4856
- descriptionTextMax: 90,
4857
- pathMax: 15
4858
- },
4859
- responsiveDisplayAd: {
4860
- headlinesMin: 1,
4861
- headlinesMax: 5,
4862
- headlineTextMax: 30,
4863
- longHeadlineTextMax: 90,
4864
- descriptionsMin: 1,
4865
- descriptionsMax: 5,
4866
- descriptionTextMax: 90,
4867
- businessNameMax: 25
4868
- },
4869
- sharedSet: {
4870
- nameMax: 255
4871
- },
4872
- asset: {
4873
- sitelinkLinkTextMax: 25,
4874
- sitelinkDescriptionMax: 35,
4875
- calloutTextMax: 25,
4876
- structuredSnippetHeaderMax: 25,
4877
- structuredSnippetValuesMin: 3,
4878
- structuredSnippetValuesMax: 10
4879
- },
4880
- conversionAction: {
4881
- nameMax: 255
4882
- },
4883
- audience: {
4884
- nameMax: 255
4885
- },
4886
- biddingStrategy: {
4887
- nameMax: 255
4888
- },
4889
- label: {
4890
- nameMax: 255
4891
- },
4892
- assetGroup: {
4893
- nameMax: 255,
4894
- headlinesMin: 3,
4895
- headlinesMax: 15,
4896
- headlineTextMax: 30,
4897
- longHeadlinesMin: 1,
4898
- longHeadlinesMax: 5,
4899
- longHeadlineTextMax: 90,
4900
- descriptionsMin: 2,
4901
- descriptionsMax: 5,
4902
- descriptionTextMax: 90,
4903
- businessNameMax: 25
4904
- }
4905
- };
4906
- var KEYWORD_MATCH_TYPES = ["EXACT", "PHRASE", "BROAD"];
4907
- var ADVERTISING_CHANNEL_TYPES = [
4908
- "SEARCH",
4909
- "DISPLAY",
4910
- "SHOPPING",
4911
- "VIDEO",
4912
- "PERFORMANCE_MAX",
4913
- "DEMAND_GEN",
4914
- "MULTI_CHANNEL",
4915
- "LOCAL",
4916
- "SMART",
4917
- "TRAVEL",
4918
- "HOTEL",
4919
- "DISCOVERY"
4920
- ];
4921
- var ADVERTISING_CHANNEL_SUB_TYPES = [
4922
- "SEARCH_MOBILE_APP",
4923
- "DISPLAY_MOBILE_APP",
4924
- "SEARCH_EXPRESS",
4925
- "DISPLAY_EXPRESS",
4926
- "SHOPPING_SMART_ADS",
4927
- "DISPLAY_GMAIL_AD",
4928
- "DISPLAY_SMART_CAMPAIGN",
4929
- "VIDEO_OUTSTREAM",
4930
- "VIDEO_ACTION",
4931
- "VIDEO_NON_SKIPPABLE",
4932
- "APP_CAMPAIGN",
4933
- "APP_CAMPAIGN_FOR_ENGAGEMENT",
4934
- "LOCAL_CAMPAIGN",
4935
- "SHOPPING_COMPARISON_LISTING_ADS",
4936
- "SEARCH_MOBILE_APP_ENGAGEMENT",
4937
- "TRAVEL_ACTIVITIES"
4938
- ];
4939
- var BIDDING_STRATEGY_TYPES = [
4940
- "MANUAL_CPC",
4941
- "MAXIMIZE_CONVERSIONS",
4942
- "MAXIMIZE_CONVERSION_VALUE",
4943
- "TARGET_SPEND",
4944
- "TARGET_CPA",
4945
- "TARGET_ROAS",
4946
- "TARGET_IMPRESSION_SHARE",
4947
- "MANUAL_CPM",
4948
- "MANUAL_CPV",
4949
- "PERCENT_CPC"
4950
- ];
4951
- var BUDGET_DELIVERY_METHODS = ["STANDARD", "ACCELERATED"];
4952
- var AD_GROUP_TYPES = [
4953
- "SEARCH_STANDARD",
4954
- "DISPLAY_STANDARD",
4955
- "SHOPPING_PRODUCT_ADS",
4956
- "VIDEO_BUMPER",
4957
- "VIDEO_TRUE_VIEW_IN_STREAM",
4958
- "VIDEO_TRUE_VIEW_IN_DISPLAY",
4959
- "VIDEO_NON_SKIPPABLE_IN_STREAM",
4960
- "VIDEO_OUTSTREAM",
4961
- "DEMAND_GEN_AD"
4962
- ];
4963
- var SHARED_SET_TYPES = [
4964
- "NEGATIVE_KEYWORDS",
4965
- "NEGATIVE_PLACEMENTS",
4966
- "ACCOUNT_LEVEL_NEGATIVE_KEYWORDS"
4967
- ];
4968
- var STAGEABLE_CREATE_STATUSES2 = ["ENABLED", "PAUSED"];
4969
- var PINNED_FIELDS = ["HEADLINE_1", "HEADLINE_2", "HEADLINE_3", "DESCRIPTION_1", "DESCRIPTION_2"];
4970
- var CONVERSION_ACTION_CATEGORIES = [
4971
- "DEFAULT",
4972
- "PAGE_VIEW",
4973
- "PURCHASE",
4974
- "SIGNUP",
4975
- "LEAD",
4976
- "DOWNLOAD",
4977
- "ADD_TO_CART",
4978
- "BEGIN_CHECKOUT",
4979
- "SUBSCRIBE_PAID",
4980
- "PHONE_CALL_LEAD",
4981
- "SUBMIT_LEAD_FORM",
4982
- "BOOK_APPOINTMENT",
4983
- "REQUEST_QUOTE",
4984
- "CONTACT"
4985
- ];
4986
- var CONVERSION_ACTION_TYPES = [
4987
- "WEBPAGE",
4988
- "UPLOAD_CLICKS",
4989
- "UPLOAD_CALLS",
4990
- "WEBSITE_CALL",
4991
- "GOOGLE_ANALYTICS_4_CUSTOM"
4992
- ];
4993
- var CONVERSION_COUNTING_TYPES = ["ONE_PER_CLICK", "MANY_PER_CLICK"];
4994
- var USER_LIST_TYPES = ["CRM_BASED", "RULE_BASED", "LOGICAL", "BASIC", "LOOKALIKE"];
4995
- var ASSET_FIELD_TYPES = [
4996
- "SITELINK",
4997
- "CALLOUT",
4998
- "STRUCTURED_SNIPPET",
4999
- "CALL",
5000
- "PRICE",
5001
- "PROMOTION",
5002
- "MOBILE_APP",
5003
- "HEADLINE",
5004
- "DESCRIPTION",
5005
- "LOGO",
5006
- "MARKETING_IMAGE",
5007
- "BUSINESS_NAME"
5008
- ];
5009
- var DEVICE_TYPES = ["MOBILE", "TABLET", "DESKTOP", "CONNECTED_TV", "OTHER"];
5010
- var DAYS_OF_WEEK = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"];
5011
- var CAMPAIGN_OBJECTIVES = [
5012
- "SALES",
5013
- "LEADS",
5014
- "WEBSITE_TRAFFIC",
5015
- "PRODUCT_BRAND_CONSIDERATION",
5016
- "BRAND_AWARENESS_REACH",
5017
- "APP_PROMOTION",
5018
- "LOCAL_STORE_VISITS"
5019
- ];
5020
- var MICROS_PER_UNIT = 1e6;
5021
- function toMicros(amount) {
5022
- return Math.round(amount * MICROS_PER_UNIT);
5023
- }
5024
- var PLAYBOOK_DAILY_BUDGET_FLOOR_MICROS = 10 * MICROS_PER_UNIT;
5025
- var TEMP_REF_REGEX2 = /^g_temp_[A-Za-z0-9_-]{2,}$/;
5026
- var NUMERIC_ID_REGEX2 = /^\d+$/;
5027
- var RESOURCE_NAME_REGEX = /^customers\/\d+\/[A-Za-z]+\/[-\w~]+$/;
5028
- var GEO_TARGET_CONSTANT_REGEX = /^geoTargetConstants\/\d+$/;
5029
- var LANGUAGE_CONSTANT_REGEX = /^languageConstants\/\d+$/;
5030
-
5031
- // ../api/src/ads-google/ops.ts
5032
- import { z as z12 } from "zod";
5033
- var tempRefSchema2 = z12.string().regex(TEMP_REF_REGEX2, "expected a g_temp_* reference");
5034
- var refSchema = z12.union([
5035
- z12.string().regex(RESOURCE_NAME_REGEX, "expected a customers/\u2026/\u2026/\u2026 resource name"),
5036
- z12.string().regex(NUMERIC_ID_REGEX2, "expected a numeric id"),
5037
- tempRefSchema2
5038
- ]);
5039
- var targetRefSchema = refSchema;
5040
- var microsSchema = z12.number().int().positive("expected a positive micros amount");
5041
- var httpsUrlSchema2 = z12.string().url().refine((u) => u.startsWith("https://"), "final URLs must be https");
5042
- var customerIdSchema = z12.string().regex(NUMERIC_ID_REGEX2, "customerId must be the bare numeric customer id");
5043
- var stageableStatusSchema2 = z12.enum(STAGEABLE_CREATE_STATUSES2);
5044
- var matchTypeSchema = z12.enum(KEYWORD_MATCH_TYPES);
5045
- var keywordTextSchema = z12.string().min(1).max(GOOGLE_ADS_LIMITS.keyword.textMax).refine((t) => t.trim().split(/\s+/).length <= GOOGLE_ADS_LIMITS.keyword.wordsMax, "keyword exceeds 10 words");
5046
- var budgetCreateSchema = z12.object({
5047
- name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.budget.nameMax),
5048
- amountMicros: microsSchema,
5049
- deliveryMethod: z12.enum(BUDGET_DELIVERY_METHODS).default("STANDARD"),
5050
- explicitlyShared: z12.boolean().default(false)
5051
- });
5052
- var budgetUpdateSchema = z12.object({
5053
- name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.budget.nameMax).optional(),
5054
- amountMicros: microsSchema.optional(),
5055
- deliveryMethod: z12.enum(BUDGET_DELIVERY_METHODS).optional()
5056
- }).refine((p) => Object.values(p).some((v) => v !== void 0), "update needs at least one field");
5057
- var biddingConfigSchema = z12.object({
5058
- type: z12.enum(BIDDING_STRATEGY_TYPES),
5059
- targetCpaMicros: microsSchema.optional(),
5060
- targetRoas: z12.number().positive().optional(),
5061
- cpcBidCeilingMicros: microsSchema.optional(),
5062
- enhancedCpcEnabled: z12.boolean().optional()
5063
- }).superRefine((p, ctx) => {
5064
- if (p.type === "TARGET_CPA" && p.targetCpaMicros === void 0) {
5065
- ctx.addIssue({ code: "custom", path: ["targetCpaMicros"], message: "TARGET_CPA needs targetCpaMicros" });
5066
- }
5067
- if (p.type === "TARGET_ROAS" && p.targetRoas === void 0) {
5068
- ctx.addIssue({ code: "custom", path: ["targetRoas"], message: "TARGET_ROAS needs targetRoas" });
5069
- }
5070
- });
5071
- var networkSettingsSchema = z12.object({
5072
- targetGoogleSearch: z12.boolean().optional(),
5073
- targetSearchNetwork: z12.boolean().optional(),
5074
- targetContentNetwork: z12.boolean().optional(),
5075
- targetPartnerSearchNetwork: z12.boolean().optional()
5076
- });
5077
- var dateSchema = z12.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected a YYYY-MM-DD date");
5078
- var campaignCreateSchema2 = z12.object({
5079
- name: z12.string().min(1).max(GOOGLE_ADS_LIMITS.campaign.nameMax),
5080
- channelType: z12.enum(ADVERTISING_CHANNEL_TYPES),
5081
- channelSubType: z12.enum(ADVERTISING_CHANNEL_SUB_TYPES).optional(),
5082
- budget: refSchema,
5083
- /** Inline standard bidding, or a portfolio strategy ref via biddingStrategy. */
5084
- bidding: biddingConfigSchema.optional(),
5085
- biddingStrategy: refSchema.optional(),
5086
- networkSettings: networkSettingsSchema.optional(),
5087
- startDate: dateSchema.optional(),
5088
- endDate: dateSchema.optional(),
5089
- /** Advisory Google Ads UI objective — drives warnings, not sent to the API. */
5090
- objective: z12.enum(CAMPAIGN_OBJECTIVES).optional(),
5091
- status: stageableStatusSchema2.default("PAUSED")
5092
- }).superRefine((p, ctx) => {
5093
- if (!p.bidding && !p.biddingStrategy) {
5094
- ctx.addIssue({
5095
- code: "custom",
5096
- path: ["bidding"],
5097
- message: "set an inline bidding strategy or reference a portfolio biddingStrategy"
5098
- });
5099
- }
5100
- if (p.bidding && p.biddingStrategy) {
5101
- ctx.addIssue({
5102
- code: "custom",
5103
- path: ["bidding"],
5104
- message: "use inline bidding OR a portfolio biddingStrategy, not both"
5105
- });
5106
- }
5107
- if (p.channelType === "PERFORMANCE_MAX" && p.bidding && p.bidding.type === "MANUAL_CPC") {
5108
- ctx.addIssue({ code: "custom", path: ["bidding"], message: "Performance Max does not support Manual CPC" });
5109
- }
5110
- if (p.startDate && p.endDate && p.endDate <= p.startDate) {
5111
- ctx.addIssue({ code: "custom", path: ["endDate"], message: "endDate must be after startDate" });
4362
+ if (p.channelType === "PERFORMANCE_MAX" && p.bidding && p.bidding.type === "MANUAL_CPC") {
4363
+ ctx.addIssue({ code: "custom", path: ["bidding"], message: "Performance Max does not support Manual CPC" });
4364
+ }
4365
+ if (p.startDate && p.endDate && p.endDate <= p.startDate) {
4366
+ ctx.addIssue({ code: "custom", path: ["endDate"], message: "endDate must be after startDate" });
5112
4367
  }
5113
4368
  });
5114
4369
  var campaignUpdateSchema2 = z12.object({
@@ -5432,250 +4687,1028 @@ var campaignCriterionAddSchema = z12.object({
5432
4687
  path: ["criterion", "endMinute"]
5433
4688
  });
5434
4689
  }
5435
- });
5436
- var GOOGLE_DRAFT_OP_KINDS = [
5437
- "google.budget.create",
5438
- "google.budget.update",
5439
- "google.campaign.create",
5440
- "google.campaign.update",
5441
- "google.campaign.pause",
5442
- "google.campaign.resume",
5443
- "google.campaign.remove",
5444
- "google.adGroup.create",
5445
- "google.adGroup.update",
5446
- "google.adGroup.pause",
5447
- "google.adGroup.resume",
5448
- "google.adGroup.remove",
5449
- "google.keyword.add",
5450
- "google.keyword.update",
5451
- "google.keyword.remove",
5452
- "google.negativeKeyword.add",
5453
- "google.negativeKeyword.remove",
5454
- "google.sharedSet.create",
5455
- "google.sharedSetMember.add",
5456
- "google.sharedSetMember.remove",
5457
- "google.campaignSharedSet.attach",
5458
- "google.campaignSharedSet.detach",
5459
- "google.ad.create",
5460
- "google.ad.update",
5461
- "google.ad.pause",
5462
- "google.ad.resume",
5463
- "google.ad.remove",
5464
- "google.asset.create",
5465
- "google.asset.update",
5466
- "google.assetLink.attach",
5467
- "google.assetLink.detach",
5468
- "google.assetGroup.create",
5469
- "google.assetGroup.update",
5470
- "google.audience.create",
5471
- "google.audienceCriterion.attach",
5472
- "google.audienceCriterion.detach",
5473
- "google.conversionAction.create",
5474
- "google.conversionAction.update",
5475
- "google.biddingStrategy.create",
5476
- "google.biddingStrategy.update",
5477
- "google.label.create",
5478
- "google.label.attach",
5479
- "google.campaignCriterion.add",
5480
- "google.campaignCriterion.remove"
4690
+ });
4691
+ var GOOGLE_DRAFT_OP_KINDS = [
4692
+ "google.budget.create",
4693
+ "google.budget.update",
4694
+ "google.campaign.create",
4695
+ "google.campaign.update",
4696
+ "google.campaign.pause",
4697
+ "google.campaign.resume",
4698
+ "google.campaign.remove",
4699
+ "google.adGroup.create",
4700
+ "google.adGroup.update",
4701
+ "google.adGroup.pause",
4702
+ "google.adGroup.resume",
4703
+ "google.adGroup.remove",
4704
+ "google.keyword.add",
4705
+ "google.keyword.update",
4706
+ "google.keyword.remove",
4707
+ "google.negativeKeyword.add",
4708
+ "google.negativeKeyword.remove",
4709
+ "google.sharedSet.create",
4710
+ "google.sharedSetMember.add",
4711
+ "google.sharedSetMember.remove",
4712
+ "google.campaignSharedSet.attach",
4713
+ "google.campaignSharedSet.detach",
4714
+ "google.ad.create",
4715
+ "google.ad.update",
4716
+ "google.ad.pause",
4717
+ "google.ad.resume",
4718
+ "google.ad.remove",
4719
+ "google.asset.create",
4720
+ "google.asset.update",
4721
+ "google.assetLink.attach",
4722
+ "google.assetLink.detach",
4723
+ "google.assetGroup.create",
4724
+ "google.assetGroup.update",
4725
+ "google.audience.create",
4726
+ "google.audienceCriterion.attach",
4727
+ "google.audienceCriterion.detach",
4728
+ "google.conversionAction.create",
4729
+ "google.conversionAction.update",
4730
+ "google.biddingStrategy.create",
4731
+ "google.biddingStrategy.update",
4732
+ "google.label.create",
4733
+ "google.label.attach",
4734
+ "google.campaignCriterion.add",
4735
+ "google.campaignCriterion.remove"
4736
+ ];
4737
+ var googleDraftOpKindSchema = z12.enum(GOOGLE_DRAFT_OP_KINDS);
4738
+ function createOp2(kind, payload) {
4739
+ return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, payload });
4740
+ }
4741
+ function updateOp2(kind, payload) {
4742
+ return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, target: targetRefSchema, payload });
4743
+ }
4744
+ function targetOp(kind) {
4745
+ return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, target: targetRefSchema });
4746
+ }
4747
+ var googleDraftOpInputSchema = z12.discriminatedUnion("kind", [
4748
+ createOp2("google.budget.create", budgetCreateSchema),
4749
+ updateOp2("google.budget.update", budgetUpdateSchema),
4750
+ createOp2("google.campaign.create", campaignCreateSchema2),
4751
+ updateOp2("google.campaign.update", campaignUpdateSchema2),
4752
+ targetOp("google.campaign.pause"),
4753
+ targetOp("google.campaign.resume"),
4754
+ targetOp("google.campaign.remove"),
4755
+ createOp2("google.adGroup.create", adGroupCreateSchema),
4756
+ updateOp2("google.adGroup.update", adGroupUpdateSchema),
4757
+ targetOp("google.adGroup.pause"),
4758
+ targetOp("google.adGroup.resume"),
4759
+ targetOp("google.adGroup.remove"),
4760
+ createOp2("google.keyword.add", keywordAddSchema),
4761
+ updateOp2("google.keyword.update", keywordUpdateSchema),
4762
+ targetOp("google.keyword.remove"),
4763
+ createOp2("google.negativeKeyword.add", negativeKeywordAddSchema),
4764
+ targetOp("google.negativeKeyword.remove"),
4765
+ createOp2("google.sharedSet.create", sharedSetCreateSchema),
4766
+ createOp2("google.sharedSetMember.add", sharedSetMemberAddSchema),
4767
+ targetOp("google.sharedSetMember.remove"),
4768
+ createOp2("google.campaignSharedSet.attach", campaignSharedSetAttachSchema),
4769
+ targetOp("google.campaignSharedSet.detach"),
4770
+ createOp2("google.ad.create", adCreateSchema),
4771
+ updateOp2("google.ad.update", adUpdateSchema),
4772
+ targetOp("google.ad.pause"),
4773
+ targetOp("google.ad.resume"),
4774
+ targetOp("google.ad.remove"),
4775
+ createOp2("google.asset.create", assetCreateSchema),
4776
+ updateOp2("google.asset.update", assetUpdateSchema),
4777
+ createOp2("google.assetLink.attach", assetLinkAttachSchema),
4778
+ targetOp("google.assetLink.detach"),
4779
+ createOp2("google.assetGroup.create", assetGroupCreateSchema),
4780
+ updateOp2("google.assetGroup.update", assetGroupUpdateSchema),
4781
+ createOp2("google.audience.create", audienceCreateSchema2),
4782
+ createOp2("google.audienceCriterion.attach", audienceCriterionAttachSchema),
4783
+ targetOp("google.audienceCriterion.detach"),
4784
+ createOp2("google.conversionAction.create", conversionActionCreateSchema),
4785
+ updateOp2("google.conversionAction.update", conversionActionUpdateSchema),
4786
+ createOp2("google.biddingStrategy.create", biddingStrategyCreateSchema),
4787
+ updateOp2("google.biddingStrategy.update", biddingStrategyUpdateSchema),
4788
+ createOp2("google.label.create", labelCreateSchema),
4789
+ createOp2("google.label.attach", labelAttachSchema),
4790
+ createOp2("google.campaignCriterion.add", campaignCriterionAddSchema),
4791
+ targetOp("google.campaignCriterion.remove")
4792
+ ]);
4793
+
4794
+ // ../api/src/ads-google/wire.ts
4795
+ import { z as z13 } from "zod";
4796
+ var googleWriteModeSchema = z13.enum(["live", "simulated"]);
4797
+ var googleDraftOpResultSchema = z13.object({
4798
+ status: z13.enum(["applied", "simulated", "failed", "skipped"]),
4799
+ resourceName: z13.string().optional(),
4800
+ error: z13.string().optional(),
4801
+ skippedBecause: z13.string().optional(),
4802
+ executedAt: z13.number().optional()
4803
+ });
4804
+ var googleDraftStageRequestSchema = z13.object({
4805
+ chatId: z13.string(),
4806
+ op: googleDraftOpInputSchema
4807
+ });
4808
+ var googleDraftStageResponseSchema = z13.discriminatedUnion("staged", [
4809
+ z13.object({
4810
+ staged: z13.literal(true),
4811
+ ref: z13.string(),
4812
+ kind: googleDraftOpKindSchema,
4813
+ mode: googleWriteModeSchema,
4814
+ dependsOn: z13.array(z13.string()),
4815
+ summary: z13.string(),
4816
+ warnings: z13.array(z13.string()),
4817
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
4818
+ amended: z13.boolean().optional()
4819
+ }),
4820
+ z13.object({
4821
+ staged: z13.literal(false),
4822
+ noop: z13.literal(true),
4823
+ kind: googleDraftOpKindSchema,
4824
+ mode: googleWriteModeSchema,
4825
+ summary: z13.string(),
4826
+ reason: z13.string()
4827
+ })
4828
+ ]);
4829
+ var googleDraftAmendRequestSchema = z13.object({
4830
+ chatId: z13.string(),
4831
+ ref: z13.string(),
4832
+ patch: z13.record(z13.string(), z13.unknown())
4833
+ });
4834
+ var googleDraftShowRequestSchema = z13.object({
4835
+ chatId: z13.string(),
4836
+ ref: z13.string()
4837
+ });
4838
+ var GOOGLE_DRAFT_BATCH_MAX = 500;
4839
+ var googleDraftStageBatchRequestSchema = z13.object({
4840
+ chatId: z13.string(),
4841
+ ops: z13.array(googleDraftOpInputSchema).min(1).max(GOOGLE_DRAFT_BATCH_MAX)
4842
+ });
4843
+ var googleDraftStageBatchResponseSchema = z13.object({
4844
+ staged: z13.literal(true),
4845
+ mode: googleWriteModeSchema,
4846
+ count: z13.number(),
4847
+ ops: z13.array(
4848
+ z13.object({
4849
+ ref: z13.string(),
4850
+ kind: googleDraftOpKindSchema,
4851
+ dependsOn: z13.array(z13.string()),
4852
+ summary: z13.string(),
4853
+ warnings: z13.array(z13.string())
4854
+ })
4855
+ ),
4856
+ skipped: z13.array(z13.object({ kind: googleDraftOpKindSchema, summary: z13.string(), reason: z13.string() })).optional()
4857
+ });
4858
+ var googleDraftOpViewSchema = z13.object({
4859
+ ref: z13.string(),
4860
+ kind: googleDraftOpKindSchema,
4861
+ customerId: z13.string(),
4862
+ target: z13.string().optional(),
4863
+ dependsOn: z13.array(z13.string()),
4864
+ summary: z13.string(),
4865
+ stagedAt: z13.number(),
4866
+ result: googleDraftOpResultSchema.optional()
4867
+ });
4868
+ var googleDraftShowResponseSchema = z13.object({
4869
+ op: googleDraftOpViewSchema.extend({
4870
+ payload: z13.unknown().optional(),
4871
+ warnings: z13.array(z13.string()).optional(),
4872
+ annotations: z13.unknown().optional()
4873
+ })
4874
+ });
4875
+ var googleDraftListRequestSchema = z13.object({
4876
+ chatId: z13.string()
4877
+ });
4878
+ var googleDraftAdvisorySchema = z13.object({
4879
+ scope: z13.enum(["campaign", "adGroup"]),
4880
+ message: z13.string()
4881
+ });
4882
+ var googleDraftStatusCollectionSchema = z13.object({
4883
+ label: z13.string(),
4884
+ added: z13.number(),
4885
+ removed: z13.number(),
4886
+ existing: z13.number()
4887
+ });
4888
+ var googleDraftChangeOperationSchema = z13.enum(["create", "update", "pause", "resume", "remove"]);
4889
+ var googleDraftStatusNodeSchema = z13.lazy(
4890
+ () => z13.object({
4891
+ entity: z13.string(),
4892
+ name: z13.string(),
4893
+ operation: googleDraftChangeOperationSchema.optional(),
4894
+ existing: z13.boolean(),
4895
+ collections: z13.array(googleDraftStatusCollectionSchema),
4896
+ children: z13.array(googleDraftStatusNodeSchema),
4897
+ warnings: z13.array(z13.string()).optional()
4898
+ })
4899
+ );
4900
+ var googleDraftListResponseSchema = z13.object({
4901
+ status: z13.enum(["active", "publishing", "applied", "discarded", "none"]),
4902
+ mode: googleWriteModeSchema,
4903
+ count: z13.number(),
4904
+ ops: z13.array(googleDraftOpViewSchema),
4905
+ /** Grouped campaign ▸ ad group ▸ ad tree for the readable CLI status view. */
4906
+ tree: z13.array(googleDraftStatusNodeSchema).optional(),
4907
+ /** Non-blocking completeness advisories for the whole draft. */
4908
+ advisories: z13.array(googleDraftAdvisorySchema).optional()
4909
+ });
4910
+ var googleDraftRemoveRequestSchema = z13.object({
4911
+ chatId: z13.string(),
4912
+ ref: z13.string()
4913
+ });
4914
+ var googleDraftRemoveResponseSchema = z13.object({
4915
+ /** The requested ref plus any dependents removed by cascade. */
4916
+ removed: z13.array(z13.string())
4917
+ });
4918
+ var googleDraftClearRequestSchema = z13.object({
4919
+ chatId: z13.string()
4920
+ });
4921
+ var googleDraftClearResponseSchema = z13.object({
4922
+ cleared: z13.number()
4923
+ });
4924
+ var googleFieldErrorSchema = z13.object({
4925
+ path: z13.string(),
4926
+ message: z13.string()
4927
+ });
4928
+ var googleDraftErrorResponseSchema = z13.object({
4929
+ code: z13.string(),
4930
+ error: z13.string(),
4931
+ fields: z13.array(googleFieldErrorSchema).optional()
4932
+ });
4933
+
4934
+ // src/commands/ads/google/changes-window.ts
4935
+ function isChangeScope(value) {
4936
+ return CHANGE_SCOPES.includes(value);
4937
+ }
4938
+ function resolveChangesWindow(input) {
4939
+ const scope = input.scope ?? "detail";
4940
+ if (!isChangeScope(scope)) {
4941
+ return {
4942
+ ok: false,
4943
+ error: {
4944
+ code: "INVALID_SCOPE",
4945
+ message: `Unknown scope '${scope}'. Use 'detail' (field-level changes, 30 days) or 'summary' (which resources changed, 90 days).`,
4946
+ fix: {
4947
+ action: "retry_with_flag",
4948
+ explanation: "Pass --scope detail or --scope summary."
4949
+ }
4950
+ }
4951
+ };
4952
+ }
4953
+ const days = input.days ?? DEFAULT_CHANGE_DAYS;
4954
+ if (!Number.isInteger(days) || days < 1) {
4955
+ return {
4956
+ ok: false,
4957
+ error: {
4958
+ code: "INVALID_LOOKBACK",
4959
+ message: `--days must be a whole number of days, got '${days}'.`,
4960
+ fix: {
4961
+ action: "retry_with_flag",
4962
+ explanation: `Pass --days as a positive integer, e.g. --days ${DEFAULT_CHANGE_DAYS}.`
4963
+ }
4964
+ }
4965
+ };
4966
+ }
4967
+ if (!isChangeLookbackWithinReach(days, scope)) {
4968
+ return { ok: false, error: lookbackTooLong(scope, days, CHANGE_SCOPE_MAX_DAYS[scope]) };
4969
+ }
4970
+ return { ok: true, scope, days, hints: hintsFor(scope, days) };
4971
+ }
4972
+ function lookbackTooLong(scope, days, max) {
4973
+ if (scope === "detail" && days <= CHANGE_SCOPE_MAX_DAYS.summary) {
4974
+ return {
4975
+ code: "LOOKBACK_TOO_LONG",
4976
+ message: `Google keeps field-level change detail for ${max} days only; ${days} days is outside that window.`,
4977
+ fix: {
4978
+ action: "narrow_date_range",
4979
+ 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.`
4980
+ }
4981
+ };
4982
+ }
4983
+ return {
4984
+ code: "LOOKBACK_TOO_LONG",
4985
+ message: `Google's account history API reaches back ${CHANGE_SCOPE_MAX_DAYS.summary} days at most; ${days} days is outside that window.`,
4986
+ fix: {
4987
+ action: "use_different_resource",
4988
+ 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."
4989
+ }
4990
+ };
4991
+ }
4992
+ function hintsFor(scope, days) {
4993
+ const hints = [];
4994
+ if (scope === "detail") {
4995
+ hints.push(
4996
+ `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).`
4997
+ );
4998
+ } else {
4999
+ hints.push(
5000
+ "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."
5001
+ );
5002
+ if (days > CHANGE_SCOPE_MAX_DAYS.detail) {
5003
+ hints.push(
5004
+ `Changes inside the last ${CHANGE_SCOPE_MAX_DAYS.detail} days also have field-level detail \u2014 re-run with --scope detail for those.`
5005
+ );
5006
+ }
5007
+ }
5008
+ return hints;
5009
+ }
5010
+
5011
+ // src/commands/ads/google/correction-table.ts
5012
+ function buildCommand(query, ctx) {
5013
+ return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
5014
+ }
5015
+ var CORRECTION_RULES = [
5016
+ // 0. EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE — some resources (e.g. campaign_budget)
5017
+ // require fields filtered in WHERE to also be selected. The error names the exact field,
5018
+ // so add it to SELECT and retry. Kept first: field-specific rules below must not shadow
5019
+ // this with an unrelated rewrite when the named field happens to match their pattern.
5020
+ {
5021
+ matchApiError: /must be present in SELECT clause:\s*'([\w.]+)'/i,
5022
+ fix: (ctx) => {
5023
+ const field = ctx.apiErrorMessage?.match(/must be present in SELECT clause:\s*'([\w.]+)'/i)?.[1];
5024
+ if (!field) {
5025
+ return {
5026
+ action: "add_required_field",
5027
+ explanation: "Add the field named in the error to the SELECT clause and retry"
5028
+ };
5029
+ }
5030
+ const corrected = ctx.originalQuery.replace(/SELECT\s+/i, `SELECT ${field}, `);
5031
+ return {
5032
+ action: "add_required_field",
5033
+ correctedCommand: buildCommand(corrected, ctx),
5034
+ explanation: `${field} is used in WHERE but this resource requires it in SELECT too \u2014 added it`
5035
+ };
5036
+ }
5037
+ },
5038
+ // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
5039
+ {
5040
+ matchQuery: /(?<![\w.])keyword\.text\b/,
5041
+ matchApiError: /keyword\.text/i,
5042
+ fix: (ctx) => {
5043
+ const corrected = ctx.originalQuery.replace(/(?<![\w.])keyword\.text\b/g, "ad_group_criterion.keyword.text");
5044
+ return {
5045
+ action: "retry_with_modified_query",
5046
+ correctedCommand: buildCommand(corrected, ctx),
5047
+ explanation: "Use ad_group_criterion.keyword.text \u2014 keywords are accessed through the criterion resource"
5048
+ };
5049
+ }
5050
+ },
5051
+ // 2. bare keyword.match_type → ad_group_criterion.keyword.match_type (leaves <resource>.keyword.match_type untouched)
5052
+ {
5053
+ matchQuery: /(?<![\w.])keyword\.match_type\b/,
5054
+ matchApiError: /keyword\.match_type/i,
5055
+ fix: (ctx) => {
5056
+ const corrected = ctx.originalQuery.replace(
5057
+ /(?<![\w.])keyword\.match_type\b/g,
5058
+ "ad_group_criterion.keyword.match_type"
5059
+ );
5060
+ return {
5061
+ action: "retry_with_modified_query",
5062
+ correctedCommand: buildCommand(corrected, ctx),
5063
+ explanation: "Use ad_group_criterion.keyword.match_type for keyword match type"
5064
+ };
5065
+ }
5066
+ },
5067
+ // 3. campaign_budget.* queried FROM campaign
5068
+ {
5069
+ matchQuery: /campaign_budget\.\w+.*FROM\s+campaign\b/i,
5070
+ matchApiError: /campaign_budget.*not.*valid.*campaign|cannot.*select.*campaign_budget/i,
5071
+ fix: (ctx) => {
5072
+ const corrected = ctx.originalQuery.replace(/FROM\s+campaign\b/i, "FROM campaign_budget");
5073
+ return {
5074
+ action: "use_different_resource",
5075
+ correctedCommand: buildCommand(corrected, ctx),
5076
+ explanation: "campaign_budget fields must be queried FROM campaign_budget, not FROM campaign"
5077
+ };
5078
+ }
5079
+ },
5080
+ // 4. CONTAINS → LIKE
5081
+ {
5082
+ matchQuery: /CONTAINS\s*\(/i,
5083
+ matchApiError: /CONTAINS.*not.*supported|invalid.*operator.*CONTAINS/i,
5084
+ fix: (ctx) => {
5085
+ const match = ctx.originalQuery.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
5086
+ if (match) {
5087
+ const field = match[1]?.trim();
5088
+ const value = match[2] ?? "";
5089
+ const corrected = ctx.originalQuery.replace(
5090
+ /CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i,
5091
+ `${field} LIKE '%${value}%'`
5092
+ );
5093
+ return {
5094
+ action: "change_operator",
5095
+ correctedCommand: buildCommand(corrected, ctx),
5096
+ explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
5097
+ };
5098
+ }
5099
+ return {
5100
+ action: "change_operator",
5101
+ explanation: "Replace CONTAINS(field, 'value') with field LIKE '%value%'"
5102
+ };
5103
+ }
5104
+ },
5105
+ // 5. ad_group_criterion query without negative filter — auto-add the field
5106
+ {
5107
+ matchQuery: /FROM\s+ad_group_criterion\b(?!.*ad_group_criterion\.negative)/i,
5108
+ fix: (ctx) => {
5109
+ const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT ad_group_criterion.negative, ");
5110
+ return {
5111
+ action: "add_required_field",
5112
+ correctedCommand: buildCommand(corrected, ctx),
5113
+ explanation: "Added ad_group_criterion.negative to distinguish positive (targeting) from negative (blocking) keywords. Filter with WHERE ad_group_criterion.negative = FALSE for positives only."
5114
+ };
5115
+ }
5116
+ },
5117
+ // 6. Missing campaign.id in ad_group queries
5118
+ {
5119
+ matchQuery: /FROM\s+ad_group\b/i,
5120
+ matchApiError: /campaign\.id.*required|must.*include.*campaign\.id/i,
5121
+ fix: (ctx) => {
5122
+ const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT campaign.id, ");
5123
+ return {
5124
+ action: "add_required_field",
5125
+ correctedCommand: buildCommand(corrected, ctx),
5126
+ explanation: "Ad group queries require campaign.id in SELECT for parent context"
5127
+ };
5128
+ }
5129
+ },
5130
+ // 6. Missing campaign.id in keyword_view queries
5131
+ {
5132
+ matchQuery: /FROM\s+keyword_view\b/i,
5133
+ matchApiError: /campaign\.id.*required.*keyword/i,
5134
+ fix: (ctx) => {
5135
+ const corrected = ctx.originalQuery.replace(/SELECT\s+/i, "SELECT campaign.id, ");
5136
+ return {
5137
+ action: "add_required_field",
5138
+ correctedCommand: buildCommand(corrected, ctx),
5139
+ explanation: "Keyword view queries require campaign.id in SELECT"
5140
+ };
5141
+ }
5142
+ },
5143
+ // 7. shopping_performance_view.product_* → segments.product_*
5144
+ {
5145
+ matchQuery: /shopping_performance_view\.product_\w+/,
5146
+ matchApiError: /shopping_performance_view\.product/i,
5147
+ fix: (ctx) => {
5148
+ const corrected = ctx.originalQuery.replace(/shopping_performance_view\.product_(\w+)/g, "segments.product_$1");
5149
+ return {
5150
+ action: "retry_with_modified_query",
5151
+ correctedCommand: buildCommand(corrected, ctx),
5152
+ explanation: "Product fields are segments (segments.product_*), not fields on shopping_performance_view"
5153
+ };
5154
+ }
5155
+ },
5156
+ // 8. Open-ended date range
5157
+ {
5158
+ matchQuery: /segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i,
5159
+ matchApiError: /date.*range.*must.*finite|open.*ended/i,
5160
+ fix: (ctx) => {
5161
+ const match = ctx.originalQuery.match(/segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i);
5162
+ const startDate = match?.[1] ?? "2024-01-01";
5163
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5164
+ const corrected = ctx.originalQuery.replace(
5165
+ /segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i,
5166
+ `segments.date BETWEEN '${startDate}' AND '${today}'`
5167
+ );
5168
+ return {
5169
+ action: "narrow_date_range",
5170
+ correctedCommand: buildCommand(corrected, ctx),
5171
+ explanation: "Use BETWEEN with explicit end date \u2014 open-ended ranges are not supported"
5172
+ };
5173
+ }
5174
+ },
5175
+ // 9. Missing LIMIT
5176
+ {
5177
+ matchQuery: /^(?!.*\bLIMIT\b)/is,
5178
+ fix: (ctx) => {
5179
+ const corrected = `${ctx.originalQuery.trimEnd()} LIMIT 200`;
5180
+ return {
5181
+ action: "retry_with_modified_query",
5182
+ correctedCommand: buildCommand(corrected, ctx),
5183
+ explanation: "Added LIMIT 200 to prevent excessive data transfer"
5184
+ };
5185
+ }
5186
+ },
5187
+ // 10a. Customer not found (wrong customer ID)
5188
+ {
5189
+ matchApiError: /CUSTOMER_NOT_FOUND|not.*found.*customer/i,
5190
+ fix: () => ({
5191
+ action: "reject",
5192
+ explanation: "Customer ID not found. Run 'baker ads google accounts' to list valid customer IDs, then retry with a valid --customer-id."
5193
+ })
5194
+ },
5195
+ // 10b. Not accessible / login-customer-id required
5196
+ {
5197
+ matchApiError: /not.*accessible|login.customer.id/i,
5198
+ fix: (ctx) => ({
5199
+ action: "reject",
5200
+ correctedCommand: buildCommand(ctx.originalQuery, ctx),
5201
+ explanation: "Account not accessible \u2014 may need a manager (MCC) login-customer-id. Run 'baker ads google accounts' to refresh the accounts cache, then retry."
5202
+ })
5203
+ },
5204
+ // 11. Campaign type by name matching
5205
+ {
5206
+ matchQuery: /campaign\.name\s*LIKE\s*'%\s*(shopping|pmax|search|display|video)\s*%'/i,
5207
+ fix: (ctx) => {
5208
+ const match = ctx.originalQuery.match(/campaign\.name\s*LIKE\s*'%\s*(shopping|pmax|search|display|video)\s*%'/i);
5209
+ const typeMap = {
5210
+ shopping: "SHOPPING",
5211
+ pmax: "PERFORMANCE_MAX",
5212
+ search: "SEARCH",
5213
+ display: "DISPLAY",
5214
+ video: "VIDEO"
5215
+ };
5216
+ const channelType = typeMap[match?.[1]?.toLowerCase() ?? ""] ?? "SEARCH";
5217
+ const corrected = ctx.originalQuery.replace(
5218
+ /campaign\.name\s*LIKE\s*'%[^']*%'/i,
5219
+ `campaign.advertising_channel_type = '${channelType}'`
5220
+ );
5221
+ return {
5222
+ action: "retry_with_modified_query",
5223
+ correctedCommand: buildCommand(corrected, ctx),
5224
+ explanation: "Filter by campaign.advertising_channel_type enum, not by name pattern"
5225
+ };
5226
+ }
5227
+ },
5228
+ // 12. Incompatible fields
5229
+ {
5230
+ matchApiError: /incompatible|mutually.*exclusive|cannot.*select.*together/i,
5231
+ fix: () => ({
5232
+ action: "split_query",
5233
+ explanation: "These fields cannot be in the same query \u2014 split into separate queries and join client-side"
5234
+ })
5235
+ },
5236
+ // 13. campaign.status = 'ACTIVE' → 'ENABLED'
5237
+ {
5238
+ matchQuery: /campaign\.status\s*=\s*'ACTIVE'/i,
5239
+ fix: (ctx) => {
5240
+ const corrected = ctx.originalQuery.replace(/campaign\.status\s*=\s*'ACTIVE'/gi, "campaign.status = 'ENABLED'");
5241
+ return {
5242
+ action: "retry_with_modified_query",
5243
+ correctedCommand: buildCommand(corrected, ctx),
5244
+ explanation: "Campaign status uses ENABLED, not ACTIVE"
5245
+ };
5246
+ }
5247
+ },
5248
+ // 14. ad_group.status = 'ACTIVE' → 'ENABLED'
5249
+ {
5250
+ matchQuery: /ad_group\.status\s*=\s*'ACTIVE'/i,
5251
+ fix: (ctx) => {
5252
+ const corrected = ctx.originalQuery.replace(/ad_group\.status\s*=\s*'ACTIVE'/gi, "ad_group.status = 'ENABLED'");
5253
+ return {
5254
+ action: "retry_with_modified_query",
5255
+ correctedCommand: buildCommand(corrected, ctx),
5256
+ explanation: "Ad group status uses ENABLED, not ACTIVE"
5257
+ };
5258
+ }
5259
+ },
5260
+ // 15. ad.final_urls → ad_group_ad.ad.final_urls
5261
+ {
5262
+ matchQuery: /\bad\.final_urls\b(?!.*ad_group_ad)/,
5263
+ matchApiError: /ad\.final_urls/i,
5264
+ fix: (ctx) => {
5265
+ const corrected = ctx.originalQuery.replace(/\bad\.final_urls\b/g, "ad_group_ad.ad.final_urls");
5266
+ return {
5267
+ action: "retry_with_modified_query",
5268
+ correctedCommand: buildCommand(corrected, ctx),
5269
+ explanation: "Use the full path: ad_group_ad.ad.final_urls"
5270
+ };
5271
+ }
5272
+ },
5273
+ // 15b. asset.<type>_asset.final_urls → asset.final_urls (final URLs live on the asset
5274
+ // itself; the typed sub-message only carries type-specific fields like link_text)
5275
+ {
5276
+ matchQuery: /asset\.\w+_asset\.final_(mobile_)?urls\b/,
5277
+ matchApiError: /asset\.\w+_asset\.final_(mobile_)?urls/i,
5278
+ fix: (ctx) => {
5279
+ const corrected = ctx.originalQuery.replace(/asset\.\w+_asset\.final_(mobile_)?urls\b/g, "asset.final_$1urls");
5280
+ return {
5281
+ action: "retry_with_modified_query",
5282
+ correctedCommand: buildCommand(corrected, ctx),
5283
+ explanation: "Final URLs are on the asset itself \u2014 use asset.final_urls, not asset.<type>_asset.final_urls"
5284
+ };
5285
+ }
5286
+ },
5287
+ // 16. Rate limit / quota
5288
+ {
5289
+ matchApiError: /RESOURCE_EXHAUSTED|quota.*exceeded|rate.*limit/i,
5290
+ fix: () => ({
5291
+ action: "wait_and_retry",
5292
+ explanation: "API quota exceeded \u2014 wait 30 seconds before retrying"
5293
+ })
5294
+ },
5295
+ // 17. WHERE on metrics
5296
+ {
5297
+ matchApiError: /cannot.*filter.*metric|WHERE.*metrics|prohibited.*where/i,
5298
+ fix: () => ({
5299
+ action: "reject",
5300
+ explanation: "Cannot filter on metrics in WHERE clause \u2014 remove the metrics filter and filter results client-side"
5301
+ })
5302
+ },
5303
+ // 18. ORDER BY field not in SELECT
5304
+ {
5305
+ matchApiError: /order.*by.*field.*not.*selected|must.*select.*field.*order/i,
5306
+ fix: (ctx) => {
5307
+ const orderMatch = ctx.originalQuery.match(/ORDER\s+BY\s+([\w.]+)/i);
5308
+ const field = orderMatch?.[1] ?? "field";
5309
+ const corrected = ctx.originalQuery.replace(/SELECT\s+/i, `SELECT ${field}, `);
5310
+ return {
5311
+ action: "add_required_field",
5312
+ correctedCommand: buildCommand(corrected, ctx),
5313
+ explanation: `Add ${field} to SELECT \u2014 ORDER BY fields must be selected`
5314
+ };
5315
+ }
5316
+ },
5317
+ // The change-history rules must stay ahead of rule 19: its `status.*filter`
5318
+ // pattern also matches Google's change_status errors ("the change_status request
5319
+ // is missing filters on ..."), and the first matching rule wins.
5320
+ // 18a. change_event past its 30-day reach → change_status, which reaches 90 days.
5321
+ // These three rules match on the error code only, never on the resource in the query.
5322
+ // A `matchQuery` here would also fire in the query-pattern pass — the fallback used
5323
+ // when no rule recognised the error — and answer every unrelated failure on a history
5324
+ // query with "switch resources", which is never the real fix. The backend forwards the
5325
+ // code as JSON (`"changeEventError":"START_DATE_TOO_OLD"`), so match that shape as well
5326
+ // as the dotted enum spelling.
5327
+ {
5328
+ matchApiError: /changeEventError\W{0,4}START_DATE_TOO_OLD/i,
5329
+ fix: (ctx) => {
5330
+ const limit = ctx.originalQuery.match(/LIMIT\s+(\d+)/i)?.[1] ?? "50";
5331
+ const requested = ctx.originalQuery.match(/change_date_time\s*>=\s*'([^']+)'/i)?.[1]?.slice(0, 10);
5332
+ const oldestStart = new Date(Date.now() - 89 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
5333
+ const start = requested && requested > oldestStart ? requested : oldestStart;
5334
+ 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}`;
5335
+ return {
5336
+ action: "use_different_resource",
5337
+ correctedCommand: buildCommand(corrected, ctx),
5338
+ 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.`
5339
+ };
5340
+ }
5341
+ },
5342
+ // 18b. Past 90 days no history resource reaches — infer the change from spend instead.
5343
+ {
5344
+ matchApiError: /changeStatusError\W{0,4}START_DATE_TOO_OLD/i,
5345
+ fix: () => ({
5346
+ action: "use_different_resource",
5347
+ 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."
5348
+ })
5349
+ },
5350
+ // 18c. change_status rejects a date range it cannot bound. Google raises this both for
5351
+ // a half-open range and for no date filter at all, so supply whichever end is missing.
5352
+ {
5353
+ matchApiError: /CHANGE_DATE_RANGE_INFINITE/i,
5354
+ fix: (ctx) => {
5355
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5356
+ const oldestStart = new Date(Date.now() - 89 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
5357
+ const hasLowerBound = /last_change_date_time\s*>=?/i.test(ctx.originalQuery);
5358
+ const hasUpperBound = /last_change_date_time\s*<=?/i.test(ctx.originalQuery);
5359
+ const bounds = [
5360
+ ...hasLowerBound ? [] : [`change_status.last_change_date_time >= '${oldestStart}'`],
5361
+ ...hasUpperBound ? [] : [`change_status.last_change_date_time <= '${today}'`]
5362
+ ];
5363
+ const hasWhere = /\bWHERE\b/i.test(ctx.originalQuery);
5364
+ const clause = hasWhere ? ` AND ${bounds.join(" AND ")}` : ` WHERE ${bounds.join(" AND ")}`;
5365
+ const corrected = /\s+(ORDER\s+BY|LIMIT)\b/i.test(ctx.originalQuery) ? ctx.originalQuery.replace(/(\s+)(ORDER\s+BY|LIMIT)\b/i, `${clause}$1$2`) : ctx.originalQuery.trimEnd() + clause;
5366
+ return {
5367
+ action: "add_required_field",
5368
+ correctedCommand: buildCommand(corrected, ctx),
5369
+ explanation: "change_status needs a date range bounded at both ends \u2014 added the missing bound(s) on last_change_date_time."
5370
+ };
5371
+ }
5372
+ },
5373
+ // 19. Results include REMOVED entities
5374
+ {
5375
+ matchApiError: /REMOVED.*entities|status.*filter/i,
5376
+ fix: (ctx) => {
5377
+ const resourceMatch = ctx.originalQuery.match(/FROM\s+(\w+)/i);
5378
+ const resource = resourceMatch?.[1] ?? "campaign";
5379
+ const hasWhere = /WHERE/i.test(ctx.originalQuery);
5380
+ const suffix = hasWhere ? ` AND ${resource}.status != 'REMOVED'` : ` WHERE ${resource}.status != 'REMOVED'`;
5381
+ const limitMatch = ctx.originalQuery.match(/(\s+LIMIT\s+\d+)/i);
5382
+ let corrected;
5383
+ if (limitMatch) {
5384
+ corrected = ctx.originalQuery.replace(/(\s+LIMIT\s+\d+)/i, `${suffix}$1`);
5385
+ } else {
5386
+ corrected = ctx.originalQuery.trimEnd() + suffix;
5387
+ }
5388
+ return {
5389
+ action: "retry_with_modified_query",
5390
+ correctedCommand: buildCommand(corrected, ctx),
5391
+ explanation: `Add status filter to exclude REMOVED ${resource}s`
5392
+ };
5393
+ }
5394
+ },
5395
+ // 20. change_event without date constraint
5396
+ {
5397
+ matchQuery: /FROM\s+change_event\b(?!.*change_date_time)/i,
5398
+ matchApiError: /change_event.*date|date.*required.*change/i,
5399
+ fix: (ctx) => {
5400
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
5401
+ const hasWhere = /WHERE/i.test(ctx.originalQuery);
5402
+ const suffix = hasWhere ? ` AND change_event.change_date_time >= '${sevenDaysAgo}'` : ` WHERE change_event.change_date_time >= '${sevenDaysAgo}'`;
5403
+ const corrected = ctx.originalQuery.trimEnd() + suffix;
5404
+ return {
5405
+ action: "add_required_field",
5406
+ correctedCommand: buildCommand(corrected, ctx),
5407
+ explanation: "change_event queries require a date constraint on change_date_time"
5408
+ };
5409
+ }
5410
+ },
5411
+ // 21. resource_name = 'customers/X/Y/Z' → resource.id = Z
5412
+ {
5413
+ matchQuery: /(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i,
5414
+ fix: (ctx) => {
5415
+ const match = ctx.originalQuery.match(/(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i);
5416
+ const resource = match?.[1] ?? "campaign";
5417
+ const id = match?.[2] ?? "ID";
5418
+ const corrected = ctx.originalQuery.replace(
5419
+ /(\w+)\.resource_name\s*=\s*'customers\/\d+\/\w+\/(\d+)'/i,
5420
+ `${resource}.id = ${id}`
5421
+ );
5422
+ return {
5423
+ action: "retry_with_modified_query",
5424
+ correctedCommand: buildCommand(corrected, ctx),
5425
+ explanation: `Simpler: use ${resource}.id = ${id} instead of resource_name path`
5426
+ };
5427
+ }
5428
+ },
5429
+ // 22. segments.click_type incompatibility
5430
+ {
5431
+ matchApiError: /click_type.*incompatible|cannot.*click_type/i,
5432
+ fix: (ctx) => {
5433
+ const corrected = ctx.originalQuery.replace(/,?\s*segments\.click_type/g, "");
5434
+ return {
5435
+ action: "split_query",
5436
+ correctedCommand: buildCommand(corrected, ctx),
5437
+ explanation: "segments.click_type is incompatible with other segments \u2014 remove it or query separately"
5438
+ };
5439
+ }
5440
+ },
5441
+ // 23. Bare resource in SELECT
5442
+ {
5443
+ matchApiError: /cannot.*select.*bare.*resource|invalid.*field/i,
5444
+ fix: () => ({
5445
+ action: "retry_with_modified_query",
5446
+ explanation: "Cannot SELECT a bare resource name \u2014 use resource.id, resource.name, or resource.resource_name"
5447
+ })
5448
+ },
5449
+ // 24. Authentication error
5450
+ {
5451
+ matchApiError: /unauthenticated|authentication.*required|invalid.*credentials/i,
5452
+ fix: () => ({
5453
+ action: "authenticate",
5454
+ explanation: "Google Ads authentication failed \u2014 reconnect Google Ads in dashboard settings"
5455
+ })
5456
+ },
5457
+ // 25. Timeout / deadline exceeded
5458
+ {
5459
+ matchApiError: /timeout|deadline.*exceeded|DEADLINE_EXCEEDED/i,
5460
+ fix: (ctx) => ({
5461
+ action: "retry_with_modified_query",
5462
+ correctedCommand: buildCommand(ctx.originalQuery, ctx),
5463
+ explanation: "Query timed out \u2014 narrow the date range, add more WHERE filters, or reduce LIMIT"
5464
+ })
5465
+ }
5481
5466
  ];
5482
- var googleDraftOpKindSchema = z12.enum(GOOGLE_DRAFT_OP_KINDS);
5483
- function createOp2(kind, payload) {
5484
- return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, payload });
5485
- }
5486
- function updateOp2(kind, payload) {
5487
- return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, target: targetRefSchema, payload });
5488
- }
5489
- function targetOp(kind) {
5490
- return z12.object({ kind: z12.literal(kind), customerId: customerIdSchema, target: targetRefSchema });
5491
- }
5492
- var googleDraftOpInputSchema = z12.discriminatedUnion("kind", [
5493
- createOp2("google.budget.create", budgetCreateSchema),
5494
- updateOp2("google.budget.update", budgetUpdateSchema),
5495
- createOp2("google.campaign.create", campaignCreateSchema2),
5496
- updateOp2("google.campaign.update", campaignUpdateSchema2),
5497
- targetOp("google.campaign.pause"),
5498
- targetOp("google.campaign.resume"),
5499
- targetOp("google.campaign.remove"),
5500
- createOp2("google.adGroup.create", adGroupCreateSchema),
5501
- updateOp2("google.adGroup.update", adGroupUpdateSchema),
5502
- targetOp("google.adGroup.pause"),
5503
- targetOp("google.adGroup.resume"),
5504
- targetOp("google.adGroup.remove"),
5505
- createOp2("google.keyword.add", keywordAddSchema),
5506
- updateOp2("google.keyword.update", keywordUpdateSchema),
5507
- targetOp("google.keyword.remove"),
5508
- createOp2("google.negativeKeyword.add", negativeKeywordAddSchema),
5509
- targetOp("google.negativeKeyword.remove"),
5510
- createOp2("google.sharedSet.create", sharedSetCreateSchema),
5511
- createOp2("google.sharedSetMember.add", sharedSetMemberAddSchema),
5512
- targetOp("google.sharedSetMember.remove"),
5513
- createOp2("google.campaignSharedSet.attach", campaignSharedSetAttachSchema),
5514
- targetOp("google.campaignSharedSet.detach"),
5515
- createOp2("google.ad.create", adCreateSchema),
5516
- updateOp2("google.ad.update", adUpdateSchema),
5517
- targetOp("google.ad.pause"),
5518
- targetOp("google.ad.resume"),
5519
- targetOp("google.ad.remove"),
5520
- createOp2("google.asset.create", assetCreateSchema),
5521
- updateOp2("google.asset.update", assetUpdateSchema),
5522
- createOp2("google.assetLink.attach", assetLinkAttachSchema),
5523
- targetOp("google.assetLink.detach"),
5524
- createOp2("google.assetGroup.create", assetGroupCreateSchema),
5525
- updateOp2("google.assetGroup.update", assetGroupUpdateSchema),
5526
- createOp2("google.audience.create", audienceCreateSchema2),
5527
- createOp2("google.audienceCriterion.attach", audienceCriterionAttachSchema),
5528
- targetOp("google.audienceCriterion.detach"),
5529
- createOp2("google.conversionAction.create", conversionActionCreateSchema),
5530
- updateOp2("google.conversionAction.update", conversionActionUpdateSchema),
5531
- createOp2("google.biddingStrategy.create", biddingStrategyCreateSchema),
5532
- updateOp2("google.biddingStrategy.update", biddingStrategyUpdateSchema),
5533
- createOp2("google.label.create", labelCreateSchema),
5534
- createOp2("google.label.attach", labelAttachSchema),
5535
- createOp2("google.campaignCriterion.add", campaignCriterionAddSchema),
5536
- targetOp("google.campaignCriterion.remove")
5537
- ]);
5538
5467
 
5539
- // ../api/src/ads-google/wire.ts
5540
- import { z as z13 } from "zod";
5541
- var googleWriteModeSchema = z13.enum(["live", "simulated"]);
5542
- var googleDraftOpResultSchema = z13.object({
5543
- status: z13.enum(["applied", "simulated", "failed", "skipped"]),
5544
- resourceName: z13.string().optional(),
5545
- error: z13.string().optional(),
5546
- skippedBecause: z13.string().optional(),
5547
- executedAt: z13.number().optional()
5548
- });
5549
- var googleDraftStageRequestSchema = z13.object({
5550
- chatId: z13.string(),
5551
- op: googleDraftOpInputSchema
5552
- });
5553
- var googleDraftStageResponseSchema = z13.discriminatedUnion("staged", [
5554
- z13.object({
5555
- staged: z13.literal(true),
5556
- ref: z13.string(),
5557
- kind: googleDraftOpKindSchema,
5558
- mode: googleWriteModeSchema,
5559
- dependsOn: z13.array(z13.string()),
5560
- summary: z13.string(),
5561
- warnings: z13.array(z13.string()),
5562
- /** True when the op amended an already-staged op in place instead of appending a new one. */
5563
- amended: z13.boolean().optional()
5564
- }),
5565
- z13.object({
5566
- staged: z13.literal(false),
5567
- noop: z13.literal(true),
5568
- kind: googleDraftOpKindSchema,
5569
- mode: googleWriteModeSchema,
5570
- summary: z13.string(),
5571
- reason: z13.string()
5572
- })
5573
- ]);
5574
- var googleDraftAmendRequestSchema = z13.object({
5575
- chatId: z13.string(),
5576
- ref: z13.string(),
5577
- patch: z13.record(z13.string(), z13.unknown())
5578
- });
5579
- var googleDraftShowRequestSchema = z13.object({
5580
- chatId: z13.string(),
5581
- ref: z13.string()
5582
- });
5583
- var GOOGLE_DRAFT_BATCH_MAX = 500;
5584
- var googleDraftStageBatchRequestSchema = z13.object({
5585
- chatId: z13.string(),
5586
- ops: z13.array(googleDraftOpInputSchema).min(1).max(GOOGLE_DRAFT_BATCH_MAX)
5587
- });
5588
- var googleDraftStageBatchResponseSchema = z13.object({
5589
- staged: z13.literal(true),
5590
- mode: googleWriteModeSchema,
5591
- count: z13.number(),
5592
- ops: z13.array(
5593
- z13.object({
5594
- ref: z13.string(),
5595
- kind: googleDraftOpKindSchema,
5596
- dependsOn: z13.array(z13.string()),
5597
- summary: z13.string(),
5598
- warnings: z13.array(z13.string())
5599
- })
5600
- ),
5601
- skipped: z13.array(z13.object({ kind: googleDraftOpKindSchema, summary: z13.string(), reason: z13.string() })).optional()
5602
- });
5603
- var googleDraftOpViewSchema = z13.object({
5604
- ref: z13.string(),
5605
- kind: googleDraftOpKindSchema,
5606
- customerId: z13.string(),
5607
- target: z13.string().optional(),
5608
- dependsOn: z13.array(z13.string()),
5609
- summary: z13.string(),
5610
- stagedAt: z13.number(),
5611
- result: googleDraftOpResultSchema.optional()
5612
- });
5613
- var googleDraftShowResponseSchema = z13.object({
5614
- op: googleDraftOpViewSchema.extend({
5615
- payload: z13.unknown().optional(),
5616
- warnings: z13.array(z13.string()).optional(),
5617
- annotations: z13.unknown().optional()
5618
- })
5619
- });
5620
- var googleDraftListRequestSchema = z13.object({
5621
- chatId: z13.string()
5622
- });
5623
- var googleDraftAdvisorySchema = z13.object({
5624
- scope: z13.enum(["campaign", "adGroup"]),
5625
- message: z13.string()
5626
- });
5627
- var googleDraftStatusCollectionSchema = z13.object({
5628
- label: z13.string(),
5629
- added: z13.number(),
5630
- removed: z13.number(),
5631
- existing: z13.number()
5632
- });
5633
- var googleDraftChangeOperationSchema = z13.enum(["create", "update", "pause", "resume", "remove"]);
5634
- var googleDraftStatusNodeSchema = z13.lazy(
5635
- () => z13.object({
5636
- entity: z13.string(),
5637
- name: z13.string(),
5638
- operation: googleDraftChangeOperationSchema.optional(),
5639
- existing: z13.boolean(),
5640
- collections: z13.array(googleDraftStatusCollectionSchema),
5641
- children: z13.array(googleDraftStatusNodeSchema),
5642
- warnings: z13.array(z13.string()).optional()
5643
- })
5644
- );
5645
- var googleDraftListResponseSchema = z13.object({
5646
- status: z13.enum(["active", "publishing", "applied", "discarded", "none"]),
5647
- mode: googleWriteModeSchema,
5648
- count: z13.number(),
5649
- ops: z13.array(googleDraftOpViewSchema),
5650
- /** Grouped campaign ▸ ad group ▸ ad tree for the readable CLI status view. */
5651
- tree: z13.array(googleDraftStatusNodeSchema).optional(),
5652
- /** Non-blocking completeness advisories for the whole draft. */
5653
- advisories: z13.array(googleDraftAdvisorySchema).optional()
5654
- });
5655
- var googleDraftRemoveRequestSchema = z13.object({
5656
- chatId: z13.string(),
5657
- ref: z13.string()
5658
- });
5659
- var googleDraftRemoveResponseSchema = z13.object({
5660
- /** The requested ref plus any dependents removed by cascade. */
5661
- removed: z13.array(z13.string())
5662
- });
5663
- var googleDraftClearRequestSchema = z13.object({
5664
- chatId: z13.string()
5468
+ // src/commands/ads/google/error-parser.ts
5469
+ function mapErrorCode(message) {
5470
+ if (/must be present in SELECT clause/i.test(message)) return "MISSING_SELECT_FIELD";
5471
+ if (/field.*not.*found|unrecognized.*field|not.*valid.*field/i.test(message)) return "FIELD_NOT_FOUND";
5472
+ if (/not.*valid.*resource|cannot.*select.*from/i.test(message)) return "WRONG_RESOURCE";
5473
+ if (/operator|CONTAINS|LIKE/i.test(message)) return "INVALID_OPERATOR";
5474
+ if (/CUSTOMER_NOT_FOUND/i.test(message)) return "CUSTOMER_NOT_FOUND";
5475
+ if (/login.customer.id|not.*accessible/i.test(message)) return "MISSING_MANAGER_ID";
5476
+ if (/incompatible|mutually.*exclusive|PROHIBITED_SEGMENT_WITH_METRIC/i.test(message)) return "INCOMPATIBLE_FIELDS";
5477
+ if (/unauthenticated|authentication/i.test(message)) return "AUTH_ERROR";
5478
+ if (/permission/i.test(message)) return "PERMISSION_DENIED";
5479
+ if (/RESOURCE_EXHAUSTED|quota|rate.*limit/i.test(message)) return "QUOTA_EXCEEDED";
5480
+ if (/timeout|DEADLINE_EXCEEDED/i.test(message)) return "TIMEOUT";
5481
+ return "API_ERROR";
5482
+ }
5483
+ function isRetryable(code) {
5484
+ return code === "QUOTA_EXCEEDED" || code === "TIMEOUT";
5485
+ }
5486
+ function getRetryDelay(code) {
5487
+ if (code === "QUOTA_EXCEEDED") return 3e4;
5488
+ if (code === "TIMEOUT") return 5e3;
5489
+ return void 0;
5490
+ }
5491
+ function parseApiError(errorMessage, originalQuery, customerId) {
5492
+ const ctx = {
5493
+ originalQuery,
5494
+ customerId,
5495
+ apiErrorMessage: errorMessage
5496
+ };
5497
+ for (const rule of CORRECTION_RULES) {
5498
+ if (rule.matchApiError?.test(errorMessage)) {
5499
+ const fix = rule.fix(ctx);
5500
+ const code2 = mapErrorCode(errorMessage);
5501
+ return {
5502
+ ok: false,
5503
+ error: {
5504
+ code: code2,
5505
+ message: errorMessage,
5506
+ fix,
5507
+ retryable: isRetryable(code2),
5508
+ retryAfterMs: getRetryDelay(code2)
5509
+ }
5510
+ };
5511
+ }
5512
+ }
5513
+ for (const rule of CORRECTION_RULES) {
5514
+ if (rule.matchQuery?.test(originalQuery)) {
5515
+ const fix = rule.fix(ctx);
5516
+ const code2 = mapErrorCode(errorMessage);
5517
+ return {
5518
+ ok: false,
5519
+ error: {
5520
+ code: code2,
5521
+ message: errorMessage,
5522
+ fix,
5523
+ retryable: isRetryable(code2),
5524
+ retryAfterMs: getRetryDelay(code2)
5525
+ }
5526
+ };
5527
+ }
5528
+ }
5529
+ const code = mapErrorCode(errorMessage);
5530
+ return {
5531
+ ok: false,
5532
+ error: {
5533
+ code,
5534
+ message: errorMessage,
5535
+ fix: {
5536
+ action: "reject",
5537
+ explanation: "Unrecognized error \u2014 verify query syntax against Google Ads GAQL reference"
5538
+ },
5539
+ retryable: isRetryable(code),
5540
+ retryAfterMs: getRetryDelay(code)
5541
+ }
5542
+ };
5543
+ }
5544
+
5545
+ // src/commands/ads/google/changes.ts
5546
+ registerSchema({
5547
+ command: "ads.google.changes",
5548
+ 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.",
5549
+ args: {
5550
+ "customer-id": {
5551
+ type: "string",
5552
+ description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
5553
+ required: false
5554
+ },
5555
+ days: {
5556
+ type: "string",
5557
+ 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.`,
5558
+ required: false,
5559
+ default: DEFAULT_CHANGE_DAYS
5560
+ },
5561
+ scope: {
5562
+ type: "string",
5563
+ 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.`,
5564
+ required: false,
5565
+ default: "detail"
5566
+ },
5567
+ "resource-type": {
5568
+ type: "string",
5569
+ description: "Filter by type: CAMPAIGN, CAMPAIGN_CRITERION, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION. CAMPAIGN_CRITERION covers campaign-level negatives, location and audience exclusions.",
5570
+ required: false
5571
+ },
5572
+ limit: { type: "string", description: "Max change events (default: 50, max: 1000)", required: false, default: 50 },
5573
+ output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" }
5574
+ }
5665
5575
  });
5666
- var googleDraftClearResponseSchema = z13.object({
5667
- cleared: z13.number()
5576
+ var changesCommand = defineCommand20({
5577
+ meta: {
5578
+ name: "changes",
5579
+ description: `Read the change history of a Google Ads account.
5580
+
5581
+ Google keeps account history in two layers, and neither reaches past 90 days:
5582
+ --scope detail (default) what changed, old \u2192 new value, who changed it \u2014 last ${CHANGE_SCOPE_MAX_DAYS.detail} days
5583
+ --scope summary which resources were added, changed or removed \u2014 last ${CHANGE_SCOPE_MAX_DAYS.summary} days
5584
+
5585
+ For anything older, infer it from performance instead \u2014 a monthly spend trend per
5586
+ campaign shows what started or stopped, just not who did it.
5587
+
5588
+ Examples:
5589
+ baker ads google changes --customer-id 1234567890
5590
+ baker ads google changes --customer-id 1234567890 --days 14 --resource-type CAMPAIGN
5591
+ baker ads google changes --customer-id 1234567890 --days 60 --scope summary`
5592
+ },
5593
+ args: {
5594
+ "customer-id": { type: "string", description: "Google Ads customer ID", required: false },
5595
+ days: { type: "string", description: `Lookback days (default ${DEFAULT_CHANGE_DAYS})`, required: false },
5596
+ scope: { type: "string", description: "detail (30 days) or summary (90 days)", required: false },
5597
+ "resource-type": { type: "string", description: "Filter by resource type", required: false },
5598
+ limit: { type: "string", description: "Max results (default 50)", required: false },
5599
+ "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false },
5600
+ output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" }
5601
+ },
5602
+ run: async ({ args }) => {
5603
+ const customerId = await resolveCustomerId(args);
5604
+ const window = resolveChangesWindow({
5605
+ days: args.days ? Number(args.days) : void 0,
5606
+ scope: args.scope
5607
+ });
5608
+ if (!window.ok) {
5609
+ writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
5610
+ process.exit(1);
5611
+ return;
5612
+ }
5613
+ const body = {
5614
+ customerId,
5615
+ days: window.days,
5616
+ scope: window.scope,
5617
+ limit: args.limit ? Number(args.limit) : 50
5618
+ };
5619
+ const managerId = getManagerIdForCustomer(customerId);
5620
+ if (managerId) body.managerId = managerId;
5621
+ if (args["resource-type"]) body.resourceType = args["resource-type"];
5622
+ if (args["no-cache"]) body.skipCache = true;
5623
+ try {
5624
+ const data = await apiPost("/api/ads/google/changes", body);
5625
+ const format = args.output || "json";
5626
+ if (format !== "json") {
5627
+ writeAdsOutput(data, format);
5628
+ return;
5629
+ }
5630
+ const first = data[0];
5631
+ const fields = first ? Object.keys(first) : [];
5632
+ const fieldDescs = getFieldDescriptions(fields);
5633
+ writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
5634
+ } catch (err) {
5635
+ if (err instanceof ApiError) {
5636
+ writeAdsJson(parseApiError(err.message, "", customerId));
5637
+ process.exit(1);
5638
+ }
5639
+ writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
5640
+ process.exit(1);
5641
+ }
5642
+ }
5668
5643
  });
5669
- var googleFieldErrorSchema = z13.object({
5670
- path: z13.string(),
5671
- message: z13.string()
5644
+
5645
+ // src/commands/ads/google/currency.ts
5646
+ import { defineCommand as defineCommand21 } from "citty";
5647
+ registerSchema({
5648
+ command: "ads.google.currency",
5649
+ description: "Get the currency code for a Google Ads account. Returns currency_code, customer_id, account_name, and access_type. Call this before interpreting cost_micros values.",
5650
+ args: {
5651
+ "customer-id": {
5652
+ type: "string",
5653
+ description: "Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.",
5654
+ required: false
5655
+ }
5656
+ }
5672
5657
  });
5673
- var googleDraftErrorResponseSchema = z13.object({
5674
- code: z13.string(),
5675
- error: z13.string(),
5676
- fields: z13.array(googleFieldErrorSchema).optional()
5658
+ var currencyCommand = defineCommand21({
5659
+ meta: {
5660
+ name: "currency",
5661
+ description: `Get account currency code. Use this to interpret metrics.cost_micros values.
5662
+
5663
+ Examples:
5664
+ baker ads google currency --customer-id 1234567890`
5665
+ },
5666
+ args: {
5667
+ "customer-id": { type: "string", description: "Google Ads customer ID (10 digits)", required: false },
5668
+ "no-cache": { type: "boolean", description: "Skip cache", required: false }
5669
+ },
5670
+ run: async ({ args }) => {
5671
+ const customerId = await resolveCustomerId(args);
5672
+ const useCache = !args["no-cache"];
5673
+ const cacheKey = `currency:${customerId}`;
5674
+ if (useCache) {
5675
+ const cached = cacheGet("accounts", cacheKey);
5676
+ if (cached) {
5677
+ writeAdsJson({ ok: true, data: cached.data, cached: true });
5678
+ return;
5679
+ }
5680
+ }
5681
+ try {
5682
+ const params = { "customer-id": customerId };
5683
+ const managerId = getManagerIdForCustomer(customerId);
5684
+ if (managerId) params["manager-id"] = managerId;
5685
+ if (!useCache) params["skip-cache"] = "true";
5686
+ const raw = await apiGet("/api/ads/google/currency", params);
5687
+ const data = {
5688
+ currency_code: raw.currencyCode,
5689
+ customer_id: raw.customerId
5690
+ };
5691
+ if (useCache) {
5692
+ cacheSet("accounts", cacheKey, data, 24 * 60 * 60 * 1e3);
5693
+ }
5694
+ writeAdsJson({ ok: true, data });
5695
+ } catch (err) {
5696
+ if (err instanceof ApiError) {
5697
+ writeAdsJson(parseApiError(err.message, "", customerId));
5698
+ process.exit(1);
5699
+ }
5700
+ writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
5701
+ process.exit(1);
5702
+ }
5703
+ }
5677
5704
  });
5678
5705
 
5706
+ // src/commands/ads/google/draft.ts
5707
+ import { defineCommand as defineCommand22 } from "citty";
5708
+
5709
+ // src/commands/ads/google/write-shared.ts
5710
+ import { readFileSync as readFileSync2 } from "fs";
5711
+
5679
5712
  // src/commands/ads/google/draft-status.ts
5680
5713
  var OPERATION_LABELS = {
5681
5714
  create: "Creating",
@@ -27066,6 +27099,53 @@ var BUZZWORDS = [
27066
27099
  "drive results",
27067
27100
  "harness the power"
27068
27101
  ];
27102
+ var AUTONOMY_CLAIMS = [
27103
+ "fully autonomous",
27104
+ "completely autonomous",
27105
+ "autonomous agent",
27106
+ "autonomously",
27107
+ "while you sleep",
27108
+ "set it and forget it",
27109
+ "set-it-and-forget-it",
27110
+ "zero human input",
27111
+ "no human intervention",
27112
+ "without human intervention",
27113
+ "hands-free",
27114
+ "hands free",
27115
+ "self-driving",
27116
+ "runs itself",
27117
+ "on autopilot",
27118
+ "end-to-end automation",
27119
+ "no manual work",
27120
+ "eliminates manual",
27121
+ "does the work for you",
27122
+ "thinks for you",
27123
+ "handles everything for you"
27124
+ ];
27125
+ var AI_CONTROL_SIGNALS = [
27126
+ "human in the loop",
27127
+ "human-in-the-loop",
27128
+ "human review",
27129
+ "human oversight",
27130
+ "approve",
27131
+ "approval",
27132
+ "you review",
27133
+ "review before",
27134
+ "review queue",
27135
+ "confirm before",
27136
+ "in control",
27137
+ "you decide",
27138
+ "override",
27139
+ "audit log",
27140
+ "audit trail",
27141
+ "guardrail",
27142
+ "dry run",
27143
+ "rollback",
27144
+ "roll back",
27145
+ "undo",
27146
+ "escalate",
27147
+ "escalation"
27148
+ ];
27069
27149
  var AI_PURPLE_HEXES = /* @__PURE__ */ new Set([
27070
27150
  "#7c3aed",
27071
27151
  "#8b5cf6",
@@ -27161,6 +27241,11 @@ var RULE_META = {
27161
27241
  severity: "warn",
27162
27242
  note: "SaaS filler ('supercharge your\u2026') is generic. Name names, use numbers, say the specific thing only this product proves."
27163
27243
  },
27244
+ "agent-washing": {
27245
+ family: "copy",
27246
+ severity: "warn",
27247
+ note: "The page claims autonomy but never says what the buyer controls. Name what it does on its own, what waits for approval, and what happens when it's wrong."
27248
+ },
27164
27249
  "aphoristic-cadence": {
27165
27250
  family: "copy",
27166
27251
  severity: "warn",
@@ -27367,6 +27452,9 @@ var LINE_MATCHERS = [
27367
27452
  fmt: (m) => cap2(m, 0).slice(0, 100)
27368
27453
  }
27369
27454
  ];
27455
+ function includesPhraseAtWordStart(haystack, phrase) {
27456
+ return new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(haystack);
27457
+ }
27370
27458
  function stripHtmlToText(html) {
27371
27459
  return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
27372
27460
  }
@@ -27486,6 +27574,24 @@ var ANALYZERS = [
27486
27574
  return [{ id: "em-dash-overuse", snippet: `${count} em-dashes in body copy`, file }];
27487
27575
  }
27488
27576
  },
27577
+ // Agent washing: an autonomy claim the page never scaffolds with a signal about
27578
+ // who oversees the agent. Page-scope — the claim and the answer routinely live
27579
+ // in different components, so a per-file check would fire on any hero that
27580
+ // defers the trust story to a section below it.
27581
+ //
27582
+ // Both lists match at a word start (see `includesPhraseAtWordStart`). Matching
27583
+ // bare substrings let "resources" satisfy "sources" and silently clear the
27584
+ // finding, which is the failure mode this rule exists to catch.
27585
+ {
27586
+ scope: "page",
27587
+ run: (text, file) => {
27588
+ const lower = stripHtmlToText(text).toLowerCase();
27589
+ const claim = AUTONOMY_CLAIMS.find((phrase) => includesPhraseAtWordStart(lower, phrase));
27590
+ if (!claim) return [];
27591
+ if (AI_CONTROL_SIGNALS.some((phrase) => includesPhraseAtWordStart(lower, phrase))) return [];
27592
+ return [{ id: "agent-washing", snippet: `"${claim}" with no control or oversight signal on the page`, file }];
27593
+ }
27594
+ },
27489
27595
  // Marketing buzzwords. Fires at ≥1 → per file (keeps the offending file named).
27490
27596
  {
27491
27597
  scope: "file",
@@ -27728,7 +27834,7 @@ function describeCounts(findings) {
27728
27834
  // src/commands/landing/snapshot.ts
27729
27835
  import { mkdir as mkdir8, rename as rename2, writeFile as writeFile11 } from "fs/promises";
27730
27836
  import path25 from "path";
27731
- var CRITIC_VERSION = "1";
27837
+ var CRITIC_VERSION = "2";
27732
27838
  function critiqueCacheDir(projectRoot) {
27733
27839
  return path25.join(projectRoot, ".cache", "landing-critique");
27734
27840
  }