@koda-sl/baker-cli 0.257.0 → 0.258.0-dev.d0de9190f

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
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-SJPRTSGY.js";
61
+ } from "./chunk-HBDGUVUH.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -2309,15 +2309,24 @@ var ASSET_FIELD_TYPES = [
2309
2309
  * `assetLink.attach` — see that message for what Google actually does with it.
2310
2310
  */
2311
2311
  "MARKETING_IMAGE",
2312
- "BUSINESS_NAME"
2312
+ "BUSINESS_NAME",
2313
+ /**
2314
+ * The one image link a Search campaign takes. It carries the advertiser's logo beside the ad and
2315
+ * is the only image field type that survives Google's field-type check at all — swept live on v23
2316
+ * and v25, campaign and customer level both answer `CUSTOMER_NOT_VERIFIED` (an account fact,
2317
+ * fixed by advertiser verification) rather than a field-type refusal. Ad group level is not a
2318
+ * level Google links it at — see {@link BUSINESS_LOGO_LEVEL_MESSAGE}.
2319
+ */
2320
+ "BUSINESS_LOGO"
2313
2321
  ];
2322
+ var BUSINESS_LOGO_LEVEL_MESSAGE = "Google links a BUSINESS_LOGO at campaign or customer level only \u2014 an ad group link comes back UNSUPPORTED_FIELD_TYPE. Attach it to the campaign instead.";
2314
2323
  var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
2315
2324
  "MARKETING_IMAGE",
2316
2325
  "SQUARE_MARKETING_IMAGE",
2317
2326
  "PORTRAIT_MARKETING_IMAGE",
2318
2327
  "AD_IMAGE"
2319
2328
  ]);
2320
- var IMAGE_ASSET_LINK_MESSAGE = "Google does not accept an image through an asset link. MARKETING_IMAGE is a Performance Max asset-group slot (use `asset-groups attach`), and AD_IMAGE \u2014 the field type Google names for a Search image extension \u2014 comes back UNSUPPORTED_FIELD_TYPE at campaign and ad group level alike. Image extensions are not reachable through the API: tell the user to add them in the Google Ads interface, and carry on with the rest of the job.";
2329
+ var IMAGE_ASSET_LINK_MESSAGE = "Google refuses the LINK, not the upload \u2014 so do not stop at this refusal, finish the handover. `assets create` with an image DOES publish: the image lands in the advertiser's Google Ads asset library and is theirs to use. Only this link is refused (MARKETING_IMAGE is a Performance Max asset-group slot \u2014 use `asset-groups attach`; AD_IMAGE comes back UNSUPPORTED_FIELD_TYPE at every level, on every API version). So: stage the image assets anyway, drop this link, and tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets \u2014 name them. Baker stages the Task for that step itself, alongside the image, so it is already in this Session's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload the images; they are uploaded. The one image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO at campaign or customer level, on a verified account.";
2321
2330
  function isImageAssetLinkFieldType(fieldType) {
2322
2331
  return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
2323
2332
  }
@@ -3214,7 +3223,9 @@ var assetLinkAttachSchema = z4.object({
3214
3223
  message: `parent is required for a ${value.level}-level asset link (--parent-ref)`
3215
3224
  });
3216
3225
  }
3217
- if (isImageAssetLinkFieldType(value.fieldType)) {
3226
+ if (value.fieldType === "BUSINESS_LOGO" && value.level === "adGroup") {
3227
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: BUSINESS_LOGO_LEVEL_MESSAGE });
3228
+ } else if (isImageAssetLinkFieldType(value.fieldType)) {
3218
3229
  ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
3219
3230
  } else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
3220
3231
  ctx.addIssue({
@@ -9909,7 +9920,7 @@ function renderSection(title, nodes, lines, showAccount) {
9909
9920
  }
9910
9921
  function renderAdvisories(data, lines) {
9911
9922
  const advisories = data.advisories ?? [];
9912
- if (advisories.length === 0) {
9923
+ if (advisories.length === 0 || data.status === "applied") {
9913
9924
  return;
9914
9925
  }
9915
9926
  lines.push("");
@@ -9918,15 +9929,37 @@ function renderAdvisories(data, lines) {
9918
9929
  lines.push(` \u2022 ${advisory.message}`);
9919
9930
  }
9920
9931
  }
9932
+ function renderOutcome(data, lines) {
9933
+ const counts = /* @__PURE__ */ new Map();
9934
+ for (const op of data.ops) {
9935
+ const status = op.result?.status ?? "pending";
9936
+ counts.set(status, (counts.get(status) ?? 0) + 1);
9937
+ }
9938
+ const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
9939
+ lines.push(`Google Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
9940
+ for (const op of data.ops) {
9941
+ const status = op.result?.status;
9942
+ if (status !== "failed" && status !== "skipped") {
9943
+ continue;
9944
+ }
9945
+ const reason = op.result?.error ?? op.result?.skippedBecause ?? "no reason recorded";
9946
+ lines.push(` \u2717 ${op.summary} \u2014 ${status}: ${reason}`);
9947
+ }
9948
+ }
9921
9949
  function renderDraftStatus(data) {
9922
9950
  if (data.status === "none" || data.count === 0) {
9923
9951
  return "No staged Google Ads changes on this chat.";
9924
9952
  }
9925
9953
  const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to Google Ads." : "Simulated \u2014 publishing completes the whole draft without calling Google Ads.";
9926
- const lines = [
9927
- `Google Ads \u2014 ${data.count} staged change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`,
9928
- modeNote
9929
- ];
9954
+ const lines = [];
9955
+ if (data.status === "applied") {
9956
+ renderOutcome(data, lines);
9957
+ } else if (data.status === "publishing") {
9958
+ lines.push(`Google Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
9959
+ } else {
9960
+ lines.push(`Google Ads \u2014 ${data.count} staged change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
9961
+ }
9962
+ lines.push(modeNote);
9930
9963
  const tree = data.tree ?? [];
9931
9964
  const showAccount = accountsInTree(tree).size > 1;
9932
9965
  renderSection(
@@ -10043,15 +10076,17 @@ function loadJsonFileArg(path38) {
10043
10076
  if (typeof path38 !== "string" || path38.length === 0) {
10044
10077
  return {};
10045
10078
  }
10079
+ const inline = path38.trimStart().startsWith("{");
10080
+ const source = inline ? "inline JSON" : path38;
10046
10081
  try {
10047
- const parsed = JSON.parse(readFileSync2(path38, "utf8"));
10082
+ const parsed = JSON.parse(inline ? path38 : readFileSync2(path38, "utf8"));
10048
10083
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
10049
- failWriteValidation(`${path38} must contain a JSON object`);
10084
+ failWriteValidation(`${source} must contain a JSON object`);
10050
10085
  }
10051
10086
  return parsed;
10052
10087
  } catch (err) {
10053
10088
  if (err instanceof SyntaxError) {
10054
- failWriteValidation(`${path38} is not valid JSON: ${err.message}`);
10089
+ failWriteValidation(`${source} is not valid JSON: ${err.message}`);
10055
10090
  }
10056
10091
  throw err;
10057
10092
  }
@@ -13877,7 +13912,10 @@ Examples:
13877
13912
  import { defineCommand as defineCommand30 } from "citty";
13878
13913
  var customerIdArg = { "customer-id": { type: "string", description: "10-digit Google Ads customer id" } };
13879
13914
  var fileArg = {
13880
- file: { type: "string", description: "JSON file with the full op payload (flags override)" }
13915
+ file: {
13916
+ type: "string",
13917
+ description: "The op payload: a JSON file path, or the JSON object inline (flags override it)"
13918
+ }
13881
13919
  };
13882
13920
  var BUDGET_DEMAND_HINTS = [
13883
13921
  "Check this daily budget against the search volume you measured for these keywords, not against the plan you had before you measured it. A budget many times larger than the traffic the target pool can absorb never spends, and reads to the client as a number nobody costed.",
@@ -14877,10 +14915,19 @@ function fileCreateCommand(name, kind, description, hints2) {
14877
14915
  if (Object.keys(payload).length === 0) {
14878
14916
  failWriteValidation(`${kind} needs --file with the op payload`);
14879
14917
  }
14880
- await stageGoogleOp({ kind, customerId, payload }, hints2);
14918
+ await stageGoogleOp({ kind, customerId, payload }, typeof hints2 === "function" ? hints2(payload) : hints2);
14881
14919
  }
14882
14920
  });
14883
14921
  }
14922
+ function imageAssetHints(payload) {
14923
+ if (payload.type !== "image") {
14924
+ return [];
14925
+ }
14926
+ return [
14927
+ "This image publishes into the advertiser's Google Ads asset library \u2014 that half works. What Google refuses is the link that puts it on a Search campaign, so do not stage an `assets attach` for it.",
14928
+ "Finish the handover rather than reporting a limitation: tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets, and name the ones you staged. Baker stages the Task for that step itself the moment you stage the image, so it is already in this chat's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload images they already have. (A Performance Max asset group is different \u2014 images do link there, with `asset-groups attach`.)"
14929
+ ];
14930
+ }
14884
14931
  var assetsUpdateCommand = defineCommand30({
14885
14932
  meta: {
14886
14933
  name: "update",
@@ -14921,17 +14968,18 @@ var assetsUpdateCommand = defineCommand30({
14921
14968
  var assetsCommand = defineCommand30({
14922
14969
  meta: {
14923
14970
  name: "assets",
14924
- description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Images are not among them: Google refuses every image asset link, so image extensions have to be added in the Google Ads interface."
14971
+ description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Image extensions are the one thing you cannot finish here \u2014 `create` uploads the image into the advertiser's account, but Google refuses the link that puts it on a Search campaign, so hand that one step over rather than telling them to upload anything. The image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO, at campaign or customer level, on a verified account."
14925
14972
  },
14926
14973
  subCommands: {
14927
14974
  create: fileCreateCommand(
14928
14975
  "create",
14929
14976
  "google.asset.create",
14930
- "Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)"
14977
+ "Stage an asset (text/image/sitelink/callout/structuredSnippet/\u2026)",
14978
+ imageAssetHints
14931
14979
  ),
14932
14980
  update: assetsUpdateCommand,
14933
14981
  attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
14934
- "Images are not attachable here at all \u2014 Google refuses every image field type through an asset link, at every level. Image extensions have to be added in the Google Ads interface; say so and carry on. Performance Max asset-group images are a different mechanism and do work (`asset-groups attach`)."
14982
+ "Image extensions are not attachable here \u2014 Google refuses AD_IMAGE and every marketing-image field type at every level, on every API version. The upload is not refused, though: stage the images with `assets create`, then tell the user they are already in their Google Ads account and only need adding to the campaign under Assets. Baker stages that Task by itself when the image is staged. Two images that DO work: the advertiser's logo through this command (`BUSINESS_LOGO`, campaign or customer level, on a verified account), and Performance Max asset-group images, which are a different mechanism (`asset-groups attach`)."
14935
14983
  ]),
14936
14984
  detach: statusCommand2("google.assetLink.detach", "asset link")
14937
14985
  }
@@ -18714,7 +18762,7 @@ function renderSection2(title, nodes, lines, showAccount) {
18714
18762
  }
18715
18763
  }
18716
18764
  var CAMPAIGN_TREE_ENTITIES = /* @__PURE__ */ new Set(["campaignGroup", "campaign", "creative"]);
18717
- function renderOutcome(data, lines) {
18765
+ function renderOutcome2(data, lines) {
18718
18766
  const counts = /* @__PURE__ */ new Map();
18719
18767
  for (const op of data.ops ?? []) {
18720
18768
  const status = op.result?.status ?? "pending";
@@ -18738,7 +18786,7 @@ function renderDraftStatus2(data) {
18738
18786
  const modeNote = data.mode === "live" ? "Live \u2014 publishing writes directly to LinkedIn." : "Simulated \u2014 publishing completes the whole draft without calling LinkedIn.";
18739
18787
  const lines = [];
18740
18788
  if (data.status === "applied") {
18741
- renderOutcome(data, lines);
18789
+ renderOutcome2(data, lines);
18742
18790
  } else if (data.status === "publishing") {
18743
18791
  lines.push(`LinkedIn Ads \u2014 publishing ${data.count} change${data.count === 1 ? "" : "s"} \xB7 ${data.mode}`);
18744
18792
  } else {
@@ -19573,7 +19621,16 @@ var META_LIMITS = {
19573
19621
  dynamicTextsMin: 1,
19574
19622
  dynamicTextsMax: 5,
19575
19623
  /** Dynamic ads mix and match best with at least this many media assets. */
19576
- dynamicAssetsGoodMin: 3
19624
+ dynamicAssetsGoodMin: 3,
19625
+ /**
19626
+ * Placement asset customization: "every `asset_feed_spec` needs to have more
19627
+ * than one customization rule attached to it" — one rule is not a
19628
+ * customization, it is an ad that serves in one place.
19629
+ * https://developers.facebook.com/docs/marketing-api/dynamic-creative/placement-asset-customization/
19630
+ */
19631
+ placementRulesMin: 2,
19632
+ /** Asset customization rules per asset feed. */
19633
+ placementRulesMax: 50
19577
19634
  },
19578
19635
  audience: {
19579
19636
  nameMax: 100,
@@ -19810,6 +19867,56 @@ function positionsMissingRequiredFeed(targeting) {
19810
19867
  }
19811
19868
  return FACEBOOK_POSITIONS_REQUIRING_FEED.filter((required) => positions.includes(required));
19812
19869
  }
19870
+ var PLACEMENT_ASSET_CUSTOMIZATION_DOC = "https://developers.facebook.com/docs/marketing-api/dynamic-creative/placement-asset-customization/";
19871
+ var CUSTOMIZATION_POSITION_KEYS = {
19872
+ facebook: "facebook_positions",
19873
+ instagram: "instagram_positions",
19874
+ messenger: "messenger_positions",
19875
+ audience_network: "audience_network_positions",
19876
+ threads: "threads_positions"
19877
+ };
19878
+ var CUSTOMIZATION_PLATFORMS = Object.keys(CUSTOMIZATION_POSITION_KEYS);
19879
+ var CUSTOMIZATION_POSITIONS = {
19880
+ facebook: ["feed", "right_hand_column", "marketplace", "video_feeds", "search", "story", "notification"],
19881
+ instagram: ["stream", "story", "explore", "explore_home", "profile_feed", "ig_search"],
19882
+ messenger: ["sponsored_messages", "story"],
19883
+ audience_network: ["classic", "instream_video", "rewarded_video"],
19884
+ threads: ["threads_stream"]
19885
+ };
19886
+ function customizationPlacementKeys(spec) {
19887
+ const keys = [];
19888
+ for (const platform of spec.publisher_platforms) {
19889
+ for (const position of spec[CUSTOMIZATION_POSITION_KEYS[platform]] ?? []) {
19890
+ keys.push(`${platform}:${position}`);
19891
+ }
19892
+ }
19893
+ return keys;
19894
+ }
19895
+ function customizationSpecProblems(spec) {
19896
+ const problems = [];
19897
+ for (const platform of spec.publisher_platforms) {
19898
+ const key = CUSTOMIZATION_POSITION_KEYS[platform];
19899
+ const positions = spec[key];
19900
+ if (!positions?.length) {
19901
+ problems.push(
19902
+ `publisher_platforms names ${platform}, so ${key} is required \u2014 Meta rejects a customization spec that selects a platform without its positions (${PLACEMENT_ASSET_CUSTOMIZATION_DOC})`
19903
+ );
19904
+ continue;
19905
+ }
19906
+ const known = CUSTOMIZATION_POSITIONS[platform];
19907
+ for (const position of positions) {
19908
+ if (!known.includes(position)) {
19909
+ problems.push(`${key} does not publish "${position}" \u2014 Meta lists ${known.join(", ")}`);
19910
+ }
19911
+ }
19912
+ }
19913
+ if (spec.publisher_platforms.includes("threads") && !spec.instagram_positions?.includes("stream")) {
19914
+ problems.push(
19915
+ 'threads placements require instagram alongside them \u2014 add "instagram" to publisher_platforms and "stream" to instagram_positions'
19916
+ );
19917
+ }
19918
+ return problems;
19919
+ }
19813
19920
 
19814
19921
  // ../api/src/ads-meta/ops.ts
19815
19922
  var tempRefSchema3 = z25.string().regex(TEMP_REF_REGEX3, "expected a meta_temp_* reference");
@@ -20290,9 +20397,7 @@ var dynamicCreativeSchema = z25.object({
20290
20397
  videos: z25.array(dynamicVideoSchema).optional(),
20291
20398
  ad_formats: z25.array(z25.enum(AD_FORMATS2)).min(1),
20292
20399
  call_to_action_types: z25.array(z25.enum(CTA_TYPES2)).optional(),
20293
- link_urls: z25.array(z25.object({ website_url: httpsUrlSchema3, display_url: z25.string().optional() })).min(1),
20294
- /** Multi-language / placement customization — structural passthrough for v1. */
20295
- asset_customization_rules: z25.array(z25.record(z25.string(), z25.unknown())).optional()
20400
+ link_urls: z25.array(z25.object({ website_url: httpsUrlSchema3, display_url: z25.string().optional() })).min(1)
20296
20401
  }).superRefine((p, ctx) => {
20297
20402
  if (!(p.images?.length || p.videos?.length)) {
20298
20403
  ctx.addIssue({
@@ -20302,6 +20407,73 @@ var dynamicCreativeSchema = z25.object({
20302
20407
  });
20303
20408
  }
20304
20409
  });
20410
+ var placementRuleSchema = z25.object({
20411
+ ...imageMediaFields,
20412
+ videoId: videoMediaFields.videoId,
20413
+ videoRef: videoMediaFields.videoRef,
20414
+ thumbnailHash: videoMediaFields.thumbnailHash,
20415
+ publisher_platforms: z25.array(z25.enum(CUSTOMIZATION_PLATFORMS)).min(1),
20416
+ facebook_positions: z25.array(z25.string()).optional(),
20417
+ instagram_positions: z25.array(z25.string()).optional(),
20418
+ messenger_positions: z25.array(z25.string()).optional(),
20419
+ audience_network_positions: z25.array(z25.string()).optional(),
20420
+ threads_positions: z25.array(z25.string()).optional()
20421
+ }).superRefine((p, ctx) => {
20422
+ const media = countImageRefs(p) + countVideoRefs(p);
20423
+ if (media !== 1) {
20424
+ ctx.addIssue({
20425
+ code: "custom",
20426
+ path: ["imageHash"],
20427
+ message: "each placement rule names exactly one image or one video"
20428
+ });
20429
+ }
20430
+ for (const problem of customizationSpecProblems(p)) {
20431
+ ctx.addIssue({ code: "custom", path: ["publisher_platforms"], message: problem });
20432
+ }
20433
+ });
20434
+ var placementCreativeSchema = z25.object({
20435
+ creativeType: z25.literal("placement"),
20436
+ ...creativeSharedFields,
20437
+ message: messageSchema,
20438
+ link: httpsUrlSchema3,
20439
+ headline: headlineSchema2.optional(),
20440
+ description: descriptionSchema.optional(),
20441
+ call_to_action: callToActionSchema.optional(),
20442
+ placements: z25.array(placementRuleSchema).min(META_LIMITS.creative.placementRulesMin).max(META_LIMITS.creative.placementRulesMax)
20443
+ }).superRefine((p, ctx) => {
20444
+ const images = p.placements.filter((rule) => countImageRefs(rule) === 1).length;
20445
+ if (images > 0 && images < p.placements.length) {
20446
+ ctx.addIssue({
20447
+ code: "custom",
20448
+ path: ["placements"],
20449
+ message: "one asset feed carries one format \u2014 every placement rule must name an image, or every one a video, not a mix; stage the other format as its own creative"
20450
+ });
20451
+ }
20452
+ const seen = /* @__PURE__ */ new Map();
20453
+ p.placements.forEach((rule, index) => {
20454
+ for (const key of customizationPlacementKeys(rule)) {
20455
+ const first = seen.get(key);
20456
+ if (first !== void 0) {
20457
+ ctx.addIssue({
20458
+ code: "custom",
20459
+ path: ["placements", index],
20460
+ message: `${key.replace(":", " ")} is already claimed by rule ${first + 1} \u2014 a placement takes one asset, so Meta cannot resolve two rules naming it`
20461
+ });
20462
+ } else {
20463
+ seen.set(key, index);
20464
+ }
20465
+ }
20466
+ });
20467
+ for (const [index, rule] of p.placements.entries()) {
20468
+ if (rule.instagram_positions?.includes("explore_home") && countVideoRefs(rule) === 1) {
20469
+ ctx.addIssue({
20470
+ code: "custom",
20471
+ path: ["placements", index],
20472
+ message: `Instagram explore_home only supports SINGLE_IMAGE, so it cannot take a video (${PLACEMENT_ASSET_CUSTOMIZATION_DOC})`
20473
+ });
20474
+ }
20475
+ }
20476
+ });
20305
20477
  var existingPostCreativeSchema = z25.object({
20306
20478
  creativeType: z25.literal("existing_post"),
20307
20479
  name: creativeSharedFields.name,
@@ -20315,6 +20487,7 @@ var creativeContentSchema2 = z25.discriminatedUnion("creativeType", [
20315
20487
  singleCreativeSchema,
20316
20488
  carouselCreativeSchema2,
20317
20489
  dynamicCreativeSchema,
20490
+ placementCreativeSchema,
20318
20491
  existingPostCreativeSchema
20319
20492
  ]);
20320
20493
  var adCreativeCreateSchema = creativeContentSchema2;
@@ -20888,13 +21061,17 @@ var adCreativesCreateCommand = defineCommand56({
20888
21061
  meta: {
20889
21062
  name: "create",
20890
21063
  description: `Stage a new ad creative. ${STAGED_NOTE2}
20891
- Single-image/video via flags; carousel/dynamic via --file (creativeType carousel|dynamic).
21064
+ Single-image/video via flags; carousel/dynamic/placement via --file (creativeType carousel|dynamic|placement).
20892
21065
  Reference media by --image-hash (already on Meta) or --image-ref meta_temp_* (a staged media upload).
21066
+ Start here for "the feed asset is 4:5, Stories needs 9:16": creativeType "placement" puts BOTH in one ad, one asset per placement \u2014 then repoint the running ad at it (ads update <adId> --creative <ref>) instead of staging a second ad that competes with it.
20893
21067
  Example: baker ads meta creatives create --page 555 --message "Save now" --link https://x.com --headline "Best cover" --image-hash abc123\u2026 --cta SHOP_NOW`
20894
21068
  },
20895
21069
  args: {
20896
21070
  ...accountArgs2,
20897
- type: { type: "string", description: "single|carousel|dynamic|existing_post (default single)" },
21071
+ type: {
21072
+ type: "string",
21073
+ description: "single|carousel|dynamic|placement|existing_post (default single). placement = one ad, a different asset per placement"
21074
+ },
20898
21075
  name: { type: "string", description: "Creative name" },
20899
21076
  page: { type: "string", description: "Facebook Page id (identity)" },
20900
21077
  "instagram-user": { type: "string", description: "Instagram account id for IG placements" },