@koda-sl/baker-cli 0.306.7 → 0.306.9

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
@@ -12194,13 +12194,25 @@ registerSchema({
12194
12194
  }
12195
12195
  });
12196
12196
  function extractFields(rows) {
12197
- if (rows.length === 0) return [];
12198
- const first = rows[0];
12199
- return first ? Object.keys(first) : [];
12197
+ const keys = /* @__PURE__ */ new Set();
12198
+ foldObservedFields(rows, keys);
12199
+ return [...keys];
12200
+ }
12201
+ function resultColumns(gaql, rows) {
12202
+ const observed = extractFields(rows);
12203
+ const columns = [];
12204
+ for (const field of parseSelectedFields(gaql)) {
12205
+ const leaves = observed.filter((key) => key.startsWith(`${field}.`));
12206
+ columns.push(...leaves.length > 0 ? leaves : [field]);
12207
+ }
12208
+ for (const key of observed) columns.push(key);
12209
+ return [...new Set(columns)];
12210
+ }
12211
+ function isCsvPath(filePath) {
12212
+ return filePath.split(".").pop()?.toLowerCase() === "csv";
12200
12213
  }
12201
12214
  function writeRowsToFile(filePath, rows, fields, append) {
12202
- const ext = filePath.split(".").pop()?.toLowerCase();
12203
- if (ext === "csv") {
12215
+ if (isCsvPath(filePath)) {
12204
12216
  if (!append || !existsSync2(filePath)) {
12205
12217
  writeFileSync(filePath, `${toCsvRow(fields)}
12206
12218
  `, "utf-8");
@@ -12208,7 +12220,7 @@ function writeRowsToFile(filePath, rows, fields, append) {
12208
12220
  const csvRows = rows.map((row) => toCsvRow(fields.map((f) => String(row[f] ?? ""))));
12209
12221
  appendFileSync(filePath, `${csvRows.join("\n")}
12210
12222
  `, "utf-8");
12211
- } else if (ext === "jsonl") {
12223
+ } else if (filePath.split(".").pop()?.toLowerCase() === "jsonl") {
12212
12224
  const lines = rows.map((row) => JSON.stringify(row));
12213
12225
  if (append && existsSync2(filePath)) {
12214
12226
  appendFileSync(filePath, `${lines.join("\n")}
@@ -12298,16 +12310,25 @@ var writeJsonlRows = (rows, _fields, notices) => {
12298
12310
  for (const line of notices) writeLine(line);
12299
12311
  for (const row of rows) writeLine(JSON.stringify(row));
12300
12312
  };
12313
+ var OMITTED = "(omitted)";
12314
+ var OMITTED_FALSE = `false ${OMITTED}`;
12315
+ var OMITTED_LEGEND = `> ${OMITTED} = Google left this field off the row, which is how it sends false, 0 and "not set": the field WAS read. A field with no column here was not read.`;
12316
+ function mdCell(row, field, booleanColumns) {
12317
+ if (!(field in row)) return booleanColumns.has(field) ? OMITTED_FALSE : OMITTED;
12318
+ return String(row[field] ?? "").replace(/\|/g, "\\|");
12319
+ }
12301
12320
  var writeMdRows = (rows, fields, notices) => {
12321
+ const booleanColumns = new Set(fields.filter((f) => rows.some((row) => typeof row[f] === "boolean")));
12302
12322
  for (const line of notices) writeLine(line);
12303
12323
  writeLine(`| ${fields.join(" | ")} |`);
12304
12324
  writeLine(`| ${fields.map(() => "---").join(" | ")} |`);
12305
- for (const row of rows) writeLine(`| ${fields.map((f) => String(row[f] ?? "")).join(" | ")} |`);
12325
+ for (const row of rows) writeLine(`| ${fields.map((f) => mdCell(row, f, booleanColumns)).join(" | ")} |`);
12326
+ if (rows.some((row) => fields.some((f) => !(f in row)))) writeLine(OMITTED_LEGEND);
12306
12327
  };
12307
12328
  var ROW_WRITERS = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };
12308
- function writeRows(format, rows, notices) {
12329
+ function writeRows(format, rows, columns, notices) {
12309
12330
  const write2 = ROW_WRITERS[format] ?? writeMdRows;
12310
- write2(rows, Object.keys(rows[0] ?? {}), notices);
12331
+ write2(rows, columns, notices);
12311
12332
  if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {
12312
12333
  for (const line of notices) writeLine(line);
12313
12334
  }
@@ -12334,7 +12355,7 @@ function outputResults(rows, fieldDescs, warnings, args, cached, read) {
12334
12355
  );
12335
12356
  return;
12336
12357
  }
12337
- writeRows(format, rows, warningLines(allWarnings, format));
12358
+ writeRows(format, rows, resultColumns(read.gaql, rows), warningLines(allWarnings, format));
12338
12359
  }
12339
12360
  function resolveGaql(args, limit) {
12340
12361
  if (args.preset) {
@@ -12453,15 +12474,27 @@ function missingFieldWarningsFor(gaql, observed, rowCount2) {
12453
12474
  }
12454
12475
  ];
12455
12476
  }
12477
+ function unwrittenColumnWarnings(header, observed) {
12478
+ const written = new Set(header);
12479
+ const unwritten = [...observed].filter((key) => !written.has(key));
12480
+ if (unwritten.length === 0) return [];
12481
+ return [
12482
+ {
12483
+ code: "UNWRITTEN_COLUMNS",
12484
+ message: `The file has no column for ${unwritten.join(", ")}: the header was fixed on the first page, which carried none of them, so their values from later pages are NOT in the file. Do not read those columns as empty. SELECT these fields by name instead of their parent, or write to a .jsonl file, then re-run.`
12485
+ }
12486
+ ];
12487
+ }
12456
12488
  function keywordServingOptions(gaql) {
12457
12489
  return { statusReasonsSelected: parseSelectedFields(gaql).includes("ad_group_criterion.primary_status_reasons") };
12458
12490
  }
12459
- async function fetchAllPages(body, args, servingOptions) {
12491
+ async function fetchAllPages(gaql, body, args, servingOptions) {
12460
12492
  let allRows = [];
12461
12493
  let pageToken = args.cursor;
12462
12494
  let totalRowsWritten = 0;
12463
12495
  let keywordLimits;
12464
12496
  const observedFields = /* @__PURE__ */ new Set();
12497
+ let fileColumns;
12465
12498
  do {
12466
12499
  const requestBody2 = { ...body };
12467
12500
  if (pageToken) requestBody2.pageToken = pageToken;
@@ -12470,7 +12503,8 @@ async function fetchAllPages(body, args, servingOptions) {
12470
12503
  pageToken = response.pageToken;
12471
12504
  if (args.out) {
12472
12505
  const shouldAppend = args.append || totalRowsWritten > 0;
12473
- writeRowsToFile(resolve(args.out), rows, extractFields(rows), shouldAppend);
12506
+ fileColumns ??= resultColumns(gaql, rows);
12507
+ writeRowsToFile(resolve(args.out), rows, fileColumns, shouldAppend);
12474
12508
  totalRowsWritten += rows.length;
12475
12509
  keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
12476
12510
  foldObservedFields(rows, observedFields);
@@ -12484,7 +12518,8 @@ async function fetchAllPages(body, args, servingOptions) {
12484
12518
  lastPageToken: pageToken,
12485
12519
  totalRowsWritten,
12486
12520
  keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),
12487
- observedFields
12521
+ observedFields,
12522
+ fileColumns: fileColumns ?? []
12488
12523
  };
12489
12524
  }
12490
12525
  async function executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey) {
@@ -12494,7 +12529,8 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
12494
12529
  if (args.cursor) body.pageToken = args.cursor;
12495
12530
  if (!useCache) body.skipCache = true;
12496
12531
  const servingOptions = keywordServingOptions(finalQuery);
12497
- const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields } = await fetchAllPages(
12532
+ const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields, fileColumns } = await fetchAllPages(
12533
+ finalQuery,
12498
12534
  body,
12499
12535
  args,
12500
12536
  servingOptions
@@ -12510,6 +12546,8 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
12510
12546
  ...warnings,
12511
12547
  ...renderKeywordLimits(keywordLimits),
12512
12548
  ...missingFieldWarningsFor(finalQuery, observedFields, totalRowsWritten),
12549
+ // Only a CSV has a header to fall out of; json and jsonl files carry every row whole.
12550
+ ...isCsvPath(args.out) ? unwrittenColumnWarnings(fileColumns, observedFields) : [],
12513
12551
  ...conversionMixWarnings(finalQuery),
12514
12552
  ...truncation
12515
12553
  ];
@@ -12541,6 +12579,7 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
12541
12579
  args,
12542
12580
  false,
12543
12581
  {
12582
+ gaql: finalQuery,
12544
12583
  effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
12545
12584
  all: Boolean(args.all),
12546
12585
  nextCursor: lastPageToken
@@ -12658,7 +12697,11 @@ Examples:
12658
12697
  ],
12659
12698
  args,
12660
12699
  true,
12661
- { effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)), all: Boolean(args.all) }
12700
+ {
12701
+ gaql: finalQuery,
12702
+ effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
12703
+ all: Boolean(args.all)
12704
+ }
12662
12705
  );
12663
12706
  return;
12664
12707
  }
@@ -21723,7 +21766,26 @@ var OPTIMIZATION_GOALS = [
21723
21766
  "THRUPLAY",
21724
21767
  "VALUE",
21725
21768
  "CONVERSATIONS",
21726
- "DERIVED_EVENTS"
21769
+ "DERIVED_EVENTS",
21770
+ // Added 2026-09-18 from Meta's own generated enum spec
21771
+ // (facebook-business-sdk-codegen › AdSet_optimization_goal). Every one of these
21772
+ // is a goal Meta accepts and Baker used to refuse at stage time — the messaging
21773
+ // and Instagram-profile goals are the ones that cost real campaigns, because a
21774
+ // click-to-WhatsApp ad set cannot be expressed without them.
21775
+ "ADVERTISER_SILOED_VALUE",
21776
+ "APP_INSTALLS_AND_OFFSITE_CONVERSIONS",
21777
+ "AUTOMATIC_OBJECTIVE",
21778
+ "ENGAGED_PAGE_VIEWS",
21779
+ "IN_APP_VALUE",
21780
+ "MEANINGFUL_CALL_ATTEMPT",
21781
+ "MESSAGING_APPOINTMENT_CONVERSION",
21782
+ "MESSAGING_DEEP_CONVERSATION_AND_FOLLOW",
21783
+ "MESSAGING_PURCHASE_CONVERSION",
21784
+ "PROFILE_AND_PAGE_ENGAGEMENT",
21785
+ "PROFILE_VISIT",
21786
+ "REMINDERS_SET",
21787
+ "SUBSCRIBERS",
21788
+ "VISIT_INSTAGRAM_PROFILE"
21727
21789
  ];
21728
21790
  var OPTIMIZATION_GOALS_REQUIRING_PAGE = [
21729
21791
  "LEAD_GENERATION",
@@ -21745,7 +21807,23 @@ var DESTINATION_TYPES = [
21745
21807
  "WHATSAPP",
21746
21808
  "PHONE_CALL",
21747
21809
  "INSTAGRAM_PROFILE",
21748
- "SHOP_AUTOMATIC"
21810
+ "SHOP_AUTOMATIC",
21811
+ // Added 2026-09-18 from adcampaign_destination_type. The four MESSAGING_* values
21812
+ // are the click-to-message destinations (Messenger / Instagram Direct / WhatsApp
21813
+ // in every combination Meta ships); without them Baker could not stage a
21814
+ // click-to-WhatsApp ad set at all.
21815
+ "APPLINKS_AUTOMATIC",
21816
+ "FACEBOOK",
21817
+ "FACEBOOK_LIVE",
21818
+ "FACEBOOK_PAGE",
21819
+ "IMAGINE",
21820
+ "INSTAGRAM_LIVE",
21821
+ "INSTAGRAM_PROFILE_AND_FACEBOOK_PAGE",
21822
+ "MESSAGING_INSTAGRAM_DIRECT_MESSENGER",
21823
+ "MESSAGING_INSTAGRAM_DIRECT_MESSENGER_WHATSAPP",
21824
+ "MESSAGING_INSTAGRAM_DIRECT_WHATSAPP",
21825
+ "MESSAGING_MESSENGER_WHATSAPP",
21826
+ "ON_EVENT"
21749
21827
  ];
21750
21828
  var CUSTOM_EVENT_TYPES = [
21751
21829
  "PURCHASE",
@@ -21759,7 +21837,28 @@ var CUSTOM_EVENT_TYPES = [
21759
21837
  "SUBSCRIBE",
21760
21838
  "START_TRIAL",
21761
21839
  "CONTACT",
21762
- "OTHER"
21840
+ "OTHER",
21841
+ // Added 2026-09-18 from AdPromotedObject_custom_event_type. SCHEDULE,
21842
+ // SUBMIT_APPLICATION, SERVICE_BOOKING_REQUEST and
21843
+ // MESSAGING_CONVERSATION_STARTED_7D are the conversions service businesses and
21844
+ // click-to-message campaigns actually optimize toward.
21845
+ "ACHIEVEMENT_UNLOCKED",
21846
+ "ADD_TO_WISHLIST",
21847
+ "AD_IMPRESSION",
21848
+ "CUSTOMIZE_PRODUCT",
21849
+ "D2_RETENTION",
21850
+ "D7_RETENTION",
21851
+ "DONATE",
21852
+ "FIND_LOCATION",
21853
+ "LEVEL_ACHIEVED",
21854
+ "LISTING_INTERACTION",
21855
+ "MESSAGING_CONVERSATION_STARTED_7D",
21856
+ "RATE",
21857
+ "SCHEDULE",
21858
+ "SERVICE_BOOKING_REQUEST",
21859
+ "SPENT_CREDITS",
21860
+ "SUBMIT_APPLICATION",
21861
+ "TUTORIAL_COMPLETION"
21763
21862
  ];
21764
21863
  var ATTRIBUTION_EVENT_TYPES = ["CLICK_THROUGH", "VIEW_THROUGH", "ENGAGED_VIDEO_VIEW"];
21765
21864
  var ADVANTAGE_AUDIENCE_OPEN_AGE_MAX = 65;
@@ -21878,7 +21977,37 @@ var CTA_TYPES2 = [
21878
21977
  "LINK_CARD",
21879
21978
  "DIAL_CODE",
21880
21979
  "FIND_YOUR_GROUPS",
21881
- "START_ORDER"
21980
+ "START_ORDER",
21981
+ // Added 2026-09-18 from Meta's generated enum spec
21982
+ // (AdCreative_call_to_action_type). Additive on purpose: 37 of the values above
21983
+ // are absent from that spec and present in the live 400, which is the
21984
+ // disagreement the note above is about — so the spec widens this list and never
21985
+ // prunes it.
21986
+ "AUDIO_CALL",
21987
+ "BROWSE_SHOP",
21988
+ "CONTACT",
21989
+ "FIND_A_GROUP",
21990
+ "FIND_OUT_MORE",
21991
+ "FOLLOW_NEWS_STORYLINE",
21992
+ "FOLLOW_PAGE",
21993
+ "FOLLOW_USER",
21994
+ "GET_DETAILS",
21995
+ "GET_STARTED",
21996
+ "SAY_THANKS",
21997
+ "SEE_SHOP",
21998
+ "SEND_A_GIFT",
21999
+ "SEND_GIFT_MONEY",
22000
+ "SHARE",
22001
+ "SHOP_WITH_AI",
22002
+ "SOTTO_SUBSCRIBE",
22003
+ "TRY_ON_WITH_AI",
22004
+ "VIDEO_ANNOTATION",
22005
+ "VIDEO_CALL",
22006
+ "VIEW_CART",
22007
+ "VIEW_IN_CART",
22008
+ "VISIT_PAGES_FEED",
22009
+ "VISIT_WEBSITE",
22010
+ "WOODHENGE_SUPPORT"
21882
22011
  ];
21883
22012
  var ASSET_FEED_CTA_TYPES = [
21884
22013
  "OPEN_LINK",
@@ -21997,6 +22126,11 @@ var ENROLL_STATUSES = ["OPT_IN", "OPT_OUT"];
21997
22126
  var STAGEABLE_CREATE_STATUSES4 = ["ACTIVE", "PAUSED"];
21998
22127
  var UPDATE_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
21999
22128
  var CUSTOM_AUDIENCE_SUBTYPES = ["CUSTOM", "WEBSITE", "ENGAGEMENT", "LOOKALIKE"];
22129
+ var CUSTOMER_FILE_SOURCES = [
22130
+ "USER_PROVIDED_ONLY",
22131
+ "PARTNER_PROVIDED_ONLY",
22132
+ "BOTH_USER_AND_PARTNER_PROVIDED"
22133
+ ];
22000
22134
  var MEDIA_KINDS = ["image", "video"];
22001
22135
  var CURRENCY_MINIMUMS2 = {
22002
22136
  USD: { dailyBudgetMin: 1 },
@@ -22014,6 +22148,119 @@ import { z as z30 } from "zod";
22014
22148
  // ../api/src/ads-meta/placements.ts
22015
22149
  var FACEBOOK_POSITIONS_REQUIRING_FEED = ["marketplace", "search", "profile_feed", "notification"];
22016
22150
  var PLACEMENT_TARGETING_DOC = "https://developers.facebook.com/docs/marketing-api/audiences/reference/placement-targeting/";
22151
+ var PUBLISHER_PLATFORMS = ["facebook", "instagram", "threads", "messenger", "audience_network"];
22152
+ var FACEBOOK_POSITIONS = [
22153
+ "feed",
22154
+ "right_hand_column",
22155
+ "marketplace",
22156
+ "video_feeds",
22157
+ "story",
22158
+ "search",
22159
+ "instream_video",
22160
+ "facebook_reels",
22161
+ "facebook_reels_overlay",
22162
+ "profile_feed",
22163
+ "notification"
22164
+ ];
22165
+ var INSTAGRAM_POSITIONS = [
22166
+ "stream",
22167
+ "story",
22168
+ "explore",
22169
+ "explore_home",
22170
+ "reels",
22171
+ "profile_feed",
22172
+ "ig_search",
22173
+ "profile_reels"
22174
+ ];
22175
+ var AUDIENCE_NETWORK_POSITIONS = ["classic", "rewarded_video"];
22176
+ var MESSENGER_POSITIONS = ["sponsored_messages", "story"];
22177
+ var THREADS_POSITIONS = ["threads_stream"];
22178
+ var DEVICE_PLATFORMS = ["mobile", "desktop", "connected_tv"];
22179
+ var PLACEMENT_VOCABULARIES = {
22180
+ publisher_platforms: PUBLISHER_PLATFORMS,
22181
+ facebook_positions: FACEBOOK_POSITIONS,
22182
+ instagram_positions: INSTAGRAM_POSITIONS,
22183
+ audience_network_positions: AUDIENCE_NETWORK_POSITIONS,
22184
+ messenger_positions: MESSENGER_POSITIONS,
22185
+ threads_positions: THREADS_POSITIONS,
22186
+ device_platforms: DEVICE_PLATFORMS
22187
+ };
22188
+ var SAME_SURFACE_ON_THIS_PLATFORM = {
22189
+ facebook_positions: { reels: "facebook_reels", stream: "feed", stories: "story", ig_search: "search" },
22190
+ instagram_positions: {
22191
+ feed: "stream",
22192
+ facebook_reels: "reels",
22193
+ stories: "story",
22194
+ search: "ig_search"
22195
+ }
22196
+ };
22197
+ function placementFieldsAccepting(value, except) {
22198
+ return Object.keys(PLACEMENT_VOCABULARIES).filter(
22199
+ (field) => field !== except && PLACEMENT_VOCABULARIES[field].includes(value)
22200
+ );
22201
+ }
22202
+ function placementValueIssue(field, value) {
22203
+ const renamed = SAME_SURFACE_ON_THIS_PLATFORM[field]?.[value];
22204
+ if (renamed) {
22205
+ return {
22206
+ field,
22207
+ value,
22208
+ kind: "wrong-field",
22209
+ fix: `Meta calls that surface "${renamed}" in ${field} \u2014 use "${renamed}"`
22210
+ };
22211
+ }
22212
+ const elsewhere = placementFieldsAccepting(value, field);
22213
+ if (elsewhere.length > 0) {
22214
+ return {
22215
+ field,
22216
+ value,
22217
+ kind: "wrong-field",
22218
+ fix: `"${value}" belongs to ${elsewhere.join(" / ")}, not ${field} \u2014 move it there or drop it`
22219
+ };
22220
+ }
22221
+ return {
22222
+ field,
22223
+ value,
22224
+ kind: "unknown",
22225
+ fix: `"${value}" is not a placement Baker knows; ${field} accepts ${PLACEMENT_VOCABULARIES[field].join(", ")}`
22226
+ };
22227
+ }
22228
+ function invalidPlacementValues(targeting) {
22229
+ const issues = [];
22230
+ for (const field of Object.keys(PLACEMENT_VOCABULARIES)) {
22231
+ const allowed = PLACEMENT_VOCABULARIES[field];
22232
+ const selected = targeting?.[field] ?? [];
22233
+ for (const value of selected) {
22234
+ if (!allowed.includes(value)) {
22235
+ issues.push(placementValueIssue(field, value));
22236
+ }
22237
+ }
22238
+ }
22239
+ return issues;
22240
+ }
22241
+ function placementCoRequirementIssues(targeting) {
22242
+ const facebook = targeting?.facebook_positions ?? [];
22243
+ const instagram = targeting?.instagram_positions ?? [];
22244
+ const threads = targeting?.threads_positions ?? [];
22245
+ const devices = targeting?.device_platforms;
22246
+ const issues = [];
22247
+ if (facebook.includes("story")) {
22248
+ if (!facebook.includes("feed") && !instagram.includes("story")) {
22249
+ issues.push(
22250
+ 'Meta refuses Facebook "story" on its own \u2014 select "feed" in facebook_positions or "story" in instagram_positions alongside it'
22251
+ );
22252
+ }
22253
+ if (devices && !devices.includes("mobile")) {
22254
+ issues.push(
22255
+ 'Facebook "story" only runs on mobile \u2014 add "mobile" to device_platforms or drop the story placement'
22256
+ );
22257
+ }
22258
+ }
22259
+ if (threads.includes("threads_stream") && !instagram.includes("stream")) {
22260
+ issues.push('Threads "threads_stream" cannot be selected alone \u2014 add "stream" to instagram_positions');
22261
+ }
22262
+ return issues;
22263
+ }
22017
22264
  var CONSTRAINED_PLACEMENTS = [
22018
22265
  {
22019
22266
  position: "facebook_reels_overlay",
@@ -22128,6 +22375,7 @@ var metaTargetingSchema = z30.object({
22128
22375
  instagram_positions: z30.array(z30.string()).optional(),
22129
22376
  audience_network_positions: z30.array(z30.string()).optional(),
22130
22377
  messenger_positions: z30.array(z30.string()).optional(),
22378
+ threads_positions: z30.array(z30.string()).optional(),
22131
22379
  device_platforms: z30.array(z30.string()).optional(),
22132
22380
  targeting_automation: z30.object({ advantage_audience: z30.union([z30.literal(0), z30.literal(1)]) }).partial().optional()
22133
22381
  }).catchall(z30.unknown());
@@ -22259,6 +22507,13 @@ var adSetFields = {
22259
22507
  start_time: z30.number().int().positive().optional(),
22260
22508
  end_time: z30.number().int().positive().optional(),
22261
22509
  targeting: metaTargetingSchema,
22510
+ /**
22511
+ * The live ad set this one was copied from, verbatim, by `duplicate`. Baker
22512
+ * only — `adSetCommonBody` never sends it to Meta. Set exclusively when the
22513
+ * targeting IS the source's targeting, because that is what it licenses:
22514
+ * placement validation is skipped for it (see `validateAdSetPlacements`).
22515
+ */
22516
+ clonedFrom: z30.string().regex(NUMERIC_ID_REGEX3).optional(),
22262
22517
  /** EU Digital Services Act: who benefits from / pays for the ad. Auto-filled from the account for EU geo when omitted. */
22263
22518
  dsa_beneficiary: z30.string().min(1).max(100).optional(),
22264
22519
  dsa_payor: z30.string().min(1).max(100).optional()
@@ -22359,6 +22614,9 @@ function validateAdvantageAudience(p, ctx) {
22359
22614
  }
22360
22615
  }
22361
22616
  function validateAdSetPlacements(p, ctx) {
22617
+ if (p.clonedFrom) {
22618
+ return;
22619
+ }
22362
22620
  const missingFeed = positionsMissingRequiredFeed(p.targeting);
22363
22621
  if (missingFeed.length > 0) {
22364
22622
  ctx.addIssue({
@@ -22367,6 +22625,19 @@ function validateAdSetPlacements(p, ctx) {
22367
22625
  message: `Meta requires "feed" alongside ${missingFeed.join(", ")} \u2014 add "feed" to facebook_positions or drop those positions (${PLACEMENT_TARGETING_DOC})`
22368
22626
  });
22369
22627
  }
22628
+ for (const issue of invalidPlacementValues(p.targeting)) {
22629
+ if (issue.kind !== "wrong-field") {
22630
+ continue;
22631
+ }
22632
+ ctx.addIssue({
22633
+ code: "custom",
22634
+ path: ["targeting", issue.field],
22635
+ message: `${issue.field} does not accept "${issue.value}". ${issue.fix} (${PLACEMENT_TARGETING_DOC})`
22636
+ });
22637
+ }
22638
+ for (const message of placementCoRequirementIssues(p.targeting)) {
22639
+ ctx.addIssue({ code: "custom", path: ["targeting"], message: `${message} (${PLACEMENT_TARGETING_DOC})` });
22640
+ }
22370
22641
  }
22371
22642
  var adSetCreateSchema = z30.object(adSetFields).superRefine((p, ctx) => {
22372
22643
  validateAdSetBudgetAndBid(p, ctx);
@@ -22665,11 +22936,25 @@ var lookalikeSpecSchema = z30.object({
22665
22936
  ratio: z30.number().min(0.01).max(0.2).optional(),
22666
22937
  country: z30.string().length(2).optional()
22667
22938
  });
22939
+ function valuesOfKey(node, key) {
22940
+ if (Array.isArray(node)) return node.flatMap((entry) => valuesOfKey(entry, key));
22941
+ if (!node || typeof node !== "object") return [];
22942
+ const record = node;
22943
+ return [
22944
+ ...key in record ? [record[key]] : [],
22945
+ ...Object.values(record).flatMap((value) => valuesOfKey(value, key))
22946
+ ];
22947
+ }
22948
+ function mixedEventSourceTypes(rule) {
22949
+ const types = valuesOfKey(rule, "event_sources").flatMap((sources) => Array.isArray(sources) ? sources : []).map((source) => source?.type).filter((type) => typeof type === "string");
22950
+ const distinct = [...new Set(types)].sort();
22951
+ return distinct.length > 1 ? distinct : [];
22952
+ }
22668
22953
  var customAudienceCreateSchema = z30.object({
22669
22954
  name: z30.string().min(1).max(META_LIMITS.audience.nameMax),
22670
22955
  subtype: z30.enum(CUSTOM_AUDIENCE_SUBTYPES),
22671
22956
  description: z30.string().max(500).optional(),
22672
- customer_file_source: z30.string().optional(),
22957
+ customer_file_source: z30.enum(CUSTOMER_FILE_SOURCES).optional(),
22673
22958
  retention_days: z30.number().int().min(1).max(META_LIMITS.audience.retentionDaysMax).optional(),
22674
22959
  lookalike_spec: lookalikeSpecSchema.optional(),
22675
22960
  /** Website/engagement rule — structural passthrough validated by Meta. */
@@ -22681,6 +22966,21 @@ var customAudienceCreateSchema = z30.object({
22681
22966
  if ((p.subtype === "WEBSITE" || p.subtype === "ENGAGEMENT") && !p.rule) {
22682
22967
  ctx.addIssue({ code: "custom", path: ["rule"], message: `${p.subtype} audiences need a rule (use --file)` });
22683
22968
  }
22969
+ if (p.rule && p.retention_days !== void 0) {
22970
+ ctx.addIssue({
22971
+ code: "custom",
22972
+ path: ["retention_days"],
22973
+ message: "a rule carries its own window in retention_seconds, so drop retention_days \u2014 Meta takes it only on a customer-file audience, and sending both leaves the review showing a window the audience will not have"
22974
+ });
22975
+ }
22976
+ const mixed = mixedEventSourceTypes(p.rule);
22977
+ if (mixed.length > 0) {
22978
+ ctx.addIssue({
22979
+ code: "custom",
22980
+ path: ["rule"],
22981
+ message: `one audience covers one kind of source, but this rule mixes ${mixed.join(" and ")} \u2014 split it into one audience per source`
22982
+ });
22983
+ }
22684
22984
  });
22685
22985
  var customAudienceUpdateSchema = z30.object({
22686
22986
  name: z30.string().min(1).max(META_LIMITS.audience.nameMax).optional(),
@@ -23421,6 +23721,7 @@ var audiencesCreateCommand2 = defineCommand57({
23421
23721
  name: "create",
23422
23722
  description: `Stage a new custom audience shell. ${STAGED_NOTE2}
23423
23723
  LOOKALIKE/WEBSITE/ENGAGEMENT specifics go via --file (lookalike_spec / rule). Member-list upload is not staged in v1.
23724
+ A WEBSITE/ENGAGEMENT rule carries its own window in retention_seconds, so --retention-days is refused next to one, and one audience covers one kind of event source (a Page and an Instagram profile are two audiences).
23424
23725
  Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WEBSITE --file rule.json`
23425
23726
  },
23426
23727
  args: {
@@ -23428,7 +23729,10 @@ Example: baker ads meta audiences create --name "Site visitors 30d" --subtype WE
23428
23729
  name: { type: "string", description: "Audience name" },
23429
23730
  subtype: { type: "string", description: "CUSTOM|WEBSITE|ENGAGEMENT|LOOKALIKE" },
23430
23731
  description: { type: "string", description: "Description" },
23431
- "retention-days": { type: "string", description: "Retention window in days" },
23732
+ "retention-days": {
23733
+ type: "string",
23734
+ description: "Retention window in days \u2014 customer-file audiences only; a rule carries its own retention_seconds"
23735
+ },
23432
23736
  file: { type: "string", description: "JSON file with lookalike_spec / rule / full payload" }
23433
23737
  },
23434
23738
  run: async ({ args }) => {
@@ -57326,7 +57630,7 @@ registerSchema({
57326
57630
  },
57327
57631
  sort: {
57328
57632
  type: "string",
57329
- description: "Sort: relevance, search_volume, competition, cpc (default: search_volume)",
57633
+ description: "Sort: search_volume, competition, cpc (default: search_volume)",
57330
57634
  required: false
57331
57635
  },
57332
57636
  type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
@@ -57355,7 +57659,7 @@ Examples:
57355
57659
  target: { type: "positional", description: "Domain or URL to analyze", required: true },
57356
57660
  location: { type: "string", description: "Country code or location code (default: us)", required: false },
57357
57661
  language: { type: "string", description: "Language code or name (default: en)", required: false },
57358
- sort: { type: "string", description: "Sort: relevance, search_volume, competition, cpc", required: false },
57662
+ sort: { type: "string", description: "Sort: search_volume, competition, cpc", required: false },
57359
57663
  type: { type: "string", description: "Filter: paid, organic, all (default: all)", required: false },
57360
57664
  limit: { type: "string", description: "Max results (default: 50, max: 1000)", required: false },
57361
57665
  output: { type: "string", description: "Format: json|csv|md|jsonl", required: false, default: "json" },