@koda-sl/baker-cli 0.106.0-dev.a605c0acb → 0.108.0-dev.d66dc8b3d

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
@@ -1006,6 +1006,15 @@ 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
1012
+
1013
+ # Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
1014
+ baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
1015
+ baker ads linkedin campaigns update li_temp_x2 --daily-budget 100 --currency EUR
1016
+ # A second update to the same real URN also merges into the already-staged update op.
1017
+
1009
1018
  # Review / undo before publish; after publish shows per-op results (applied/simulated/failed/skipped)
1010
1019
  baker ads linkedin draft
1011
1020
  baker ads linkedin draft remove li_temp_x2 # removing a create cascades to dependents
package/dist/cli.js CHANGED
@@ -1042,6 +1042,7 @@ var IMAGE_URN_REGEX = /^urn:li:image:.+$/;
1042
1042
  var VIDEO_URN_REGEX = /^urn:li:video:.+$/;
1043
1043
  var DOCUMENT_URN_REGEX = /^urn:li:document:.+$/;
1044
1044
  var POST_URN_REGEX = /^urn:li:(share|ugcPost):\d+$/;
1045
+ var EVENT_URN_REGEX = /^urn:li:event:\d+$/;
1045
1046
  var ORGANIZATION_URN_REGEX = /^urn:li:organization:\d+$/;
1046
1047
  var TARGETING_FACET_URN_REGEX = /^urn:li:adTargetingFacet:[a-zA-Z]+$/;
1047
1048
 
@@ -1055,7 +1056,8 @@ var parentRefSchema = z3.union([
1055
1056
  ]);
1056
1057
  var moneySchema = z3.object({
1057
1058
  amount: z3.string().regex(/^\d+(\.\d{1,2})?$/, "expected a decimal amount like 50 or 50.00").refine((val) => Number(val) > 0, "amount must be greater than zero"),
1058
- currencyCode: z3.string().length(3)
1059
+ /** LinkedIn ad accounts are single-currency — when omitted, staging fills it from the account. */
1060
+ currencyCode: z3.string().length(3).optional()
1059
1061
  });
1060
1062
  var runScheduleSchema = z3.object({
1061
1063
  start: z3.number().int().positive(),
@@ -1117,14 +1119,14 @@ function validateCampaignBudgets(p, ctx, { requireBudget }) {
1117
1119
  if (requireBudget && !p.dailyBudget && !p.totalBudget) {
1118
1120
  ctx.addIssue({ code: "custom", path: ["dailyBudget"], message: "set dailyBudget and/or totalBudget" });
1119
1121
  }
1120
- if (p.totalBudget && !p.dailyBudget && !p.runSchedule?.end) {
1122
+ if (requireBudget && p.totalBudget && !p.dailyBudget && !p.runSchedule?.end) {
1121
1123
  ctx.addIssue({
1122
1124
  code: "custom",
1123
1125
  path: ["runSchedule", "end"],
1124
1126
  message: "totalBudget-only campaigns need runSchedule.end"
1125
1127
  });
1126
1128
  }
1127
- if (p.dailyBudget) {
1129
+ if (p.dailyBudget?.currencyCode) {
1128
1130
  const min = currencyMinimums(p.dailyBudget.currencyCode).dailyBudgetMin;
1129
1131
  if (Number(p.dailyBudget.amount) < min) {
1130
1132
  ctx.addIssue({
@@ -1134,7 +1136,7 @@ function validateCampaignBudgets(p, ctx, { requireBudget }) {
1134
1136
  });
1135
1137
  }
1136
1138
  }
1137
- if (p.unitCost) {
1139
+ if (p.unitCost?.currencyCode) {
1138
1140
  const min = currencyMinimums(p.unitCost.currencyCode).unitCostMin;
1139
1141
  if (Number(p.unitCost.amount) < min) {
1140
1142
  ctx.addIssue({
@@ -1212,6 +1214,9 @@ var textCreativeSchema = z3.object({
1212
1214
  landingUrl: httpsUrlSchema,
1213
1215
  imageId: bakerMediaIdSchema.optional(),
1214
1216
  imageUrn: z3.string().regex(IMAGE_URN_REGEX).optional()
1217
+ }).refine((p) => p.imageId ? !p.imageUrn : Boolean(p.imageUrn), {
1218
+ message: "text ads require a 100\xD7100 image \u2014 provide exactly one of imageId (Baker library) or imageUrn (already on LinkedIn)",
1219
+ path: ["imageId"]
1215
1220
  });
1216
1221
  var spotlightCreativeSchema = z3.object({
1217
1222
  format: z3.literal("spotlight"),
@@ -1272,6 +1277,23 @@ var jobsCreativeSchema = z3.object({
1272
1277
  headlinePreset: z3.string().min(1),
1273
1278
  buttonLabelPreset: z3.string().min(1)
1274
1279
  });
1280
+ var eventCreativeSchema = z3.object({
1281
+ format: z3.literal("event"),
1282
+ commentary: commentarySchema,
1283
+ eventUrn: z3.string().regex(EVENT_URN_REGEX, "expected urn:li:event:*")
1284
+ });
1285
+ var articleCreativeSchema = z3.object({
1286
+ format: z3.literal("article"),
1287
+ commentary: commentarySchema,
1288
+ source: httpsUrlSchema,
1289
+ title: z3.string().min(1).max(200),
1290
+ description: z3.string().max(300).optional(),
1291
+ thumbnailImageId: bakerMediaIdSchema.optional(),
1292
+ thumbnailUrn: z3.string().regex(IMAGE_URN_REGEX).optional()
1293
+ }).refine((p) => !(p.thumbnailImageId && p.thumbnailUrn), {
1294
+ message: "provide at most one of thumbnailImageId (Baker library) or thumbnailUrn (already on LinkedIn)",
1295
+ path: ["thumbnailImageId"]
1296
+ });
1275
1297
  var creativeContentSchema = z3.discriminatedUnion("format", [
1276
1298
  imageCreativeSchema,
1277
1299
  videoCreativeSchema,
@@ -1282,7 +1304,9 @@ var creativeContentSchema = z3.discriminatedUnion("format", [
1282
1304
  carouselCreativeSchema,
1283
1305
  conversationCreativeSchema,
1284
1306
  tlaCreativeSchema,
1285
- jobsCreativeSchema
1307
+ jobsCreativeSchema,
1308
+ eventCreativeSchema,
1309
+ articleCreativeSchema
1286
1310
  ]);
1287
1311
  var creativeCreateSchema = z3.object({
1288
1312
  campaign: parentRefSchema,
@@ -1294,7 +1318,14 @@ var creativeUpdateSchema = z3.object({
1294
1318
  name: z3.string().max(255).optional(),
1295
1319
  headline: headlineSchema.optional(),
1296
1320
  landingUrl: httpsUrlSchema.optional(),
1297
- cta: z3.enum(CTA_TYPES).optional()
1321
+ cta: z3.enum(CTA_TYPES).optional(),
1322
+ /**
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.
1327
+ */
1328
+ content: z3.record(z3.string(), z3.unknown()).optional()
1298
1329
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1299
1330
  var listRowSchema = z3.record(z3.string(), z3.string());
1300
1331
  var audienceCreateSchema = z3.object({
@@ -1391,7 +1422,7 @@ var LINKEDIN_DRAFT_OP_KINDS = [
1391
1422
  ];
1392
1423
  var linkedinDraftOpKindSchema = z3.enum(LINKEDIN_DRAFT_OP_KINDS);
1393
1424
  var accountIdSchema = z3.string().regex(NUMERIC_ID_REGEX, "accountId must be the bare numeric ad account id");
1394
- var updateTargetSchema = z3.union([z3.string().regex(URN_REGEX), z3.string().regex(NUMERIC_ID_REGEX)]);
1425
+ var updateTargetSchema = z3.union([z3.string().regex(URN_REGEX), z3.string().regex(NUMERIC_ID_REGEX), tempRefSchema]);
1395
1426
  function createOp(kind, payload) {
1396
1427
  return z3.object({ kind: z3.literal(kind), accountId: accountIdSchema, payload });
1397
1428
  }
@@ -1434,7 +1465,9 @@ var linkedinDraftStageResponseSchema = z4.object({
1434
1465
  mode: linkedinWriteModeSchema,
1435
1466
  dependsOn: z4.array(z4.string()),
1436
1467
  summary: z4.string(),
1437
- warnings: z4.array(z4.string())
1468
+ warnings: z4.array(z4.string()),
1469
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
1470
+ amended: z4.boolean().optional()
1438
1471
  });
1439
1472
  var linkedinDraftOpViewSchema = z4.object({
1440
1473
  ref: z4.string(),
@@ -5381,22 +5414,34 @@ registerSchema({
5381
5414
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)", required: true },
5382
5415
  end: { type: "string", description: "Run schedule end", required: false },
5383
5416
  "total-budget": { type: "string", description: "Lifetime budget amount", required: false },
5384
- currency: { type: "string", description: "3-letter currency code (required with money flags)", required: false },
5417
+ currency: {
5418
+ type: "string",
5419
+ description: "3-letter currency code (defaults to the ad account currency)",
5420
+ required: false
5421
+ },
5385
5422
  status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
5386
5423
  file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
5387
5424
  }
5388
5425
  });
5389
5426
  registerSchema({
5390
5427
  command: "ads.linkedin.campaign-groups.update",
5391
- description: "Stage changes to an existing campaign group (name, schedule, budget, status). Captures a before-snapshot for the dashboard diff. Applies on chat publish.",
5428
+ description: "Stage changes to an existing campaign group (name, schedule, budget, status). Captures a before-snapshot for the dashboard diff. Pass a li_temp_* ref to AMEND a group staged in this chat. Applies on chat publish.",
5392
5429
  args: {
5393
- id: { type: "positional", description: "Campaign group id or URN", required: true },
5430
+ id: {
5431
+ type: "positional",
5432
+ description: "Campaign group id/URN, or li_temp_* ref staged in this chat",
5433
+ required: true
5434
+ },
5394
5435
  ...writeAccountArgs,
5395
5436
  name: { type: "string", description: "New name", required: false },
5396
5437
  start: { type: "string", description: "New schedule start", required: false },
5397
5438
  end: { type: "string", description: "New schedule end", required: false },
5398
5439
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
5399
- currency: { type: "string", description: "3-letter currency code", required: false },
5440
+ currency: {
5441
+ type: "string",
5442
+ description: "3-letter currency code (defaults to the ad account currency)",
5443
+ required: false
5444
+ },
5400
5445
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
5401
5446
  file: { type: "string", description: "JSON file with fields to change", required: false }
5402
5447
  }
@@ -5414,7 +5459,11 @@ registerSchema({
5414
5459
  bid: { type: "string", description: "Manual bid amount (unitCost)", required: false },
5415
5460
  "daily-budget": { type: "string", description: "Daily budget amount", required: false },
5416
5461
  "total-budget": { type: "string", description: "Lifetime budget amount", required: false },
5417
- currency: { type: "string", description: "3-letter currency code (required with money flags)", required: false },
5462
+ currency: {
5463
+ type: "string",
5464
+ description: "3-letter currency code (defaults to the ad account currency)",
5465
+ required: false
5466
+ },
5418
5467
  start: { type: "string", description: "Run schedule start", required: false },
5419
5468
  end: { type: "string", description: "Run schedule end", required: false },
5420
5469
  locale: { type: "string", description: "Profile locale like en_US (default en_US)", required: false },
@@ -5434,16 +5483,20 @@ registerSchema({
5434
5483
  });
5435
5484
  registerSchema({
5436
5485
  command: "ads.linkedin.campaigns.update",
5437
- description: "Stage changes to an existing campaign \u2014 budget, bid, targeting, schedule, flags, status. Only pass what changes; a before-snapshot powers the dashboard diff. Applies on chat publish.",
5486
+ description: "Stage changes to an existing campaign \u2014 budget, bid, targeting, schedule, flags, status. Only pass what changes; a before-snapshot powers the dashboard diff. Pass a li_temp_* ref to AMEND a campaign staged in this chat (fields merge into the staged create). Applies on chat publish.",
5438
5487
  args: {
5439
- id: { type: "positional", description: "Campaign id or URN", required: true },
5488
+ id: { type: "positional", description: "Campaign id/URN, or li_temp_* ref staged in this chat", required: true },
5440
5489
  ...writeAccountArgs,
5441
5490
  name: { type: "string", description: "New name", required: false },
5442
5491
  objective: { type: "string", description: "New objective", required: false },
5443
5492
  bid: { type: "string", description: "New manual bid", required: false },
5444
5493
  "daily-budget": { type: "string", description: "New daily budget", required: false },
5445
5494
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
5446
- currency: { type: "string", description: "3-letter currency code", required: false },
5495
+ currency: {
5496
+ type: "string",
5497
+ description: "3-letter currency code (defaults to the ad account currency)",
5498
+ required: false
5499
+ },
5447
5500
  start: { type: "string", description: "New schedule start", required: false },
5448
5501
  end: { type: "string", description: "New schedule end", required: false },
5449
5502
  "targeting-file": { type: "string", description: "JSON file with replacement targetingCriteria", required: false },
@@ -5512,18 +5565,37 @@ registerSchema({
5512
5565
  });
5513
5566
  registerSchema({
5514
5567
  command: "ads.linkedin.creatives.duplicate",
5515
- description: "Stage a copy of an existing creative by re-sponsoring its underlying post into a new DRAFT creative (--file can override the target campaign).",
5516
- args: duplicateArgs
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.",
5569
+ args: {
5570
+ id: { type: "positional", description: "Source creative id or URN", required: true },
5571
+ ...writeAccountArgs,
5572
+ campaign: { type: "string", description: "Stage the copy under a different ad set", required: false },
5573
+ "intended-status": { type: "string", description: "Status for the copy (default DRAFT)", required: false },
5574
+ replace: { type: "boolean", description: "Also pause the original once the new ad publishes", required: false },
5575
+ headline: { type: "string", description: "New headline", required: false },
5576
+ description: { type: "string", description: "New description (text/spotlight)", required: false },
5577
+ intro: { type: "string", description: "New commentary / intro text", required: false },
5578
+ "landing-url": { type: "string", description: "New https destination URL", required: false },
5579
+ cta: { type: "string", description: "New CTA", required: false },
5580
+ "cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
5581
+ "image-id": { type: "string", description: "Baker image library id", required: false },
5582
+ "image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
5583
+ "video-id": { type: "string", description: "Baker video library id", required: false },
5584
+ "video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
5585
+ "logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
5586
+ title: { type: "string", description: "Media title", required: false },
5587
+ file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
5588
+ }
5517
5589
  });
5518
5590
  registerSchema({
5519
5591
  command: "ads.linkedin.creatives.create",
5520
- description: "Stage a new creative (ad). Formats: image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs. 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.",
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.",
5521
5593
  args: {
5522
5594
  ...writeAccountArgs,
5523
5595
  campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
5524
5596
  format: {
5525
5597
  type: "string",
5526
- description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs",
5598
+ description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article",
5527
5599
  required: true
5528
5600
  },
5529
5601
  "image-id": { type: "string", description: "Baker image library id", required: false },
@@ -5545,13 +5617,28 @@ registerSchema({
5545
5617
  });
5546
5618
  registerSchema({
5547
5619
  command: "ads.linkedin.creatives.update",
5548
- description: "Stage changes to an existing creative (intendedStatus, name). Applies on chat publish.",
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.",
5549
5621
  args: {
5550
- id: { type: "positional", description: "Creative id or URN", required: true },
5622
+ id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
5551
5623
  ...writeAccountArgs,
5552
5624
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
5553
5625
  name: { type: "string", description: "New creative name", required: false },
5554
- file: { type: "string", description: "JSON file with fields to change", 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 },
5629
+ "landing-url": { type: "string", description: "New https destination URL", required: false },
5630
+ 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 },
5635
+ "thumbnail-image-id": {
5636
+ type: "string",
5637
+ description: "Amend only: Baker image id for the article thumbnail",
5638
+ required: false
5639
+ },
5640
+ title: { type: "string", description: "Amend only: media or article title", required: false },
5641
+ file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
5555
5642
  }
5556
5643
  });
5557
5644
  registerSchema({
@@ -6555,8 +6642,11 @@ function parseMoneyFlag(amount, currency, flag) {
6555
6642
  if (amount === void 0 || amount === null || amount === "") {
6556
6643
  return void 0;
6557
6644
  }
6645
+ if (currency === void 0 || currency === null || currency === "") {
6646
+ return { amount: String(amount) };
6647
+ }
6558
6648
  if (typeof currency !== "string" || currency.length !== 3) {
6559
- failWriteValidation(`${flag} needs --currency (3-letter code matching the ad account, e.g. EUR)`);
6649
+ failWriteValidation(`${flag} got an invalid --currency (expected a 3-letter code like EUR)`);
6560
6650
  }
6561
6651
  return { amount: String(amount), currencyCode: currency.toUpperCase() };
6562
6652
  }
@@ -6736,7 +6826,7 @@ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-
6736
6826
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
6737
6827
  end: { type: "string", description: "Run schedule end" },
6738
6828
  "total-budget": { type: "string", description: "Lifetime budget amount" },
6739
- currency: { type: "string", description: "3-letter currency code (required with money flags)" },
6829
+ currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
6740
6830
  status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
6741
6831
  file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
6742
6832
  },
@@ -6755,6 +6845,7 @@ var campaignGroupsUpdateCommand = defineCommand36({
6755
6845
  meta: {
6756
6846
  name: "update",
6757
6847
  description: `Stage changes to an existing campaign group. ${STAGED_NOTE}
6848
+ Pass a li_temp_* ref to amend a group staged in this chat \u2014 fields merge into the staged create.
6758
6849
  Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000 --currency EUR`
6759
6850
  },
6760
6851
  args: {
@@ -6764,7 +6855,7 @@ Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000
6764
6855
  start: { type: "string", description: "New run schedule start" },
6765
6856
  end: { type: "string", description: "New run schedule end" },
6766
6857
  "total-budget": { type: "string", description: "New lifetime budget" },
6767
- currency: { type: "string", description: "3-letter currency code" },
6858
+ currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
6768
6859
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|\u2026" },
6769
6860
  file: { type: "string", description: "JSON file with fields to change" }
6770
6861
  },
@@ -6815,7 +6906,7 @@ var campaignFlagArgs = {
6815
6906
  bid: { type: "string", description: "Manual bid (unitCost) \u2014 playbook mandates manual bidding" },
6816
6907
  "daily-budget": { type: "string", description: "Daily budget amount" },
6817
6908
  "total-budget": { type: "string", description: "Lifetime budget amount" },
6818
- currency: { type: "string", description: "3-letter currency code (required with money flags)" },
6909
+ currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
6819
6910
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
6820
6911
  end: { type: "string", description: "Run schedule end" },
6821
6912
  locale: { type: "string", description: "Profile locale like en_US" },
@@ -6849,6 +6940,7 @@ var campaignsUpdateCommand = defineCommand36({
6849
6940
  meta: {
6850
6941
  name: "update",
6851
6942
  description: `Stage changes to an existing campaign (budget, bid, targeting, flags, schedule, status). ${STAGED_NOTE}
6943
+ Pass a li_temp_* ref to amend a campaign staged in this chat \u2014 fields merge into the staged create.
6852
6944
  Example: baker ads linkedin campaigns update 123456 --daily-budget 100 --currency EUR --audience-expansion off`
6853
6945
  },
6854
6946
  args: {
@@ -6936,7 +7028,63 @@ Example: baker ads linkedin ${cliGroup} duplicate 123456 --name "New variant"`
6936
7028
  }
6937
7029
  var campaignsDuplicateCommand = duplicateCommand("campaign", "campaigns");
6938
7030
  var campaignGroupsDuplicateCommand = duplicateCommand("campaignGroup", "campaign-groups");
6939
- var creativesDuplicateCommand = duplicateCommand("creative", "creatives");
7031
+ var creativesDuplicateCommand = defineCommand36({
7032
+ meta: {
7033
+ name: "duplicate",
7034
+ 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). ${STAGED_NOTE}
7035
+ --replace also pauses the original once the new ad publishes (skipped if the new ad fails).
7036
+ Examples:
7037
+ baker ads linkedin creatives duplicate 1458413484 --headline "10x Demos, Same Team" --description "AI handles every demo request." --replace
7038
+ baker ads linkedin creatives duplicate urn:li:sponsoredCreative:789 --campaign 799305464`
7039
+ },
7040
+ args: {
7041
+ id: { type: "positional", description: "Source creative id or URN", required: true },
7042
+ ...accountArgs,
7043
+ 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" },
7046
+ intro: { type: "string", description: "New commentary / intro text" },
7047
+ headline: { type: "string", description: "New headline" },
7048
+ description: { type: "string", description: "New description (text/spotlight ads)" },
7049
+ "landing-url": { type: "string", description: "New https destination URL" },
7050
+ cta: { type: "string", description: "New CTA (LEARN_MORE|REQUEST_DEMO|\u2026)" },
7051
+ "cta-label": { type: "string", description: "Spotlight custom CTA label" },
7052
+ "image-id": { type: "string", description: "Baker image library id" },
7053
+ "image-urn": { type: "string", description: "Existing urn:li:image:*" },
7054
+ "video-id": { type: "string", description: "Baker video library id" },
7055
+ "video-urn": { type: "string", description: "Existing urn:li:video:*" },
7056
+ "logo-image-id": { type: "string", description: "Baker image id for the logo" },
7057
+ title: { type: "string", description: "Media title" },
7058
+ file: { type: "string", description: "JSON file with field overrides for the copy" }
7059
+ },
7060
+ run: async ({ args }) => {
7061
+ try {
7062
+ const chatId = requireChatId();
7063
+ const file = loadJsonFileArg(args.file);
7064
+ const fileContent = file.content !== null && typeof file.content === "object" && !Array.isArray(file.content) ? file.content : {};
7065
+ const content = { ...fileContent, ...creativeContentPatch(args) };
7066
+ const overrides = mergePayload(file, {
7067
+ campaign: args.campaign,
7068
+ intendedStatus: args["intended-status"] ? String(args["intended-status"]).toUpperCase() : void 0,
7069
+ content: Object.keys(content).length > 0 ? content : void 0
7070
+ });
7071
+ const response = await apiPost(
7072
+ "/api/ads/linkedin/draft/duplicate",
7073
+ {
7074
+ chatId,
7075
+ accountId: bareAccountId(args),
7076
+ entity: "creative",
7077
+ sourceId: requireTarget(args, "creative"),
7078
+ ...Object.keys(overrides).length > 0 ? { overrides } : {},
7079
+ ...args.replace ? { replace: true } : {}
7080
+ }
7081
+ );
7082
+ writeJsonEnvelope(response);
7083
+ } catch (err) {
7084
+ handleLinkedinError(err);
7085
+ }
7086
+ }
7087
+ });
6940
7088
  var CREATIVE_FORMATS = [
6941
7089
  "image",
6942
7090
  "video",
@@ -6947,7 +7095,9 @@ var CREATIVE_FORMATS = [
6947
7095
  "carousel",
6948
7096
  "conversation",
6949
7097
  "tla",
6950
- "jobs"
7098
+ "jobs",
7099
+ "event",
7100
+ "article"
6951
7101
  ];
6952
7102
  var creativesCreateCommand = defineCommand36({
6953
7103
  meta: {
@@ -6974,8 +7124,12 @@ Examples:
6974
7124
  cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026" },
6975
7125
  "cta-label": { type: "string", description: "Spotlight custom CTA label (\u226418 chars)" },
6976
7126
  "post-urn": { type: "string", description: "TLA: existing post to sponsor (urn:li:share|ugcPost:*)" },
7127
+ "event-urn": { type: "string", description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)" },
7128
+ "article-url": { type: "string", description: "Article ads: https URL of the article/newsletter to sponsor" },
7129
+ "thumbnail-image-id": { type: "string", description: "Article ads: Baker image id for the link thumbnail" },
7130
+ "thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail" },
6977
7131
  "intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
6978
- title: { type: "string", description: "Media title" },
7132
+ title: { type: "string", description: "Media title (image/video) or article title" },
6979
7133
  file: { type: "string", description: "JSON file with the full content object; flags override" }
6980
7134
  },
6981
7135
  run: async ({ args }) => {
@@ -7001,7 +7155,12 @@ Examples:
7001
7155
  cta: args.cta ? String(args.cta).toUpperCase() : void 0,
7002
7156
  ctaLabel: args["cta-label"],
7003
7157
  postUrn: args["post-urn"],
7004
- mediaTitle: args.title
7158
+ eventUrn: args["event-urn"],
7159
+ source: args["article-url"],
7160
+ thumbnailImageId: args["thumbnail-image-id"],
7161
+ thumbnailUrn: args["thumbnail-urn"],
7162
+ mediaTitle: args.title,
7163
+ title: args.title
7005
7164
  });
7006
7165
  await stageOp({
7007
7166
  kind: "creative.create",
@@ -7014,31 +7173,91 @@ Examples:
7014
7173
  });
7015
7174
  }
7016
7175
  });
7176
+ var CREATIVE_AMEND_CONTENT_FLAGS = [
7177
+ ["intro", "commentary"],
7178
+ ["headline", "headline"],
7179
+ ["description", "description"],
7180
+ ["landing-url", "landingUrl"],
7181
+ ["cta-label", "ctaLabel"],
7182
+ ["image-id", "imageId"],
7183
+ ["image-urn", "imageUrn"],
7184
+ ["video-id", "videoId"],
7185
+ ["video-urn", "videoUrn"],
7186
+ ["logo-image-id", "logoImageId"],
7187
+ ["thumbnail-image-id", "thumbnailImageId"],
7188
+ ["thumbnail-urn", "thumbnailUrn"],
7189
+ ["title", "title"]
7190
+ ];
7191
+ var CREATIVE_LIVE_BLOCKED_FILE_KEYS = ["headline", "landingUrl", "cta", "content"];
7192
+ function creativeContentPatch(args) {
7193
+ const patch = {};
7194
+ for (const [flag, key] of CREATIVE_AMEND_CONTENT_FLAGS) {
7195
+ if (args[flag] !== void 0) {
7196
+ patch[key] = args[flag];
7197
+ }
7198
+ }
7199
+ if (args.cta !== void 0) {
7200
+ patch.cta = String(args.cta).toUpperCase();
7201
+ }
7202
+ return patch;
7203
+ }
7017
7204
  var creativesUpdateCommand = defineCommand36({
7018
7205
  meta: {
7019
7206
  name: "update",
7020
- description: `Stage changes to an existing creative. ${STAGED_NOTE}
7021
- Example: baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED`
7207
+ 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\`).
7209
+ 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>`
7022
7212
  },
7023
7213
  args: {
7024
- id: { type: "positional", description: "Creative id or URN", required: true },
7214
+ id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
7025
7215
  ...accountArgs,
7026
7216
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT" },
7027
7217
  name: { type: "string", description: "New creative name" },
7028
- file: { type: "string", description: "JSON file with fields to change" }
7218
+ intro: { type: "string", description: "Amend only: commentary / intro text" },
7219
+ headline: { type: "string", description: "New headline" },
7220
+ description: { type: "string", description: "Amend only: description (text/spotlight ads)" },
7221
+ "landing-url": { type: "string", description: "New https destination URL" },
7222
+ 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" },
7232
+ file: { type: "string", description: "JSON file with fields to change; flags override file keys" }
7029
7233
  },
7030
7234
  run: async ({ args }) => {
7031
7235
  const accountId = bareAccountId(args);
7032
- const payload = mergePayload(loadJsonFileArg(args.file), {
7033
- intendedStatus: args["intended-status"] ? String(args["intended-status"]).toUpperCase() : void 0,
7034
- name: args.name
7035
- });
7036
- await stageOp({
7037
- kind: "creative.update",
7038
- accountId,
7039
- target: requireTarget(args, "creative"),
7040
- payload
7041
- });
7236
+ const target = requireTarget(args, "creative");
7237
+ const file = loadJsonFileArg(args.file);
7238
+ const contentPatch = creativeContentPatch(args);
7239
+ 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 });
7260
+ await stageOp({ kind: "creative.update", accountId, target, payload });
7042
7261
  }
7043
7262
  });
7044
7263
  var AUDIENCE_TYPES2 = ["company-list", "user-list", "retargeting", "engagement"];