@koda-sl/baker-cli 0.392.0-dev.9666ad9b3 → 0.393.0-dev.10b6da011

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
@@ -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
  }
@@ -34346,7 +34363,7 @@ var SHARED_ARGS = {
34346
34363
  description: { type: "string", description: "Internal description" },
34347
34364
  "context-hints": {
34348
34365
  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"
34366
+ 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. On update it replaces the whole list; none clears it"
34350
34367
  },
34351
34368
  "bid-strategy": {
34352
34369
  type: "string",
@@ -34372,6 +34389,16 @@ var UPDATE_ARGS2 = {
34372
34389
  id: { type: "positional", description: "Ad group id (adgrp_\u2026) or a staged chatgpt_temp_ ref", required: true },
34373
34390
  ...SHARED_ARGS
34374
34391
  };
34392
+ function contextHintsArg(value) {
34393
+ if (typeof value !== "string" || value.trim() === "") {
34394
+ return void 0;
34395
+ }
34396
+ if (value.trim().toLowerCase() === "none") {
34397
+ return [];
34398
+ }
34399
+ const separator = value.includes("|") ? "|" : ",";
34400
+ return value.split(separator).map((hint) => hint.trim()).filter(Boolean);
34401
+ }
34375
34402
  function productSetArg(value) {
34376
34403
  const filters = productFiltersArg(value);
34377
34404
  return filters === void 0 ? void 0 : { filters };
@@ -34380,7 +34407,7 @@ function sharedPayload(args8) {
34380
34407
  return {
34381
34408
  name: args8.name,
34382
34409
  description: args8.description,
34383
- contextHints: csvArg(args8["context-hints"]),
34410
+ contextHints: contextHintsArg(args8["context-hints"]),
34384
34411
  bidStrategy: lowerArg(args8["bid-strategy"]),
34385
34412
  maxBidMicros: microsFromArg(args8["max-bid"], "--max-bid"),
34386
34413
  trackingTemplate: clearableArg(args8["tracking-template"]),
@@ -34408,7 +34435,7 @@ Examples:
34408
34435
  args: CREATE_ARGS,
34409
34436
  run: async ({ args: args8 }) => {
34410
34437
  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`
34438
+ adGroupNextHint(staged.ref, args8["product-filters"] !== void 0)
34412
34439
  ]);
34413
34440
  }
34414
34441
  });
@@ -34437,6 +34464,10 @@ Examples:
34437
34464
  var adGroupsPauseCommand = statusCommand2("adGroup", "pause", REFERENCE2);
34438
34465
  var adGroupsResumeCommand = statusCommand2("adGroup", "resume", REFERENCE2);
34439
34466
  var adGroupsArchiveCommand = statusCommand2("adGroup", "archive", REFERENCE2);
34467
+ function adGroupNextHint(ref, productFilters) {
34468
+ const productAd = `baker ads chatgpt ads create-product --ad-group-id ${ref} --name "\u2026"`;
34469
+ 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}`;
34470
+ }
34440
34471
 
34441
34472
  // src/commands/ads/chatgpt/ad-groups.ts
34442
34473
  var args = {
@@ -34707,7 +34738,7 @@ function sha256(value) {
34707
34738
  }
34708
34739
  function normalizedEmail(value) {
34709
34740
  const email = value.trim().toLowerCase();
34710
- return /^[^@\s]+@[^@\s]+$/.test(email) ? email : void 0;
34741
+ return /^[^@\s;,|]+@[^@\s;,|]+\.[^@\s;,|]+$/.test(email) ? email : void 0;
34711
34742
  }
34712
34743
  function normalizedPhone(value, defaultCountry) {
34713
34744
  const country = defaultCountry && isSupportedCountry(defaultCountry) ? defaultCountry : void 0;
@@ -34728,13 +34759,22 @@ function hashedMembers(input) {
34728
34759
  }
34729
34760
  return { emailHashes: [...emailHashes], phoneHashes: [...phoneHashes], rejected };
34730
34761
  }
34731
- var EMAIL_COLUMNS = ["email", "e-mail", "correo", "mail"];
34732
- var PHONE_COLUMNS = ["phone", "phone_number", "telefono", "tel\xE9fono", "mobile", "movil", "m\xF3vil"];
34762
+ function headerWords(cell3) {
34763
+ return cell3.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
34764
+ }
34765
+ var EMAIL_HEADER = /\b(e ?mail|correo|mail)\b/;
34766
+ var PHONE_HEADER = /\b(phone|telefono|movil|mobile|tel|celular)\b/;
34733
34767
  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));
34768
+ const rows = parse(content, {
34769
+ bom: true,
34770
+ skip_empty_lines: true,
34771
+ relax_column_count: true,
34772
+ trim: true,
34773
+ delimiter: [",", ";", " "]
34774
+ });
34775
+ const header = (rows[0] ?? []).map(headerWords);
34776
+ const emailColumn = header.findIndex((cell3) => EMAIL_HEADER.test(cell3));
34777
+ const phoneColumn = header.findIndex((cell3) => !EMAIL_HEADER.test(cell3) && PHONE_HEADER.test(cell3));
34738
34778
  if (emailColumn >= 0 || phoneColumn >= 0) {
34739
34779
  const cells = (column) => column < 0 ? [] : rows.slice(1).flatMap((row) => row[column] ? [row[column]] : []);
34740
34780
  return { emails: cells(emailColumn), phones: cells(phoneColumn) };
@@ -34756,21 +34796,23 @@ var MEMBER_ARGS = {
34756
34796
  };
34757
34797
  function membersFromArgs(args8, readFile34 = (path48) => readFileSync2(path48, "utf8")) {
34758
34798
  const fromFile = typeof args8.file === "string" && args8.file ? membersFromFile(readFile34(args8.file)) : { emails: [], phones: [] };
34799
+ const given = [...csvArg(args8.emails) ?? [], ...fromFile.emails];
34800
+ const givenPhones = [...csvArg(args8.phones) ?? [], ...fromFile.phones];
34759
34801
  const members = hashedMembers({
34760
- emails: [...csvArg(args8.emails) ?? [], ...fromFile.emails],
34761
- phones: [...csvArg(args8.phones) ?? [], ...fromFile.phones],
34802
+ emails: given,
34803
+ phones: givenPhones,
34762
34804
  defaultCountry: typeof args8.country === "string" ? args8.country.toUpperCase() : void 0
34763
34805
  });
34764
34806
  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
- }
34807
+ const offered = given.length + givenPhones.length + (typeof args8.file === "string" && args8.file ? 1 : 0);
34808
+ 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
34809
  return {
34769
34810
  payload: {
34770
34811
  ...members.emailHashes.length > 0 ? { emailHashes: members.emailHashes } : {},
34771
34812
  ...members.phoneHashes.length > 0 ? { phoneHashes: members.phoneHashes } : {}
34772
34813
  },
34773
- rejected: members.rejected.length
34814
+ rejected: members.rejected.length,
34815
+ ...refusal ? { refusal } : {}
34774
34816
  };
34775
34817
  }
34776
34818
  function rejectedHint(rejected) {
@@ -34825,7 +34867,10 @@ Examples:
34825
34867
  },
34826
34868
  args: CREATE_ARGS3,
34827
34869
  run: async ({ args: args8 }) => {
34828
- const { payload, rejected } = membersFromArgs(args8);
34870
+ const { payload, rejected, refusal } = membersFromArgs(args8);
34871
+ if (refusal) {
34872
+ failWriteValidation(refusal);
34873
+ }
34829
34874
  await stageChatgptOp(
34830
34875
  {
34831
34876
  kind: "audience.create",
@@ -34859,7 +34904,10 @@ Example:
34859
34904
  },
34860
34905
  args: args8,
34861
34906
  run: async ({ args: parsed }) => {
34862
- const { payload, rejected } = membersFromArgs(parsed);
34907
+ const { payload, rejected, refusal } = membersFromArgs(parsed);
34908
+ if (refusal) {
34909
+ failWriteValidation(refusal);
34910
+ }
34863
34911
  if (!payload.emailHashes && !payload.phoneHashes) {
34864
34912
  failWriteValidation("name at least one email or phone with --emails, --phones or --file");
34865
34913
  }
@@ -35025,7 +35073,8 @@ Examples:
35025
35073
  args: CREATE_ARGS4,
35026
35074
  run: async ({ args: args8 }) => {
35027
35075
  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`
35076
+ `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`,
35077
+ "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
35078
  ]);
35030
35079
  }
35031
35080
  });