@koda-sl/baker-cli 0.379.0 → 0.380.0

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
@@ -942,6 +942,8 @@ baker ads meta custom-conversions --account-id act_123 --full # raw rule JSON,
942
942
 
943
943
  Meta writes mirror the Google/LinkedIn model: they **never hit the Graph API when you run them**. Each op is validated at stage time (field shapes, budget rules, page/Instagram identity ownership, CBO/ABO exclusivity, status-transition legality), shows in the dashboard chat as a pending Meta change, and applies only when the chat is published. Meta has no atomic batch mutate, so ops apply **sequentially** on publish — one failure skips its dependents but the publish completes.
944
944
 
945
+ Staging also refuses what LinkedIn would reject at publish: a campaign group created `PAUSED` (only `DRAFT`/`ACTIVE`), a group `--total-budget` without `--end`, a `PAUSED` campaign in a `DRAFT` group, archiving a `DRAFT` ad, and a library image that is not JPG, PNG or GIF. Every campaign carries `--political-intent` (`NOT_DECLARED` by default; `NOT_POLITICAL` or `POLITICAL` are the advertiser's declaration). A write to an ad account LinkedIn has not enabled for Baker fails with a message saying so.
946
+
945
947
  **That publish reaches the real ad account for every company** — Meta writes are generally available, with no per-company flag. Discarding the chat before it completes is what undoes a staged change.
946
948
 
947
949
  Chain a full ad from scratch with `meta_temp_*` refs (each create returns one):
@@ -1107,7 +1109,7 @@ baker ads linkedin bid-pricing --account-id 503001492 \
1107
1109
  - URN normalization: every URN field becomes `{type, id, urn}` so rows are self-describing.
1108
1110
  - Wide windows (>180d) auto-segment into 30-day chunks and combine, since `q=analytics` caps at 15,000 rows with no pagination.
1109
1111
 
1110
- **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set.
1112
+ **Listings** (`campaign-groups`/`campaigns`/`creatives`) default to `ACTIVE` only — pass `--all-statuses` to widen, or `--statuses ACTIVE,PAUSED` for a custom set. Only campaigns can be `COMPLETED`; each listing's `--help` names the statuses it accepts, and any other value is refused with `INVALID_STATUS` and that list before LinkedIn is called.
1111
1113
 
1112
1114
  **`creatives` is compact by default** — `id`, `urn`, `campaign`, `intendedStatus`, `isServing`, `reviewStatus`, `format`, and the resolved destination. The destination is read out of the format-specific `content` block for you: direct-content ads (text, spotlight) report a `landingUrl`; post-based ads (image, video, carousel, document, article, event, TLA) report a `postUrn`, because their destination lives in the sponsored post and is not reachable through the ads API. Pass `--fields id,content.textAd.landingPage` for specific dotted paths, or `--full` for the complete raw records.
1113
1115
 
@@ -1168,7 +1170,7 @@ Write commands never call the LinkedIn API at stage time. Each stages an op on t
1168
1170
 
1169
1171
  ```bash
1170
1172
  # Chain creates via li_temp_* refs — group → campaign → creative in one chat
1171
- baker ads linkedin campaign-groups create --name "Q3 ABM" --total-budget 5000 --currency EUR # → li_temp_x1
1173
+ baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-08-01 --end 2026-10-31 --total-budget 5000 --currency EUR # → li_temp_x1
1172
1174
  baker ads linkedin campaigns create --name "ABM Tier-1" --group li_temp_x1 \
1173
1175
  --objective WEBSITE_VISIT --type SPONSORED_UPDATES --cost-type CPC \
1174
1176
  --bid 8.50 --daily-budget 75 --currency EUR --locale en_US --targeting-file targeting.json # → li_temp_x2
package/dist/cli.js CHANGED
@@ -975,6 +975,9 @@ var OBJECTIVE_CAMPAIGN_TYPES = {
975
975
  ENGAGEMENT: ["SPONSORED_UPDATES", "DYNAMIC"]
976
976
  };
977
977
  var STAGEABLE_CREATE_STATUSES = ["DRAFT", "ACTIVE", "PAUSED"];
978
+ var CAMPAIGN_GROUP_CREATE_STATUSES = ["DRAFT", "ACTIVE"];
979
+ var CAMPAIGN_GROUP_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED", "CANCELED", "DRAFT"];
980
+ var POLITICAL_INTENTS = ["NOT_DECLARED", "NOT_POLITICAL", "POLITICAL"];
978
981
  var CREATIVE_INTENDED_STATUSES = ["ACTIVE", "PAUSED", "DRAFT"];
979
982
  var CTA_TYPES = [
980
983
  "APPLY",
@@ -1105,17 +1108,26 @@ var entityStatusSchema = z3.enum(["ACTIVE", "PAUSED", "ARCHIVED", "CANCELED", "D
1105
1108
  function currencyMinimums(currencyCode) {
1106
1109
  return CURRENCY_MINIMUMS[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM;
1107
1110
  }
1108
- var campaignGroupCreateSchema = z3.object({
1111
+ var campaignGroupFieldsSchema = z3.object({
1109
1112
  name: z3.string().min(1).max(LINKEDIN_LIMITS.campaignGroup.nameMax),
1110
1113
  runSchedule: runScheduleSchema,
1111
1114
  totalBudget: moneySchema.optional(),
1112
- status: stageableStatusSchema.default("DRAFT")
1115
+ status: z3.enum(CAMPAIGN_GROUP_CREATE_STATUSES).default("DRAFT")
1116
+ });
1117
+ var campaignGroupCreateSchema = campaignGroupFieldsSchema.superRefine((p, ctx) => {
1118
+ if (p.totalBudget && !p.runSchedule.end) {
1119
+ ctx.addIssue({
1120
+ code: "custom",
1121
+ path: ["runSchedule", "end"],
1122
+ message: "a group with totalBudget needs runSchedule.end"
1123
+ });
1124
+ }
1113
1125
  });
1114
1126
  var campaignGroupUpdateSchema = z3.object({
1115
1127
  name: z3.string().min(1).max(LINKEDIN_LIMITS.campaignGroup.nameMax).optional(),
1116
1128
  runSchedule: runScheduleSchema.optional(),
1117
1129
  totalBudget: moneySchema.optional(),
1118
- status: entityStatusSchema.optional()
1130
+ status: z3.enum(CAMPAIGN_GROUP_STATUSES).optional()
1119
1131
  }).refine((p) => Object.values(p).some((val) => val !== void 0), "update needs at least one field");
1120
1132
  var campaignFields = {
1121
1133
  name: z3.string().min(1).max(LINKEDIN_LIMITS.campaign.nameMax),
@@ -1134,6 +1146,7 @@ var campaignFields = {
1134
1146
  connectedTelevisionOnly: z3.boolean().default(false),
1135
1147
  creativeSelection: z3.enum(CREATIVE_SELECTIONS).optional(),
1136
1148
  associatedEntity: z3.string().regex(ORGANIZATION_URN_REGEX).optional(),
1149
+ politicalIntent: z3.enum(POLITICAL_INTENTS).default("NOT_DECLARED"),
1137
1150
  status: stageableStatusSchema.default("DRAFT")
1138
1151
  };
1139
1152
  function validateCampaignBudgets(p, ctx, { requireBudget }) {
@@ -1191,6 +1204,7 @@ var campaignUpdateSchema = z3.object({
1191
1204
  offsiteDeliveryEnabled: z3.boolean().optional(),
1192
1205
  connectedTelevisionOnly: z3.boolean().optional(),
1193
1206
  creativeSelection: z3.enum(CREATIVE_SELECTIONS).optional(),
1207
+ politicalIntent: z3.enum(POLITICAL_INTENTS).optional(),
1194
1208
  status: entityStatusSchema.optional(),
1195
1209
  // Create-only on LinkedIn — a live campaign can't change these in place.
1196
1210
  // Accepted here only so `draft amend` can carry them into a staged CREATE
@@ -1816,6 +1830,17 @@ var linkedinDraftOpInputSchema = z3.discriminatedUnion("kind", [
1816
1830
  updateOp("leadForm.update", leadFormUpdateSchema)
1817
1831
  ]);
1818
1832
 
1833
+ // ../api/src/ads-linkedin/search-statuses.ts
1834
+ var LINKEDIN_SEARCH_STATUSES = {
1835
+ campaignGroups: ["ACTIVE", "PAUSED", "ARCHIVED", "CANCELED", "DRAFT", "PENDING_DELETION", "REMOVED"],
1836
+ campaigns: ["ACTIVE", "PAUSED", "ARCHIVED", "COMPLETED", "CANCELED", "DRAFT", "PENDING_DELETION", "REMOVED"],
1837
+ creatives: ["ACTIVE", "PAUSED", "ARCHIVED", "CANCELED", "DRAFT", "PENDING_DELETION", "REMOVED"]
1838
+ };
1839
+ function unsupportedLinkedinSearchStatuses(level, statuses) {
1840
+ const accepted = LINKEDIN_SEARCH_STATUSES[level];
1841
+ return statuses.filter((status) => !accepted.includes(status));
1842
+ }
1843
+
1819
1844
  // ../api/src/ads-linkedin/wire.ts
1820
1845
  import { z as z7 } from "zod";
1821
1846
 
@@ -20066,10 +20091,6 @@ Full guide: __tooling__/docs/tools/baker/ads-google.md`
20066
20091
  // src/commands/ads/linkedin/index.ts
20067
20092
  import { defineCommand as defineCommand53 } from "citty";
20068
20093
 
20069
- // src/commands/ads/linkedin/write-shared.ts
20070
- import { createHash } from "crypto";
20071
- import { readFileSync as readFileSync5 } from "fs";
20072
-
20073
20094
  // src/commands/ads/linkedin/shared.ts
20074
20095
  var DAY_MS2 = 864e5;
20075
20096
  function handleLinkedinError(err) {
@@ -20162,18 +20183,48 @@ function daysAgoIso2(days) {
20162
20183
  function csvOrJson2(args5) {
20163
20184
  return args5.output ?? "json";
20164
20185
  }
20165
- function resolveStatusFilter(args5) {
20186
+ function statusesArgDescription(level) {
20187
+ return `CSV of statuses (${LINKEDIN_SEARCH_STATUSES[level].join(",")})`;
20188
+ }
20189
+ function statusFilterError(level, statuses) {
20190
+ const unsupported = unsupportedLinkedinSearchStatuses(level, statuses);
20191
+ if (unsupported.length === 0) {
20192
+ return null;
20193
+ }
20194
+ const accepted = LINKEDIN_SEARCH_STATUSES[level].join(",");
20195
+ return {
20196
+ ok: false,
20197
+ error: {
20198
+ code: "INVALID_STATUS",
20199
+ message: `LinkedIn does not accept status ${unsupported.join(", ")} for this listing.`,
20200
+ fix: {
20201
+ action: "retry_with_flag",
20202
+ explanation: `Re-run with --statuses from ${accepted}, or --all-statuses to drop the filter.`
20203
+ },
20204
+ retryable: false
20205
+ }
20206
+ };
20207
+ }
20208
+ function resolveStatusFilter(args5, level) {
20166
20209
  if (args5["all-statuses"]) {
20167
20210
  return void 0;
20168
20211
  }
20169
20212
  const explicit = args5.statuses;
20170
- if (explicit) {
20171
- return explicit.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
20213
+ if (!explicit) {
20214
+ return ["ACTIVE"];
20172
20215
  }
20173
- return ["ACTIVE"];
20216
+ const statuses = explicit.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
20217
+ const error = statusFilterError(level, statuses);
20218
+ if (error) {
20219
+ writeAdsJson(error);
20220
+ process.exit(1);
20221
+ }
20222
+ return statuses;
20174
20223
  }
20175
20224
 
20176
20225
  // src/commands/ads/linkedin/write-shared.ts
20226
+ import { createHash } from "crypto";
20227
+ import { readFileSync as readFileSync5 } from "fs";
20177
20228
  var TOTAL_BUDGET_HELP = "Lifetime budget \u2014 on something already running it must exceed spend to date, or LinkedIn rejects it";
20178
20229
  function bareAccountId(args5) {
20179
20230
  const raw = resolveAccountIdArg2(args5);
@@ -20500,7 +20551,7 @@ registerSchema({
20500
20551
  "account-id": { type: "string", description: "Account ID", required: false },
20501
20552
  "account-urn": { type: "string", description: "Alias for --account-id", required: false },
20502
20553
  "all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
20503
- statuses: { type: "string", description: "CSV of statuses", required: false },
20554
+ statuses: { type: "string", description: statusesArgDescription("campaignGroups"), required: false },
20504
20555
  limit: { type: "string", description: "Max rows (default 500)", required: false },
20505
20556
  "no-cache": NO_CACHE_ARG,
20506
20557
  output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
@@ -20514,7 +20565,7 @@ registerSchema({
20514
20565
  "account-urn": { type: "string", description: "Alias", required: false },
20515
20566
  "campaign-group-id": { type: "string", description: "Filter by campaign group", required: false },
20516
20567
  "all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
20517
- statuses: { type: "string", description: "CSV of statuses", required: false },
20568
+ statuses: { type: "string", description: statusesArgDescription("campaigns"), required: false },
20518
20569
  limit: { type: "string", description: "Max rows", required: false },
20519
20570
  "no-cache": NO_CACHE_ARG,
20520
20571
  output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
@@ -20528,7 +20579,7 @@ registerSchema({
20528
20579
  "account-urn": { type: "string", description: "Alias", required: false },
20529
20580
  "campaign-id": { type: "string", description: "Filter by campaign", required: false },
20530
20581
  "all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
20531
- statuses: { type: "string", description: "CSV of statuses", required: false },
20582
+ statuses: { type: "string", description: statusesArgDescription("creatives"), required: false },
20532
20583
  fields: {
20533
20584
  type: "string",
20534
20585
  description: "CSV of dotted paths to return instead of the compact row, e.g. id,content.textAd.landingPage",
@@ -20801,7 +20852,11 @@ registerSchema({
20801
20852
  description: "3-letter currency code (defaults to the ad account currency)",
20802
20853
  required: false
20803
20854
  },
20804
- status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
20855
+ status: {
20856
+ type: "string",
20857
+ description: "DRAFT|ACTIVE (default DRAFT) \u2014 LinkedIn refuses a new group created PAUSED",
20858
+ required: false
20859
+ },
20805
20860
  file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
20806
20861
  }
20807
20862
  });
@@ -20859,7 +20914,16 @@ registerSchema({
20859
20914
  ctv: { type: "string", description: "Connected TV on|off (default off)", required: false },
20860
20915
  "creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
20861
20916
  "associated-entity": { type: "string", description: "Organization URN", required: false },
20862
- status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
20917
+ "political-intent": {
20918
+ type: "string",
20919
+ description: "NOT_DECLARED (default)|NOT_POLITICAL|POLITICAL \u2014 the advertiser's declaration; NOT_POLITICAL only after the user confirms their ads are not political advertising",
20920
+ required: false
20921
+ },
20922
+ status: {
20923
+ type: "string",
20924
+ description: "DRAFT|ACTIVE|PAUSED (default DRAFT) \u2014 PAUSED needs a group that is not DRAFT",
20925
+ required: false
20926
+ },
20863
20927
  file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
20864
20928
  }
20865
20929
  });
@@ -20887,6 +20951,11 @@ registerSchema({
20887
20951
  lan: { type: "string", description: "on|off", required: false },
20888
20952
  ctv: { type: "string", description: "on|off", required: false },
20889
20953
  "creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
20954
+ "political-intent": {
20955
+ type: "string",
20956
+ description: "NOT_DECLARED (default)|NOT_POLITICAL|POLITICAL \u2014 the advertiser's declaration; NOT_POLITICAL only after the user confirms their ads are not political advertising",
20957
+ required: false
20958
+ },
20890
20959
  status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
20891
20960
  file: { type: "string", description: "JSON file with fields to change", required: false }
20892
20961
  }
@@ -21282,7 +21351,8 @@ var campaignGroupsCreateCommand = defineCommand33({
21282
21351
  meta: {
21283
21352
  name: "create",
21284
21353
  description: `Stage a new campaign group. ${STAGED_NOTE}
21285
- Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-07-10 --total-budget 5000 --currency EUR`
21354
+ A group can't be created PAUSED, and a --total-budget needs an --end.
21355
+ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-07-10 --end 2026-09-30 --total-budget 5000 --currency EUR`
21286
21356
  },
21287
21357
  args: {
21288
21358
  ...accountArgs,
@@ -21291,7 +21361,10 @@ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-
21291
21361
  end: { type: "string", description: "Run schedule end" },
21292
21362
  "total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
21293
21363
  currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
21294
- status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
21364
+ status: {
21365
+ type: "string",
21366
+ description: "DRAFT|ACTIVE (default DRAFT) \u2014 LinkedIn refuses a new group created PAUSED; pause it once it exists"
21367
+ },
21295
21368
  file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
21296
21369
  },
21297
21370
  run: async ({ args: args5 }) => {
@@ -21357,9 +21430,11 @@ function campaignPayloadFromFlags(args5) {
21357
21430
  connectedTelevisionOnly: parseOnOffFlag(args5.ctv, "--ctv"),
21358
21431
  creativeSelection: args5["creative-selection"] ? String(args5["creative-selection"]).toUpperCase() : void 0,
21359
21432
  associatedEntity: args5["associated-entity"],
21433
+ politicalIntent: args5["political-intent"] ? String(args5["political-intent"]).toUpperCase() : void 0,
21360
21434
  status: args5.status ? String(args5.status).toUpperCase() : void 0
21361
21435
  };
21362
21436
  }
21437
+ var POLITICAL_INTENT_HELP = `NOT_DECLARED (default)|NOT_POLITICAL|POLITICAL \u2014 the advertiser's own declaration. Pass NOT_POLITICAL only after the user confirms: "I confirm this is not political advertising. None of my ads qualify as political advertising under the law of the targeted countries, including EU law for ads targeted to the EU."`;
21363
21438
  var campaignFlagArgs = {
21364
21439
  ...accountArgs,
21365
21440
  name: { type: "string", description: "Campaign name (\u2264255 chars)" },
@@ -21380,7 +21455,8 @@ var campaignFlagArgs = {
21380
21455
  ctv: { type: "string", description: "Connected TV on|off" },
21381
21456
  "creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN" },
21382
21457
  "associated-entity": { type: "string", description: "Organization URN" },
21383
- status: { type: "string", description: "DRAFT|ACTIVE|PAUSED" },
21458
+ "political-intent": { type: "string", description: POLITICAL_INTENT_HELP },
21459
+ status: { type: "string", description: "DRAFT|ACTIVE|PAUSED \u2014 PAUSED needs a group that is not DRAFT" },
21384
21460
  file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
21385
21461
  };
21386
21462
  var campaignsCreateCommand = defineCommand33({
@@ -22897,7 +22973,7 @@ var listArgs = {
22897
22973
  "account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
22898
22974
  "account-urn": { type: "string", description: "Alias for --account-id" },
22899
22975
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter" },
22900
- statuses: { type: "string", description: "CSV of statuses (ACTIVE,PAUSED,ARCHIVED,COMPLETED,DRAFT)" },
22976
+ statuses: { type: "string", description: statusesArgDescription("campaignGroups") },
22901
22977
  limit: { type: "string", description: "Max rows (default: 500)" },
22902
22978
  "no-cache": NO_CACHE_ARG,
22903
22979
  output: { type: "string", description: "json|csv|jsonl|md", default: "json" }
@@ -22912,7 +22988,7 @@ var campaignGroupsListCommand = defineCommand40({
22912
22988
  const accountId = resolveAccountIdArg2(args5);
22913
22989
  try {
22914
22990
  const params = { "account-id": accountId };
22915
- const statuses = resolveStatusFilter(args5);
22991
+ const statuses = resolveStatusFilter(args5, "campaignGroups");
22916
22992
  if (statuses) params.statuses = statuses.join(",");
22917
22993
  if (args5.limit) params.limit = String(args5.limit);
22918
22994
  if (args5["no-cache"]) params["skip-cache"] = "true";
@@ -22963,7 +23039,7 @@ var listArgs2 = {
22963
23039
  "account-urn": { type: "string", description: "Alias for --account-id" },
22964
23040
  "campaign-group-id": { type: "string", description: "Filter by campaign group" },
22965
23041
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter" },
22966
- statuses: { type: "string", description: "CSV of statuses" },
23042
+ statuses: { type: "string", description: statusesArgDescription("campaigns") },
22967
23043
  limit: { type: "string", description: "Max rows (default: 500)" },
22968
23044
  "no-cache": NO_CACHE_ARG,
22969
23045
  output: { type: "string", description: "json|csv|jsonl|md", default: "json" }
@@ -22978,7 +23054,7 @@ var campaignsListCommand = defineCommand41({
22978
23054
  const accountId = resolveAccountIdArg2(args5);
22979
23055
  try {
22980
23056
  const params = { "account-id": accountId };
22981
- const statuses = resolveStatusFilter(args5);
23057
+ const statuses = resolveStatusFilter(args5, "campaigns");
22982
23058
  if (statuses) params.statuses = statuses.join(",");
22983
23059
  if (args5["campaign-group-id"]) params["campaign-group-id"] = String(args5["campaign-group-id"]);
22984
23060
  if (args5.limit) params.limit = String(args5.limit);
@@ -23328,7 +23404,7 @@ var listArgs3 = {
23328
23404
  "account-urn": { type: "string", description: "Alias for --account-id" },
23329
23405
  "campaign-id": { type: "string", description: "Filter by campaign ID" },
23330
23406
  "all-statuses": { type: "boolean", description: "Drop the default ACTIVE filter" },
23331
- statuses: { type: "string", description: "CSV of intended statuses (ACTIVE,PAUSED,DRAFT,ARCHIVED)" },
23407
+ statuses: { type: "string", description: statusesArgDescription("creatives") },
23332
23408
  fields: {
23333
23409
  type: "string",
23334
23410
  description: "CSV of dotted paths to return instead of the compact row, e.g. id,content.textAd.landingPage"
@@ -23348,7 +23424,7 @@ var creativesListCommand = defineCommand44({
23348
23424
  const accountId = resolveAccountIdArg2(args5);
23349
23425
  try {
23350
23426
  const params = { "account-id": accountId };
23351
- const statuses = resolveStatusFilter(args5);
23427
+ const statuses = resolveStatusFilter(args5, "creatives");
23352
23428
  if (statuses) params.statuses = statuses.join(",");
23353
23429
  if (args5["campaign-id"]) params["campaign-id"] = String(args5["campaign-id"]);
23354
23430
  if (args5.limit) params.limit = String(args5.limit);