@koda-sl/baker-cli 0.109.0-dev.2acd65459 → 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"` |
@@ -1029,7 +1029,7 @@ Notes:
1029
1029
 
1030
1030
  - All write commands take `--file <json>` payloads; explicit flags override file keys. `baker schema ads.linkedin.campaigns.create` for exact args.
1031
1031
  - Money flags (`--bid`, `--daily-budget`, `--total-budget`) require `--currency`.
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. 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`.
1033
1033
  - Lead forms are file-first (`lead-forms create --file form.json`): name, headline (≤60), privacyPolicyUrl, questions[] (≤12; playbook: ≤4 for completion).
1034
1034
 
1035
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,
@@ -1263,10 +1267,68 @@ var carouselCreativeSchema = z3.object({
1263
1267
  })
1264
1268
  ).min(LINKEDIN_LIMITS.creative.carouselCardsMin).max(LINKEDIN_LIMITS.creative.carouselCardsMax)
1265
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
+ }
1266
1310
  var conversationCreativeSchema = z3.object({
1267
1311
  format: z3.literal("conversation"),
1268
1312
  name: z3.string().max(255).optional(),
1269
- 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
+ }
1270
1332
  });
1271
1333
  var tlaCreativeSchema = z3.object({
1272
1334
  format: z3.literal("tla"),
@@ -1323,10 +1385,9 @@ var creativeUpdateSchema = z3.object({
1323
1385
  landingUrl: httpsUrlSchema.optional(),
1324
1386
  cta: z3.enum(CTA_TYPES).optional(),
1325
1387
  /**
1326
- * Amend-only content patch: valid when the target is a li_temp_* staged
1327
- * create merged into the staged content and re-validated against the
1328
- * full creative schema. Creatives already on LinkedIn have immutable
1329
- * 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.
1330
1391
  */
1331
1392
  content: z3.record(z3.string(), z3.unknown()).optional()
1332
1393
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
@@ -3203,12 +3264,12 @@ function buildCommand(query, ctx) {
3203
3264
  return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
3204
3265
  }
3205
3266
  var CORRECTION_RULES = [
3206
- // 1. keyword.text → ad_group_criterion.keyword.text
3267
+ // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
3207
3268
  {
3208
- matchQuery: /\bkeyword\.text\b/,
3269
+ matchQuery: /(?<![\w.])keyword\.text\b/,
3209
3270
  matchApiError: /keyword\.text/i,
3210
3271
  fix: (ctx) => {
3211
- 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");
3212
3273
  return {
3213
3274
  action: "retry_with_modified_query",
3214
3275
  correctedCommand: buildCommand(corrected, ctx),
@@ -3216,12 +3277,15 @@ var CORRECTION_RULES = [
3216
3277
  };
3217
3278
  }
3218
3279
  },
3219
- // 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)
3220
3281
  {
3221
- matchQuery: /\bkeyword\.match_type\b/,
3282
+ matchQuery: /(?<![\w.])keyword\.match_type\b/,
3222
3283
  matchApiError: /keyword\.match_type/i,
3223
3284
  fix: (ctx) => {
3224
- 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
+ );
3225
3289
  return {
3226
3290
  action: "retry_with_modified_query",
3227
3291
  correctedCommand: buildCommand(corrected, ctx),
@@ -4573,12 +4637,12 @@ function buildCommand2(query, customerId) {
4573
4637
  function applyAutoFixes(query, limit) {
4574
4638
  let corrected = query;
4575
4639
  const warnings = [];
4576
- if (/\bkeyword\.text\b/.test(corrected) && !/ad_group_criterion\.keyword\.text/.test(corrected)) {
4577
- 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");
4578
4642
  warnings.push({ code: "FIELD_RENAMED", message: "keyword.text \u2192 ad_group_criterion.keyword.text" });
4579
4643
  }
4580
- if (/\bkeyword\.match_type\b/.test(corrected) && !/ad_group_criterion\.keyword\.match_type/.test(corrected)) {
4581
- 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");
4582
4646
  warnings.push({ code: "FIELD_RENAMED", message: "keyword.match_type \u2192 ad_group_criterion.keyword.match_type" });
4583
4647
  }
4584
4648
  if (/shopping_performance_view\.product_\w+/.test(corrected)) {
@@ -5492,6 +5556,7 @@ registerSchema({
5492
5556
  ...writeAccountArgs,
5493
5557
  name: { type: "string", description: "New name", required: false },
5494
5558
  objective: { type: "string", description: "New objective", required: false },
5559
+ "cost-type": { type: "string", description: "CPC|CPM|CPV", required: false },
5495
5560
  bid: { type: "string", description: "New manual bid", required: false },
5496
5561
  "daily-budget": { type: "string", description: "New daily budget", required: false },
5497
5562
  "total-budget": { type: "string", description: "New lifetime budget", required: false },
@@ -5592,7 +5657,7 @@ registerSchema({
5592
5657
  });
5593
5658
  registerSchema({
5594
5659
  command: "ads.linkedin.creatives.create",
5595
- 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.",
5596
5661
  args: {
5597
5662
  ...writeAccountArgs,
5598
5663
  campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
@@ -5613,8 +5678,24 @@ registerSchema({
5613
5678
  cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026", required: false },
5614
5679
  "cta-label": { type: "string", description: "Spotlight CTA label (\u226418 chars)", required: false },
5615
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 },
5616
5697
  "intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
5617
- title: { type: "string", description: "Media title", required: false },
5698
+ title: { type: "string", description: "Media title (image/video) or article title", required: false },
5618
5699
  file: { type: "string", description: "JSON file with the full content object", required: false }
5619
5700
  }
5620
5701
  });
@@ -5631,15 +5712,18 @@ registerSchema({
5631
5712
  description: { type: "string", description: "Description (text/spotlight)", required: false },
5632
5713
  "landing-url": { type: "string", description: "New https destination URL", required: false },
5633
5714
  cta: { type: "string", description: "New CTA", required: false },
5715
+ "cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
5634
5716
  "image-id": { type: "string", description: "Baker image library id", required: false },
5635
5717
  "image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
5636
5718
  "video-id": { type: "string", description: "Baker video library id", required: false },
5719
+ "video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
5637
5720
  "logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
5638
5721
  "thumbnail-image-id": {
5639
5722
  type: "string",
5640
5723
  description: "Baker image id for the article thumbnail",
5641
5724
  required: false
5642
5725
  },
5726
+ "thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail", required: false },
5643
5727
  title: { type: "string", description: "Media or article title", required: false },
5644
5728
  file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
5645
5729
  }
@@ -7112,7 +7196,7 @@ var creativesCreateCommand = defineCommand36({
7112
7196
  meta: {
7113
7197
  name: "create",
7114
7198
  description: `Stage a new creative (ad). ${STAGED_NOTE}
7115
- 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).
7116
7200
  Examples:
7117
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
7118
7202
  baker ads linkedin creatives create --campaign 123456 --format tla --post-urn urn:li:ugcPost:789`