@koda-sl/baker-cli 0.108.0-dev.d66dc8b3d → 0.110.1-dev.02d8aed71

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
@@ -236,7 +236,7 @@ The CLI rejects or automatically corrects common GAQL mistakes before hitting th
236
236
  | Pattern | Behavior | Warning/error emitted |
237
237
  |---------|----------|-----------------------|
238
238
  | Missing LIMIT | Adds `LIMIT 200` | `"Added LIMIT 200. Use --limit to override."` |
239
- | `keyword.text` | Rewrites to `ad_group_criterion.keyword.text` | `"keyword.text → ad_group_criterion.keyword.text"` |
239
+ | Bare `keyword.text` / `keyword.match_type` | Rewrites to `ad_group_criterion.*` (resource-scoped forms like `shared_criterion.keyword.text` pass through untouched) | `"keyword.text → ad_group_criterion.keyword.text"` |
240
240
  | `shopping_performance_view.product_*` | Rewrites to `segments.product_*` | Field renamed |
241
241
  | `campaign.status = 'ACTIVE'` | Rewrites to `= 'ENABLED'` | Enum corrected |
242
242
  | `segments.conversion_action_name` with `metrics.cost_micros` | Rejects; split into separate conversion-action and spend queries | `INCOMPATIBLE_FIELDS` with `fix.action: "split_query"` |
@@ -1006,9 +1006,13 @@ baker ads linkedin conversions create --name "Demo booked" --type LEAD --method
1006
1006
  # Safe duplication — stage-time read of the source → staged DRAFT create ("Duplicate ad", never "Link to original")
1007
1007
  baker ads linkedin campaigns duplicate 123456 --name "New variant"
1008
1008
 
1009
- # Change copy/media/URL on a LIVE ad — LinkedIn makes ad content immutable, so this is ONE command:
1010
- # duplicate with overrides (unchanged fields carry over) + --replace pauses the original once the copy publishes
1011
- baker ads linkedin creatives duplicate 1458413484 --headline "New headline" --description "New copy." --replace
1009
+ # Change copy/media/URL on a LIVE ad — direct-content ads (text/spotlight/follower/jobs) update IN PLACE;
1010
+ # pass only the changed fields, the rest of the ad's current content carries over
1011
+ baker ads linkedin creatives update 1458423674 --headline "New headline" --description "New copy."
1012
+
1013
+ # Post-based ads (image/video/carousel/…) sponsor a post whose content can't be edited — replace in ONE command:
1014
+ # duplicate with overrides (unchanged fields + status carry over) + --replace pauses the original once the copy publishes
1015
+ baker ads linkedin creatives duplicate 1458413484 --headline "New headline" --replace
1012
1016
 
1013
1017
  # Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
1014
1018
  baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
@@ -1025,7 +1029,7 @@ Notes:
1025
1029
 
1026
1030
  - All write commands take `--file <json>` payloads; explicit flags override file keys. `baker schema ads.linkedin.campaigns.create` for exact args.
1027
1031
  - Money flags (`--bid`, `--daily-budget`, `--total-budget`) require `--currency`.
1028
- - Creative media comes from the Baker library (`--image-id`/`--video-id` from `baker images`/`baker videos` — uploaded to LinkedIn at publish) or as LinkedIn URNs (`--image-urn`/`--video-urn`). Formats: `image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs`; complex formats take `--file` with the full content object. Limits: headline ≤70, text-ad 25/75, intro soft-truncates at 600 chars. TLA sponsors an existing post via `--post-urn`.
1032
+ - Creative media comes from the Baker library (`--image-id`/`--video-id` from `baker images`/`baker videos` — uploaded to LinkedIn at publish) or as LinkedIn URNs (`--image-urn`/`--video-urn`). Formats: `image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs`; complex formats take `--file` with the full content object; conversation ads take `--file` with the message flow (`{message: {subject, body, senderName?, buttons[]}}` — buttons `NESTED` (with `nestedMessage`) or `LANDING_PAGE` (with `landingPageUrl`), ≤25 messages, bodies ≤500 chars, labels ≤25). Limits: headline ≤70, text-ad 25/75, intro soft-truncates at 600 chars. TLA sponsors an existing post via `--post-urn`.
1029
1033
  - Lead forms are file-first (`lead-forms create --file form.json`): name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion).
1030
1034
 
1031
1035
  #### `audit` — playbook diagnostic
package/dist/cli.js CHANGED
@@ -957,7 +957,11 @@ var LINKEDIN_LIMITS = {
957
957
  carouselCardHeadlineMax: 45,
958
958
  carouselCardsMin: 2,
959
959
  carouselCardsMax: 10,
960
- conversationButtonLabelMax: 25
960
+ conversationButtonLabelMax: 25,
961
+ conversationBodyMax: 500,
962
+ conversationButtonsMax: 5,
963
+ conversationMessagesMax: 25,
964
+ conversationSubjectMax: 60
961
965
  },
962
966
  audience: {
963
967
  nameMax: 100,
@@ -1226,14 +1230,17 @@ var spotlightCreativeSchema = z3.object({
1226
1230
  ctaLabel: z3.string().min(1).max(LINKEDIN_LIMITS.creative.spotlightCtaMax),
1227
1231
  logoImageId: bakerMediaIdSchema.optional(),
1228
1232
  logoUrn: z3.string().regex(IMAGE_URN_REGEX).optional(),
1229
- showMemberProfilePhoto: z3.boolean().default(true)
1233
+ showMemberProfilePhoto: z3.boolean().default(true),
1234
+ /** Carried from the live snapshot on updates so LinkedIn's whole-content $set preserves it. */
1235
+ organizationName: z3.string().min(1).max(200).optional()
1230
1236
  });
1231
1237
  var followerCreativeSchema = z3.object({
1232
1238
  format: z3.literal("follower"),
1233
1239
  headline: z3.string().max(LINKEDIN_LIMITS.creative.spotlightHeadlineMax).optional(),
1234
1240
  description: z3.string().max(LINKEDIN_LIMITS.creative.spotlightDescriptionMax).optional(),
1235
1241
  logoImageId: bakerMediaIdSchema.optional(),
1236
- logoUrn: z3.string().regex(IMAGE_URN_REGEX).optional()
1242
+ logoUrn: z3.string().regex(IMAGE_URN_REGEX).optional(),
1243
+ organizationName: z3.string().min(1).max(200).optional()
1237
1244
  });
1238
1245
  var documentCreativeSchema = z3.object({
1239
1246
  format: z3.literal("document"),
@@ -1260,10 +1267,68 @@ var carouselCreativeSchema = z3.object({
1260
1267
  })
1261
1268
  ).min(LINKEDIN_LIMITS.creative.carouselCardsMin).max(LINKEDIN_LIMITS.creative.carouselCardsMax)
1262
1269
  });
1270
+ var conversationButtonSchema = z3.lazy(
1271
+ () => z3.object({
1272
+ label: z3.string().min(1).max(LINKEDIN_LIMITS.creative.conversationButtonLabelMax),
1273
+ type: z3.enum(["NESTED", "LANDING_PAGE"]),
1274
+ nestedMessage: conversationMessageSchema.optional(),
1275
+ landingPageUrl: httpsUrlSchema.optional()
1276
+ }).superRefine((button, ctx) => {
1277
+ if (button.type === "NESTED" && !button.nestedMessage) {
1278
+ ctx.addIssue({ code: "custom", path: ["nestedMessage"], message: "NESTED buttons need a nestedMessage" });
1279
+ }
1280
+ if (button.type === "LANDING_PAGE" && !button.landingPageUrl) {
1281
+ ctx.addIssue({
1282
+ code: "custom",
1283
+ path: ["landingPageUrl"],
1284
+ message: "LANDING_PAGE buttons need a landingPageUrl"
1285
+ });
1286
+ }
1287
+ if (button.type === "LANDING_PAGE" && button.nestedMessage) {
1288
+ ctx.addIssue({
1289
+ code: "custom",
1290
+ path: ["nestedMessage"],
1291
+ message: "LANDING_PAGE buttons cannot nest a message"
1292
+ });
1293
+ }
1294
+ })
1295
+ );
1296
+ var conversationMessageSchema = z3.lazy(
1297
+ () => z3.object({
1298
+ subject: z3.string().min(1).max(LINKEDIN_LIMITS.creative.conversationSubjectMax).optional(),
1299
+ body: z3.string().min(1).max(LINKEDIN_LIMITS.creative.conversationBodyMax),
1300
+ senderName: z3.string().min(1).max(100).optional(),
1301
+ buttons: z3.array(conversationButtonSchema).max(LINKEDIN_LIMITS.creative.conversationButtonsMax).optional()
1302
+ })
1303
+ );
1304
+ function countConversationMessages(message) {
1305
+ return 1 + (message.buttons ?? []).reduce(
1306
+ (sum, button) => sum + (button.nestedMessage ? countConversationMessages(button.nestedMessage) : 0),
1307
+ 0
1308
+ );
1309
+ }
1263
1310
  var conversationCreativeSchema = z3.object({
1264
1311
  format: z3.literal("conversation"),
1265
1312
  name: z3.string().max(255).optional(),
1266
- raw: z3.record(z3.string(), z3.unknown())
1313
+ /** Structured flow — publishable via the Conversation Ads API. */
1314
+ message: conversationMessageSchema.optional(),
1315
+ /** Legacy free-form passthrough — preview-only; restage with `message` to publish. */
1316
+ raw: z3.record(z3.string(), z3.unknown()).optional()
1317
+ }).superRefine((p, ctx) => {
1318
+ if (Boolean(p.message) === Boolean(p.raw)) {
1319
+ ctx.addIssue({
1320
+ code: "custom",
1321
+ path: ["message"],
1322
+ message: "provide the conversation flow as `message` (legacy `raw` is accepted alone, not alongside it)"
1323
+ });
1324
+ }
1325
+ if (p.message && countConversationMessages(p.message) > LINKEDIN_LIMITS.creative.conversationMessagesMax) {
1326
+ ctx.addIssue({
1327
+ code: "custom",
1328
+ path: ["message"],
1329
+ message: `conversation flows are capped at ${LINKEDIN_LIMITS.creative.conversationMessagesMax} messages`
1330
+ });
1331
+ }
1267
1332
  });
1268
1333
  var tlaCreativeSchema = z3.object({
1269
1334
  format: z3.literal("tla"),
@@ -1320,10 +1385,9 @@ var creativeUpdateSchema = z3.object({
1320
1385
  landingUrl: httpsUrlSchema.optional(),
1321
1386
  cta: z3.enum(CTA_TYPES).optional(),
1322
1387
  /**
1323
- * Amend-only content patch: valid when the target is a li_temp_* staged
1324
- * create merged into the staged content and re-validated against the
1325
- * full creative schema. Creatives already on LinkedIn have immutable
1326
- * content; the backend rejects it for real targets.
1388
+ * Content patch: merged into the staged create (li_temp_* targets) or the
1389
+ * live ad's snapshot content (real targets, direct-content formats) and
1390
+ * re-validated against the full creative schema. Post-based ads reject it.
1327
1391
  */
1328
1392
  content: z3.record(z3.string(), z3.unknown()).optional()
1329
1393
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
@@ -3200,12 +3264,12 @@ function buildCommand(query, ctx) {
3200
3264
  return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
3201
3265
  }
3202
3266
  var CORRECTION_RULES = [
3203
- // 1. keyword.text → ad_group_criterion.keyword.text
3267
+ // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
3204
3268
  {
3205
- matchQuery: /\bkeyword\.text\b/,
3269
+ matchQuery: /(?<![\w.])keyword\.text\b/,
3206
3270
  matchApiError: /keyword\.text/i,
3207
3271
  fix: (ctx) => {
3208
- const corrected = ctx.originalQuery.replace(/\bkeyword\.text\b/g, "ad_group_criterion.keyword.text");
3272
+ const corrected = ctx.originalQuery.replace(/(?<![\w.])keyword\.text\b/g, "ad_group_criterion.keyword.text");
3209
3273
  return {
3210
3274
  action: "retry_with_modified_query",
3211
3275
  correctedCommand: buildCommand(corrected, ctx),
@@ -3213,12 +3277,15 @@ var CORRECTION_RULES = [
3213
3277
  };
3214
3278
  }
3215
3279
  },
3216
- // 2. keyword.match_type → ad_group_criterion.keyword.match_type
3280
+ // 2. bare keyword.match_type → ad_group_criterion.keyword.match_type (leaves <resource>.keyword.match_type untouched)
3217
3281
  {
3218
- matchQuery: /\bkeyword\.match_type\b/,
3282
+ matchQuery: /(?<![\w.])keyword\.match_type\b/,
3219
3283
  matchApiError: /keyword\.match_type/i,
3220
3284
  fix: (ctx) => {
3221
- const corrected = ctx.originalQuery.replace(/\bkeyword\.match_type\b/g, "ad_group_criterion.keyword.match_type");
3285
+ const corrected = ctx.originalQuery.replace(
3286
+ /(?<![\w.])keyword\.match_type\b/g,
3287
+ "ad_group_criterion.keyword.match_type"
3288
+ );
3222
3289
  return {
3223
3290
  action: "retry_with_modified_query",
3224
3291
  correctedCommand: buildCommand(corrected, ctx),
@@ -4570,12 +4637,12 @@ function buildCommand2(query, customerId) {
4570
4637
  function applyAutoFixes(query, limit) {
4571
4638
  let corrected = query;
4572
4639
  const warnings = [];
4573
- if (/\bkeyword\.text\b/.test(corrected) && !/ad_group_criterion\.keyword\.text/.test(corrected)) {
4574
- corrected = corrected.replace(/\bkeyword\.text\b/g, "ad_group_criterion.keyword.text");
4640
+ if (/(?<![\w.])keyword\.text\b/.test(corrected)) {
4641
+ corrected = corrected.replace(/(?<![\w.])keyword\.text\b/g, "ad_group_criterion.keyword.text");
4575
4642
  warnings.push({ code: "FIELD_RENAMED", message: "keyword.text \u2192 ad_group_criterion.keyword.text" });
4576
4643
  }
4577
- if (/\bkeyword\.match_type\b/.test(corrected) && !/ad_group_criterion\.keyword\.match_type/.test(corrected)) {
4578
- corrected = corrected.replace(/\bkeyword\.match_type\b/g, "ad_group_criterion.keyword.match_type");
4644
+ if (/(?<![\w.])keyword\.match_type\b/.test(corrected)) {
4645
+ corrected = corrected.replace(/(?<![\w.])keyword\.match_type\b/g, "ad_group_criterion.keyword.match_type");
4579
4646
  warnings.push({ code: "FIELD_RENAMED", message: "keyword.match_type \u2192 ad_group_criterion.keyword.match_type" });
4580
4647
  }
4581
4648
  if (/shopping_performance_view\.product_\w+/.test(corrected)) {
@@ -5489,6 +5556,7 @@ registerSchema({
5489
5556
  ...writeAccountArgs,
5490
5557
  name: { type: "string", description: "New name", required: false },
5491
5558
  objective: { type: "string", description: "New objective", required: false },
5559
+ "cost-type": { type: "string", description: "CPC|CPM|CPV", required: false },
5492
5560
  bid: { type: "string", description: "New manual bid", required: false },
5493
5561
  "daily-budget": { type: "string", description: "New daily budget", required: false },
5494
5562
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
@@ -5565,7 +5633,7 @@ registerSchema({
5565
5633
  });
5566
5634
  registerSchema({
5567
5635
  command: "ads.linkedin.creatives.duplicate",
5568
- description: "Stage a copy of an existing ad, optionally with new content \u2014 the ONLY way to change copy/media/URL on a live ad (LinkedIn makes ad content immutable after creation). Direct-content ads (text/spotlight/follower/jobs) copy field-by-field; post-based ads re-sponsor the same post. --replace also stages pausing the original, gated on the new ad publishing successfully.",
5636
+ description: "Stage a copy of an existing ad, optionally with new content. Direct-content ads (text/spotlight/follower/jobs) copy field-by-field; post-based ads re-sponsor the same post \u2014 and since post content can't be edited, duplicate --replace is how post-based ads change copy (direct-content ads update in place via creatives.update instead). --replace stages pausing the original, gated on the new ad publishing successfully, and the copy inherits the source's status.",
5569
5637
  args: {
5570
5638
  id: { type: "positional", description: "Source creative id or URN", required: true },
5571
5639
  ...writeAccountArgs,
@@ -5589,7 +5657,7 @@ registerSchema({
5589
5657
  });
5590
5658
  registerSchema({
5591
5659
  command: "ads.linkedin.creatives.create",
5592
- description: "Stage a new creative (ad). Formats: image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article. Media: --image-id/--video-id reference the Baker library (`baker images`/`baker videos`) and upload to LinkedIn at publish; --image-urn/--video-urn reference media already on LinkedIn. Limits: headline \u226470 (text ads \u226425), intro soft-truncates at 600 chars, landing URL must be https. Complex formats take --file with the full content object. Staged until publish.",
5660
+ description: "Stage a new creative (ad). Formats: image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article. Media: --image-id/--video-id reference the Baker library (`baker images`/`baker videos`) and upload to LinkedIn at publish; --image-urn/--video-urn reference media already on LinkedIn. Limits: headline \u226470 (text ads \u226425), intro soft-truncates at 600 chars, landing URL must be https. Complex formats take --file with the full content object; conversation ads take --file with the message flow ({message: {subject, body (\u2264500), senderName?, buttons \u22645 \xD7 {label \u226425, type NESTED|LANDING_PAGE, nestedMessage?|landingPageUrl?}}}, \u226425 messages). Staged until publish.",
5593
5661
  args: {
5594
5662
  ...writeAccountArgs,
5595
5663
  campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
@@ -5610,34 +5678,53 @@ registerSchema({
5610
5678
  cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026", required: false },
5611
5679
  "cta-label": { type: "string", description: "Spotlight CTA label (\u226418 chars)", required: false },
5612
5680
  "post-urn": { type: "string", description: "TLA: post to sponsor (urn:li:share|ugcPost:*)", required: false },
5681
+ "event-urn": {
5682
+ type: "string",
5683
+ description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)",
5684
+ required: false
5685
+ },
5686
+ "article-url": {
5687
+ type: "string",
5688
+ description: "Article ads: https URL of the article/newsletter to sponsor",
5689
+ required: false
5690
+ },
5691
+ "thumbnail-image-id": {
5692
+ type: "string",
5693
+ description: "Article ads: Baker image id for the link thumbnail",
5694
+ required: false
5695
+ },
5696
+ "thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail", required: false },
5613
5697
  "intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
5614
- title: { type: "string", description: "Media title", required: false },
5698
+ title: { type: "string", description: "Media title (image/video) or article title", required: false },
5615
5699
  file: { type: "string", description: "JSON file with the full content object", required: false }
5616
5700
  }
5617
5701
  });
5618
5702
  registerSchema({
5619
5703
  command: "ads.linkedin.creatives.update",
5620
- description: "Stage changes to an existing creative (intendedStatus, name only \u2014 LinkedIn makes ad content immutable; to change copy/media/URL use creatives.duplicate with --replace), or AMEND a creative staged in this chat by passing its li_temp_* ref \u2014 amends accept the content flags from creatives.create (headline, intro, description, image-id, landing-url, cta, \u2026) and merge into the staged ad, re-validating the result. Use amends to apply feedback to staged ads instead of remove + re-create. Applies on chat publish.",
5704
+ description: "Stage changes to an existing creative. Direct-content ads (text/spotlight/follower/jobs) update copy/media/URL IN PLACE \u2014 pass only the changed fields (headline, description, landing-url, image-id, \u2026); the ad's current content carries over. Post-based ads (image/video/carousel/\u2026) can't edit content \u2014 use creatives.duplicate with --replace for those. Passing a li_temp_* ref AMENDS the creative staged in this chat instead. Applies on chat publish.",
5621
5705
  args: {
5622
5706
  id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
5623
5707
  ...writeAccountArgs,
5624
5708
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
5625
5709
  name: { type: "string", description: "New creative name", required: false },
5626
- headline: { type: "string", description: "Amend only: new headline", required: false },
5627
- intro: { type: "string", description: "Amend only: commentary / intro text", required: false },
5628
- description: { type: "string", description: "Amend only: description (text/spotlight)", required: false },
5710
+ headline: { type: "string", description: "New headline", required: false },
5711
+ intro: { type: "string", description: "Commentary / intro text", required: false },
5712
+ description: { type: "string", description: "Description (text/spotlight)", required: false },
5629
5713
  "landing-url": { type: "string", description: "New https destination URL", required: false },
5630
5714
  cta: { type: "string", description: "New CTA", required: false },
5631
- "image-id": { type: "string", description: "Amend only: Baker image library id", required: false },
5632
- "image-urn": { type: "string", description: "Amend only: existing urn:li:image:*", required: false },
5633
- "video-id": { type: "string", description: "Amend only: Baker video library id", required: false },
5634
- "logo-image-id": { type: "string", description: "Amend only: Baker image id for the logo", required: false },
5715
+ "cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
5716
+ "image-id": { type: "string", description: "Baker image library id", required: false },
5717
+ "image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
5718
+ "video-id": { type: "string", description: "Baker video library id", required: false },
5719
+ "video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
5720
+ "logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
5635
5721
  "thumbnail-image-id": {
5636
5722
  type: "string",
5637
- description: "Amend only: Baker image id for the article thumbnail",
5723
+ description: "Baker image id for the article thumbnail",
5638
5724
  required: false
5639
5725
  },
5640
- title: { type: "string", description: "Amend only: media or article title", required: false },
5726
+ "thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail", required: false },
5727
+ title: { type: "string", description: "Media or article title", required: false },
5641
5728
  file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
5642
5729
  }
5643
5730
  });
@@ -7041,8 +7128,14 @@ Examples:
7041
7128
  id: { type: "positional", description: "Source creative id or URN", required: true },
7042
7129
  ...accountArgs,
7043
7130
  campaign: { type: "string", description: "Stage the copy under a different ad set (id/URN or li_temp_*)" },
7044
- "intended-status": { type: "string", description: "Status for the copy (default DRAFT)" },
7045
- replace: { type: "boolean", description: "Also pause the original once the new ad publishes" },
7131
+ "intended-status": {
7132
+ type: "string",
7133
+ description: "Status for the copy (default DRAFT; with --replace, the source's status)"
7134
+ },
7135
+ replace: {
7136
+ type: "boolean",
7137
+ description: "Also pause the original once the new ad publishes; the copy inherits the source's status"
7138
+ },
7046
7139
  intro: { type: "string", description: "New commentary / intro text" },
7047
7140
  headline: { type: "string", description: "New headline" },
7048
7141
  description: { type: "string", description: "New description (text/spotlight ads)" },
@@ -7103,7 +7196,7 @@ var creativesCreateCommand = defineCommand36({
7103
7196
  meta: {
7104
7197
  name: "create",
7105
7198
  description: `Stage a new creative (ad). ${STAGED_NOTE}
7106
- Formats: ${CREATIVE_FORMATS.join("|")}. Media comes from the Baker library (--image-id / --video-id from \`baker images\`/\`baker videos\`) or as LinkedIn URNs. Complex formats (carousel, conversation, jobs) take --file with the full content object.
7199
+ Formats: ${CREATIVE_FORMATS.join("|")}. Media comes from the Baker library (--image-id / --video-id from \`baker images\`/\`baker videos\`) or as LinkedIn URNs. Complex formats (carousel, jobs) take --file with the full content object. Conversation ads take --file with the message flow ({message: {subject, body, buttons[]}} \u2014 buttons NESTED|LANDING_PAGE, nested messages recurse).
7107
7200
  Examples:
7108
7201
  baker ads linkedin creatives create --campaign li_temp_x2 --format image --image-id <bakerImageId> --intro "Meet the platform\u2026" --headline "Book a demo" --landing-url https://example.com/demo --cta REQUEST_DEMO
7109
7202
  baker ads linkedin creatives create --campaign 123456 --format tla --post-urn urn:li:ugcPost:789`
@@ -7188,7 +7281,6 @@ var CREATIVE_AMEND_CONTENT_FLAGS = [
7188
7281
  ["thumbnail-urn", "thumbnailUrn"],
7189
7282
  ["title", "title"]
7190
7283
  ];
7191
- var CREATIVE_LIVE_BLOCKED_FILE_KEYS = ["headline", "landingUrl", "cta", "content"];
7192
7284
  function creativeContentPatch(args) {
7193
7285
  const patch = {};
7194
7286
  for (const [flag, key] of CREATIVE_AMEND_CONTENT_FLAGS) {
@@ -7205,30 +7297,30 @@ var creativesUpdateCommand = defineCommand36({
7205
7297
  meta: {
7206
7298
  name: "update",
7207
7299
  description: `Stage changes to an existing creative, or amend a creative staged in this chat by passing its li_temp_* ref. ${STAGED_NOTE}
7208
- Staged creatives (li_temp_*) accept the content flags from \`creatives create\` \u2014 fields merge into the staged ad and re-validate. Creatives already on LinkedIn only accept --intended-status/--name (LinkedIn makes ad content immutable \u2014 to change copy/media/URL use \`creatives duplicate <id> --headline "\u2026" --replace\`).
7300
+ Direct-content ads (text/spotlight/follower/jobs) update copy/media/URL IN PLACE \u2014 changed fields merge into the ad's current content. Post-based ads (image/video/carousel/\u2026) can't edit content; use \`creatives duplicate <id> --headline "\u2026" --replace\` for those. Staged creatives (li_temp_*) accept all content flags and re-validate.
7209
7301
  Examples:
7210
- baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED
7211
- baker ads linkedin creatives update li_temp_a1b2 --headline "Sharper headline" --image-id <bakerImageId>`
7302
+ baker ads linkedin creatives update 1458423674 --headline "Sharper headline" --description "New copy."
7303
+ baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED`
7212
7304
  },
7213
7305
  args: {
7214
7306
  id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
7215
7307
  ...accountArgs,
7216
7308
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT" },
7217
7309
  name: { type: "string", description: "New creative name" },
7218
- intro: { type: "string", description: "Amend only: commentary / intro text" },
7310
+ intro: { type: "string", description: "Commentary / intro text" },
7219
7311
  headline: { type: "string", description: "New headline" },
7220
- description: { type: "string", description: "Amend only: description (text/spotlight ads)" },
7312
+ description: { type: "string", description: "Description (text/spotlight ads)" },
7221
7313
  "landing-url": { type: "string", description: "New https destination URL" },
7222
7314
  cta: { type: "string", description: "New CTA (LEARN_MORE|REQUEST_DEMO|\u2026)" },
7223
- "cta-label": { type: "string", description: "Amend only: spotlight custom CTA label" },
7224
- "image-id": { type: "string", description: "Amend only: Baker image library id" },
7225
- "image-urn": { type: "string", description: "Amend only: existing urn:li:image:*" },
7226
- "video-id": { type: "string", description: "Amend only: Baker video library id" },
7227
- "video-urn": { type: "string", description: "Amend only: existing urn:li:video:*" },
7228
- "logo-image-id": { type: "string", description: "Amend only: Baker image id for the logo" },
7229
- "thumbnail-image-id": { type: "string", description: "Amend only: Baker image id for the article thumbnail" },
7230
- "thumbnail-urn": { type: "string", description: "Amend only: existing urn:li:image:* thumbnail" },
7231
- title: { type: "string", description: "Amend only: media or article title" },
7315
+ "cta-label": { type: "string", description: "Spotlight custom CTA label" },
7316
+ "image-id": { type: "string", description: "Baker image library id" },
7317
+ "image-urn": { type: "string", description: "Existing urn:li:image:*" },
7318
+ "video-id": { type: "string", description: "Baker video library id" },
7319
+ "video-urn": { type: "string", description: "Existing urn:li:video:*" },
7320
+ "logo-image-id": { type: "string", description: "Baker image id for the logo" },
7321
+ "thumbnail-image-id": { type: "string", description: "Baker image id for the article thumbnail" },
7322
+ "thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail" },
7323
+ title: { type: "string", description: "Media or article title" },
7232
7324
  file: { type: "string", description: "JSON file with fields to change; flags override file keys" }
7233
7325
  },
7234
7326
  run: async ({ args }) => {
@@ -7237,26 +7329,13 @@ Examples:
7237
7329
  const file = loadJsonFileArg(args.file);
7238
7330
  const contentPatch = creativeContentPatch(args);
7239
7331
  const intendedStatus = args["intended-status"] ? String(args["intended-status"]).toUpperCase() : void 0;
7240
- if (target.startsWith("li_temp_")) {
7241
- const fileContent = file.content !== null && typeof file.content === "object" && !Array.isArray(file.content) ? file.content : {};
7242
- const content = { ...fileContent, ...contentPatch };
7243
- const payload2 = mergePayload(file, {
7244
- intendedStatus,
7245
- content: Object.keys(content).length > 0 ? content : void 0
7246
- });
7247
- await stageOp({ kind: "creative.update", accountId, target, payload: payload2 });
7248
- return;
7249
- }
7250
- const blocked = [
7251
- ...Object.keys(contentPatch),
7252
- ...CREATIVE_LIVE_BLOCKED_FILE_KEYS.filter((key) => file[key] !== void 0)
7253
- ];
7254
- if (blocked.length > 0) {
7255
- failWriteValidation(
7256
- `LinkedIn can't change ad content once created (${[...new Set(blocked)].join(", ")}) \u2014 stage a replacement instead: \`baker ads linkedin creatives duplicate ${target} --headline "\u2026" --replace\` copies this ad with your changes and pauses the original when published. (li_temp_* refs staged in this chat stay fully editable.)`
7257
- );
7258
- }
7259
- const payload = mergePayload(file, { intendedStatus, name: args.name });
7332
+ const fileContent = file.content !== null && typeof file.content === "object" && !Array.isArray(file.content) ? file.content : {};
7333
+ const content = { ...fileContent, ...contentPatch };
7334
+ const payload = mergePayload(file, {
7335
+ intendedStatus,
7336
+ name: args.name,
7337
+ content: Object.keys(content).length > 0 ? content : void 0
7338
+ });
7260
7339
  await stageOp({ kind: "creative.update", accountId, target, payload });
7261
7340
  }
7262
7341
  });