@koda-sl/baker-cli 0.107.0-dev.aaab11a02 → 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,10 @@ 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
+
1009
1013
  # Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
1010
1014
  baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
1011
1015
  baker ads linkedin campaigns update li_temp_x2 --daily-budget 100 --currency EUR
package/dist/cli.js CHANGED
@@ -1056,7 +1056,8 @@ var parentRefSchema = z3.union([
1056
1056
  ]);
1057
1057
  var moneySchema = z3.object({
1058
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"),
1059
- 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()
1060
1061
  });
1061
1062
  var runScheduleSchema = z3.object({
1062
1063
  start: z3.number().int().positive(),
@@ -1125,7 +1126,7 @@ function validateCampaignBudgets(p, ctx, { requireBudget }) {
1125
1126
  message: "totalBudget-only campaigns need runSchedule.end"
1126
1127
  });
1127
1128
  }
1128
- if (p.dailyBudget) {
1129
+ if (p.dailyBudget?.currencyCode) {
1129
1130
  const min = currencyMinimums(p.dailyBudget.currencyCode).dailyBudgetMin;
1130
1131
  if (Number(p.dailyBudget.amount) < min) {
1131
1132
  ctx.addIssue({
@@ -1135,7 +1136,7 @@ function validateCampaignBudgets(p, ctx, { requireBudget }) {
1135
1136
  });
1136
1137
  }
1137
1138
  }
1138
- if (p.unitCost) {
1139
+ if (p.unitCost?.currencyCode) {
1139
1140
  const min = currencyMinimums(p.unitCost.currencyCode).unitCostMin;
1140
1141
  if (Number(p.unitCost.amount) < min) {
1141
1142
  ctx.addIssue({
@@ -5413,7 +5414,11 @@ registerSchema({
5413
5414
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)", required: true },
5414
5415
  end: { type: "string", description: "Run schedule end", required: false },
5415
5416
  "total-budget": { type: "string", description: "Lifetime budget amount", required: false },
5416
- 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
+ },
5417
5422
  status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
5418
5423
  file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
5419
5424
  }
@@ -5432,7 +5437,11 @@ registerSchema({
5432
5437
  start: { type: "string", description: "New schedule start", required: false },
5433
5438
  end: { type: "string", description: "New schedule end", required: false },
5434
5439
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
5435
- 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
+ },
5436
5445
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
5437
5446
  file: { type: "string", description: "JSON file with fields to change", required: false }
5438
5447
  }
@@ -5450,7 +5459,11 @@ registerSchema({
5450
5459
  bid: { type: "string", description: "Manual bid amount (unitCost)", required: false },
5451
5460
  "daily-budget": { type: "string", description: "Daily budget amount", required: false },
5452
5461
  "total-budget": { type: "string", description: "Lifetime budget amount", required: false },
5453
- 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
+ },
5454
5467
  start: { type: "string", description: "Run schedule start", required: false },
5455
5468
  end: { type: "string", description: "Run schedule end", required: false },
5456
5469
  locale: { type: "string", description: "Profile locale like en_US (default en_US)", required: false },
@@ -5479,7 +5492,11 @@ registerSchema({
5479
5492
  bid: { type: "string", description: "New manual bid", required: false },
5480
5493
  "daily-budget": { type: "string", description: "New daily budget", required: false },
5481
5494
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
5482
- 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
+ },
5483
5500
  start: { type: "string", description: "New schedule start", required: false },
5484
5501
  end: { type: "string", description: "New schedule end", required: false },
5485
5502
  "targeting-file": { type: "string", description: "JSON file with replacement targetingCriteria", required: false },
@@ -5548,8 +5565,27 @@ registerSchema({
5548
5565
  });
5549
5566
  registerSchema({
5550
5567
  command: "ads.linkedin.creatives.duplicate",
5551
- 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).",
5552
- 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
+ }
5553
5589
  });
5554
5590
  registerSchema({
5555
5591
  command: "ads.linkedin.creatives.create",
@@ -5581,13 +5617,13 @@ registerSchema({
5581
5617
  });
5582
5618
  registerSchema({
5583
5619
  command: "ads.linkedin.creatives.update",
5584
- description: "Stage changes to an existing creative (intendedStatus, name, headline, landing-url, cta), 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 this to apply feedback to staged ads instead of remove + re-create. 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.",
5585
5621
  args: {
5586
5622
  id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
5587
5623
  ...writeAccountArgs,
5588
5624
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
5589
5625
  name: { type: "string", description: "New creative name", required: false },
5590
- headline: { type: "string", description: "New headline", required: false },
5626
+ headline: { type: "string", description: "Amend only: new headline", required: false },
5591
5627
  intro: { type: "string", description: "Amend only: commentary / intro text", required: false },
5592
5628
  description: { type: "string", description: "Amend only: description (text/spotlight)", required: false },
5593
5629
  "landing-url": { type: "string", description: "New https destination URL", required: false },
@@ -6606,8 +6642,11 @@ function parseMoneyFlag(amount, currency, flag) {
6606
6642
  if (amount === void 0 || amount === null || amount === "") {
6607
6643
  return void 0;
6608
6644
  }
6645
+ if (currency === void 0 || currency === null || currency === "") {
6646
+ return { amount: String(amount) };
6647
+ }
6609
6648
  if (typeof currency !== "string" || currency.length !== 3) {
6610
- 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)`);
6611
6650
  }
6612
6651
  return { amount: String(amount), currencyCode: currency.toUpperCase() };
6613
6652
  }
@@ -6787,7 +6826,7 @@ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-
6787
6826
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
6788
6827
  end: { type: "string", description: "Run schedule end" },
6789
6828
  "total-budget": { type: "string", description: "Lifetime budget amount" },
6790
- 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)" },
6791
6830
  status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
6792
6831
  file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
6793
6832
  },
@@ -6816,7 +6855,7 @@ Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000
6816
6855
  start: { type: "string", description: "New run schedule start" },
6817
6856
  end: { type: "string", description: "New run schedule end" },
6818
6857
  "total-budget": { type: "string", description: "New lifetime budget" },
6819
- currency: { type: "string", description: "3-letter currency code" },
6858
+ currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
6820
6859
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|\u2026" },
6821
6860
  file: { type: "string", description: "JSON file with fields to change" }
6822
6861
  },
@@ -6867,7 +6906,7 @@ var campaignFlagArgs = {
6867
6906
  bid: { type: "string", description: "Manual bid (unitCost) \u2014 playbook mandates manual bidding" },
6868
6907
  "daily-budget": { type: "string", description: "Daily budget amount" },
6869
6908
  "total-budget": { type: "string", description: "Lifetime budget amount" },
6870
- 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)" },
6871
6910
  start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
6872
6911
  end: { type: "string", description: "Run schedule end" },
6873
6912
  locale: { type: "string", description: "Profile locale like en_US" },
@@ -6989,7 +7028,63 @@ Example: baker ads linkedin ${cliGroup} duplicate 123456 --name "New variant"`
6989
7028
  }
6990
7029
  var campaignsDuplicateCommand = duplicateCommand("campaign", "campaigns");
6991
7030
  var campaignGroupsDuplicateCommand = duplicateCommand("campaignGroup", "campaign-groups");
6992
- 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
+ });
6993
7088
  var CREATIVE_FORMATS = [
6994
7089
  "image",
6995
7090
  "video",
@@ -7093,7 +7188,7 @@ var CREATIVE_AMEND_CONTENT_FLAGS = [
7093
7188
  ["thumbnail-urn", "thumbnailUrn"],
7094
7189
  ["title", "title"]
7095
7190
  ];
7096
- var CREATIVE_LIVE_UPDATE_KEYS = /* @__PURE__ */ new Set(["headline", "landingUrl", "cta"]);
7191
+ var CREATIVE_LIVE_BLOCKED_FILE_KEYS = ["headline", "landingUrl", "cta", "content"];
7097
7192
  function creativeContentPatch(args) {
7098
7193
  const patch = {};
7099
7194
  for (const [flag, key] of CREATIVE_AMEND_CONTENT_FLAGS) {
@@ -7110,7 +7205,7 @@ var creativesUpdateCommand = defineCommand36({
7110
7205
  meta: {
7111
7206
  name: "update",
7112
7207
  description: `Stage changes to an existing creative, or amend a creative staged in this chat by passing its li_temp_* ref. ${STAGED_NOTE}
7113
- 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/--headline/--landing-url/--cta (content is immutable \u2014 use \`creatives duplicate\` for a new version).
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\`).
7114
7209
  Examples:
7115
7210
  baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED
7116
7211
  baker ads linkedin creatives update li_temp_a1b2 --headline "Sharper headline" --image-id <bakerImageId>`
@@ -7152,19 +7247,16 @@ Examples:
7152
7247
  await stageOp({ kind: "creative.update", accountId, target, payload: payload2 });
7153
7248
  return;
7154
7249
  }
7155
- const immutable = Object.keys(contentPatch).filter((key) => !CREATIVE_LIVE_UPDATE_KEYS.has(key));
7156
- if (immutable.length > 0) {
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) {
7157
7255
  failWriteValidation(
7158
- `creative content is immutable on LinkedIn (${immutable.join(", ")}) \u2014 pass a li_temp_* ref to amend a creative staged in this chat, or use \`creatives duplicate\``
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.)`
7159
7257
  );
7160
7258
  }
7161
- const payload = mergePayload(file, {
7162
- intendedStatus,
7163
- name: args.name,
7164
- headline: contentPatch.headline,
7165
- landingUrl: contentPatch.landingUrl,
7166
- cta: contentPatch.cta
7167
- });
7259
+ const payload = mergePayload(file, { intendedStatus, name: args.name });
7168
7260
  await stageOp({ kind: "creative.update", accountId, target, payload });
7169
7261
  }
7170
7262
  });