@koda-sl/baker-cli 0.106.0-dev.a605c0acb → 0.107.0-dev.aaab11a02

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,11 @@ 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
+ # Amend staged ops in place — `update <li_temp_ref>` merges into the staged create and re-validates
1010
+ baker ads linkedin creatives update li_temp_x3 --headline "Sharper headline" --image-id <bakerImageId>
1011
+ baker ads linkedin campaigns update li_temp_x2 --daily-budget 100 --currency EUR
1012
+ # A second update to the same real URN also merges into the already-staged update op.
1013
+
1009
1014
  # Review / undo before publish; after publish shows per-op results (applied/simulated/failed/skipped)
1010
1015
  baker ads linkedin draft
1011
1016
  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
 
@@ -1117,7 +1118,7 @@ function validateCampaignBudgets(p, ctx, { requireBudget }) {
1117
1118
  if (requireBudget && !p.dailyBudget && !p.totalBudget) {
1118
1119
  ctx.addIssue({ code: "custom", path: ["dailyBudget"], message: "set dailyBudget and/or totalBudget" });
1119
1120
  }
1120
- if (p.totalBudget && !p.dailyBudget && !p.runSchedule?.end) {
1121
+ if (requireBudget && p.totalBudget && !p.dailyBudget && !p.runSchedule?.end) {
1121
1122
  ctx.addIssue({
1122
1123
  code: "custom",
1123
1124
  path: ["runSchedule", "end"],
@@ -1212,6 +1213,9 @@ var textCreativeSchema = z3.object({
1212
1213
  landingUrl: httpsUrlSchema,
1213
1214
  imageId: bakerMediaIdSchema.optional(),
1214
1215
  imageUrn: z3.string().regex(IMAGE_URN_REGEX).optional()
1216
+ }).refine((p) => p.imageId ? !p.imageUrn : Boolean(p.imageUrn), {
1217
+ message: "text ads require a 100\xD7100 image \u2014 provide exactly one of imageId (Baker library) or imageUrn (already on LinkedIn)",
1218
+ path: ["imageId"]
1215
1219
  });
1216
1220
  var spotlightCreativeSchema = z3.object({
1217
1221
  format: z3.literal("spotlight"),
@@ -1272,6 +1276,23 @@ var jobsCreativeSchema = z3.object({
1272
1276
  headlinePreset: z3.string().min(1),
1273
1277
  buttonLabelPreset: z3.string().min(1)
1274
1278
  });
1279
+ var eventCreativeSchema = z3.object({
1280
+ format: z3.literal("event"),
1281
+ commentary: commentarySchema,
1282
+ eventUrn: z3.string().regex(EVENT_URN_REGEX, "expected urn:li:event:*")
1283
+ });
1284
+ var articleCreativeSchema = z3.object({
1285
+ format: z3.literal("article"),
1286
+ commentary: commentarySchema,
1287
+ source: httpsUrlSchema,
1288
+ title: z3.string().min(1).max(200),
1289
+ description: z3.string().max(300).optional(),
1290
+ thumbnailImageId: bakerMediaIdSchema.optional(),
1291
+ thumbnailUrn: z3.string().regex(IMAGE_URN_REGEX).optional()
1292
+ }).refine((p) => !(p.thumbnailImageId && p.thumbnailUrn), {
1293
+ message: "provide at most one of thumbnailImageId (Baker library) or thumbnailUrn (already on LinkedIn)",
1294
+ path: ["thumbnailImageId"]
1295
+ });
1275
1296
  var creativeContentSchema = z3.discriminatedUnion("format", [
1276
1297
  imageCreativeSchema,
1277
1298
  videoCreativeSchema,
@@ -1282,7 +1303,9 @@ var creativeContentSchema = z3.discriminatedUnion("format", [
1282
1303
  carouselCreativeSchema,
1283
1304
  conversationCreativeSchema,
1284
1305
  tlaCreativeSchema,
1285
- jobsCreativeSchema
1306
+ jobsCreativeSchema,
1307
+ eventCreativeSchema,
1308
+ articleCreativeSchema
1286
1309
  ]);
1287
1310
  var creativeCreateSchema = z3.object({
1288
1311
  campaign: parentRefSchema,
@@ -1294,7 +1317,14 @@ var creativeUpdateSchema = z3.object({
1294
1317
  name: z3.string().max(255).optional(),
1295
1318
  headline: headlineSchema.optional(),
1296
1319
  landingUrl: httpsUrlSchema.optional(),
1297
- cta: z3.enum(CTA_TYPES).optional()
1320
+ cta: z3.enum(CTA_TYPES).optional(),
1321
+ /**
1322
+ * Amend-only content patch: valid when the target is a li_temp_* staged
1323
+ * create — merged into the staged content and re-validated against the
1324
+ * full creative schema. Creatives already on LinkedIn have immutable
1325
+ * content; the backend rejects it for real targets.
1326
+ */
1327
+ content: z3.record(z3.string(), z3.unknown()).optional()
1298
1328
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1299
1329
  var listRowSchema = z3.record(z3.string(), z3.string());
1300
1330
  var audienceCreateSchema = z3.object({
@@ -1391,7 +1421,7 @@ var LINKEDIN_DRAFT_OP_KINDS = [
1391
1421
  ];
1392
1422
  var linkedinDraftOpKindSchema = z3.enum(LINKEDIN_DRAFT_OP_KINDS);
1393
1423
  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)]);
1424
+ var updateTargetSchema = z3.union([z3.string().regex(URN_REGEX), z3.string().regex(NUMERIC_ID_REGEX), tempRefSchema]);
1395
1425
  function createOp(kind, payload) {
1396
1426
  return z3.object({ kind: z3.literal(kind), accountId: accountIdSchema, payload });
1397
1427
  }
@@ -1434,7 +1464,9 @@ var linkedinDraftStageResponseSchema = z4.object({
1434
1464
  mode: linkedinWriteModeSchema,
1435
1465
  dependsOn: z4.array(z4.string()),
1436
1466
  summary: z4.string(),
1437
- warnings: z4.array(z4.string())
1467
+ warnings: z4.array(z4.string()),
1468
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
1469
+ amended: z4.boolean().optional()
1438
1470
  });
1439
1471
  var linkedinDraftOpViewSchema = z4.object({
1440
1472
  ref: z4.string(),
@@ -5388,9 +5420,13 @@ registerSchema({
5388
5420
  });
5389
5421
  registerSchema({
5390
5422
  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.",
5423
+ 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
5424
  args: {
5393
- id: { type: "positional", description: "Campaign group id or URN", required: true },
5425
+ id: {
5426
+ type: "positional",
5427
+ description: "Campaign group id/URN, or li_temp_* ref staged in this chat",
5428
+ required: true
5429
+ },
5394
5430
  ...writeAccountArgs,
5395
5431
  name: { type: "string", description: "New name", required: false },
5396
5432
  start: { type: "string", description: "New schedule start", required: false },
@@ -5434,9 +5470,9 @@ registerSchema({
5434
5470
  });
5435
5471
  registerSchema({
5436
5472
  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.",
5473
+ 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
5474
  args: {
5439
- id: { type: "positional", description: "Campaign id or URN", required: true },
5475
+ id: { type: "positional", description: "Campaign id/URN, or li_temp_* ref staged in this chat", required: true },
5440
5476
  ...writeAccountArgs,
5441
5477
  name: { type: "string", description: "New name", required: false },
5442
5478
  objective: { type: "string", description: "New objective", required: false },
@@ -5517,13 +5553,13 @@ registerSchema({
5517
5553
  });
5518
5554
  registerSchema({
5519
5555
  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.",
5556
+ 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
5557
  args: {
5522
5558
  ...writeAccountArgs,
5523
5559
  campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
5524
5560
  format: {
5525
5561
  type: "string",
5526
- description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs",
5562
+ description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article",
5527
5563
  required: true
5528
5564
  },
5529
5565
  "image-id": { type: "string", description: "Baker image library id", required: false },
@@ -5545,13 +5581,28 @@ registerSchema({
5545
5581
  });
5546
5582
  registerSchema({
5547
5583
  command: "ads.linkedin.creatives.update",
5548
- description: "Stage changes to an existing creative (intendedStatus, name). Applies on chat publish.",
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.",
5549
5585
  args: {
5550
- id: { type: "positional", description: "Creative id or URN", required: true },
5586
+ id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
5551
5587
  ...writeAccountArgs,
5552
5588
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
5553
5589
  name: { type: "string", description: "New creative name", required: false },
5554
- file: { type: "string", description: "JSON file with fields to change", required: false }
5590
+ headline: { type: "string", description: "New headline", required: false },
5591
+ intro: { type: "string", description: "Amend only: commentary / intro text", required: false },
5592
+ description: { type: "string", description: "Amend only: description (text/spotlight)", required: false },
5593
+ "landing-url": { type: "string", description: "New https destination URL", required: false },
5594
+ cta: { type: "string", description: "New CTA", required: false },
5595
+ "image-id": { type: "string", description: "Amend only: Baker image library id", required: false },
5596
+ "image-urn": { type: "string", description: "Amend only: existing urn:li:image:*", required: false },
5597
+ "video-id": { type: "string", description: "Amend only: Baker video library id", required: false },
5598
+ "logo-image-id": { type: "string", description: "Amend only: Baker image id for the logo", required: false },
5599
+ "thumbnail-image-id": {
5600
+ type: "string",
5601
+ description: "Amend only: Baker image id for the article thumbnail",
5602
+ required: false
5603
+ },
5604
+ title: { type: "string", description: "Amend only: media or article title", required: false },
5605
+ file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
5555
5606
  }
5556
5607
  });
5557
5608
  registerSchema({
@@ -6755,6 +6806,7 @@ var campaignGroupsUpdateCommand = defineCommand36({
6755
6806
  meta: {
6756
6807
  name: "update",
6757
6808
  description: `Stage changes to an existing campaign group. ${STAGED_NOTE}
6809
+ Pass a li_temp_* ref to amend a group staged in this chat \u2014 fields merge into the staged create.
6758
6810
  Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000 --currency EUR`
6759
6811
  },
6760
6812
  args: {
@@ -6849,6 +6901,7 @@ var campaignsUpdateCommand = defineCommand36({
6849
6901
  meta: {
6850
6902
  name: "update",
6851
6903
  description: `Stage changes to an existing campaign (budget, bid, targeting, flags, schedule, status). ${STAGED_NOTE}
6904
+ Pass a li_temp_* ref to amend a campaign staged in this chat \u2014 fields merge into the staged create.
6852
6905
  Example: baker ads linkedin campaigns update 123456 --daily-budget 100 --currency EUR --audience-expansion off`
6853
6906
  },
6854
6907
  args: {
@@ -6947,7 +7000,9 @@ var CREATIVE_FORMATS = [
6947
7000
  "carousel",
6948
7001
  "conversation",
6949
7002
  "tla",
6950
- "jobs"
7003
+ "jobs",
7004
+ "event",
7005
+ "article"
6951
7006
  ];
6952
7007
  var creativesCreateCommand = defineCommand36({
6953
7008
  meta: {
@@ -6974,8 +7029,12 @@ Examples:
6974
7029
  cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026" },
6975
7030
  "cta-label": { type: "string", description: "Spotlight custom CTA label (\u226418 chars)" },
6976
7031
  "post-urn": { type: "string", description: "TLA: existing post to sponsor (urn:li:share|ugcPost:*)" },
7032
+ "event-urn": { type: "string", description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)" },
7033
+ "article-url": { type: "string", description: "Article ads: https URL of the article/newsletter to sponsor" },
7034
+ "thumbnail-image-id": { type: "string", description: "Article ads: Baker image id for the link thumbnail" },
7035
+ "thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail" },
6977
7036
  "intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
6978
- title: { type: "string", description: "Media title" },
7037
+ title: { type: "string", description: "Media title (image/video) or article title" },
6979
7038
  file: { type: "string", description: "JSON file with the full content object; flags override" }
6980
7039
  },
6981
7040
  run: async ({ args }) => {
@@ -7001,7 +7060,12 @@ Examples:
7001
7060
  cta: args.cta ? String(args.cta).toUpperCase() : void 0,
7002
7061
  ctaLabel: args["cta-label"],
7003
7062
  postUrn: args["post-urn"],
7004
- mediaTitle: args.title
7063
+ eventUrn: args["event-urn"],
7064
+ source: args["article-url"],
7065
+ thumbnailImageId: args["thumbnail-image-id"],
7066
+ thumbnailUrn: args["thumbnail-urn"],
7067
+ mediaTitle: args.title,
7068
+ title: args.title
7005
7069
  });
7006
7070
  await stageOp({
7007
7071
  kind: "creative.create",
@@ -7014,31 +7078,94 @@ Examples:
7014
7078
  });
7015
7079
  }
7016
7080
  });
7081
+ var CREATIVE_AMEND_CONTENT_FLAGS = [
7082
+ ["intro", "commentary"],
7083
+ ["headline", "headline"],
7084
+ ["description", "description"],
7085
+ ["landing-url", "landingUrl"],
7086
+ ["cta-label", "ctaLabel"],
7087
+ ["image-id", "imageId"],
7088
+ ["image-urn", "imageUrn"],
7089
+ ["video-id", "videoId"],
7090
+ ["video-urn", "videoUrn"],
7091
+ ["logo-image-id", "logoImageId"],
7092
+ ["thumbnail-image-id", "thumbnailImageId"],
7093
+ ["thumbnail-urn", "thumbnailUrn"],
7094
+ ["title", "title"]
7095
+ ];
7096
+ var CREATIVE_LIVE_UPDATE_KEYS = /* @__PURE__ */ new Set(["headline", "landingUrl", "cta"]);
7097
+ function creativeContentPatch(args) {
7098
+ const patch = {};
7099
+ for (const [flag, key] of CREATIVE_AMEND_CONTENT_FLAGS) {
7100
+ if (args[flag] !== void 0) {
7101
+ patch[key] = args[flag];
7102
+ }
7103
+ }
7104
+ if (args.cta !== void 0) {
7105
+ patch.cta = String(args.cta).toUpperCase();
7106
+ }
7107
+ return patch;
7108
+ }
7017
7109
  var creativesUpdateCommand = defineCommand36({
7018
7110
  meta: {
7019
7111
  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`
7112
+ 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).
7114
+ Examples:
7115
+ baker ads linkedin creatives update urn:li:sponsoredCreative:789 --intended-status PAUSED
7116
+ baker ads linkedin creatives update li_temp_a1b2 --headline "Sharper headline" --image-id <bakerImageId>`
7022
7117
  },
7023
7118
  args: {
7024
- id: { type: "positional", description: "Creative id or URN", required: true },
7119
+ id: { type: "positional", description: "Creative id/URN, or li_temp_* ref staged in this chat", required: true },
7025
7120
  ...accountArgs,
7026
7121
  "intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT" },
7027
7122
  name: { type: "string", description: "New creative name" },
7028
- file: { type: "string", description: "JSON file with fields to change" }
7123
+ intro: { type: "string", description: "Amend only: commentary / intro text" },
7124
+ headline: { type: "string", description: "New headline" },
7125
+ description: { type: "string", description: "Amend only: description (text/spotlight ads)" },
7126
+ "landing-url": { type: "string", description: "New https destination URL" },
7127
+ cta: { type: "string", description: "New CTA (LEARN_MORE|REQUEST_DEMO|\u2026)" },
7128
+ "cta-label": { type: "string", description: "Amend only: spotlight custom CTA label" },
7129
+ "image-id": { type: "string", description: "Amend only: Baker image library id" },
7130
+ "image-urn": { type: "string", description: "Amend only: existing urn:li:image:*" },
7131
+ "video-id": { type: "string", description: "Amend only: Baker video library id" },
7132
+ "video-urn": { type: "string", description: "Amend only: existing urn:li:video:*" },
7133
+ "logo-image-id": { type: "string", description: "Amend only: Baker image id for the logo" },
7134
+ "thumbnail-image-id": { type: "string", description: "Amend only: Baker image id for the article thumbnail" },
7135
+ "thumbnail-urn": { type: "string", description: "Amend only: existing urn:li:image:* thumbnail" },
7136
+ title: { type: "string", description: "Amend only: media or article title" },
7137
+ file: { type: "string", description: "JSON file with fields to change; flags override file keys" }
7029
7138
  },
7030
7139
  run: async ({ args }) => {
7031
7140
  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
7141
+ const target = requireTarget(args, "creative");
7142
+ const file = loadJsonFileArg(args.file);
7143
+ const contentPatch = creativeContentPatch(args);
7144
+ const intendedStatus = args["intended-status"] ? String(args["intended-status"]).toUpperCase() : void 0;
7145
+ if (target.startsWith("li_temp_")) {
7146
+ const fileContent = file.content !== null && typeof file.content === "object" && !Array.isArray(file.content) ? file.content : {};
7147
+ const content = { ...fileContent, ...contentPatch };
7148
+ const payload2 = mergePayload(file, {
7149
+ intendedStatus,
7150
+ content: Object.keys(content).length > 0 ? content : void 0
7151
+ });
7152
+ await stageOp({ kind: "creative.update", accountId, target, payload: payload2 });
7153
+ return;
7154
+ }
7155
+ const immutable = Object.keys(contentPatch).filter((key) => !CREATIVE_LIVE_UPDATE_KEYS.has(key));
7156
+ if (immutable.length > 0) {
7157
+ 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\``
7159
+ );
7160
+ }
7161
+ const payload = mergePayload(file, {
7162
+ intendedStatus,
7163
+ name: args.name,
7164
+ headline: contentPatch.headline,
7165
+ landingUrl: contentPatch.landingUrl,
7166
+ cta: contentPatch.cta
7041
7167
  });
7168
+ await stageOp({ kind: "creative.update", accountId, target, payload });
7042
7169
  }
7043
7170
  });
7044
7171
  var AUDIENCE_TYPES2 = ["company-list", "user-list", "retargeting", "engagement"];