@koda-sl/baker-cli 0.392.0-dev.9666ad9b3 → 0.394.0-dev.d28cef496

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -876,7 +876,7 @@ Read the company's ChatGPT Ads account (OpenAI's ads inside ChatGPT conversation
876
876
  | `conversions` | Pixels (with the pixel id a landing page installs) and conversions, with the campaigns using each |
877
877
  | `insights` | Impressions, clicks, spend, CTR, CPC, CPM, conversions, CPA, ROAS — per `--level account\|campaign\|ad_group\|ad`, over `--days N` or `--since/--until`, `--granularity`, `--segment country\|device\|platform\|product` |
878
878
  | `campaigns create\|update\|pause\|resume\|archive` | Stage a campaign change: objective, daily or lifetime budget (currency units), schedule, countries and regions included and excluded, audiences, platforms, tracking template, status, the conversions it measures (`--conversion-event-ids`), a catalog (`--catalog-id`) |
879
- | `ad-groups create\|update\|pause\|resume\|archive` | Stage an ad group change: context hints, bid strategy, max bid, tracking template, product filters, status |
879
+ | `ad-groups create\|update\|pause\|resume\|archive` | Stage an ad group change: context hints, bid strategy, max bid, audience bid adjustments, tracking template, product filters, status |
880
880
  | `ads create\|update\|pause\|resume\|archive` | Stage a chat card: title, body, landing page, image and its crop, tracking template, status |
881
881
  | `ads create-product` | Stage a catalog ad group's product ad: a title template the catalog's products fill in |
882
882
  | `pixels create` | Stage a new website pixel |
package/dist/cli.js CHANGED
@@ -33300,7 +33300,13 @@ var imageCropSchema = z31.strictObject({
33300
33300
  var budgetMicrosSchema = z31.number().int("budgets are integer micros \u2014 \u20AC20.00 is 20000000").min(CHATGPT_LIMITS.budgetMinMicros, "a budget is at least one unit of the account currency");
33301
33301
  var bidMicrosSchema = z31.number().int("bids are integer micros \u2014 \u20AC2.50 is 2500000").min(1);
33302
33302
  var httpsUrlSchema3 = z31.string().trim().max(CHATGPT_LIMITS.urlMax).url("expected a full URL").refine((value) => value.startsWith("https://"), "ChatGPT Ads needs an https:// address");
33303
- var isoTimeSchema2 = z31.string().refine((value) => !Number.isNaN(Date.parse(value)), "expected an ISO 8601 date or timestamp").transform((value) => new Date(value).toISOString());
33303
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
33304
+ var LOCAL_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/;
33305
+ var ZONED_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
33306
+ var isoTimeSchema2 = z31.string().trim().refine(
33307
+ (value) => DATE_ONLY.test(value) || LOCAL_TIME.test(value) || ZONED_TIME.test(value),
33308
+ "a date is YYYY-MM-DD, or YYYY-MM-DDTHH:MM in the account's time zone \u2014 e.g. 2026-10-12"
33309
+ ).refine((value) => !Number.isNaN(Date.parse(value)), "that date does not exist").transform((value) => ZONED_TIME.test(value) ? new Date(value).toISOString() : value);
33304
33310
  var LAST_SCHEDULABLE_MS = Date.parse("2100-01-01T00:00:00Z");
33305
33311
  function notPast(what) {
33306
33312
  return isoTimeSchema2.refine((value) => Date.parse(value) > Date.now() - 6e4, `the ${what} is in the past`).refine(
@@ -33308,7 +33314,7 @@ function notPast(what) {
33308
33314
  `the ${what} is after 2099, which ChatGPT Ads cannot store`
33309
33315
  );
33310
33316
  }
33311
- var startTimeSchema = notPast("start \u2014 leave it out to start when the chat is published");
33317
+ var startTimeSchema = notPast("start");
33312
33318
  var endTimeSchema = notPast("end");
33313
33319
  var countrySchema = z31.string().trim().regex(/^[A-Za-z]{2}$/, "countries are two-letter codes, e.g. ES").transform((value) => value.toUpperCase()).refine((value) => import_i18n_iso_countries.default.isValid(value), "that is not a country code (ISO 3166-1, e.g. ES)");
33314
33320
  var locationIdSchema = z31.string().trim().regex(/^\d+$/, "a region id is the number `baker ads chatgpt locations <place>` returns");
@@ -33382,6 +33388,13 @@ var conversionEventCreateSchema = z31.strictObject({
33382
33388
  message: "a custom conversion needs the event name the site sends"
33383
33389
  });
33384
33390
  }
33391
+ if (p.eventType === "app_installed" || p.eventType === "app_opened") {
33392
+ ctx.addIssue({
33393
+ code: "custom",
33394
+ path: ["eventType"],
33395
+ message: "an app event reaches ChatGPT Ads only through the Conversions API, never a website pixel \u2014 and a conversion cannot be deleted, so it is not staged"
33396
+ });
33397
+ }
33385
33398
  if (p.eventType !== "custom" && p.customEventName !== void 0) {
33386
33399
  ctx.addIssue({
33387
33400
  code: "custom",
@@ -33422,17 +33435,21 @@ var COUNTRY_NAMES = new Map(
33422
33435
  CHATGPT_GEOTARGETS.filter(([, , , type]) => type === "country").map(([, name, countryCode2]) => [countryCode2, name])
33423
33436
  );
33424
33437
  var MAX_PLACES = 2500;
33438
+ function uniqueList(item, min, max) {
33439
+ const list = z31.array(item).min(min);
33440
+ return (max === void 0 ? list : list.max(max)).transform((values) => [...new Set(values)]);
33441
+ }
33425
33442
  function targetingFields(min) {
33426
33443
  return {
33427
- countries: z31.array(countrySchema).min(min).max(MAX_PLACES).optional(),
33444
+ countries: uniqueList(countrySchema, min, MAX_PLACES).optional(),
33428
33445
  /** Region, market and postal-code ids from ChatGPT Ads' location lookup. */
33429
- locationIds: z31.array(locationIdSchema).min(min).max(MAX_PLACES).optional(),
33446
+ locationIds: uniqueList(locationIdSchema, min, MAX_PLACES).optional(),
33430
33447
  /** Countries and places the campaign never serves in, even inside an included one. */
33431
- excludedCountries: z31.array(countrySchema).min(min).max(MAX_PLACES).optional(),
33432
- excludedLocationIds: z31.array(locationIdSchema).min(min).max(MAX_PLACES).optional(),
33448
+ excludedCountries: uniqueList(countrySchema, min, MAX_PLACES).optional(),
33449
+ excludedLocationIds: uniqueList(locationIdSchema, min, MAX_PLACES).optional(),
33433
33450
  /** Custom audiences the campaign serves only to, and ones it never serves. */
33434
- audienceIds: z31.array(audienceIdSchema).min(min).optional(),
33435
- excludedAudienceIds: z31.array(audienceIdSchema).min(min).optional(),
33451
+ audienceIds: uniqueList(audienceIdSchema, min).optional(),
33452
+ excludedAudienceIds: uniqueList(audienceIdSchema, min).optional(),
33436
33453
  platforms: z31.array(z31.enum(CHATGPT_PLATFORMS)).min(1).optional()
33437
33454
  };
33438
33455
  }
@@ -33514,6 +33531,21 @@ var campaignUpdateSchema4 = z31.strictObject({
33514
33531
  checkRunWindow2(p.startTime, p.endTime, ctx);
33515
33532
  checkTargeting(p, ctx);
33516
33533
  });
33534
+ var audienceBidMultipliersSchema = z31.array(
33535
+ z31.strictObject({
33536
+ audienceId: audienceIdSchema,
33537
+ multiplierMicros: z31.number().int().min(1e5, "a bid adjustment is 0.1\xD7 to 10\xD7 \u2014 100000 to 10000000 micros").max(1e7, "a bid adjustment is 0.1\xD7 to 10\xD7 \u2014 100000 to 10000000 micros")
33538
+ })
33539
+ ).max(100);
33540
+ function checkMultipliers(strategy, multipliers, ctx) {
33541
+ if (strategy !== void 0 && strategy !== "fixed_bid" && (multipliers?.length ?? 0) > 0) {
33542
+ ctx.addIssue({
33543
+ code: "custom",
33544
+ path: ["audienceBidMultipliers"],
33545
+ message: "an audience bid adjustment changes a fixed bid \u2014 Maximize Results sets the bid itself"
33546
+ });
33547
+ }
33548
+ }
33517
33549
  function checkBidPair2(strategy, maxBid, ctx) {
33518
33550
  if (strategy === "fixed_bid" && maxBid === void 0) {
33519
33551
  ctx.addIssue({ code: "custom", path: ["maxBidMicros"], message: "a fixed bid needs its amount" });
@@ -33541,8 +33573,12 @@ var adGroupCreateSchema2 = z31.strictObject({
33541
33573
  /** Only under a catalog campaign; which of its products this ad group may show. */
33542
33574
  productSet: productSetSchema.optional(),
33543
33575
  /** The campaign's catalog, filled in at stage time — the product set must name it. */
33544
- productFeedId: z31.string().trim().min(1).optional()
33545
- }).superRefine((p, ctx) => checkBidPair2(p.bidStrategy, p.maxBidMicros, ctx));
33576
+ productFeedId: z31.string().trim().min(1).optional(),
33577
+ audienceBidMultipliers: audienceBidMultipliersSchema.optional()
33578
+ }).superRefine((p, ctx) => {
33579
+ checkBidPair2(p.bidStrategy, p.maxBidMicros, ctx);
33580
+ checkMultipliers(p.bidStrategy, p.audienceBidMultipliers, ctx);
33581
+ });
33546
33582
  var adGroupUpdateSchema2 = z31.strictObject({
33547
33583
  name: nameSchema3.optional(),
33548
33584
  description: z31.string().trim().max(CHATGPT_LIMITS.nameMax).optional(),
@@ -33554,9 +33590,12 @@ var adGroupUpdateSchema2 = z31.strictObject({
33554
33590
  billingEvent: z31.enum(CHATGPT_BILLING_EVENTS).optional(),
33555
33591
  /** Replaces the whole product set; `{ filters: [] }` shows the whole catalog. */
33556
33592
  productSet: productSetSchema.optional(),
33557
- productFeedId: z31.string().trim().min(1).optional()
33593
+ productFeedId: z31.string().trim().min(1).optional(),
33594
+ /** Replaces the ad group's adjustments; an empty list clears them. */
33595
+ audienceBidMultipliers: audienceBidMultipliersSchema.optional()
33558
33596
  }).superRefine((p, ctx) => {
33559
33597
  noEmptyUpdate(p, ctx);
33598
+ checkMultipliers(p.bidStrategy, p.audienceBidMultipliers, ctx);
33560
33599
  checkBidPair2(p.bidStrategy, p.maxBidMicros, ctx);
33561
33600
  });
33562
33601
  var titleSchema = z31.string().trim().refine(
@@ -34346,7 +34385,7 @@ var SHARED_ARGS = {
34346
34385
  description: { type: "string", description: "Internal description" },
34347
34386
  "context-hints": {
34348
34387
  type: "string",
34349
- description: "When the ad would be useful, comma-separated short phrases \u2014 ChatGPT's counterpart to keywords. On update it replaces the whole list"
34388
+ description: "When the ad would be useful, short phrases separated by commas \u2014 or by | when a phrase has a comma. ChatGPT's counterpart to keywords, written in the language of the campaign's countries, as people ask ChatGPT. On update it replaces the whole list; none clears it"
34350
34389
  },
34351
34390
  "bid-strategy": {
34352
34391
  type: "string",
@@ -34357,6 +34396,10 @@ var SHARED_ARGS = {
34357
34396
  type: "string",
34358
34397
  description: "Query string added to this ad group's landing pages, e.g. utm_content={ad_group_id} \u2014 none clears"
34359
34398
  },
34399
+ "audience-bids": {
34400
+ type: "string",
34401
+ description: "Raise or lower the fixed bid for people in an audience: audience=multiplier pairs, 0.1 to 10, e.g. caud_123=1.5,caud_456=0.8. Audiences of ~25,000 people only; not in the EEA or Switzerland. On update it replaces them; none clears"
34402
+ },
34360
34403
  "product-filters": {
34361
34404
  type: "string",
34362
34405
  description: "Catalog campaigns only: which products it shows \u2014 e.g. brand in Acme; price lte 5000 (preview with `catalogs products`). On update it replaces the set; none shows the whole catalog"
@@ -34372,6 +34415,32 @@ var UPDATE_ARGS2 = {
34372
34415
  id: { type: "positional", description: "Ad group id (adgrp_\u2026) or a staged chatgpt_temp_ ref", required: true },
34373
34416
  ...SHARED_ARGS
34374
34417
  };
34418
+ function contextHintsArg(value) {
34419
+ if (typeof value !== "string" || value.trim() === "") {
34420
+ return void 0;
34421
+ }
34422
+ if (value.trim().toLowerCase() === "none") {
34423
+ return [];
34424
+ }
34425
+ const separator = value.includes("|") ? "|" : ",";
34426
+ return value.split(separator).map((hint) => hint.trim()).filter(Boolean);
34427
+ }
34428
+ function audienceBidsArg(value) {
34429
+ if (typeof value !== "string" || value.trim() === "") {
34430
+ return void 0;
34431
+ }
34432
+ if (value.trim().toLowerCase() === "none") {
34433
+ return [];
34434
+ }
34435
+ return value.split(",").map((pair) => pair.trim()).filter(Boolean).map((pair) => {
34436
+ const [audienceId = "", multiplier = ""] = pair.split("=").map((part) => part.trim());
34437
+ const times = Number(multiplier.replace(",", "."));
34438
+ if (!audienceId || !Number.isFinite(times)) {
34439
+ failWriteValidation(`"${pair}" is not an audience bid \u2014 write audience=multiplier, e.g. caud_123=1.5`);
34440
+ }
34441
+ return { audienceId, multiplierMicros: Math.round(times * 1e6) };
34442
+ });
34443
+ }
34375
34444
  function productSetArg(value) {
34376
34445
  const filters = productFiltersArg(value);
34377
34446
  return filters === void 0 ? void 0 : { filters };
@@ -34380,11 +34449,12 @@ function sharedPayload(args8) {
34380
34449
  return {
34381
34450
  name: args8.name,
34382
34451
  description: args8.description,
34383
- contextHints: csvArg(args8["context-hints"]),
34452
+ contextHints: contextHintsArg(args8["context-hints"]),
34384
34453
  bidStrategy: lowerArg(args8["bid-strategy"]),
34385
34454
  maxBidMicros: microsFromArg(args8["max-bid"], "--max-bid"),
34386
34455
  trackingTemplate: clearableArg(args8["tracking-template"]),
34387
34456
  productSet: productSetArg(args8["product-filters"]),
34457
+ audienceBidMultipliers: audienceBidsArg(args8["audience-bids"]),
34388
34458
  status: lowerArg(args8.status)
34389
34459
  };
34390
34460
  }
@@ -34408,7 +34478,7 @@ Examples:
34408
34478
  args: CREATE_ARGS,
34409
34479
  run: async ({ args: args8 }) => {
34410
34480
  await stageChatgptOp(adGroupCreateOp(args8), REFERENCE2, (staged) => [
34411
- `Next, give it a card: baker ads chatgpt ads create --ad-group-id ${staged.ref} --name "\u2026" --title "\u2026" --body "\u2026" --target-url https://\u2026 --image-url https://\u2026`
34481
+ adGroupNextHint(staged.ref, args8["product-filters"] !== void 0)
34412
34482
  ]);
34413
34483
  }
34414
34484
  });
@@ -34437,6 +34507,10 @@ Examples:
34437
34507
  var adGroupsPauseCommand = statusCommand2("adGroup", "pause", REFERENCE2);
34438
34508
  var adGroupsResumeCommand = statusCommand2("adGroup", "resume", REFERENCE2);
34439
34509
  var adGroupsArchiveCommand = statusCommand2("adGroup", "archive", REFERENCE2);
34510
+ function adGroupNextHint(ref, productFilters) {
34511
+ const productAd = `baker ads chatgpt ads create-product --ad-group-id ${ref} --name "\u2026"`;
34512
+ return productFilters ? `Next, give it its product ad: ${productAd}` : `Next, give it a card: baker ads chatgpt ads create --ad-group-id ${ref} --name "\u2026" --title "\u2026" --body "\u2026" --target-url https://\u2026 --image-url https://\u2026 \u2014 under a catalog campaign, its product ad instead: ${productAd}`;
34513
+ }
34440
34514
 
34441
34515
  // src/commands/ads/chatgpt/ad-groups.ts
34442
34516
  var args = {
@@ -34707,7 +34781,7 @@ function sha256(value) {
34707
34781
  }
34708
34782
  function normalizedEmail(value) {
34709
34783
  const email = value.trim().toLowerCase();
34710
- return /^[^@\s]+@[^@\s]+$/.test(email) ? email : void 0;
34784
+ return /^[^@\s;,|]+@[^@\s;,|]+\.[^@\s;,|]+$/.test(email) ? email : void 0;
34711
34785
  }
34712
34786
  function normalizedPhone(value, defaultCountry) {
34713
34787
  const country = defaultCountry && isSupportedCountry(defaultCountry) ? defaultCountry : void 0;
@@ -34728,13 +34802,22 @@ function hashedMembers(input) {
34728
34802
  }
34729
34803
  return { emailHashes: [...emailHashes], phoneHashes: [...phoneHashes], rejected };
34730
34804
  }
34731
- var EMAIL_COLUMNS = ["email", "e-mail", "correo", "mail"];
34732
- var PHONE_COLUMNS = ["phone", "phone_number", "telefono", "tel\xE9fono", "mobile", "movil", "m\xF3vil"];
34805
+ function headerWords(cell3) {
34806
+ return cell3.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
34807
+ }
34808
+ var EMAIL_HEADER = /\b(e ?mail|correo|mail)\b/;
34809
+ var PHONE_HEADER = /\b(phone|telefono|movil|mobile|tel|celular)\b/;
34733
34810
  function membersFromFile(content) {
34734
- const rows = parse(content, { bom: true, skip_empty_lines: true, relax_column_count: true, trim: true });
34735
- const header = (rows[0] ?? []).map((cell3) => cell3.toLowerCase());
34736
- const emailColumn = header.findIndex((cell3) => EMAIL_COLUMNS.includes(cell3));
34737
- const phoneColumn = header.findIndex((cell3) => PHONE_COLUMNS.includes(cell3));
34811
+ const rows = parse(content, {
34812
+ bom: true,
34813
+ skip_empty_lines: true,
34814
+ relax_column_count: true,
34815
+ trim: true,
34816
+ delimiter: [",", ";", " "]
34817
+ });
34818
+ const header = (rows[0] ?? []).map(headerWords);
34819
+ const emailColumn = header.findIndex((cell3) => EMAIL_HEADER.test(cell3));
34820
+ const phoneColumn = header.findIndex((cell3) => !EMAIL_HEADER.test(cell3) && PHONE_HEADER.test(cell3));
34738
34821
  if (emailColumn >= 0 || phoneColumn >= 0) {
34739
34822
  const cells = (column) => column < 0 ? [] : rows.slice(1).flatMap((row) => row[column] ? [row[column]] : []);
34740
34823
  return { emails: cells(emailColumn), phones: cells(phoneColumn) };
@@ -34756,21 +34839,23 @@ var MEMBER_ARGS = {
34756
34839
  };
34757
34840
  function membersFromArgs(args8, readFile34 = (path48) => readFileSync2(path48, "utf8")) {
34758
34841
  const fromFile = typeof args8.file === "string" && args8.file ? membersFromFile(readFile34(args8.file)) : { emails: [], phones: [] };
34842
+ const given = [...csvArg(args8.emails) ?? [], ...fromFile.emails];
34843
+ const givenPhones = [...csvArg(args8.phones) ?? [], ...fromFile.phones];
34759
34844
  const members = hashedMembers({
34760
- emails: [...csvArg(args8.emails) ?? [], ...fromFile.emails],
34761
- phones: [...csvArg(args8.phones) ?? [], ...fromFile.phones],
34845
+ emails: given,
34846
+ phones: givenPhones,
34762
34847
  defaultCountry: typeof args8.country === "string" ? args8.country.toUpperCase() : void 0
34763
34848
  });
34764
34849
  const count2 = members.emailHashes.length + members.phoneHashes.length;
34765
- if (count2 > CHATGPT_MAX_STAGED_MEMBERS) {
34766
- failWriteValidation(`${count2} members \u2014 at most ${CHATGPT_MAX_STAGED_MEMBERS} per change; split the file`);
34767
- }
34850
+ const offered = given.length + givenPhones.length + (typeof args8.file === "string" && args8.file ? 1 : 0);
34851
+ const refusal = count2 > CHATGPT_MAX_STAGED_MEMBERS ? `${count2} members \u2014 at most ${CHATGPT_MAX_STAGED_MEMBERS} per change; split the file` : count2 === 0 && offered > 0 ? "none of the values was an email or a valid phone \u2014 name the columns email and phone, and give phones a +country code or --country" : void 0;
34768
34852
  return {
34769
34853
  payload: {
34770
34854
  ...members.emailHashes.length > 0 ? { emailHashes: members.emailHashes } : {},
34771
34855
  ...members.phoneHashes.length > 0 ? { phoneHashes: members.phoneHashes } : {}
34772
34856
  },
34773
- rejected: members.rejected.length
34857
+ rejected: members.rejected.length,
34858
+ ...refusal ? { refusal } : {}
34774
34859
  };
34775
34860
  }
34776
34861
  function rejectedHint(rejected) {
@@ -34825,7 +34910,10 @@ Examples:
34825
34910
  },
34826
34911
  args: CREATE_ARGS3,
34827
34912
  run: async ({ args: args8 }) => {
34828
- const { payload, rejected } = membersFromArgs(args8);
34913
+ const { payload, rejected, refusal } = membersFromArgs(args8);
34914
+ if (refusal) {
34915
+ failWriteValidation(refusal);
34916
+ }
34829
34917
  await stageChatgptOp(
34830
34918
  {
34831
34919
  kind: "audience.create",
@@ -34859,7 +34947,10 @@ Example:
34859
34947
  },
34860
34948
  args: args8,
34861
34949
  run: async ({ args: parsed }) => {
34862
- const { payload, rejected } = membersFromArgs(parsed);
34950
+ const { payload, rejected, refusal } = membersFromArgs(parsed);
34951
+ if (refusal) {
34952
+ failWriteValidation(refusal);
34953
+ }
34863
34954
  if (!payload.emailHashes && !payload.phoneHashes) {
34864
34955
  failWriteValidation("name at least one email or phone with --emails, --phones or --file");
34865
34956
  }
@@ -35025,7 +35116,8 @@ Examples:
35025
35116
  args: CREATE_ARGS4,
35026
35117
  run: async ({ args: args8 }) => {
35027
35118
  await stageChatgptOp(campaignCreateOp(args8), REFERENCE5, (staged) => [
35028
- `Next, give it an ad group: baker ads chatgpt ad-groups create --campaign-id ${staged.ref} --name "\u2026" --context-hints "\u2026" --max-bid 2.50`
35119
+ `Next, give it an ad group: baker ads chatgpt ad-groups create --campaign-id ${staged.ref} --name "\u2026" --context-hints "\u2026" --max-bid 2.50`,
35120
+ "Check this summary against everything the user asked for \u2014 countries, places to leave out, budget, dates \u2014 and stage what is missing before saying it is ready."
35029
35121
  ]);
35030
35122
  }
35031
35123
  });