@koda-sl/baker-cli 0.169.2 → 0.170.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 +13 -2
- package/dist/cli.js +1809 -1746
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4866,6 +4866,19 @@ var ASSET_FIELD_TYPES = [
|
|
|
4866
4866
|
"MARKETING_IMAGE",
|
|
4867
4867
|
"BUSINESS_NAME"
|
|
4868
4868
|
];
|
|
4869
|
+
var ASSET_GROUP_ASSET_FIELD_TYPES = [
|
|
4870
|
+
"MARKETING_IMAGE",
|
|
4871
|
+
"SQUARE_MARKETING_IMAGE",
|
|
4872
|
+
"PORTRAIT_MARKETING_IMAGE",
|
|
4873
|
+
"LOGO",
|
|
4874
|
+
"LANDSCAPE_LOGO",
|
|
4875
|
+
"YOUTUBE_VIDEO",
|
|
4876
|
+
"HEADLINE",
|
|
4877
|
+
"LONG_HEADLINE",
|
|
4878
|
+
"DESCRIPTION",
|
|
4879
|
+
"BUSINESS_NAME",
|
|
4880
|
+
"CALL_TO_ACTION_SELECTION"
|
|
4881
|
+
];
|
|
4869
4882
|
var DEVICE_TYPES = ["MOBILE", "TABLET", "DESKTOP", "CONNECTED_TV", "OTHER"];
|
|
4870
4883
|
var DAYS_OF_WEEK = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"];
|
|
4871
4884
|
var CAMPAIGN_OBJECTIVES = [
|
|
@@ -5242,9 +5255,15 @@ var assetGroupCreateSchema = z15.object({
|
|
|
5242
5255
|
businessName: z15.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.businessNameMax),
|
|
5243
5256
|
imageAssets: z15.array(refSchema).optional(),
|
|
5244
5257
|
squareImageAssets: z15.array(refSchema).optional(),
|
|
5258
|
+
portraitImageAssets: z15.array(refSchema).optional(),
|
|
5245
5259
|
logoAssets: z15.array(refSchema).optional(),
|
|
5246
5260
|
status: z15.enum(["ENABLED", "PAUSED"]).default("PAUSED")
|
|
5247
5261
|
});
|
|
5262
|
+
var assetGroupAssetAttachSchema = z15.object({
|
|
5263
|
+
assetGroup: refSchema,
|
|
5264
|
+
asset: refSchema,
|
|
5265
|
+
fieldType: z15.enum(ASSET_GROUP_ASSET_FIELD_TYPES)
|
|
5266
|
+
});
|
|
5248
5267
|
var assetGroupUpdateSchema = z15.object({
|
|
5249
5268
|
name: z15.string().min(1).max(GOOGLE_ADS_LIMITS.assetGroup.nameMax).optional(),
|
|
5250
5269
|
finalUrls: z15.array(httpsUrlSchema2).min(1).optional(),
|
|
@@ -5415,6 +5434,8 @@ var GOOGLE_DRAFT_OP_KINDS = [
|
|
|
5415
5434
|
"google.assetLink.detach",
|
|
5416
5435
|
"google.assetGroup.create",
|
|
5417
5436
|
"google.assetGroup.update",
|
|
5437
|
+
"google.assetGroupAsset.attach",
|
|
5438
|
+
"google.assetGroupAsset.detach",
|
|
5418
5439
|
"google.audience.create",
|
|
5419
5440
|
"google.audienceCriterion.attach",
|
|
5420
5441
|
"google.audienceCriterion.detach",
|
|
@@ -5472,6 +5493,8 @@ var googleDraftOpInputSchema = z15.discriminatedUnion("kind", [
|
|
|
5472
5493
|
targetOp("google.assetLink.detach"),
|
|
5473
5494
|
createOp2("google.assetGroup.create", assetGroupCreateSchema),
|
|
5474
5495
|
updateOp2("google.assetGroup.update", assetGroupUpdateSchema),
|
|
5496
|
+
createOp2("google.assetGroupAsset.attach", assetGroupAssetAttachSchema),
|
|
5497
|
+
targetOp("google.assetGroupAsset.detach"),
|
|
5475
5498
|
createOp2("google.audience.create", audienceCreateSchema2),
|
|
5476
5499
|
createOp2("google.audienceCriterion.attach", audienceCriterionAttachSchema),
|
|
5477
5500
|
targetOp("google.audienceCriterion.detach"),
|
|
@@ -9442,8 +9465,15 @@ var assetGroupsCommand = defineCommand30({
|
|
|
9442
9465
|
"long-headlines": { type: "string", description: "Comma-separated long headlines (1\u20135, \u226490 chars)" },
|
|
9443
9466
|
descriptions: { type: "string", description: "Comma-separated descriptions (2\u20135, \u226490 chars)" },
|
|
9444
9467
|
"business-name": { type: "string" },
|
|
9445
|
-
"image-assets": { type: "string", description: "Comma-separated marketing image asset refs" },
|
|
9446
|
-
"square-image-assets": {
|
|
9468
|
+
"image-assets": { type: "string", description: "Comma-separated marketing image asset refs (1.91:1)" },
|
|
9469
|
+
"square-image-assets": {
|
|
9470
|
+
type: "string",
|
|
9471
|
+
description: "Comma-separated square marketing image asset refs (1:1)"
|
|
9472
|
+
},
|
|
9473
|
+
"portrait-image-assets": {
|
|
9474
|
+
type: "string",
|
|
9475
|
+
description: "Comma-separated portrait marketing image asset refs (4:5)"
|
|
9476
|
+
},
|
|
9447
9477
|
"logo-assets": { type: "string", description: "Comma-separated logo image asset refs" },
|
|
9448
9478
|
status: { type: "string", description: "ENABLED | PAUSED (default PAUSED)" }
|
|
9449
9479
|
},
|
|
@@ -9462,6 +9492,7 @@ var assetGroupsCommand = defineCommand30({
|
|
|
9462
9492
|
businessName: args["business-name"],
|
|
9463
9493
|
imageAssets: listFlag(args["image-assets"]),
|
|
9464
9494
|
squareImageAssets: listFlag(args["square-image-assets"]),
|
|
9495
|
+
portraitImageAssets: listFlag(args["portrait-image-assets"]),
|
|
9465
9496
|
logoAssets: listFlag(args["logo-assets"]),
|
|
9466
9497
|
status: args.status
|
|
9467
9498
|
});
|
|
@@ -9487,7 +9518,40 @@ var assetGroupsCommand = defineCommand30({
|
|
|
9487
9518
|
});
|
|
9488
9519
|
await stageUpdate("google.assetGroup.update", customerId, target, payload);
|
|
9489
9520
|
}
|
|
9490
|
-
})
|
|
9521
|
+
}),
|
|
9522
|
+
attach: defineCommand30({
|
|
9523
|
+
meta: {
|
|
9524
|
+
name: "attach",
|
|
9525
|
+
description: "Add one existing asset to an existing asset group. Start here to refresh a live PMax group's creatives: attach the new image, then `detach` the old one \u2014 never detach first."
|
|
9526
|
+
},
|
|
9527
|
+
args: {
|
|
9528
|
+
...customerIdArg,
|
|
9529
|
+
...fileArg,
|
|
9530
|
+
"asset-group-ref": { type: "string", description: "Asset group ref (g_temp_*, id, or resource name)" },
|
|
9531
|
+
"asset-ref": { type: "string", description: "Asset ref (g_temp_*, id, or resource name)" },
|
|
9532
|
+
"field-type": {
|
|
9533
|
+
type: "string",
|
|
9534
|
+
description: `Slot the asset fills: ${ASSET_GROUP_ASSET_FIELD_TYPES.join(" | ")}`
|
|
9535
|
+
}
|
|
9536
|
+
},
|
|
9537
|
+
run: async ({ args }) => {
|
|
9538
|
+
const customerId = requireCustomerId(args);
|
|
9539
|
+
const payload = mergePayload(loadJsonFileArg(args.file), {
|
|
9540
|
+
assetGroup: args["asset-group-ref"],
|
|
9541
|
+
asset: args["asset-ref"],
|
|
9542
|
+
fieldType: args["field-type"]
|
|
9543
|
+
});
|
|
9544
|
+
await stageCreate("google.assetGroupAsset.attach", customerId, payload, [
|
|
9545
|
+
"Swapping a creative? Stage the matching `asset-groups detach` for the image this one replaces \u2014 an attach alone grows the group instead of refreshing it.",
|
|
9546
|
+
"The field type must match the image's real ratio (1.91:1 \u2192 MARKETING_IMAGE, 1:1 \u2192 SQUARE_MARKETING_IMAGE, 4:5 \u2192 PORTRAIT_MARKETING_IMAGE); Google rejects a mismatch at publish.",
|
|
9547
|
+
"Slots have ceilings as well as minimums: BUSINESS_NAME and CALL_TO_ACTION_SELECTION hold 1, LOGO 5, LONG_HEADLINE and DESCRIPTION 5, HEADLINE 15, YOUTUBE_VIDEO 15, images 20. Adding to a full slot is rejected exactly like removing below an empty one, so count what the group already holds (`SELECT asset_group_asset.field_type FROM asset_group_asset WHERE asset_group.id = \u2026`) and stage the detach alongside when it is full."
|
|
9548
|
+
]);
|
|
9549
|
+
}
|
|
9550
|
+
}),
|
|
9551
|
+
detach: statusCommand2("google.assetGroupAsset.detach", "asset group asset", [
|
|
9552
|
+
"Stage order is execution order \u2014 if you are replacing this image, its `asset-groups attach` must already be staged BEFORE this detach, or Google sees the group below its per-ratio minimum and rejects the removal.",
|
|
9553
|
+
"Pass the full composite resource name (customers/{cid}/assetGroupAssets/{assetGroupId}~{assetId}~{FIELD_TYPE}) \u2014 read it from `asset_group_asset.resource_name` via `ads google query`."
|
|
9554
|
+
])
|
|
9491
9555
|
}
|
|
9492
9556
|
});
|
|
9493
9557
|
function fileCreateCommand(name, kind, description, hints) {
|
|
@@ -9778,1905 +9842,1904 @@ Full guide: __tooling__/docs/tools/baker/ads-google.md`
|
|
|
9778
9842
|
// src/commands/ads/linkedin/index.ts
|
|
9779
9843
|
import { defineCommand as defineCommand51 } from "citty";
|
|
9780
9844
|
|
|
9781
|
-
// src/commands/ads/linkedin/
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
}
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9845
|
+
// src/commands/ads/linkedin/write-shared.ts
|
|
9846
|
+
import { createHash } from "crypto";
|
|
9847
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9848
|
+
|
|
9849
|
+
// src/commands/ads/linkedin/shared.ts
|
|
9850
|
+
var DAY_MS2 = 864e5;
|
|
9851
|
+
function handleLinkedinError(err) {
|
|
9852
|
+
if (err instanceof ApiError) {
|
|
9853
|
+
if (err.code === "UNAUTHORIZED") {
|
|
9854
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9855
|
+
}
|
|
9856
|
+
if (err.code === "NOT_FOUND") {
|
|
9857
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9858
|
+
}
|
|
9859
|
+
if (isNoAccountsSelectedError(err.code, err.message)) {
|
|
9860
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9861
|
+
}
|
|
9862
|
+
const explanation = explainLinkedinError(err);
|
|
9863
|
+
const envelope = {
|
|
9864
|
+
ok: false,
|
|
9865
|
+
error: {
|
|
9866
|
+
code: err.code,
|
|
9867
|
+
message: err.message,
|
|
9868
|
+
fix: {
|
|
9869
|
+
action: explanation.action,
|
|
9870
|
+
explanation: explanation.message,
|
|
9871
|
+
correctedCommand: explanation.correctedCommand
|
|
9872
|
+
},
|
|
9873
|
+
retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
|
|
9874
|
+
}
|
|
9875
|
+
};
|
|
9876
|
+
writeAdsJson(envelope);
|
|
9877
|
+
process.exit(1);
|
|
9800
9878
|
}
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9879
|
+
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
9880
|
+
writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message } });
|
|
9881
|
+
process.exit(1);
|
|
9882
|
+
}
|
|
9883
|
+
function isNoAccountsSelectedError(code, message) {
|
|
9884
|
+
return code === "VALIDATION_ERROR" && /no linkedin ad accounts selected/i.test(message);
|
|
9885
|
+
}
|
|
9886
|
+
function explainLinkedinError(err) {
|
|
9887
|
+
const lower = err.message.toLowerCase();
|
|
9888
|
+
if (err.code === "VALIDATION_ERROR" && lower.includes("granularity") && lower.includes("daily")) {
|
|
9889
|
+
return {
|
|
9890
|
+
action: "retry_with_flag",
|
|
9891
|
+
message: "LinkedIn rejects DAILY granularity when pivoting on demographic dims (job-title, company, etc.). Re-run with --granularity ALL or MONTHLY.",
|
|
9892
|
+
correctedCommand: void 0
|
|
9893
|
+
};
|
|
9813
9894
|
}
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
"account-id": { type: "string", description: "Account ID", required: false },
|
|
9820
|
-
"account-urn": { type: "string", description: "Alias", required: false },
|
|
9821
|
-
"campaign-group-id": { type: "string", description: "Filter by campaign group", required: false },
|
|
9822
|
-
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
9823
|
-
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
9824
|
-
limit: { type: "string", description: "Max rows", required: false },
|
|
9825
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
9826
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
9895
|
+
if (err.code === "RATE_LIMITED") {
|
|
9896
|
+
return {
|
|
9897
|
+
action: "wait_and_retry",
|
|
9898
|
+
message: "Hit a LinkedIn throttle. The CLI already backs off automatically \u2014 if you see this you've exhausted the retry budget. Wait ~60s and retry."
|
|
9899
|
+
};
|
|
9827
9900
|
}
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
"account-id": { type: "string", description: "Account ID", required: false },
|
|
9834
|
-
"account-urn": { type: "string", description: "Alias", required: false },
|
|
9835
|
-
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
9836
|
-
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
9837
|
-
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
9838
|
-
fields: {
|
|
9839
|
-
type: "string",
|
|
9840
|
-
description: "CSV of dotted paths to return instead of the compact row, e.g. id,content.textAd.landingPage",
|
|
9841
|
-
required: false
|
|
9842
|
-
},
|
|
9843
|
-
full: {
|
|
9844
|
-
type: "boolean",
|
|
9845
|
-
description: "Return the complete raw records, including the opaque 'content' block",
|
|
9846
|
-
required: false
|
|
9847
|
-
},
|
|
9848
|
-
limit: { type: "string", description: "Max rows", required: false },
|
|
9849
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
9850
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
9901
|
+
if (err.code === "INTERNAL_ERROR" && lower.includes("deprecat")) {
|
|
9902
|
+
return {
|
|
9903
|
+
action: "reject",
|
|
9904
|
+
message: "LinkedIn-Version header is deprecated. The backend constant LINKEDIN_API_VERSION needs to be bumped \u2014 file an issue or update the constant."
|
|
9905
|
+
};
|
|
9851
9906
|
}
|
|
9852
|
-
}
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
args
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
"creative-id": { type: "string", description: "CSV when level=creative", required: false },
|
|
9868
|
-
ids: { type: "string", description: "Generic CSV alternative", required: false },
|
|
9869
|
-
intent: {
|
|
9870
|
-
type: "string",
|
|
9871
|
-
description: "Field bundle",
|
|
9872
|
-
required: false,
|
|
9873
|
-
default: "baseline",
|
|
9874
|
-
enum: [
|
|
9875
|
-
"baseline",
|
|
9876
|
-
"revenue",
|
|
9877
|
-
"funnel",
|
|
9878
|
-
"engagement",
|
|
9879
|
-
"video",
|
|
9880
|
-
"lead-gen",
|
|
9881
|
-
"inmail",
|
|
9882
|
-
"document",
|
|
9883
|
-
"ranking",
|
|
9884
|
-
"identity"
|
|
9885
|
-
]
|
|
9886
|
-
},
|
|
9887
|
-
pivot: {
|
|
9888
|
-
type: "string",
|
|
9889
|
-
description: "Demographic / firmographic dim",
|
|
9890
|
-
required: false,
|
|
9891
|
-
default: "none",
|
|
9892
|
-
enum: [
|
|
9893
|
-
"none",
|
|
9894
|
-
"campaign",
|
|
9895
|
-
"campaign-group",
|
|
9896
|
-
"creative",
|
|
9897
|
-
"company",
|
|
9898
|
-
"account",
|
|
9899
|
-
"conversion",
|
|
9900
|
-
"job-title",
|
|
9901
|
-
"job-function",
|
|
9902
|
-
"seniority",
|
|
9903
|
-
"industry",
|
|
9904
|
-
"company-size",
|
|
9905
|
-
"country",
|
|
9906
|
-
"region",
|
|
9907
|
-
"device",
|
|
9908
|
-
"placement",
|
|
9909
|
-
"serving-location",
|
|
9910
|
-
"card-index",
|
|
9911
|
-
"objective",
|
|
9912
|
-
"conversation-node",
|
|
9913
|
-
"conversation-node-button"
|
|
9914
|
-
]
|
|
9915
|
-
},
|
|
9916
|
-
metrics: { type: "string", description: "CSV metric override (escape hatch)", required: false },
|
|
9917
|
-
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
9918
|
-
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
9919
|
-
"last-days": { type: "string", description: "Window (default 7)", required: false },
|
|
9920
|
-
granularity: {
|
|
9921
|
-
type: "string",
|
|
9922
|
-
description: "DAILY|MONTHLY|YEARLY|ALL",
|
|
9923
|
-
required: false,
|
|
9924
|
-
default: "DAILY",
|
|
9925
|
-
enum: ["DAILY", "MONTHLY", "YEARLY", "ALL"]
|
|
9926
|
-
},
|
|
9927
|
-
limit: { type: "string", description: "Max rows", required: false },
|
|
9928
|
-
"no-sort": { type: "boolean", description: "Skip default sort", required: false },
|
|
9929
|
-
"list-intents": { type: "boolean", description: "List intents and exit", required: false },
|
|
9930
|
-
"list-pivots": { type: "boolean", description: "List pivots and exit", required: false },
|
|
9931
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
9932
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
9907
|
+
return { action: "reject", message: err.message };
|
|
9908
|
+
}
|
|
9909
|
+
var LI_ACCOUNT_ID_RE = /^(urn:li:sponsoredAccount:)?\d+$/;
|
|
9910
|
+
function resolveAccountIdArg2(args) {
|
|
9911
|
+
const fromArgs = args["account-id"] ?? args["account-urn"];
|
|
9912
|
+
const id = fromArgs || getEnv().BAKER_LINKEDIN_AD_ACCOUNT_ID;
|
|
9913
|
+
if (!id) {
|
|
9914
|
+
writeAdsJson({
|
|
9915
|
+
ok: false,
|
|
9916
|
+
error: {
|
|
9917
|
+
code: "MISSING_ACCOUNT_ID",
|
|
9918
|
+
message: "Pass --account-id (numeric) or --account-urn (urn:li:sponsoredAccount:N), or set BAKER_LINKEDIN_AD_ACCOUNT_ID. Run `baker ads linkedin accounts` to find IDs."
|
|
9919
|
+
}
|
|
9920
|
+
});
|
|
9921
|
+
process.exit(1);
|
|
9933
9922
|
}
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
9944
|
-
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
9945
|
-
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
9946
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
9947
|
-
output: { type: "string", description: "json", required: false, default: "json" }
|
|
9923
|
+
if (!LI_ACCOUNT_ID_RE.test(id)) {
|
|
9924
|
+
writeAdsJson({
|
|
9925
|
+
ok: false,
|
|
9926
|
+
error: {
|
|
9927
|
+
code: "INVALID_ACCOUNT_ID",
|
|
9928
|
+
message: `Invalid LinkedIn account ID "${id}". Expected numeric or urn:li:sponsoredAccount:N.`
|
|
9929
|
+
}
|
|
9930
|
+
});
|
|
9931
|
+
process.exit(1);
|
|
9948
9932
|
}
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9933
|
+
return id.startsWith("urn:") ? id.split(":").pop() ?? id : id;
|
|
9934
|
+
}
|
|
9935
|
+
function todayIso2() {
|
|
9936
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9937
|
+
}
|
|
9938
|
+
function daysAgoIso2(days) {
|
|
9939
|
+
return new Date(Date.now() - days * DAY_MS2).toISOString().slice(0, 10);
|
|
9940
|
+
}
|
|
9941
|
+
function csvOrJson2(args) {
|
|
9942
|
+
return args.output ?? "json";
|
|
9943
|
+
}
|
|
9944
|
+
function resolveStatusFilter(args) {
|
|
9945
|
+
if (args["all-statuses"]) {
|
|
9946
|
+
return void 0;
|
|
9962
9947
|
}
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
description: "List every targeting facet LinkedIn supports (industries, seniorities, titles, employers, growthRate, etc).",
|
|
9967
|
-
args: {
|
|
9968
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
9969
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
9948
|
+
const explicit = args.statuses;
|
|
9949
|
+
if (explicit) {
|
|
9950
|
+
return explicit.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
9970
9951
|
}
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9952
|
+
return ["ACTIVE"];
|
|
9953
|
+
}
|
|
9954
|
+
|
|
9955
|
+
// src/commands/ads/linkedin/write-shared.ts
|
|
9956
|
+
var TOTAL_BUDGET_HELP = "Lifetime budget \u2014 on something already running it must exceed spend to date, or LinkedIn rejects it";
|
|
9957
|
+
function bareAccountId(args) {
|
|
9958
|
+
const raw = resolveAccountIdArg2(args);
|
|
9959
|
+
return raw.replace(/^urn:li:sponsoredAccount:/, "").replace(/^sponsoredAccount:/, "");
|
|
9960
|
+
}
|
|
9961
|
+
function failWriteValidation2(message) {
|
|
9962
|
+
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
9963
|
+
process.exit(1);
|
|
9964
|
+
}
|
|
9965
|
+
function loadJsonFileArg2(path28) {
|
|
9966
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
9967
|
+
return {};
|
|
9982
9968
|
}
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9969
|
+
try {
|
|
9970
|
+
const parsed = JSON.parse(readFileSync4(path28, "utf8"));
|
|
9971
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9972
|
+
failWriteValidation2(`${path28} must contain a JSON object`);
|
|
9973
|
+
}
|
|
9974
|
+
return parsed;
|
|
9975
|
+
} catch (err) {
|
|
9976
|
+
if (err instanceof SyntaxError) {
|
|
9977
|
+
failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
|
|
9978
|
+
}
|
|
9979
|
+
throw err;
|
|
9994
9980
|
}
|
|
9995
|
-
}
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
objective: {
|
|
10003
|
-
type: "string",
|
|
10004
|
-
description: "Objective type (WEBSITE_CONVERSION, LEAD_GENERATION, etc.)",
|
|
10005
|
-
required: true
|
|
10006
|
-
},
|
|
10007
|
-
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true, enum: ["CPC", "CPM", "CPV", "CPS"] },
|
|
10008
|
-
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10009
|
-
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10010
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10011
|
-
output: { type: "string", description: "json", required: false, default: "json" }
|
|
9981
|
+
}
|
|
9982
|
+
function mergePayload2(file, flags) {
|
|
9983
|
+
const merged = { ...file };
|
|
9984
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
9985
|
+
if (value !== void 0) {
|
|
9986
|
+
merged[key] = value;
|
|
9987
|
+
}
|
|
10012
9988
|
}
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
args:
|
|
10018
|
-
|
|
10019
|
-
"
|
|
10020
|
-
objective: { type: "string", description: "Objective type", required: true },
|
|
10021
|
-
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true },
|
|
10022
|
-
"daily-budget": { type: "string", description: "e.g. '200 USD'", required: false },
|
|
10023
|
-
"total-budget": { type: "string", description: "e.g. '6000 USD'", required: false },
|
|
10024
|
-
bid: { type: "string", description: "Bid e.g. '8 USD'", required: false },
|
|
10025
|
-
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10026
|
-
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10027
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10028
|
-
output: { type: "string", description: "json", required: false, default: "json" }
|
|
9989
|
+
return merged;
|
|
9990
|
+
}
|
|
9991
|
+
function loadPatchArg2(args) {
|
|
9992
|
+
const file = typeof args.file === "string" && args.file.length > 0 ? args.file : void 0;
|
|
9993
|
+
const inline = typeof args.patch === "string" && args.patch.length > 0 ? args.patch : void 0;
|
|
9994
|
+
if (Boolean(file) === Boolean(inline)) {
|
|
9995
|
+
failWriteValidation2("pass exactly one of --file <patch.json> or --patch '<json>'");
|
|
10029
9996
|
}
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
command: "ads.linkedin.leads",
|
|
10033
|
-
description: "List Lead Gen Form responses (90-day LinkedIn retention \u2014 sync to CRM via this endpoint). Filter by --form-id, --campaign-id, or --since-days.",
|
|
10034
|
-
args: {
|
|
10035
|
-
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10036
|
-
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10037
|
-
"form-id": { type: "string", description: "Filter by form ID", required: false },
|
|
10038
|
-
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
10039
|
-
"since-days": { type: "string", description: "Last N days", required: false },
|
|
10040
|
-
"since-ms": { type: "string", description: "Epoch ms (alternative to --since-days)", required: false },
|
|
10041
|
-
limit: { type: "string", description: "Max rows", required: false },
|
|
10042
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10043
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
9997
|
+
if (file) {
|
|
9998
|
+
return loadJsonFileArg2(file);
|
|
10044
9999
|
}
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10000
|
+
try {
|
|
10001
|
+
const parsed = JSON.parse(inline);
|
|
10002
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10003
|
+
failWriteValidation2("--patch must be a JSON object");
|
|
10004
|
+
}
|
|
10005
|
+
return parsed;
|
|
10006
|
+
} catch (err) {
|
|
10007
|
+
if (err instanceof SyntaxError) {
|
|
10008
|
+
failWriteValidation2(`--patch is not valid JSON: ${err.message}`);
|
|
10009
|
+
}
|
|
10010
|
+
throw err;
|
|
10054
10011
|
}
|
|
10055
|
-
}
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
args: {
|
|
10060
|
-
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10061
|
-
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10062
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10063
|
-
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10012
|
+
}
|
|
10013
|
+
function parseMoneyFlag(amount, currency, flag) {
|
|
10014
|
+
if (amount === void 0 || amount === null || amount === "") {
|
|
10015
|
+
return void 0;
|
|
10064
10016
|
}
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
command: "ads.linkedin.conversation",
|
|
10068
|
-
description: "Per-button click rates inside Sponsored Messaging / Conversation Ads. Wraps analytics --pivot conversation-node-button.",
|
|
10069
|
-
args: {
|
|
10070
|
-
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10071
|
-
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10072
|
-
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10073
|
-
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10074
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10075
|
-
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10017
|
+
if (currency === void 0 || currency === null || currency === "") {
|
|
10018
|
+
return { amount: String(amount) };
|
|
10076
10019
|
}
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
command: "ads.linkedin.audit",
|
|
10080
|
-
description: "Run a 30+ check playbook audit (Settings, Tracking, Audience, Campaigns, Creative, Performance, Bidding, Compliance, Hygiene). Each finding: {id, area, check, status, severity, evidence, fix} with playbook citations. --format md renders a deliverable-ready table.",
|
|
10081
|
-
args: {
|
|
10082
|
-
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10083
|
-
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10084
|
-
"campaign-id": { type: "string", description: "Narrow to a single campaign", required: false },
|
|
10085
|
-
"window-days": { type: "string", description: "Performance lookback (default 30)", required: false },
|
|
10086
|
-
severity: { type: "string", description: "CSV severity filter (critical,high,medium,low)", required: false },
|
|
10087
|
-
area: { type: "string", description: "CSV area filter", required: false },
|
|
10088
|
-
format: { type: "string", description: "json | md", required: false, default: "json" },
|
|
10089
|
-
"skip-cache": { type: "boolean", description: "Bypass cache", required: false }
|
|
10020
|
+
if (typeof currency !== "string" || currency.length !== 3) {
|
|
10021
|
+
failWriteValidation2(`${flag} got an invalid --currency (expected a 3-letter code like EUR)`);
|
|
10090
10022
|
}
|
|
10091
|
-
}
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
registerSchema({
|
|
10097
|
-
command: "ads.linkedin.campaign-groups.create",
|
|
10098
|
-
description: "Stage a new campaign group (staged until publish; review with `baker ads linkedin draft`). Returns a li_temp_* ref that later `campaigns create --group` calls can reference.",
|
|
10099
|
-
args: {
|
|
10100
|
-
...writeAccountArgs,
|
|
10101
|
-
name: { type: "string", description: "Name (\u2264100 chars)", required: true },
|
|
10102
|
-
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)", required: true },
|
|
10103
|
-
end: { type: "string", description: "Run schedule end", required: false },
|
|
10104
|
-
"total-budget": { type: "string", description: "Lifetime budget amount", required: false },
|
|
10105
|
-
currency: {
|
|
10106
|
-
type: "string",
|
|
10107
|
-
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10108
|
-
required: false
|
|
10109
|
-
},
|
|
10110
|
-
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10111
|
-
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10023
|
+
return { amount: String(amount), currencyCode: currency.toUpperCase() };
|
|
10024
|
+
}
|
|
10025
|
+
function parseDateFlag(value, flag) {
|
|
10026
|
+
if (value === void 0 || value === null || value === "") {
|
|
10027
|
+
return void 0;
|
|
10112
10028
|
}
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
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.",
|
|
10117
|
-
args: {
|
|
10118
|
-
id: {
|
|
10119
|
-
type: "positional",
|
|
10120
|
-
description: "Campaign group id/URN, or li_temp_* ref staged in this chat",
|
|
10121
|
-
required: true
|
|
10122
|
-
},
|
|
10123
|
-
...writeAccountArgs,
|
|
10124
|
-
name: { type: "string", description: "New name", required: false },
|
|
10125
|
-
start: { type: "string", description: "New schedule start", required: false },
|
|
10126
|
-
end: { type: "string", description: "New schedule end", required: false },
|
|
10127
|
-
"total-budget": { type: "string", description: "New lifetime budget", required: false },
|
|
10128
|
-
currency: {
|
|
10129
|
-
type: "string",
|
|
10130
|
-
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10131
|
-
required: false
|
|
10132
|
-
},
|
|
10133
|
-
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10134
|
-
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10029
|
+
const raw = String(value);
|
|
10030
|
+
if (/^\d{10,}$/.test(raw)) {
|
|
10031
|
+
return Number(raw);
|
|
10135
10032
|
}
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
description: "Stage a new campaign. --group accepts a real id/URN or a li_temp_* ref staged earlier in this chat. Targeting goes in --targeting-file as LinkedIn criteria: {include:{and:[{or:{'urn:li:adTargetingFacet:locations':['urn:li:geo:\u2026']}}]}} \u2014 find facet values with `baker ads linkedin facets values`. Playbook defaults: manual bid (--bid), audience expansion off, LAN off. Staged until publish.",
|
|
10140
|
-
args: {
|
|
10141
|
-
...writeAccountArgs,
|
|
10142
|
-
name: { type: "string", description: "Name (\u2264255 chars)", required: true },
|
|
10143
|
-
group: { type: "string", description: "Campaign group id/URN or li_temp_* ref", required: true },
|
|
10144
|
-
type: { type: "string", description: "SPONSORED_UPDATES|TEXT_AD|SPONSORED_INMAILS|DYNAMIC", required: true },
|
|
10145
|
-
objective: { type: "string", description: "LEAD_GENERATION|WEBSITE_VISIT|BRAND_AWARENESS|\u2026", required: false },
|
|
10146
|
-
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: true },
|
|
10147
|
-
bid: { type: "string", description: "Manual bid amount (unitCost)", required: false },
|
|
10148
|
-
"daily-budget": { type: "string", description: "Daily budget amount", required: false },
|
|
10149
|
-
"total-budget": { type: "string", description: "Lifetime budget amount", required: false },
|
|
10150
|
-
currency: {
|
|
10151
|
-
type: "string",
|
|
10152
|
-
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10153
|
-
required: false
|
|
10154
|
-
},
|
|
10155
|
-
start: { type: "string", description: "Run schedule start", required: false },
|
|
10156
|
-
end: { type: "string", description: "Run schedule end", required: false },
|
|
10157
|
-
locale: { type: "string", description: "Profile locale like en_US (default en_US)", required: false },
|
|
10158
|
-
"targeting-file": { type: "string", description: "JSON file with targetingCriteria", required: true },
|
|
10159
|
-
"audience-expansion": { type: "string", description: "on|off (playbook: off; default off)", required: false },
|
|
10160
|
-
lan: {
|
|
10161
|
-
type: "string",
|
|
10162
|
-
description: "LinkedIn Audience Network on|off (playbook: off; default off)",
|
|
10163
|
-
required: false
|
|
10164
|
-
},
|
|
10165
|
-
ctv: { type: "string", description: "Connected TV on|off (default off)", required: false },
|
|
10166
|
-
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10167
|
-
"associated-entity": { type: "string", description: "Organization URN", required: false },
|
|
10168
|
-
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10169
|
-
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10033
|
+
const parsed = Date.parse(raw);
|
|
10034
|
+
if (Number.isNaN(parsed)) {
|
|
10035
|
+
failWriteValidation2(`${flag} must be an ISO date (2026-07-10) or epoch milliseconds`);
|
|
10170
10036
|
}
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
id: { type: "positional", description: "Campaign id/URN, or li_temp_* ref staged in this chat", required: true },
|
|
10177
|
-
...writeAccountArgs,
|
|
10178
|
-
name: { type: "string", description: "New name", required: false },
|
|
10179
|
-
objective: { type: "string", description: "New objective", required: false },
|
|
10180
|
-
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: false },
|
|
10181
|
-
bid: { type: "string", description: "New manual bid", required: false },
|
|
10182
|
-
"daily-budget": { type: "string", description: "New daily budget", required: false },
|
|
10183
|
-
"total-budget": { type: "string", description: "New lifetime budget", required: false },
|
|
10184
|
-
currency: {
|
|
10185
|
-
type: "string",
|
|
10186
|
-
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10187
|
-
required: false
|
|
10188
|
-
},
|
|
10189
|
-
start: { type: "string", description: "New schedule start", required: false },
|
|
10190
|
-
end: { type: "string", description: "New schedule end", required: false },
|
|
10191
|
-
"targeting-file": { type: "string", description: "JSON file with replacement targetingCriteria", required: false },
|
|
10192
|
-
"audience-expansion": { type: "string", description: "on|off", required: false },
|
|
10193
|
-
lan: { type: "string", description: "on|off", required: false },
|
|
10194
|
-
ctv: { type: "string", description: "on|off", required: false },
|
|
10195
|
-
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10196
|
-
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10197
|
-
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10037
|
+
return parsed;
|
|
10038
|
+
}
|
|
10039
|
+
function parseOnOffFlag(value, flag) {
|
|
10040
|
+
if (value === void 0 || value === null || value === "") {
|
|
10041
|
+
return void 0;
|
|
10198
10042
|
}
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
description: "Start here for UTMs: stage the URL tracking parameters on an ad set. LinkedIn appends them to the link of EVERY ad in the ad set, including ads already running, so this is the level to set campaign-wide UTMs at \u2014 editing each ad's landing URL instead risks the same key landing twice. --param takes fixed values (repeatable or &-joined); --dynamic takes a value LinkedIn fills in per ad (ACCOUNT_ID, ACCOUNT_NAME, CAMPAIGN_GROUP_ID, CAMPAIGN_GROUP_NAME, CAMPAIGN_ID, CAMPAIGN_NAME, CREATIVE_ID, CREATIVE_NAME). Applies on chat publish; message and conversation ads are unaffected.",
|
|
10203
|
-
args: {
|
|
10204
|
-
id: { type: "positional", description: "Ad set (campaign) id or URN", required: true },
|
|
10205
|
-
...writeAccountArgs,
|
|
10206
|
-
param: {
|
|
10207
|
-
type: "string",
|
|
10208
|
-
description: 'Fixed key=value, repeatable or &-joined \u2014 e.g. "utm_source=linkedin&utm_medium=paid-social"',
|
|
10209
|
-
required: false
|
|
10210
|
-
},
|
|
10211
|
-
dynamic: {
|
|
10212
|
-
type: "string",
|
|
10213
|
-
description: "key=PLACEHOLDER filled in per ad, repeatable \u2014 e.g. utm_campaign=CAMPAIGN_NAME",
|
|
10214
|
-
required: false
|
|
10215
|
-
},
|
|
10216
|
-
clear: { type: "boolean", description: "Remove every tracking parameter from this ad set", required: false },
|
|
10217
|
-
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10043
|
+
const raw = String(value).toLowerCase();
|
|
10044
|
+
if (raw === "on" || raw === "true") {
|
|
10045
|
+
return true;
|
|
10218
10046
|
}
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
id: {
|
|
10222
|
-
type: "positional",
|
|
10223
|
-
description: "Entity id or URN \u2014 comma-separate several to change them all in one call",
|
|
10224
|
-
required: false
|
|
10225
|
-
},
|
|
10226
|
-
"ids-file": { type: "string", description: "File of ids, one per line or comma-separated", required: false },
|
|
10227
|
-
...writeAccountArgs
|
|
10228
|
-
};
|
|
10229
|
-
var MULTI_ID_NOTE = "Takes one id or many: comma-separate the ids, or pass --ids-file. Several ids stage together as one set \u2014 all of them or none. The list form is for real ids/URNs only: a li_temp_* ref AMENDS what this chat already staged, and an amend must be passed on its own.";
|
|
10230
|
-
registerSchema({
|
|
10231
|
-
command: "ads.linkedin.campaigns.pause",
|
|
10232
|
-
description: `Stage pausing a campaign (status \u2192 PAUSED on publish). ${MULTI_ID_NOTE}`,
|
|
10233
|
-
args: statusSugarArgs
|
|
10234
|
-
});
|
|
10235
|
-
registerSchema({
|
|
10236
|
-
command: "ads.linkedin.campaigns.resume",
|
|
10237
|
-
description: `Stage resuming a paused campaign (status \u2192 ACTIVE on publish). ${MULTI_ID_NOTE}`,
|
|
10238
|
-
args: statusSugarArgs
|
|
10239
|
-
});
|
|
10240
|
-
registerSchema({
|
|
10241
|
-
command: "ads.linkedin.campaigns.archive",
|
|
10242
|
-
description: `Stage archiving a campaign (status \u2192 ARCHIVED on publish). ${MULTI_ID_NOTE}`,
|
|
10243
|
-
args: statusSugarArgs
|
|
10244
|
-
});
|
|
10245
|
-
registerSchema({
|
|
10246
|
-
command: "ads.linkedin.campaign-groups.pause",
|
|
10247
|
-
description: `Stage pausing a campaign group. ${MULTI_ID_NOTE}`,
|
|
10248
|
-
args: statusSugarArgs
|
|
10249
|
-
});
|
|
10250
|
-
registerSchema({
|
|
10251
|
-
command: "ads.linkedin.campaign-groups.resume",
|
|
10252
|
-
description: `Stage resuming a campaign group. ${MULTI_ID_NOTE}`,
|
|
10253
|
-
args: statusSugarArgs
|
|
10254
|
-
});
|
|
10255
|
-
registerSchema({
|
|
10256
|
-
command: "ads.linkedin.creatives.pause",
|
|
10257
|
-
description: `Stage pausing a creative (intendedStatus \u2192 PAUSED on publish). Playbook early-kill rule: >30 impressions with 0 clicks. ${MULTI_ID_NOTE}`,
|
|
10258
|
-
args: statusSugarArgs
|
|
10259
|
-
});
|
|
10260
|
-
registerSchema({
|
|
10261
|
-
command: "ads.linkedin.creatives.resume",
|
|
10262
|
-
description: `Stage resuming a paused creative. ${MULTI_ID_NOTE}`,
|
|
10263
|
-
args: statusSugarArgs
|
|
10264
|
-
});
|
|
10265
|
-
var duplicateArgs = {
|
|
10266
|
-
id: { type: "positional", description: "Source id or URN", required: true },
|
|
10267
|
-
...writeAccountArgs,
|
|
10268
|
-
name: { type: "string", description: "Name for the copy", required: false },
|
|
10269
|
-
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10270
|
-
};
|
|
10271
|
-
registerSchema({
|
|
10272
|
-
command: "ads.linkedin.campaigns.duplicate",
|
|
10273
|
-
description: "Stage a copy of an existing campaign as a new DRAFT create (playbook: duplicate, never link-to-original). Reads the source at stage time; override fields via --name/--file.",
|
|
10274
|
-
args: duplicateArgs
|
|
10275
|
-
});
|
|
10276
|
-
registerSchema({
|
|
10277
|
-
command: "ads.linkedin.campaign-groups.duplicate",
|
|
10278
|
-
description: "Stage a copy of an existing campaign group as a new DRAFT create.",
|
|
10279
|
-
args: duplicateArgs
|
|
10280
|
-
});
|
|
10281
|
-
registerSchema({
|
|
10282
|
-
command: "ads.linkedin.creatives.duplicate",
|
|
10283
|
-
description: "Stage a copy of an existing ad, optionally with new content. Direct-content ads (text/spotlight/follower/jobs) copy field-by-field, so a copy is how their immutable content changes (they can also update in place via creatives.update). Post-based ads re-sponsor the SAME post \u2014 the post owns the copy, media and destination, so content flags are rejected here: stage creatives.create with the new content instead, then pause the old ad. --replace stages pausing the original, gated on the new ad publishing successfully, and the copy inherits the source's status.",
|
|
10284
|
-
args: {
|
|
10285
|
-
id: { type: "positional", description: "Source creative id or URN", required: true },
|
|
10286
|
-
...writeAccountArgs,
|
|
10287
|
-
campaign: { type: "string", description: "Stage the copy under a different ad set", required: false },
|
|
10288
|
-
"intended-status": { type: "string", description: "Status for the copy (default DRAFT)", required: false },
|
|
10289
|
-
replace: { type: "boolean", description: "Also pause the original once the new ad publishes", required: false },
|
|
10290
|
-
headline: { type: "string", description: "New headline", required: false },
|
|
10291
|
-
description: { type: "string", description: "New description (text/spotlight)", required: false },
|
|
10292
|
-
intro: { type: "string", description: "New commentary / intro text", required: false },
|
|
10293
|
-
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10294
|
-
cta: { type: "string", description: "New CTA", required: false },
|
|
10295
|
-
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10296
|
-
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10297
|
-
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10298
|
-
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10299
|
-
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10300
|
-
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10301
|
-
title: { type: "string", description: "Media title", required: false },
|
|
10302
|
-
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10047
|
+
if (raw === "off" || raw === "false") {
|
|
10048
|
+
return false;
|
|
10303
10049
|
}
|
|
10304
|
-
});
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
...writeAccountArgs,
|
|
10310
|
-
campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
|
|
10311
|
-
format: {
|
|
10312
|
-
type: "string",
|
|
10313
|
-
description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article",
|
|
10314
|
-
required: true
|
|
10315
|
-
},
|
|
10316
|
-
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10317
|
-
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10318
|
-
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10319
|
-
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10320
|
-
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10321
|
-
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10322
|
-
headline: { type: "string", description: "Headline", required: false },
|
|
10323
|
-
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10324
|
-
"landing-url": { type: "string", description: "https destination URL", required: false },
|
|
10325
|
-
cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026", required: false },
|
|
10326
|
-
"cta-label": { type: "string", description: "Spotlight CTA label (\u226418 chars)", required: false },
|
|
10327
|
-
"post-urn": { type: "string", description: "TLA: post to sponsor (urn:li:share|ugcPost:*)", required: false },
|
|
10328
|
-
"event-urn": {
|
|
10329
|
-
type: "string",
|
|
10330
|
-
description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)",
|
|
10331
|
-
required: false
|
|
10332
|
-
},
|
|
10333
|
-
"article-url": {
|
|
10334
|
-
type: "string",
|
|
10335
|
-
description: "Article ads: https URL of the article/newsletter to sponsor",
|
|
10336
|
-
required: false
|
|
10337
|
-
},
|
|
10338
|
-
"thumbnail-image-id": {
|
|
10339
|
-
type: "string",
|
|
10340
|
-
description: "Article ads: Baker image id for the link thumbnail",
|
|
10341
|
-
required: false
|
|
10342
|
-
},
|
|
10343
|
-
"thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail", required: false },
|
|
10344
|
-
"intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10345
|
-
title: { type: "string", description: "Media title (image/video) or article title", required: false },
|
|
10346
|
-
file: { type: "string", description: "JSON file with the full content object", required: false }
|
|
10050
|
+
failWriteValidation2(`${flag} must be on|off`);
|
|
10051
|
+
}
|
|
10052
|
+
function parseLocaleFlag(value) {
|
|
10053
|
+
if (value === void 0 || value === null || value === "") {
|
|
10054
|
+
return void 0;
|
|
10347
10055
|
}
|
|
10348
|
-
});
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
description: `Stage changes to an existing creative. Direct-content ads (text/spotlight/follower/jobs) update copy/media/URL IN PLACE \u2014 pass only the changed fields (headline, description, landing-url, image-id, \u2026); the ad's current content carries over. Post-based ads (image/video/carousel/\u2026) sponsor a post that owns the copy, media and destination \u2014 neither this command nor creatives.duplicate can change it; stage creatives.create with the new content, then pause the old ad. Passing a li_temp_* ref AMENDS the creative staged in this chat instead. ${MULTI_ID_NOTE} Applies on chat publish.`,
|
|
10352
|
-
args: {
|
|
10353
|
-
id: {
|
|
10354
|
-
type: "positional",
|
|
10355
|
-
description: "Creative id/URN \u2014 comma-separate several. A li_temp_* ref amends what this chat staged and must be passed on its own.",
|
|
10356
|
-
required: false
|
|
10357
|
-
},
|
|
10358
|
-
"ids-file": {
|
|
10359
|
-
type: "string",
|
|
10360
|
-
description: "File of creative ids, one per line or comma-separated",
|
|
10361
|
-
required: false
|
|
10362
|
-
},
|
|
10363
|
-
...writeAccountArgs,
|
|
10364
|
-
"intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
|
|
10365
|
-
name: { type: "string", description: "New creative name", required: false },
|
|
10366
|
-
campaign: {
|
|
10367
|
-
type: "string",
|
|
10368
|
-
description: "Re-parent to a different ad set (id/URN or li_temp_*) \u2014 staged creatives only",
|
|
10369
|
-
required: false
|
|
10370
|
-
},
|
|
10371
|
-
headline: { type: "string", description: "New headline", required: false },
|
|
10372
|
-
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10373
|
-
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10374
|
-
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10375
|
-
cta: { type: "string", description: "New CTA", required: false },
|
|
10376
|
-
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10377
|
-
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10378
|
-
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10379
|
-
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10380
|
-
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10381
|
-
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10382
|
-
"thumbnail-image-id": {
|
|
10383
|
-
type: "string",
|
|
10384
|
-
description: "Baker image id for the article thumbnail",
|
|
10385
|
-
required: false
|
|
10386
|
-
},
|
|
10387
|
-
"thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail", required: false },
|
|
10388
|
-
title: { type: "string", description: "Media or article title", required: false },
|
|
10389
|
-
file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
|
|
10390
|
-
}
|
|
10391
|
-
});
|
|
10392
|
-
registerSchema({
|
|
10393
|
-
command: "ads.linkedin.audiences.create",
|
|
10394
|
-
description: "Stage a new matched audience. company-list/user-list take --list-file (CSV with header; companyName,domain for companies; email column is SHA-256 hashed locally). LinkedIn needs 300+ MATCHED members to serve \u2014 upload well above that. retargeting/engagement configs go in --file. Staged until publish.",
|
|
10395
|
-
args: {
|
|
10396
|
-
...writeAccountArgs,
|
|
10397
|
-
name: { type: "string", description: "Audience name (\u2264100 chars)", required: true },
|
|
10398
|
-
type: { type: "string", description: "company-list|user-list|retargeting|engagement", required: true },
|
|
10399
|
-
"list-file": { type: "string", description: "CSV file with the list rows", required: false },
|
|
10400
|
-
file: { type: "string", description: "JSON raw source config for retargeting/engagement", required: false }
|
|
10056
|
+
const match = /^([a-z]{2})[_-]([A-Za-z]{2})$/.exec(String(value));
|
|
10057
|
+
if (!match) {
|
|
10058
|
+
failWriteValidation2("--locale must look like en_US (language_COUNTRY)");
|
|
10401
10059
|
}
|
|
10402
|
-
}
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
id: { type: "positional", description: "Segment URN/id or li_temp_* ref", required: true },
|
|
10408
|
-
...writeAccountArgs,
|
|
10409
|
-
"list-file": { type: "string", description: "CSV file with the rows to add", required: true }
|
|
10060
|
+
return { language: match[1], country: match[2].toUpperCase() };
|
|
10061
|
+
}
|
|
10062
|
+
function loadTargetingFileArg(path28) {
|
|
10063
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10064
|
+
return void 0;
|
|
10410
10065
|
}
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
name: { type: "string", description: "Rule name", required: true },
|
|
10418
|
-
type: {
|
|
10419
|
-
type: "string",
|
|
10420
|
-
description: "LEAD|SIGN_UP|DOWNLOAD|PURCHASE|KEY_PAGE_VIEW|ADD_TO_CART|INSTALL|OTHER",
|
|
10421
|
-
required: true
|
|
10422
|
-
},
|
|
10423
|
-
method: { type: "string", description: "INSIGHT_TAG|CONVERSIONS_API", required: true },
|
|
10424
|
-
"post-click-window": { type: "string", description: "1|7|30|90 days", required: false },
|
|
10425
|
-
"view-window": { type: "string", description: "1|7|30 days", required: false },
|
|
10426
|
-
"attribution-type": {
|
|
10427
|
-
type: "string",
|
|
10428
|
-
description: "LAST_TOUCH_BY_CAMPAIGN|LAST_TOUCH_BY_CONVERSION",
|
|
10429
|
-
required: false
|
|
10430
|
-
},
|
|
10431
|
-
"associate-all-campaigns": {
|
|
10432
|
-
type: "boolean",
|
|
10433
|
-
description: "Auto-associate with active campaigns",
|
|
10434
|
-
required: false
|
|
10435
|
-
},
|
|
10436
|
-
file: { type: "string", description: "JSON payload file (e.g. urlMatchRuleExpression)", required: false }
|
|
10066
|
+
const parsed = loadJsonFileArg2(path28);
|
|
10067
|
+
const criteria = parsed.targetingCriteria ?? parsed;
|
|
10068
|
+
if (!criteria.include) {
|
|
10069
|
+
failWriteValidation2(
|
|
10070
|
+
`${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
10071
|
+
);
|
|
10437
10072
|
}
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10073
|
+
return criteria;
|
|
10074
|
+
}
|
|
10075
|
+
function sha256Lower(value) {
|
|
10076
|
+
return createHash("sha256").update(value.trim().toLowerCase()).digest("hex");
|
|
10077
|
+
}
|
|
10078
|
+
function parseCsvLine(line) {
|
|
10079
|
+
const cells = [];
|
|
10080
|
+
let current = "";
|
|
10081
|
+
let inQuotes = false;
|
|
10082
|
+
for (let i = 0; i < line.length; i++) {
|
|
10083
|
+
const char = line[i];
|
|
10084
|
+
if (inQuotes) {
|
|
10085
|
+
if (char === '"' && line[i + 1] === '"') {
|
|
10086
|
+
current += '"';
|
|
10087
|
+
i++;
|
|
10088
|
+
} else if (char === '"') {
|
|
10089
|
+
inQuotes = false;
|
|
10090
|
+
} else {
|
|
10091
|
+
current += char;
|
|
10092
|
+
}
|
|
10093
|
+
} else if (char === '"') {
|
|
10094
|
+
inQuotes = true;
|
|
10095
|
+
} else if (char === ",") {
|
|
10096
|
+
cells.push(current);
|
|
10097
|
+
current = "";
|
|
10098
|
+
} else {
|
|
10099
|
+
current += char;
|
|
10100
|
+
}
|
|
10451
10101
|
}
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
file: { type: "string", description: "JSON form definition file", required: true },
|
|
10459
|
-
name: { type: "string", description: "Override the form name", required: false }
|
|
10102
|
+
cells.push(current);
|
|
10103
|
+
return cells.map((cell) => cell.trim());
|
|
10104
|
+
}
|
|
10105
|
+
function parseListFileArg(path28, maxRows) {
|
|
10106
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10107
|
+
return void 0;
|
|
10460
10108
|
}
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
args: {
|
|
10466
|
-
id: { type: "positional", description: "Lead form id or URN", required: true },
|
|
10467
|
-
...writeAccountArgs,
|
|
10468
|
-
file: { type: "string", description: "JSON file with the fields to change", required: true },
|
|
10469
|
-
name: { type: "string", description: "New form name", required: false }
|
|
10109
|
+
const raw = readFileSync4(path28, "utf8");
|
|
10110
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
10111
|
+
if (lines.length < 2) {
|
|
10112
|
+
failWriteValidation2(`${path28} needs a header row and at least one data row`);
|
|
10470
10113
|
}
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10114
|
+
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
10115
|
+
const rows = [];
|
|
10116
|
+
for (const line of lines.slice(1)) {
|
|
10117
|
+
const cells = parseCsvLine(line);
|
|
10118
|
+
const row = {};
|
|
10119
|
+
columns.forEach((column, index) => {
|
|
10120
|
+
const value = cells[index] ?? "";
|
|
10121
|
+
if (!value) {
|
|
10122
|
+
return;
|
|
10123
|
+
}
|
|
10124
|
+
row[column] = column.toLowerCase() === "email" ? sha256Lower(value) : value;
|
|
10125
|
+
});
|
|
10126
|
+
if (Object.keys(row).length > 0) {
|
|
10127
|
+
rows.push(row);
|
|
10480
10128
|
}
|
|
10481
10129
|
}
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
command: "ads.linkedin.draft.remove",
|
|
10485
|
-
description: "Drop one staged LinkedIn op from this chat's draft. Removing a create cascades to every op that depends on it (a campaign removal drops its staged creatives).",
|
|
10486
|
-
args: {
|
|
10487
|
-
ref: {
|
|
10488
|
-
type: "positional",
|
|
10489
|
-
description: "Op ref (li_temp_* for creates, target id/URN for updates)",
|
|
10490
|
-
required: true
|
|
10491
|
-
}
|
|
10130
|
+
if (rows.length > maxRows) {
|
|
10131
|
+
failWriteValidation2(`${path28} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
10492
10132
|
}
|
|
10493
|
-
}
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
// src/commands/ads/linkedin/shared.ts
|
|
10504
|
-
var DAY_MS2 = 864e5;
|
|
10505
|
-
function handleLinkedinError(err) {
|
|
10506
|
-
if (err instanceof ApiError) {
|
|
10507
|
-
if (err.code === "UNAUTHORIZED") {
|
|
10508
|
-
handleConnectionError("linkedin_ads", err.message);
|
|
10509
|
-
}
|
|
10510
|
-
if (err.code === "NOT_FOUND") {
|
|
10511
|
-
handleConnectionError("linkedin_ads", err.message);
|
|
10512
|
-
}
|
|
10513
|
-
if (isNoAccountsSelectedError(err.code, err.message)) {
|
|
10514
|
-
handleConnectionError("linkedin_ads", err.message);
|
|
10133
|
+
return { columns, rows };
|
|
10134
|
+
}
|
|
10135
|
+
function failPreflight(fields) {
|
|
10136
|
+
writeJsonEnvelope({
|
|
10137
|
+
ok: false,
|
|
10138
|
+
error: {
|
|
10139
|
+
code: "VALIDATION_ERROR",
|
|
10140
|
+
message: fields.map((field) => field.path ? `${field.path}: ${field.message}` : field.message).join("; "),
|
|
10141
|
+
fields,
|
|
10142
|
+
fix: capabilityFix("linkedin-ads")
|
|
10515
10143
|
}
|
|
10516
|
-
|
|
10517
|
-
const envelope = {
|
|
10518
|
-
ok: false,
|
|
10519
|
-
error: {
|
|
10520
|
-
code: err.code,
|
|
10521
|
-
message: err.message,
|
|
10522
|
-
fix: {
|
|
10523
|
-
action: explanation.action,
|
|
10524
|
-
explanation: explanation.message,
|
|
10525
|
-
correctedCommand: explanation.correctedCommand
|
|
10526
|
-
},
|
|
10527
|
-
retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
|
|
10528
|
-
}
|
|
10529
|
-
};
|
|
10530
|
-
writeAdsJson(envelope);
|
|
10531
|
-
process.exit(1);
|
|
10532
|
-
}
|
|
10533
|
-
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
10534
|
-
writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message } });
|
|
10144
|
+
});
|
|
10535
10145
|
process.exit(1);
|
|
10536
10146
|
}
|
|
10537
|
-
|
|
10538
|
-
|
|
10147
|
+
var BATCH_NUDGE = "This command takes several ids at once \u2014 pass them comma-separated or with --ids-file instead of one call per id.";
|
|
10148
|
+
function batchNudge(data) {
|
|
10149
|
+
return data.sameKindStaged === void 0 ? [] : [BATCH_NUDGE];
|
|
10539
10150
|
}
|
|
10540
|
-
function
|
|
10541
|
-
const
|
|
10542
|
-
if (
|
|
10543
|
-
|
|
10544
|
-
action: "retry_with_flag",
|
|
10545
|
-
message: "LinkedIn rejects DAILY granularity when pivoting on demographic dims (job-title, company, etc.). Re-run with --granularity ALL or MONTHLY.",
|
|
10546
|
-
correctedCommand: void 0
|
|
10547
|
-
};
|
|
10548
|
-
}
|
|
10549
|
-
if (err.code === "RATE_LIMITED") {
|
|
10550
|
-
return {
|
|
10551
|
-
action: "wait_and_retry",
|
|
10552
|
-
message: "Hit a LinkedIn throttle. The CLI already backs off automatically \u2014 if you see this you've exhausted the retry budget. Wait ~60s and retry."
|
|
10553
|
-
};
|
|
10151
|
+
async function stageOp(raw, hints) {
|
|
10152
|
+
const preflight = linkedinDraftOpInputSchema.safeParse(raw);
|
|
10153
|
+
if (!preflight.success) {
|
|
10154
|
+
failPreflight(preflight.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message })));
|
|
10554
10155
|
}
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10156
|
+
try {
|
|
10157
|
+
const chatId = requireChatId();
|
|
10158
|
+
const response = await apiPost("/api/ads/linkedin/draft/stage", {
|
|
10159
|
+
chatId,
|
|
10160
|
+
op: preflight.data
|
|
10161
|
+
});
|
|
10162
|
+
const allHints = [...batchNudge(response.data), ...hints ?? [], ...adCopyHints(preflight.data)];
|
|
10163
|
+
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
10164
|
+
} catch (err) {
|
|
10165
|
+
handleLinkedinError(err);
|
|
10560
10166
|
}
|
|
10561
|
-
return { action: "reject", message: err.message };
|
|
10562
10167
|
}
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
if (!id) {
|
|
10568
|
-
writeAdsJson({
|
|
10569
|
-
ok: false,
|
|
10570
|
-
error: {
|
|
10571
|
-
code: "MISSING_ACCOUNT_ID",
|
|
10572
|
-
message: "Pass --account-id (numeric) or --account-urn (urn:li:sponsoredAccount:N), or set BAKER_LINKEDIN_AD_ACCOUNT_ID. Run `baker ads linkedin accounts` to find IDs."
|
|
10573
|
-
}
|
|
10574
|
-
});
|
|
10575
|
-
process.exit(1);
|
|
10168
|
+
async function stageLinkedinOps(rawOps, hints) {
|
|
10169
|
+
if (rawOps.length === 1 && rawOps[0]) {
|
|
10170
|
+
await stageOp(rawOps[0], hints);
|
|
10171
|
+
return;
|
|
10576
10172
|
}
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10173
|
+
const ops = [];
|
|
10174
|
+
for (const [index, raw] of rawOps.entries()) {
|
|
10175
|
+
const preflight = linkedinDraftOpInputSchema.safeParse(raw);
|
|
10176
|
+
if (!preflight.success) {
|
|
10177
|
+
failPreflight(
|
|
10178
|
+
preflight.error.issues.map((issue) => ({
|
|
10179
|
+
path: `ops[${index}]${issue.path.length > 0 ? `.${issue.path.join(".")}` : ""}`,
|
|
10180
|
+
message: issue.message
|
|
10181
|
+
}))
|
|
10182
|
+
);
|
|
10183
|
+
}
|
|
10184
|
+
ops.push(preflight.data);
|
|
10185
|
+
}
|
|
10186
|
+
try {
|
|
10187
|
+
const chatId = requireChatId();
|
|
10188
|
+
const response = await apiPost("/api/ads/linkedin/draft/stage-batch", { chatId, ops });
|
|
10189
|
+
const allHints = [...hints ?? [], ...adCopyHints(ops)];
|
|
10190
|
+
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
10191
|
+
} catch (err) {
|
|
10192
|
+
handleLinkedinError(err);
|
|
10586
10193
|
}
|
|
10587
|
-
return id.startsWith("urn:") ? id.split(":").pop() ?? id : id;
|
|
10588
10194
|
}
|
|
10589
|
-
function
|
|
10590
|
-
|
|
10195
|
+
async function stageStatusChange(input) {
|
|
10196
|
+
const payload = input.kind === "creative.update" ? { intendedStatus: input.status } : { status: input.status };
|
|
10197
|
+
await stageLinkedinOps(
|
|
10198
|
+
input.targets.map((target) => ({ kind: input.kind, accountId: input.accountId, target, payload }))
|
|
10199
|
+
);
|
|
10591
10200
|
}
|
|
10592
|
-
function
|
|
10593
|
-
|
|
10201
|
+
function requireTarget2(args, entity) {
|
|
10202
|
+
const inverted = booleanWordPositionalError(args, entity);
|
|
10203
|
+
if (inverted !== void 0) {
|
|
10204
|
+
failWriteValidation2(inverted);
|
|
10205
|
+
}
|
|
10206
|
+
const positionals = readPositionals(args);
|
|
10207
|
+
const target = args.id ?? args.target ?? args.ref ?? positionals[0];
|
|
10208
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
10209
|
+
failWriteValidation2(`pass the ${entity} id or URN as the positional argument`);
|
|
10210
|
+
}
|
|
10211
|
+
if (positionals.length > 1 || splitIdList(target).length > 1) {
|
|
10212
|
+
failWriteValidation2(
|
|
10213
|
+
`this command takes one ${entity} id \u2014 run it once per id. Only pause/resume/archive and 'creatives update' accept several ids in one call.`
|
|
10214
|
+
);
|
|
10215
|
+
}
|
|
10216
|
+
return target;
|
|
10594
10217
|
}
|
|
10595
|
-
function
|
|
10596
|
-
|
|
10218
|
+
function readPositionals(args) {
|
|
10219
|
+
if (!Array.isArray(args._)) {
|
|
10220
|
+
return [];
|
|
10221
|
+
}
|
|
10222
|
+
return args._.filter((value) => typeof value === "string" && value.length > 0);
|
|
10597
10223
|
}
|
|
10598
|
-
function
|
|
10599
|
-
|
|
10600
|
-
|
|
10224
|
+
function splitIdList(raw) {
|
|
10225
|
+
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
10226
|
+
}
|
|
10227
|
+
function idsFileEntries(path28) {
|
|
10228
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10229
|
+
return [];
|
|
10601
10230
|
}
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10231
|
+
return readFileSync4(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
|
|
10232
|
+
}
|
|
10233
|
+
function requireTargets(args, entity) {
|
|
10234
|
+
const positionals = readPositionals(args);
|
|
10235
|
+
const named = [args.id, args.target, args.ref].filter(
|
|
10236
|
+
(value) => typeof value === "string" && value.length > 0
|
|
10237
|
+
);
|
|
10238
|
+
const fromArgs = positionals.length > 0 ? positionals : named;
|
|
10239
|
+
const targets = [.../* @__PURE__ */ new Set([...fromArgs.flatMap(splitIdList), ...idsFileEntries(args["ids-file"])])];
|
|
10240
|
+
if (targets.length === 0) {
|
|
10241
|
+
failWriteValidation2(
|
|
10242
|
+
`pass the ${entity} id or URN as the positional argument (comma-separated for several), or --ids-file <path>`
|
|
10243
|
+
);
|
|
10605
10244
|
}
|
|
10606
|
-
|
|
10245
|
+
if (targets.length > LINKEDIN_DRAFT_BATCH_MAX) {
|
|
10246
|
+
failWriteValidation2(
|
|
10247
|
+
`${targets.length} ids exceeds the batch limit of ${LINKEDIN_DRAFT_BATCH_MAX} \u2014 split the list`
|
|
10248
|
+
);
|
|
10249
|
+
}
|
|
10250
|
+
return targets;
|
|
10607
10251
|
}
|
|
10608
10252
|
|
|
10609
|
-
// src/commands/ads/linkedin/
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
description: `Single LinkedIn ad account detail (currency, status, type).
|
|
10614
|
-
|
|
10615
|
-
Examples:
|
|
10616
|
-
baker ads linkedin account --account-id 503001492
|
|
10617
|
-
baker ads linkedin account --account-urn urn:li:sponsoredAccount:503001492 --output md`
|
|
10618
|
-
},
|
|
10253
|
+
// src/commands/ads/linkedin/schemas.ts
|
|
10254
|
+
registerSchema({
|
|
10255
|
+
command: "ads.linkedin.accounts",
|
|
10256
|
+
description: "List LinkedIn ad accounts in the company's connected scope. Pass --include-all to list every account the OAuth token can see.",
|
|
10619
10257
|
args: {
|
|
10620
|
-
"
|
|
10621
|
-
"
|
|
10622
|
-
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
10623
|
-
output: { type: "string", description: "json|csv|jsonl|md", default: "json" }
|
|
10624
|
-
},
|
|
10625
|
-
run: async ({ args }) => {
|
|
10626
|
-
const accountId = resolveAccountIdArg2(args);
|
|
10627
|
-
try {
|
|
10628
|
-
const params = { "account-id": accountId };
|
|
10629
|
-
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
10630
|
-
const data = await apiGet("/api/ads/linkedin/account", params);
|
|
10631
|
-
const fmt = csvOrJson2(args);
|
|
10632
|
-
if (fmt !== "json") {
|
|
10633
|
-
writeAdsOutput([data], fmt);
|
|
10634
|
-
return;
|
|
10635
|
-
}
|
|
10636
|
-
writeAdsJson({ ok: true, data });
|
|
10637
|
-
} catch (err) {
|
|
10638
|
-
handleLinkedinError(err);
|
|
10639
|
-
}
|
|
10258
|
+
"include-all": { type: "boolean", description: "Ignore picker scope", required: false },
|
|
10259
|
+
"no-cache": { type: "boolean", description: "Skip CLI-side cache", required: false },
|
|
10260
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache", required: false },
|
|
10261
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10640
10262
|
}
|
|
10641
10263
|
});
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10264
|
+
registerSchema({
|
|
10265
|
+
command: "ads.linkedin.account",
|
|
10266
|
+
description: "Single LinkedIn ad account detail (currency, status, type). Resolves the urn \u2192 {id, name, currency, status, type}.",
|
|
10267
|
+
args: {
|
|
10268
|
+
"account-id": { type: "string", description: "Numeric ID or urn:li:sponsoredAccount:N", required: false },
|
|
10269
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false },
|
|
10270
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10271
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10649
10272
|
}
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
resources: accounts.map((account) => ({ id: account.externalId, label: account.name }))
|
|
10655
|
-
});
|
|
10656
|
-
}
|
|
10657
|
-
var accountsCommand2 = defineCommand33({
|
|
10658
|
-
meta: {
|
|
10659
|
-
name: "accounts",
|
|
10660
|
-
description: `List LinkedIn ad accounts in this company's connected scope.
|
|
10661
|
-
|
|
10662
|
-
Examples:
|
|
10663
|
-
baker ads linkedin accounts
|
|
10664
|
-
baker ads linkedin accounts --include-all # ignore picker scope, list every account the token can see
|
|
10665
|
-
baker ads linkedin accounts --output csv`
|
|
10666
|
-
},
|
|
10273
|
+
});
|
|
10274
|
+
registerSchema({
|
|
10275
|
+
command: "ads.linkedin.campaign-groups",
|
|
10276
|
+
description: "List LinkedIn campaign groups in an account. Default ACTIVE-only \u2014 pass --all-statuses to widen.",
|
|
10667
10277
|
args: {
|
|
10668
|
-
"
|
|
10669
|
-
"
|
|
10670
|
-
"
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
const useCache = !args["no-cache"];
|
|
10676
|
-
const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
|
|
10677
|
-
if (useCache) {
|
|
10678
|
-
const cached = cacheGet("linkedin-accounts", cacheKey);
|
|
10679
|
-
if (cached) {
|
|
10680
|
-
writeJsonEnvelope({
|
|
10681
|
-
ok: true,
|
|
10682
|
-
data: cached.data,
|
|
10683
|
-
cached: true,
|
|
10684
|
-
hints: accountHints2(cached.data, includeAll)
|
|
10685
|
-
});
|
|
10686
|
-
return;
|
|
10687
|
-
}
|
|
10688
|
-
}
|
|
10689
|
-
try {
|
|
10690
|
-
const params = {};
|
|
10691
|
-
if (includeAll) params["include-all"] = "true";
|
|
10692
|
-
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
10693
|
-
const data = await apiGet("/api/ads/linkedin/accounts", params);
|
|
10694
|
-
if (useCache) {
|
|
10695
|
-
cacheSet("linkedin-accounts", cacheKey, data, ACCOUNTS_TTL_MS);
|
|
10696
|
-
}
|
|
10697
|
-
const fmt = csvOrJson2(args);
|
|
10698
|
-
if (fmt !== "json") {
|
|
10699
|
-
writeAdsOutput(data, fmt);
|
|
10700
|
-
return;
|
|
10701
|
-
}
|
|
10702
|
-
writeJsonEnvelope({ ok: true, data, hints: accountHints2(data, includeAll) });
|
|
10703
|
-
} catch (err) {
|
|
10704
|
-
handleLinkedinError(err);
|
|
10705
|
-
}
|
|
10278
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10279
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false },
|
|
10280
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10281
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10282
|
+
limit: { type: "string", description: "Max rows (default 500)", required: false },
|
|
10283
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10284
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10706
10285
|
}
|
|
10707
10286
|
});
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
revenue: {
|
|
10721
|
-
name: "revenue",
|
|
10722
|
-
description: "Spend, conversions, conversion value. 'How much money are we making?'",
|
|
10723
|
-
metrics: [
|
|
10724
|
-
"costInUsd",
|
|
10725
|
-
"externalWebsiteConversions",
|
|
10726
|
-
"externalWebsitePostClickConversions",
|
|
10727
|
-
"externalWebsitePostViewConversions",
|
|
10728
|
-
"conversionValueInLocalCurrency",
|
|
10729
|
-
"oneClickLeads"
|
|
10730
|
-
]
|
|
10731
|
-
},
|
|
10732
|
-
funnel: {
|
|
10733
|
-
name: "funnel",
|
|
10734
|
-
description: "Impression \u2192 click \u2192 landing \u2192 form open \u2192 lead \u2192 conversion. 'Where do users drop off?'",
|
|
10735
|
-
metrics: [
|
|
10736
|
-
"impressions",
|
|
10737
|
-
"clicks",
|
|
10738
|
-
"landingPageClicks",
|
|
10739
|
-
"oneClickLeadFormOpens",
|
|
10740
|
-
"oneClickLeads",
|
|
10741
|
-
"externalWebsiteConversions",
|
|
10742
|
-
"costInUsd"
|
|
10743
|
-
],
|
|
10744
|
-
derived: ["ctr", "leadCompletionRate"]
|
|
10745
|
-
},
|
|
10746
|
-
engagement: {
|
|
10747
|
-
name: "engagement",
|
|
10748
|
-
description: "Likes, comments, shares, follows + viral spillover. TLA-aware \u2014 measures organic reach effect.",
|
|
10749
|
-
metrics: [
|
|
10750
|
-
"impressions",
|
|
10751
|
-
"likes",
|
|
10752
|
-
"comments",
|
|
10753
|
-
"shares",
|
|
10754
|
-
"reactions",
|
|
10755
|
-
"follows",
|
|
10756
|
-
"totalEngagements",
|
|
10757
|
-
"viralImpressions",
|
|
10758
|
-
"viralLikes",
|
|
10759
|
-
"viralShares",
|
|
10760
|
-
"viralOneClickLeads",
|
|
10761
|
-
"costInUsd"
|
|
10762
|
-
]
|
|
10763
|
-
},
|
|
10764
|
-
video: {
|
|
10765
|
-
name: "video",
|
|
10766
|
-
description: "Video watch quartiles + cost. 'How are video creatives holding attention?'",
|
|
10767
|
-
metrics: [
|
|
10768
|
-
"videoStarts",
|
|
10769
|
-
"videoFirstQuartileCompletions",
|
|
10770
|
-
"videoMidpointCompletions",
|
|
10771
|
-
"videoThirdQuartileCompletions",
|
|
10772
|
-
"videoCompletions",
|
|
10773
|
-
"videoViews",
|
|
10774
|
-
"fullScreenPlays",
|
|
10775
|
-
"costInUsd",
|
|
10776
|
-
"impressions"
|
|
10777
|
-
]
|
|
10778
|
-
},
|
|
10779
|
-
"lead-gen": {
|
|
10780
|
-
name: "lead-gen",
|
|
10781
|
-
description: "Lead Gen Form opens + completions. CLI derives form-completion-rate.",
|
|
10782
|
-
metrics: [
|
|
10783
|
-
"oneClickLeadFormOpens",
|
|
10784
|
-
"oneClickLeads",
|
|
10785
|
-
"viralOneClickLeads",
|
|
10786
|
-
"viralOneClickLeadFormOpens",
|
|
10787
|
-
"costInUsd"
|
|
10788
|
-
],
|
|
10789
|
-
derived: ["leadCompletionRate"]
|
|
10790
|
-
},
|
|
10791
|
-
inmail: {
|
|
10792
|
-
name: "inmail",
|
|
10793
|
-
description: "Sponsored Message / Conversation Ads \u2014 sends, opens, clicks, lead-gen taps.",
|
|
10794
|
-
metrics: [
|
|
10795
|
-
"sends",
|
|
10796
|
-
"opens",
|
|
10797
|
-
"clicks",
|
|
10798
|
-
"costInUsd",
|
|
10799
|
-
"leadGenerationMailContactInfoShares",
|
|
10800
|
-
"leadGenerationMailInterestedClicks"
|
|
10801
|
-
]
|
|
10802
|
-
},
|
|
10803
|
-
document: {
|
|
10804
|
-
name: "document",
|
|
10805
|
-
description: "Document Ads \u2014 quartile read-through, completion, downloads.",
|
|
10806
|
-
metrics: [
|
|
10807
|
-
"documentFirstQuartileCompletions",
|
|
10808
|
-
"documentMidpointCompletions",
|
|
10809
|
-
"documentThirdQuartileCompletions",
|
|
10810
|
-
"documentCompletions",
|
|
10811
|
-
"downloadClicks",
|
|
10812
|
-
"costInUsd",
|
|
10813
|
-
"impressions"
|
|
10814
|
-
]
|
|
10815
|
-
},
|
|
10816
|
-
ranking: {
|
|
10817
|
-
name: "ranking",
|
|
10818
|
-
description: "Frequency + reach metrics for fatigue detection. CLI derives frequency = impressions/uniqueImpressions.",
|
|
10819
|
-
metrics: ["impressions", "clicks", "costInUsd", "approximateUniqueImpressions"],
|
|
10820
|
-
derived: ["frequency", "ctr"]
|
|
10821
|
-
},
|
|
10822
|
-
identity: {
|
|
10823
|
-
name: "identity",
|
|
10824
|
-
description: "Just spend + impressions. Cheap roll-up.",
|
|
10825
|
-
metrics: ["costInUsd", "impressions"]
|
|
10287
|
+
registerSchema({
|
|
10288
|
+
command: "ads.linkedin.campaigns",
|
|
10289
|
+
description: "List LinkedIn campaigns. Returns audit-relevant settings (audienceExpansionEnabled, offsiteDeliveryEnabled, optimizationTargetType, costType, budgets, runSchedule, targetingCriteria).",
|
|
10290
|
+
args: {
|
|
10291
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10292
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10293
|
+
"campaign-group-id": { type: "string", description: "Filter by campaign group", required: false },
|
|
10294
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10295
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10296
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10297
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10298
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10826
10299
|
}
|
|
10827
|
-
};
|
|
10828
|
-
|
|
10829
|
-
"
|
|
10830
|
-
"
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
"company-size",
|
|
10851
|
-
"country",
|
|
10852
|
-
"region",
|
|
10853
|
-
"device",
|
|
10854
|
-
"placement",
|
|
10855
|
-
"serving-location",
|
|
10856
|
-
"card-index",
|
|
10857
|
-
"objective",
|
|
10858
|
-
"conversation-node",
|
|
10859
|
-
"conversation-node-button"
|
|
10860
|
-
];
|
|
10861
|
-
var ALL_LEVELS = ["account", "campaign-group", "campaign", "creative"];
|
|
10862
|
-
function listIntents() {
|
|
10863
|
-
return Object.values(INTENTS);
|
|
10864
|
-
}
|
|
10865
|
-
function isPivot(slug) {
|
|
10866
|
-
return ALL_PIVOTS.includes(slug);
|
|
10867
|
-
}
|
|
10868
|
-
function isLevel(slug) {
|
|
10869
|
-
return ALL_LEVELS.includes(slug);
|
|
10870
|
-
}
|
|
10871
|
-
function composeFields(intent, _level, metricsOverride) {
|
|
10872
|
-
const base = metricsOverride && metricsOverride.length > 0 ? metricsOverride : INTENTS[intent].metrics;
|
|
10873
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10874
|
-
const out = [];
|
|
10875
|
-
for (const f of ["pivotValues", "dateRange", ...base]) {
|
|
10876
|
-
if (!seen.has(f)) {
|
|
10877
|
-
seen.add(f);
|
|
10878
|
-
out.push(f);
|
|
10879
|
-
}
|
|
10300
|
+
});
|
|
10301
|
+
registerSchema({
|
|
10302
|
+
command: "ads.linkedin.creatives",
|
|
10303
|
+
description: "List LinkedIn creatives. Compact by default: id, urn, campaign, status, review status, format, and the resolved destination (landingUrl, or postUrn for post-based ads). The opaque 'content' block is NOT returned unless you pass --full; --fields takes dotted paths for anything narrower.",
|
|
10304
|
+
args: {
|
|
10305
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10306
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10307
|
+
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
10308
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10309
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10310
|
+
fields: {
|
|
10311
|
+
type: "string",
|
|
10312
|
+
description: "CSV of dotted paths to return instead of the compact row, e.g. id,content.textAd.landingPage",
|
|
10313
|
+
required: false
|
|
10314
|
+
},
|
|
10315
|
+
full: {
|
|
10316
|
+
type: "boolean",
|
|
10317
|
+
description: "Return the complete raw records, including the opaque 'content' block",
|
|
10318
|
+
required: false
|
|
10319
|
+
},
|
|
10320
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10321
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10322
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10880
10323
|
}
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10324
|
+
});
|
|
10325
|
+
registerSchema({
|
|
10326
|
+
command: "ads.linkedin.analytics",
|
|
10327
|
+
description: "Performance reporting (3-axis: --level \xD7 --intent \xD7 --pivot). LinkedIn's superpower: pivot=job-title|company|industry|seniority. Demographic pivots auto-force granularity=ALL and surface a delayed-data warning.",
|
|
10328
|
+
args: {
|
|
10329
|
+
level: {
|
|
10330
|
+
type: "string",
|
|
10331
|
+
description: "account|campaign-group|campaign|creative",
|
|
10332
|
+
required: false,
|
|
10333
|
+
default: "account"
|
|
10334
|
+
},
|
|
10335
|
+
"account-id": { type: "string", description: "When level=account", required: false },
|
|
10336
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10337
|
+
"campaign-group-id": { type: "string", description: "CSV when level=campaign-group", required: false },
|
|
10338
|
+
"campaign-id": { type: "string", description: "CSV when level=campaign", required: false },
|
|
10339
|
+
"creative-id": { type: "string", description: "CSV when level=creative", required: false },
|
|
10340
|
+
ids: { type: "string", description: "Generic CSV alternative", required: false },
|
|
10341
|
+
intent: {
|
|
10342
|
+
type: "string",
|
|
10343
|
+
description: "Field bundle",
|
|
10344
|
+
required: false,
|
|
10345
|
+
default: "baseline",
|
|
10346
|
+
enum: [
|
|
10347
|
+
"baseline",
|
|
10348
|
+
"revenue",
|
|
10349
|
+
"funnel",
|
|
10350
|
+
"engagement",
|
|
10351
|
+
"video",
|
|
10352
|
+
"lead-gen",
|
|
10353
|
+
"inmail",
|
|
10354
|
+
"document",
|
|
10355
|
+
"ranking",
|
|
10356
|
+
"identity"
|
|
10357
|
+
]
|
|
10358
|
+
},
|
|
10359
|
+
pivot: {
|
|
10360
|
+
type: "string",
|
|
10361
|
+
description: "Demographic / firmographic dim",
|
|
10362
|
+
required: false,
|
|
10363
|
+
default: "none",
|
|
10364
|
+
enum: [
|
|
10365
|
+
"none",
|
|
10366
|
+
"campaign",
|
|
10367
|
+
"campaign-group",
|
|
10368
|
+
"creative",
|
|
10369
|
+
"company",
|
|
10370
|
+
"account",
|
|
10371
|
+
"conversion",
|
|
10372
|
+
"job-title",
|
|
10373
|
+
"job-function",
|
|
10374
|
+
"seniority",
|
|
10375
|
+
"industry",
|
|
10376
|
+
"company-size",
|
|
10377
|
+
"country",
|
|
10378
|
+
"region",
|
|
10379
|
+
"device",
|
|
10380
|
+
"placement",
|
|
10381
|
+
"serving-location",
|
|
10382
|
+
"card-index",
|
|
10383
|
+
"objective",
|
|
10384
|
+
"conversation-node",
|
|
10385
|
+
"conversation-node-button"
|
|
10386
|
+
]
|
|
10387
|
+
},
|
|
10388
|
+
metrics: { type: "string", description: "CSV metric override (escape hatch)", required: false },
|
|
10389
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10390
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10391
|
+
"last-days": { type: "string", description: "Window (default 7)", required: false },
|
|
10392
|
+
granularity: {
|
|
10393
|
+
type: "string",
|
|
10394
|
+
description: "DAILY|MONTHLY|YEARLY|ALL",
|
|
10395
|
+
required: false,
|
|
10396
|
+
default: "DAILY",
|
|
10397
|
+
enum: ["DAILY", "MONTHLY", "YEARLY", "ALL"]
|
|
10398
|
+
},
|
|
10399
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10400
|
+
"no-sort": { type: "boolean", description: "Skip default sort", required: false },
|
|
10401
|
+
"list-intents": { type: "boolean", description: "List intents and exit", required: false },
|
|
10402
|
+
"list-pivots": { type: "boolean", description: "List pivots and exit", required: false },
|
|
10403
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10404
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10893
10405
|
}
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10406
|
+
});
|
|
10407
|
+
registerSchema({
|
|
10408
|
+
command: "ads.linkedin.demographics",
|
|
10409
|
+
description: "Sweep all firmographic pivots (job-title, company, industry, seniority, function, company-size) in one call. Returns top-N rows per pivot.",
|
|
10410
|
+
args: {
|
|
10411
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10412
|
+
pivots: { type: "string", description: "CSV of pivots (default: all firmographic)", required: false },
|
|
10413
|
+
"top-n": { type: "string", description: "Top rows per pivot (default 10)", required: false },
|
|
10414
|
+
intent: { type: "string", description: "Field bundle", required: false, default: "baseline" },
|
|
10415
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10416
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10417
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10418
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10419
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10420
|
+
}
|
|
10421
|
+
});
|
|
10422
|
+
registerSchema({
|
|
10423
|
+
command: "ads.linkedin.top-companies",
|
|
10424
|
+
description: "Top companies whose employees saw / clicked / converted. Wrapper for analytics --pivot company. The ABM feedback loop.",
|
|
10425
|
+
args: {
|
|
10426
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10427
|
+
intent: { type: "string", description: "Field bundle (default baseline)", required: false },
|
|
10428
|
+
"top-n": { type: "string", description: "Top rows by impressions (default 25)", required: false },
|
|
10429
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10430
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10431
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10432
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10433
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10434
|
+
}
|
|
10435
|
+
});
|
|
10436
|
+
registerSchema({
|
|
10437
|
+
command: "ads.linkedin.facets.list",
|
|
10438
|
+
description: "List every targeting facet LinkedIn supports (industries, seniorities, titles, employers, growthRate, etc).",
|
|
10439
|
+
args: {
|
|
10440
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10441
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10442
|
+
}
|
|
10443
|
+
});
|
|
10444
|
+
registerSchema({
|
|
10445
|
+
command: "ads.linkedin.facets.values",
|
|
10446
|
+
description: "Look up entity values for a facet \u2014 full list (q=adTargetingFacet) or typeahead search (q=typeahead, with --query).",
|
|
10447
|
+
args: {
|
|
10448
|
+
facet: { type: "string", description: "Facet name (industries) or URN", required: true },
|
|
10449
|
+
query: { type: "string", description: "Typeahead query (auto-switches finder)", required: false },
|
|
10450
|
+
finder: { type: "string", description: "adTargetingFacet|typeahead|similarEntities", required: false },
|
|
10451
|
+
locale: { type: "string", description: "Locale (default en_US)", required: false },
|
|
10452
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10453
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10454
|
+
}
|
|
10455
|
+
});
|
|
10456
|
+
registerSchema({
|
|
10457
|
+
command: "ads.linkedin.audience-size",
|
|
10458
|
+
description: "Estimate audience size for a targetingCriteria payload. Returns total + active + playbook \xA704 sweet-spot warnings.",
|
|
10459
|
+
args: {
|
|
10460
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10461
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10462
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10463
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10464
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10465
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10466
|
+
}
|
|
10467
|
+
});
|
|
10468
|
+
registerSchema({
|
|
10469
|
+
command: "ads.linkedin.bid-pricing",
|
|
10470
|
+
description: "LinkedIn's suggested bid range + playbook \xA706 floor (2/3 of suggested). Inputs: targeting + objective + cost type.",
|
|
10471
|
+
args: {
|
|
10472
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10473
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10474
|
+
objective: {
|
|
10475
|
+
type: "string",
|
|
10476
|
+
description: "Objective type (WEBSITE_CONVERSION, LEAD_GENERATION, etc.)",
|
|
10477
|
+
required: true
|
|
10478
|
+
},
|
|
10479
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true, enum: ["CPC", "CPM", "CPV", "CPS"] },
|
|
10480
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10481
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10482
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10483
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10484
|
+
}
|
|
10485
|
+
});
|
|
10486
|
+
registerSchema({
|
|
10487
|
+
command: "ads.linkedin.forecast",
|
|
10488
|
+
description: "Forecast reach + impressions + clicks + spend for a hypothetical campaign. Useful for \xA714 greenfield planning.",
|
|
10489
|
+
args: {
|
|
10490
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10491
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10492
|
+
objective: { type: "string", description: "Objective type", required: true },
|
|
10493
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true },
|
|
10494
|
+
"daily-budget": { type: "string", description: "e.g. '200 USD'", required: false },
|
|
10495
|
+
"total-budget": { type: "string", description: "e.g. '6000 USD'", required: false },
|
|
10496
|
+
bid: { type: "string", description: "Bid e.g. '8 USD'", required: false },
|
|
10497
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10498
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10499
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10500
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10501
|
+
}
|
|
10502
|
+
});
|
|
10503
|
+
registerSchema({
|
|
10504
|
+
command: "ads.linkedin.leads",
|
|
10505
|
+
description: "List Lead Gen Form responses (90-day LinkedIn retention \u2014 sync to CRM via this endpoint). Filter by --form-id, --campaign-id, or --since-days.",
|
|
10506
|
+
args: {
|
|
10507
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10508
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10509
|
+
"form-id": { type: "string", description: "Filter by form ID", required: false },
|
|
10510
|
+
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
10511
|
+
"since-days": { type: "string", description: "Last N days", required: false },
|
|
10512
|
+
"since-ms": { type: "string", description: "Epoch ms (alternative to --since-days)", required: false },
|
|
10513
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10514
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10515
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10516
|
+
}
|
|
10517
|
+
});
|
|
10518
|
+
registerSchema({
|
|
10519
|
+
command: "ads.linkedin.conversions.list",
|
|
10520
|
+
description: "List conversion rules (Insight Tag + Conversions API).",
|
|
10521
|
+
args: {
|
|
10522
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10523
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10524
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10525
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10900
10526
|
}
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10527
|
+
});
|
|
10528
|
+
registerSchema({
|
|
10529
|
+
command: "ads.linkedin.conversions.health",
|
|
10530
|
+
description: "Playbook \xA707 5-point health check: rules enabled? lead/purchase event? CAPI fired in 7d? view-through \u22647d? lead de-dup correct?",
|
|
10531
|
+
args: {
|
|
10532
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10533
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10534
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10535
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10907
10536
|
}
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
"
|
|
10914
|
-
"
|
|
10915
|
-
"
|
|
10916
|
-
"
|
|
10917
|
-
"
|
|
10918
|
-
"
|
|
10919
|
-
"inmail",
|
|
10920
|
-
"document",
|
|
10921
|
-
"ranking",
|
|
10922
|
-
"identity"
|
|
10923
|
-
];
|
|
10924
|
-
if (!known.includes(v)) {
|
|
10925
|
-
handleLinkedinError(new Error(`Invalid --intent "${raw}". Run --list-intents to see options.`));
|
|
10537
|
+
});
|
|
10538
|
+
registerSchema({
|
|
10539
|
+
command: "ads.linkedin.conversation",
|
|
10540
|
+
description: "Per-button click rates inside Sponsored Messaging / Conversation Ads. Wraps analytics --pivot conversation-node-button.",
|
|
10541
|
+
args: {
|
|
10542
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10543
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10544
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10545
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10546
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10547
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10926
10548
|
}
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10549
|
+
});
|
|
10550
|
+
registerSchema({
|
|
10551
|
+
command: "ads.linkedin.audit",
|
|
10552
|
+
description: "Run a 30+ check playbook audit (Settings, Tracking, Audience, Campaigns, Creative, Performance, Bidding, Compliance, Hygiene). Each finding: {id, area, check, status, severity, evidence, fix} with playbook citations. --format md renders a deliverable-ready table.",
|
|
10553
|
+
args: {
|
|
10554
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10555
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10556
|
+
"campaign-id": { type: "string", description: "Narrow to a single campaign", required: false },
|
|
10557
|
+
"window-days": { type: "string", description: "Performance lookback (default 30)", required: false },
|
|
10558
|
+
severity: { type: "string", description: "CSV severity filter (critical,high,medium,low)", required: false },
|
|
10559
|
+
area: { type: "string", description: "CSV area filter", required: false },
|
|
10560
|
+
format: { type: "string", description: "json | md", required: false, default: "json" },
|
|
10561
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false }
|
|
10934
10562
|
}
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10563
|
+
});
|
|
10564
|
+
var writeAccountArgs = {
|
|
10565
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N", required: false },
|
|
10566
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false }
|
|
10567
|
+
};
|
|
10568
|
+
registerSchema({
|
|
10569
|
+
command: "ads.linkedin.campaign-groups.create",
|
|
10570
|
+
description: "Stage a new campaign group (staged until publish; review with `baker ads linkedin draft`). Returns a li_temp_* ref that later `campaigns create --group` calls can reference.",
|
|
10571
|
+
args: {
|
|
10572
|
+
...writeAccountArgs,
|
|
10573
|
+
name: { type: "string", description: "Name (\u2264100 chars)", required: true },
|
|
10574
|
+
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)", required: true },
|
|
10575
|
+
end: { type: "string", description: "Run schedule end", required: false },
|
|
10576
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10577
|
+
currency: {
|
|
10578
|
+
type: "string",
|
|
10579
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10580
|
+
required: false
|
|
10581
|
+
},
|
|
10582
|
+
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10583
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10939
10584
|
}
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10585
|
+
});
|
|
10586
|
+
registerSchema({
|
|
10587
|
+
command: "ads.linkedin.campaign-groups.update",
|
|
10588
|
+
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.",
|
|
10589
|
+
args: {
|
|
10590
|
+
id: {
|
|
10591
|
+
type: "positional",
|
|
10592
|
+
description: "Campaign group id/URN, or li_temp_* ref staged in this chat",
|
|
10593
|
+
required: true
|
|
10594
|
+
},
|
|
10595
|
+
...writeAccountArgs,
|
|
10596
|
+
name: { type: "string", description: "New name", required: false },
|
|
10597
|
+
start: { type: "string", description: "New schedule start", required: false },
|
|
10598
|
+
end: { type: "string", description: "New schedule end", required: false },
|
|
10599
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10600
|
+
currency: {
|
|
10601
|
+
type: "string",
|
|
10602
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10603
|
+
required: false
|
|
10604
|
+
},
|
|
10605
|
+
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10606
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10946
10607
|
}
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10608
|
+
});
|
|
10609
|
+
registerSchema({
|
|
10610
|
+
command: "ads.linkedin.campaigns.create",
|
|
10611
|
+
description: "Stage a new campaign. --group accepts a real id/URN or a li_temp_* ref staged earlier in this chat. Targeting goes in --targeting-file as LinkedIn criteria: {include:{and:[{or:{'urn:li:adTargetingFacet:locations':['urn:li:geo:\u2026']}}]}} \u2014 find facet values with `baker ads linkedin facets values`. Playbook defaults: manual bid (--bid), audience expansion off, LAN off. Staged until publish.",
|
|
10612
|
+
args: {
|
|
10613
|
+
...writeAccountArgs,
|
|
10614
|
+
name: { type: "string", description: "Name (\u2264255 chars)", required: true },
|
|
10615
|
+
group: { type: "string", description: "Campaign group id/URN or li_temp_* ref", required: true },
|
|
10616
|
+
type: { type: "string", description: "SPONSORED_UPDATES|TEXT_AD|SPONSORED_INMAILS|DYNAMIC", required: true },
|
|
10617
|
+
objective: { type: "string", description: "LEAD_GENERATION|WEBSITE_VISIT|BRAND_AWARENESS|\u2026", required: false },
|
|
10618
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: true },
|
|
10619
|
+
bid: { type: "string", description: "Manual bid amount (unitCost)", required: false },
|
|
10620
|
+
"daily-budget": { type: "string", description: "Daily budget amount", required: false },
|
|
10621
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10622
|
+
currency: {
|
|
10623
|
+
type: "string",
|
|
10624
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10625
|
+
required: false
|
|
10626
|
+
},
|
|
10627
|
+
start: { type: "string", description: "Run schedule start", required: false },
|
|
10628
|
+
end: { type: "string", description: "Run schedule end", required: false },
|
|
10629
|
+
locale: { type: "string", description: "Profile locale like en_US (default en_US)", required: false },
|
|
10630
|
+
"targeting-file": { type: "string", description: "JSON file with targetingCriteria", required: true },
|
|
10631
|
+
"audience-expansion": { type: "string", description: "on|off (playbook: off; default off)", required: false },
|
|
10632
|
+
lan: {
|
|
10633
|
+
type: "string",
|
|
10634
|
+
description: "LinkedIn Audience Network on|off (playbook: off; default off)",
|
|
10635
|
+
required: false
|
|
10636
|
+
},
|
|
10637
|
+
ctv: { type: "string", description: "Connected TV on|off (default off)", required: false },
|
|
10638
|
+
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10639
|
+
"associated-entity": { type: "string", description: "Organization URN", required: false },
|
|
10640
|
+
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10641
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10953
10642
|
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10643
|
+
});
|
|
10644
|
+
registerSchema({
|
|
10645
|
+
command: "ads.linkedin.campaigns.update",
|
|
10646
|
+
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.",
|
|
10647
|
+
args: {
|
|
10648
|
+
id: { type: "positional", description: "Campaign id/URN, or li_temp_* ref staged in this chat", required: true },
|
|
10649
|
+
...writeAccountArgs,
|
|
10650
|
+
name: { type: "string", description: "New name", required: false },
|
|
10651
|
+
objective: { type: "string", description: "New objective", required: false },
|
|
10652
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: false },
|
|
10653
|
+
bid: { type: "string", description: "New manual bid", required: false },
|
|
10654
|
+
"daily-budget": { type: "string", description: "New daily budget", required: false },
|
|
10655
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10656
|
+
currency: {
|
|
10657
|
+
type: "string",
|
|
10658
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10659
|
+
required: false
|
|
10660
|
+
},
|
|
10661
|
+
start: { type: "string", description: "New schedule start", required: false },
|
|
10662
|
+
end: { type: "string", description: "New schedule end", required: false },
|
|
10663
|
+
"targeting-file": { type: "string", description: "JSON file with replacement targetingCriteria", required: false },
|
|
10664
|
+
"audience-expansion": { type: "string", description: "on|off", required: false },
|
|
10665
|
+
lan: { type: "string", description: "on|off", required: false },
|
|
10666
|
+
ctv: { type: "string", description: "on|off", required: false },
|
|
10667
|
+
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10668
|
+
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10669
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10960
10670
|
}
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10671
|
+
});
|
|
10672
|
+
registerSchema({
|
|
10673
|
+
command: "ads.linkedin.campaigns.url-params",
|
|
10674
|
+
description: "Start here for UTMs: stage the URL tracking parameters on an ad set. LinkedIn appends them to the link of EVERY ad in the ad set, including ads already running, so this is the level to set campaign-wide UTMs at \u2014 editing each ad's landing URL instead risks the same key landing twice. --param takes fixed values (repeatable or &-joined); --dynamic takes a value LinkedIn fills in per ad (ACCOUNT_ID, ACCOUNT_NAME, CAMPAIGN_GROUP_ID, CAMPAIGN_GROUP_NAME, CAMPAIGN_ID, CAMPAIGN_NAME, CREATIVE_ID, CREATIVE_NAME). Applies on chat publish; message and conversation ads are unaffected.",
|
|
10675
|
+
args: {
|
|
10676
|
+
id: { type: "positional", description: "Ad set (campaign) id or URN", required: true },
|
|
10677
|
+
...writeAccountArgs,
|
|
10678
|
+
param: {
|
|
10679
|
+
type: "string",
|
|
10680
|
+
description: 'Fixed key=value, repeatable or &-joined \u2014 e.g. "utm_source=linkedin&utm_medium=paid-social"',
|
|
10681
|
+
required: false
|
|
10682
|
+
},
|
|
10683
|
+
dynamic: {
|
|
10684
|
+
type: "string",
|
|
10685
|
+
description: "key=PLACEHOLDER filled in per ad, repeatable \u2014 e.g. utm_campaign=CAMPAIGN_NAME",
|
|
10686
|
+
required: false
|
|
10687
|
+
},
|
|
10688
|
+
clear: { type: "boolean", description: "Remove every tracking parameter from this ad set", required: false },
|
|
10689
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10690
|
+
}
|
|
10691
|
+
});
|
|
10692
|
+
var statusSugarArgs = {
|
|
10693
|
+
id: {
|
|
10694
|
+
type: "positional",
|
|
10695
|
+
description: "Entity id or URN \u2014 comma-separate several to change them all in one call",
|
|
10696
|
+
required: false
|
|
10697
|
+
},
|
|
10698
|
+
"ids-file": { type: "string", description: "File of ids, one per line or comma-separated", required: false },
|
|
10699
|
+
...writeAccountArgs
|
|
10700
|
+
};
|
|
10701
|
+
var MULTI_ID_NOTE = "Takes one id or many: comma-separate the ids, or pass --ids-file. Several ids stage together as one set \u2014 all of them or none. The list form is for real ids/URNs only: a li_temp_* ref AMENDS what this chat already staged, and an amend must be passed on its own.";
|
|
10702
|
+
registerSchema({
|
|
10703
|
+
command: "ads.linkedin.campaigns.pause",
|
|
10704
|
+
description: `Stage pausing a campaign (status \u2192 PAUSED on publish). ${MULTI_ID_NOTE}`,
|
|
10705
|
+
args: statusSugarArgs
|
|
10706
|
+
});
|
|
10707
|
+
registerSchema({
|
|
10708
|
+
command: "ads.linkedin.campaigns.resume",
|
|
10709
|
+
description: `Stage resuming a paused campaign (status \u2192 ACTIVE on publish). ${MULTI_ID_NOTE}`,
|
|
10710
|
+
args: statusSugarArgs
|
|
10711
|
+
});
|
|
10712
|
+
registerSchema({
|
|
10713
|
+
command: "ads.linkedin.campaigns.archive",
|
|
10714
|
+
description: `Stage archiving a campaign (status \u2192 ARCHIVED on publish). ${MULTI_ID_NOTE}`,
|
|
10715
|
+
args: statusSugarArgs
|
|
10716
|
+
});
|
|
10717
|
+
registerSchema({
|
|
10718
|
+
command: "ads.linkedin.campaign-groups.pause",
|
|
10719
|
+
description: `Stage pausing a campaign group. ${MULTI_ID_NOTE}`,
|
|
10720
|
+
args: statusSugarArgs
|
|
10721
|
+
});
|
|
10722
|
+
registerSchema({
|
|
10723
|
+
command: "ads.linkedin.campaign-groups.resume",
|
|
10724
|
+
description: `Stage resuming a campaign group. ${MULTI_ID_NOTE}`,
|
|
10725
|
+
args: statusSugarArgs
|
|
10726
|
+
});
|
|
10727
|
+
registerSchema({
|
|
10728
|
+
command: "ads.linkedin.creatives.pause",
|
|
10729
|
+
description: `Stage pausing a creative (intendedStatus \u2192 PAUSED on publish). Playbook early-kill rule: >30 impressions with 0 clicks. ${MULTI_ID_NOTE}`,
|
|
10730
|
+
args: statusSugarArgs
|
|
10731
|
+
});
|
|
10732
|
+
registerSchema({
|
|
10733
|
+
command: "ads.linkedin.creatives.resume",
|
|
10734
|
+
description: `Stage resuming a paused creative. ${MULTI_ID_NOTE}`,
|
|
10735
|
+
args: statusSugarArgs
|
|
10736
|
+
});
|
|
10737
|
+
var duplicateArgs = {
|
|
10738
|
+
id: { type: "positional", description: "Source id or URN", required: true },
|
|
10739
|
+
...writeAccountArgs,
|
|
10740
|
+
name: { type: "string", description: "Name for the copy", required: false },
|
|
10741
|
+
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10742
|
+
};
|
|
10743
|
+
registerSchema({
|
|
10744
|
+
command: "ads.linkedin.campaigns.duplicate",
|
|
10745
|
+
description: "Stage a copy of an existing campaign as a new DRAFT create (playbook: duplicate, never link-to-original). Reads the source at stage time; override fields via --name/--file.",
|
|
10746
|
+
args: duplicateArgs
|
|
10747
|
+
});
|
|
10748
|
+
registerSchema({
|
|
10749
|
+
command: "ads.linkedin.campaign-groups.duplicate",
|
|
10750
|
+
description: "Stage a copy of an existing campaign group as a new DRAFT create.",
|
|
10751
|
+
args: duplicateArgs
|
|
10752
|
+
});
|
|
10753
|
+
registerSchema({
|
|
10754
|
+
command: "ads.linkedin.creatives.duplicate",
|
|
10755
|
+
description: "Stage a copy of an existing ad, optionally with new content. Direct-content ads (text/spotlight/follower/jobs) copy field-by-field, so a copy is how their immutable content changes (they can also update in place via creatives.update). Post-based ads re-sponsor the SAME post \u2014 the post owns the copy, media and destination, so content flags are rejected here: stage creatives.create with the new content instead, then pause the old ad. --replace stages pausing the original, gated on the new ad publishing successfully, and the copy inherits the source's status.",
|
|
10756
|
+
args: {
|
|
10757
|
+
id: { type: "positional", description: "Source creative id or URN", required: true },
|
|
10758
|
+
...writeAccountArgs,
|
|
10759
|
+
campaign: { type: "string", description: "Stage the copy under a different ad set", required: false },
|
|
10760
|
+
"intended-status": { type: "string", description: "Status for the copy (default DRAFT)", required: false },
|
|
10761
|
+
replace: { type: "boolean", description: "Also pause the original once the new ad publishes", required: false },
|
|
10762
|
+
headline: { type: "string", description: "New headline", required: false },
|
|
10763
|
+
description: { type: "string", description: "New description (text/spotlight)", required: false },
|
|
10764
|
+
intro: { type: "string", description: "New commentary / intro text", required: false },
|
|
10765
|
+
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10766
|
+
cta: { type: "string", description: "New CTA", required: false },
|
|
10767
|
+
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10768
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10769
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10770
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10771
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10772
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10773
|
+
title: { type: "string", description: "Media title", required: false },
|
|
10774
|
+
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10964
10775
|
}
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10776
|
+
});
|
|
10777
|
+
registerSchema({
|
|
10778
|
+
command: "ads.linkedin.creatives.create",
|
|
10779
|
+
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.",
|
|
10780
|
+
args: {
|
|
10781
|
+
...writeAccountArgs,
|
|
10782
|
+
campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
|
|
10783
|
+
format: {
|
|
10784
|
+
type: "string",
|
|
10785
|
+
description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article",
|
|
10786
|
+
required: true
|
|
10787
|
+
},
|
|
10788
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10789
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10790
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10791
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10792
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10793
|
+
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10794
|
+
headline: { type: "string", description: "Headline", required: false },
|
|
10795
|
+
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10796
|
+
"landing-url": { type: "string", description: "https destination URL", required: false },
|
|
10797
|
+
cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026", required: false },
|
|
10798
|
+
"cta-label": { type: "string", description: "Spotlight CTA label (\u226418 chars)", required: false },
|
|
10799
|
+
"post-urn": { type: "string", description: "TLA: post to sponsor (urn:li:share|ugcPost:*)", required: false },
|
|
10800
|
+
"event-urn": {
|
|
10801
|
+
type: "string",
|
|
10802
|
+
description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)",
|
|
10803
|
+
required: false
|
|
10804
|
+
},
|
|
10805
|
+
"article-url": {
|
|
10806
|
+
type: "string",
|
|
10807
|
+
description: "Article ads: https URL of the article/newsletter to sponsor",
|
|
10808
|
+
required: false
|
|
10809
|
+
},
|
|
10810
|
+
"thumbnail-image-id": {
|
|
10811
|
+
type: "string",
|
|
10812
|
+
description: "Article ads: Baker image id for the link thumbnail",
|
|
10813
|
+
required: false
|
|
10814
|
+
},
|
|
10815
|
+
"thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail", required: false },
|
|
10816
|
+
"intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10817
|
+
title: { type: "string", description: "Media title (image/video) or article title", required: false },
|
|
10818
|
+
file: { type: "string", description: "JSON file with the full content object", required: false }
|
|
10979
10819
|
}
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
name: "analytics",
|
|
10985
|
-
description: `Performance reporting \u2014 the workhorse for AI agents.
|
|
10986
|
-
|
|
10987
|
-
Three-axis design (LinkedIn's superpower over Meta/Google):
|
|
10988
|
-
--level account | campaign-group | campaign | creative
|
|
10989
|
-
--intent baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity
|
|
10990
|
-
--pivot none | job-title | company | industry | seniority | function | company-size | country | region
|
|
10991
|
-
| device | placement | serving-location | card-index | objective
|
|
10992
|
-
| conversation-node | conversation-node-button
|
|
10993
|
-
|
|
10994
|
-
Smart defaults:
|
|
10995
|
-
--level account, --intent baseline, --pivot none, --granularity DAILY, last 7 days
|
|
10996
|
-
Pivot on a MEMBER_* dim \u2192 granularity auto-forced to ALL (LinkedIn rejects DAILY+demographic)
|
|
10997
|
-
Demographic data delayed 12-24h with \u22653-event privacy floor \u2014 the CLI flags this in warnings.
|
|
10998
|
-
Derived metrics injected client-side: ctr, cpc, cpm, frequency, leadCompletionRate
|
|
10999
|
-
|
|
11000
|
-
Examples \u2014 common AI questions:
|
|
11001
|
-
# "How is this account doing this week?"
|
|
11002
|
-
baker ads linkedin analytics
|
|
11003
|
-
|
|
11004
|
-
# "Who are we reaching, by job title?" (the LinkedIn killer)
|
|
11005
|
-
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot job-title --intent baseline --last-days 30
|
|
11006
|
-
|
|
11007
|
-
# "Top companies seeing my ads" (ABM feedback loop)
|
|
11008
|
-
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot company --last-days 30
|
|
11009
|
-
|
|
11010
|
-
# "Revenue by campaign over Q1"
|
|
11011
|
-
baker ads linkedin analytics --level campaign --campaign-id 1,2,3 --intent revenue --start 2026-01-01 --end 2026-03-31 --granularity MONTHLY
|
|
11012
|
-
|
|
11013
|
-
# "How is the lead form converting?"
|
|
11014
|
-
baker ads linkedin analytics --level campaign --campaign-id 1234 --intent lead-gen
|
|
11015
|
-
|
|
11016
|
-
# Custom field set (escape hatch)
|
|
11017
|
-
baker ads linkedin analytics --metrics impressions,clicks,oneClickLeads --intent identity`
|
|
11018
|
-
},
|
|
10820
|
+
});
|
|
10821
|
+
registerSchema({
|
|
10822
|
+
command: "ads.linkedin.creatives.update",
|
|
10823
|
+
description: `Stage changes to an existing creative. Direct-content ads (text/spotlight/follower/jobs) update copy/media/URL IN PLACE \u2014 pass only the changed fields (headline, description, landing-url, image-id, \u2026); the ad's current content carries over. Post-based ads (image/video/carousel/\u2026) sponsor a post that owns the copy, media and destination \u2014 neither this command nor creatives.duplicate can change it; stage creatives.create with the new content, then pause the old ad. Passing a li_temp_* ref AMENDS the creative staged in this chat instead. ${MULTI_ID_NOTE} Applies on chat publish.`,
|
|
11019
10824
|
args: {
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
"
|
|
11026
|
-
ids: { type: "string", description: "Generic CSV of IDs at the chosen level (alternative to per-level flags)" },
|
|
11027
|
-
intent: {
|
|
10825
|
+
id: {
|
|
10826
|
+
type: "positional",
|
|
10827
|
+
description: "Creative id/URN \u2014 comma-separate several. A li_temp_* ref amends what this chat staged and must be passed on its own.",
|
|
10828
|
+
required: false
|
|
10829
|
+
},
|
|
10830
|
+
"ids-file": {
|
|
11028
10831
|
type: "string",
|
|
11029
|
-
description: "
|
|
10832
|
+
description: "File of creative ids, one per line or comma-separated",
|
|
10833
|
+
required: false
|
|
11030
10834
|
},
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
}
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
process.stderr.write(`note: pivot=${pivot} forces --granularity ALL (LinkedIn rejects DAILY + demographic).
|
|
11059
|
-
`);
|
|
11060
|
-
granularity = "ALL";
|
|
11061
|
-
}
|
|
11062
|
-
const ids = resolveScopeIds(args, level);
|
|
11063
|
-
if (ids.length === 0) {
|
|
11064
|
-
handleLinkedinError(new Error(`No IDs resolved for --level ${level}`));
|
|
11065
|
-
}
|
|
11066
|
-
const range = resolveDateRange(args);
|
|
11067
|
-
const metrics = commaSplit(args.metrics);
|
|
11068
|
-
const limit = args.limit ? Number(args.limit) : 1e3;
|
|
11069
|
-
const request = {
|
|
11070
|
-
level,
|
|
11071
|
-
ids,
|
|
11072
|
-
intent,
|
|
11073
|
-
pivot,
|
|
11074
|
-
metrics,
|
|
11075
|
-
start: range.start,
|
|
11076
|
-
end: range.end,
|
|
11077
|
-
granularity,
|
|
11078
|
-
limit
|
|
11079
|
-
};
|
|
11080
|
-
try {
|
|
11081
|
-
const data = await apiPost("/api/ads/linkedin/analytics", {
|
|
11082
|
-
request,
|
|
11083
|
-
skipCache: Boolean(args["skip-cache"])
|
|
11084
|
-
});
|
|
11085
|
-
const rows = args["no-sort"] ? data.rows : sortRows(data.rows);
|
|
11086
|
-
const fmt = csvOrJson2(args);
|
|
11087
|
-
if (fmt !== "json") {
|
|
11088
|
-
writeAdsOutput(rows, fmt, data.fields);
|
|
11089
|
-
return;
|
|
11090
|
-
}
|
|
11091
|
-
writeAdsJson({
|
|
11092
|
-
ok: true,
|
|
11093
|
-
data: {
|
|
11094
|
-
rows,
|
|
11095
|
-
fields: data.fields,
|
|
11096
|
-
warnings: data.warnings,
|
|
11097
|
-
query: data.query,
|
|
11098
|
-
// Echo composed fields so an offline agent can pre-flight w/o this network call.
|
|
11099
|
-
composedFields: composeFields(intent, level, metrics)
|
|
11100
|
-
}
|
|
11101
|
-
});
|
|
11102
|
-
} catch (err) {
|
|
11103
|
-
handleLinkedinError(err);
|
|
11104
|
-
}
|
|
10835
|
+
...writeAccountArgs,
|
|
10836
|
+
"intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
|
|
10837
|
+
name: { type: "string", description: "New creative name", required: false },
|
|
10838
|
+
campaign: {
|
|
10839
|
+
type: "string",
|
|
10840
|
+
description: "Re-parent to a different ad set (id/URN or li_temp_*) \u2014 staged creatives only",
|
|
10841
|
+
required: false
|
|
10842
|
+
},
|
|
10843
|
+
headline: { type: "string", description: "New headline", required: false },
|
|
10844
|
+
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10845
|
+
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10846
|
+
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10847
|
+
cta: { type: "string", description: "New CTA", required: false },
|
|
10848
|
+
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10849
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10850
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10851
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10852
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10853
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10854
|
+
"thumbnail-image-id": {
|
|
10855
|
+
type: "string",
|
|
10856
|
+
description: "Baker image id for the article thumbnail",
|
|
10857
|
+
required: false
|
|
10858
|
+
},
|
|
10859
|
+
"thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail", required: false },
|
|
10860
|
+
title: { type: "string", description: "Media or article title", required: false },
|
|
10861
|
+
file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
|
|
11105
10862
|
}
|
|
11106
10863
|
});
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
} catch {
|
|
11117
|
-
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11118
|
-
}
|
|
10864
|
+
registerSchema({
|
|
10865
|
+
command: "ads.linkedin.audiences.create",
|
|
10866
|
+
description: "Stage a new matched audience. company-list/user-list take --list-file (CSV with header; companyName,domain for companies; email column is SHA-256 hashed locally). LinkedIn needs 300+ MATCHED members to serve \u2014 upload well above that. retargeting/engagement configs go in --file. Staged until publish.",
|
|
10867
|
+
args: {
|
|
10868
|
+
...writeAccountArgs,
|
|
10869
|
+
name: { type: "string", description: "Audience name (\u2264100 chars)", required: true },
|
|
10870
|
+
type: { type: "string", description: "company-list|user-list|retargeting|engagement", required: true },
|
|
10871
|
+
"list-file": { type: "string", description: "CSV file with the list rows", required: false },
|
|
10872
|
+
file: { type: "string", description: "JSON raw source config for retargeting/engagement", required: false }
|
|
11119
10873
|
}
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
10874
|
+
});
|
|
10875
|
+
registerSchema({
|
|
10876
|
+
command: "ads.linkedin.audiences.upload",
|
|
10877
|
+
description: "Stage adding rows to an existing audience segment (or a li_temp_* segment staged in this chat). Same CSV rules as audiences.create.",
|
|
10878
|
+
args: {
|
|
10879
|
+
id: { type: "positional", description: "Segment URN/id or li_temp_* ref", required: true },
|
|
10880
|
+
...writeAccountArgs,
|
|
10881
|
+
"list-file": { type: "string", description: "CSV file with the rows to add", required: true }
|
|
10882
|
+
}
|
|
10883
|
+
});
|
|
10884
|
+
registerSchema({
|
|
10885
|
+
command: "ads.linkedin.conversions.create",
|
|
10886
|
+
description: "Stage a new conversion rule. Playbook defaults: --post-click-window 30 --view-window 7, counting ONE_TIME_EACH_MEMBER for leads. Staged until publish.",
|
|
10887
|
+
args: {
|
|
10888
|
+
...writeAccountArgs,
|
|
10889
|
+
name: { type: "string", description: "Rule name", required: true },
|
|
10890
|
+
type: {
|
|
10891
|
+
type: "string",
|
|
10892
|
+
description: "LEAD|SIGN_UP|DOWNLOAD|PURCHASE|KEY_PAGE_VIEW|ADD_TO_CART|INSTALL|OTHER",
|
|
10893
|
+
required: true
|
|
10894
|
+
},
|
|
10895
|
+
method: { type: "string", description: "INSIGHT_TAG|CONVERSIONS_API", required: true },
|
|
10896
|
+
"post-click-window": { type: "string", description: "1|7|30|90 days", required: false },
|
|
10897
|
+
"view-window": { type: "string", description: "1|7|30 days", required: false },
|
|
10898
|
+
"attribution-type": {
|
|
10899
|
+
type: "string",
|
|
10900
|
+
description: "LAST_TOUCH_BY_CAMPAIGN|LAST_TOUCH_BY_CONVERSION",
|
|
10901
|
+
required: false
|
|
10902
|
+
},
|
|
10903
|
+
"associate-all-campaigns": {
|
|
10904
|
+
type: "boolean",
|
|
10905
|
+
description: "Auto-associate with active campaigns",
|
|
10906
|
+
required: false
|
|
10907
|
+
},
|
|
10908
|
+
file: { type: "string", description: "JSON payload file (e.g. urlMatchRuleExpression)", required: false }
|
|
10909
|
+
}
|
|
10910
|
+
});
|
|
10911
|
+
registerSchema({
|
|
10912
|
+
command: "ads.linkedin.conversions.update",
|
|
10913
|
+
description: "Stage changes to a conversion rule (windows, attribution, enabled, name). Applies on chat publish.",
|
|
10914
|
+
args: {
|
|
10915
|
+
id: { type: "positional", description: "Conversion rule id or URN", required: true },
|
|
10916
|
+
...writeAccountArgs,
|
|
10917
|
+
name: { type: "string", description: "New name", required: false },
|
|
10918
|
+
"post-click-window": { type: "string", description: "1|7|30|90 days", required: false },
|
|
10919
|
+
"view-window": { type: "string", description: "1|7|30 days", required: false },
|
|
10920
|
+
"attribution-type": { type: "string", description: "New attribution type", required: false },
|
|
10921
|
+
enabled: { type: "string", description: "on|off", required: false },
|
|
10922
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10923
|
+
}
|
|
10924
|
+
});
|
|
10925
|
+
registerSchema({
|
|
10926
|
+
command: "ads.linkedin.lead-forms.create",
|
|
10927
|
+
description: "Stage a new Lead Gen Form from a JSON file: {name, headline \u226460, description? \u2264160, privacyPolicyUrl, questions[] \u226412 (playbook: \u22644 for completion), thankYou?, legalDisclaimer?}. Staged until publish.",
|
|
10928
|
+
args: {
|
|
10929
|
+
...writeAccountArgs,
|
|
10930
|
+
file: { type: "string", description: "JSON form definition file", required: true },
|
|
10931
|
+
name: { type: "string", description: "Override the form name", required: false }
|
|
10932
|
+
}
|
|
10933
|
+
});
|
|
10934
|
+
registerSchema({
|
|
10935
|
+
command: "ads.linkedin.lead-forms.update",
|
|
10936
|
+
description: "Stage changes to a Lead Gen Form from a JSON file. Applies on chat publish.",
|
|
10937
|
+
args: {
|
|
10938
|
+
id: { type: "positional", description: "Lead form id or URN", required: true },
|
|
10939
|
+
...writeAccountArgs,
|
|
10940
|
+
file: { type: "string", description: "JSON file with the fields to change", required: true },
|
|
10941
|
+
name: { type: "string", description: "New form name", required: false }
|
|
10942
|
+
}
|
|
10943
|
+
});
|
|
10944
|
+
registerSchema({
|
|
10945
|
+
command: "ads.linkedin.draft.list",
|
|
10946
|
+
description: "Show every LinkedIn write op staged in this chat: ref, summary, dependencies, warnings, mode (simulated = publish will NOT hit LinkedIn), and post-publish results (applied/simulated/failed/skipped). Ops are invisible to the read commands until publish \u2014 check here before finishing.",
|
|
10947
|
+
args: {
|
|
10948
|
+
json: {
|
|
10949
|
+
type: "boolean",
|
|
10950
|
+
description: "Print the raw JSON envelope instead of the readable tree",
|
|
10951
|
+
required: false
|
|
11128
10952
|
}
|
|
11129
10953
|
}
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
name: "audience-size",
|
|
11135
|
-
description: `Estimate audience size for a targeting payload \u2014 pre-launch sanity check.
|
|
11136
|
-
|
|
11137
|
-
Returns total + active counts plus playbook \xA704 warnings:
|
|
11138
|
-
size <20K \u2192 too narrow (will not deliver)
|
|
11139
|
-
size <50K \u2192 outside the 50K\u2013500K sweet spot
|
|
11140
|
-
size >500K \u2192 outside the sweet spot
|
|
11141
|
-
size >1M \u2192 too broad, refine ICP
|
|
11142
|
-
|
|
11143
|
-
The targeting payload must follow LinkedIn's 'targetingCriteria' shape \u2014 an
|
|
11144
|
-
include/exclude tree of and/or boolean operators with adTargetingFacet keys.
|
|
11145
|
-
The simplest form:
|
|
11146
|
-
{ "include": { "and": [{ "or": { "urn:li:adTargetingFacet:locations": ["urn:li:geo:103644278"] }}]}}
|
|
11147
|
-
|
|
11148
|
-
Examples:
|
|
11149
|
-
baker ads linkedin audience-size --account-id 503001492 --targeting-file targeting.json
|
|
11150
|
-
baker ads linkedin audience-size --account-id 503001492 --targeting '{"include":{"and":[]}}'`
|
|
11151
|
-
},
|
|
10954
|
+
});
|
|
10955
|
+
registerSchema({
|
|
10956
|
+
command: "ads.linkedin.draft.remove",
|
|
10957
|
+
description: "Drop one staged LinkedIn op from this chat's draft. Removing a create cascades to every op that depends on it (a campaign removal drops its staged creatives).",
|
|
11152
10958
|
args: {
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11158
|
-
output: { type: "string", description: "json", default: "json" }
|
|
11159
|
-
},
|
|
11160
|
-
run: async ({ args }) => {
|
|
11161
|
-
const accountId = resolveAccountIdArg2(args);
|
|
11162
|
-
const targetingCriteria = loadTargeting(args);
|
|
11163
|
-
try {
|
|
11164
|
-
const data = await apiPost("/api/ads/linkedin/audience-size", {
|
|
11165
|
-
accountId,
|
|
11166
|
-
targetingCriteria,
|
|
11167
|
-
skipCache: Boolean(args["skip-cache"])
|
|
11168
|
-
});
|
|
11169
|
-
writeAdsJson({ ok: true, data });
|
|
11170
|
-
} catch (err) {
|
|
11171
|
-
handleLinkedinError(err);
|
|
10959
|
+
ref: {
|
|
10960
|
+
type: "positional",
|
|
10961
|
+
description: "Op ref (li_temp_* for creates, target id/URN for updates)",
|
|
10962
|
+
required: true
|
|
11172
10963
|
}
|
|
11173
10964
|
}
|
|
11174
10965
|
});
|
|
10966
|
+
registerSchema({
|
|
10967
|
+
command: "ads.linkedin.draft.clear",
|
|
10968
|
+
description: "Discard ALL staged LinkedIn ops in this chat's draft. Nothing applies on publish.",
|
|
10969
|
+
args: {}
|
|
10970
|
+
});
|
|
11175
10971
|
|
|
11176
|
-
// src/commands/ads/linkedin/
|
|
11177
|
-
import { defineCommand as
|
|
11178
|
-
var
|
|
11179
|
-
critical: 0,
|
|
11180
|
-
high: 1,
|
|
11181
|
-
medium: 2,
|
|
11182
|
-
low: 3
|
|
11183
|
-
};
|
|
11184
|
-
var STATUS_RANK = {
|
|
11185
|
-
fail: 0,
|
|
11186
|
-
warn: 1,
|
|
11187
|
-
partial: 2,
|
|
11188
|
-
pass: 3,
|
|
11189
|
-
n_a: 4
|
|
11190
|
-
};
|
|
11191
|
-
function filterFindings(findings, severity, area) {
|
|
11192
|
-
return findings.filter((f) => {
|
|
11193
|
-
if (severity && !severity.includes(f.severity)) return false;
|
|
11194
|
-
if (area && !area.includes(f.area)) return false;
|
|
11195
|
-
return true;
|
|
11196
|
-
});
|
|
11197
|
-
}
|
|
11198
|
-
function sortFindings(findings) {
|
|
11199
|
-
return [...findings].sort((a, b) => {
|
|
11200
|
-
const sa = STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
11201
|
-
if (sa !== 0) return sa;
|
|
11202
|
-
return SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
11203
|
-
});
|
|
11204
|
-
}
|
|
11205
|
-
function renderMarkdown(result) {
|
|
11206
|
-
const lines = [];
|
|
11207
|
-
lines.push(`# LinkedIn Ads Audit \u2014 ${result.account.name} (${result.account.id})`);
|
|
11208
|
-
lines.push("");
|
|
11209
|
-
lines.push(
|
|
11210
|
-
`**Scope:** ${result.scope.campaigns} campaigns, ${result.scope.creatives} creatives, last ${result.scope.windowDays}d`
|
|
11211
|
-
);
|
|
11212
|
-
lines.push("");
|
|
11213
|
-
lines.push(
|
|
11214
|
-
`**Summary:** ${result.summary.pass} pass \u2022 ${result.summary.critical} critical \u2022 ${result.summary.high} high \u2022 ${result.summary.medium} medium \u2022 ${result.summary.low} low \u2022 ${result.summary.n_a} n/a (${result.summary.totalChecks} total)`
|
|
11215
|
-
);
|
|
11216
|
-
lines.push("");
|
|
11217
|
-
lines.push("| # | Area | Check | Status | Severity | Notes |");
|
|
11218
|
-
lines.push("|---|------|-------|--------|----------|-------|");
|
|
11219
|
-
result.findings.forEach((f, idx) => {
|
|
11220
|
-
const note = noteOf(f);
|
|
11221
|
-
lines.push(
|
|
11222
|
-
`| ${idx + 1} | ${f.area} | ${escapeMd(f.check)} | ${f.status.toUpperCase()} | ${f.severity} | ${escapeMd(note)} |`
|
|
11223
|
-
);
|
|
11224
|
-
});
|
|
11225
|
-
return lines.join("\n");
|
|
11226
|
-
}
|
|
11227
|
-
function escapeMd(text) {
|
|
11228
|
-
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
11229
|
-
}
|
|
11230
|
-
function noteOf(f) {
|
|
11231
|
-
if (f.status === "pass") return "";
|
|
11232
|
-
const ev = f.evidence ? JSON.stringify(f.evidence) : "";
|
|
11233
|
-
const fix = f.fix?.explanation ?? "";
|
|
11234
|
-
return [fix, ev].filter(Boolean).join(" \u2014 ");
|
|
11235
|
-
}
|
|
11236
|
-
var auditCommand = defineCommand36({
|
|
10972
|
+
// src/commands/ads/linkedin/account.ts
|
|
10973
|
+
import { defineCommand as defineCommand32 } from "citty";
|
|
10974
|
+
var accountCommand = defineCommand32({
|
|
11237
10975
|
meta: {
|
|
11238
|
-
name: "
|
|
11239
|
-
description: `
|
|
11240
|
-
Audience, Campaigns, Creative, Performance, Bidding, and Compliance.
|
|
11241
|
-
|
|
11242
|
-
Each finding has {id, area, check, status, severity, evidence, fix} fields with
|
|
11243
|
-
playbook citations. The default JSON output is agent-friendly; --format md
|
|
11244
|
-
renders a deliverable-ready table that mirrors the google-ads-playbook
|
|
11245
|
-
10-audit-summary.md style.
|
|
11246
|
-
|
|
11247
|
-
Exit code 0 always. Use the JSON 'summary' counts to decide if action is needed.
|
|
10976
|
+
name: "account",
|
|
10977
|
+
description: `Single LinkedIn ad account detail (currency, status, type).
|
|
11248
10978
|
|
|
11249
10979
|
Examples:
|
|
11250
|
-
baker ads linkedin
|
|
11251
|
-
baker ads linkedin
|
|
11252
|
-
baker ads linkedin audit --account-id 503001492 --window-days 90
|
|
11253
|
-
baker ads linkedin audit --account-id 503001492 --severity critical,high
|
|
11254
|
-
baker ads linkedin audit --account-id 503001492 --area Settings,Tracking
|
|
11255
|
-
baker ads linkedin audit --account-id 503001492 --format md`
|
|
10980
|
+
baker ads linkedin account --account-id 503001492
|
|
10981
|
+
baker ads linkedin account --account-urn urn:li:sponsoredAccount:503001492 --output md`
|
|
11256
10982
|
},
|
|
11257
10983
|
args: {
|
|
11258
|
-
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11259
|
-
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11260
|
-
"
|
|
11261
|
-
|
|
11262
|
-
severity: { type: "string", description: "CSV severity filter: critical,high,medium,low" },
|
|
11263
|
-
area: {
|
|
11264
|
-
type: "string",
|
|
11265
|
-
description: "CSV area filter: Settings,Tracking,Audience,Campaigns,Creative,Performance,Bidding,Compliance,Hygiene"
|
|
11266
|
-
},
|
|
11267
|
-
format: { type: "string", description: "Output format: json | md (default: json)" },
|
|
11268
|
-
"skip-cache": { type: "boolean", description: "Bypass server-side cache" }
|
|
10984
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
10985
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
10986
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
10987
|
+
output: { type: "string", description: "json|csv|jsonl|md", default: "json" }
|
|
11269
10988
|
},
|
|
11270
10989
|
run: async ({ args }) => {
|
|
11271
10990
|
const accountId = resolveAccountIdArg2(args);
|
|
11272
|
-
const windowDays = args["window-days"] ? Number(args["window-days"]) : 30;
|
|
11273
|
-
if (!Number.isFinite(windowDays) || windowDays < 1 || windowDays > 365) {
|
|
11274
|
-
handleLinkedinError(new Error("--window-days must be 1..365"));
|
|
11275
|
-
}
|
|
11276
10991
|
try {
|
|
11277
|
-
const
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
const severityFilter = args.severity?.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
11284
|
-
const areaFilter = args.area?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11285
|
-
const filtered = filterFindings(data.findings, severityFilter, areaFilter);
|
|
11286
|
-
const sorted = sortFindings(filtered);
|
|
11287
|
-
const result = { ...data, findings: sorted };
|
|
11288
|
-
const fmt = args.format ?? "json";
|
|
11289
|
-
if (fmt === "md") {
|
|
11290
|
-
process.stdout.write(`${renderMarkdown(result)}
|
|
11291
|
-
`);
|
|
10992
|
+
const params = { "account-id": accountId };
|
|
10993
|
+
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
10994
|
+
const data = await apiGet("/api/ads/linkedin/account", params);
|
|
10995
|
+
const fmt = csvOrJson2(args);
|
|
10996
|
+
if (fmt !== "json") {
|
|
10997
|
+
writeAdsOutput([data], fmt);
|
|
11292
10998
|
return;
|
|
11293
10999
|
}
|
|
11294
|
-
writeAdsJson({ ok: true, data
|
|
11000
|
+
writeAdsJson({ ok: true, data });
|
|
11295
11001
|
} catch (err) {
|
|
11296
11002
|
handleLinkedinError(err);
|
|
11297
11003
|
}
|
|
11298
11004
|
}
|
|
11299
11005
|
});
|
|
11300
11006
|
|
|
11301
|
-
// src/commands/ads/linkedin/
|
|
11302
|
-
import {
|
|
11303
|
-
|
|
11304
|
-
function
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
try {
|
|
11308
|
-
return JSON.parse(inline);
|
|
11309
|
-
} catch {
|
|
11310
|
-
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11311
|
-
}
|
|
11312
|
-
}
|
|
11313
|
-
const file = args["targeting-file"];
|
|
11314
|
-
if (file) {
|
|
11315
|
-
try {
|
|
11316
|
-
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
11317
|
-
} catch (e) {
|
|
11318
|
-
handleLinkedinError(
|
|
11319
|
-
new Error(`Failed to read --targeting-file: ${e instanceof Error ? e.message : "I/O error"}`)
|
|
11320
|
-
);
|
|
11321
|
-
}
|
|
11007
|
+
// src/commands/ads/linkedin/accounts.ts
|
|
11008
|
+
import { defineCommand as defineCommand33 } from "citty";
|
|
11009
|
+
var ACCOUNTS_TTL_MS = 60 * 60 * 1e3;
|
|
11010
|
+
function accountHints2(accounts, includeAll) {
|
|
11011
|
+
if (includeAll) {
|
|
11012
|
+
return [];
|
|
11322
11013
|
}
|
|
11323
|
-
|
|
11014
|
+
return connectedResourceHints({
|
|
11015
|
+
plural: "LinkedIn ad accounts",
|
|
11016
|
+
singular: "LinkedIn ad account",
|
|
11017
|
+
flag: "--account-id",
|
|
11018
|
+
resources: accounts.map((account) => ({ id: account.externalId, label: account.name }))
|
|
11019
|
+
});
|
|
11324
11020
|
}
|
|
11325
|
-
var
|
|
11021
|
+
var accountsCommand2 = defineCommand33({
|
|
11326
11022
|
meta: {
|
|
11327
|
-
name: "
|
|
11328
|
-
description: `
|
|
11329
|
-
|
|
11330
|
-
Returns LinkedIn's min / suggested / max bid plus the playbook \xA706 floor
|
|
11331
|
-
(2/3 of suggested \u2014 the recommended manual CPC starting point).
|
|
11332
|
-
|
|
11333
|
-
Objective types: BRAND_AWARENESS | WEBSITE_TRAFFIC | WEBSITE_VISIT |
|
|
11334
|
-
ENGAGEMENT | WEBSITE_CONVERSION | LEAD_GENERATION |
|
|
11335
|
-
JOB_APPLICANT | VIDEO_VIEW
|
|
11336
|
-
Cost types: CPC | CPM | CPV | CPS
|
|
11023
|
+
name: "accounts",
|
|
11024
|
+
description: `List LinkedIn ad accounts in this company's connected scope.
|
|
11337
11025
|
|
|
11338
11026
|
Examples:
|
|
11339
|
-
baker ads linkedin
|
|
11340
|
-
baker ads linkedin
|
|
11027
|
+
baker ads linkedin accounts
|
|
11028
|
+
baker ads linkedin accounts --include-all # ignore picker scope, list every account the token can see
|
|
11029
|
+
baker ads linkedin accounts --output csv`
|
|
11341
11030
|
},
|
|
11342
11031
|
args: {
|
|
11343
|
-
"
|
|
11344
|
-
"
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
description: "Objective type (e.g. WEBSITE_CONVERSION, LEAD_GENERATION)",
|
|
11348
|
-
required: true
|
|
11349
|
-
},
|
|
11350
|
-
"cost-type": { type: "string", description: "CPC | CPM | CPV | CPS", required: true },
|
|
11351
|
-
targeting: { type: "string", description: "Inline JSON targetingCriteria" },
|
|
11352
|
-
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11353
|
-
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11354
|
-
output: { type: "string", description: "json", default: "json" }
|
|
11032
|
+
"include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
|
|
11033
|
+
"no-cache": { type: "boolean", description: "Skip CLI-side cache" },
|
|
11034
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache (force re-fetch)" },
|
|
11035
|
+
output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
|
|
11355
11036
|
},
|
|
11356
11037
|
run: async ({ args }) => {
|
|
11357
|
-
const
|
|
11358
|
-
const
|
|
11038
|
+
const includeAll = args["include-all"] === true;
|
|
11039
|
+
const useCache = !args["no-cache"];
|
|
11040
|
+
const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
|
|
11041
|
+
if (useCache) {
|
|
11042
|
+
const cached = cacheGet("linkedin-accounts", cacheKey);
|
|
11043
|
+
if (cached) {
|
|
11044
|
+
writeJsonEnvelope({
|
|
11045
|
+
ok: true,
|
|
11046
|
+
data: cached.data,
|
|
11047
|
+
cached: true,
|
|
11048
|
+
hints: accountHints2(cached.data, includeAll)
|
|
11049
|
+
});
|
|
11050
|
+
return;
|
|
11051
|
+
}
|
|
11052
|
+
}
|
|
11359
11053
|
try {
|
|
11360
|
-
const
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
}
|
|
11367
|
-
|
|
11054
|
+
const params = {};
|
|
11055
|
+
if (includeAll) params["include-all"] = "true";
|
|
11056
|
+
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
11057
|
+
const data = await apiGet("/api/ads/linkedin/accounts", params);
|
|
11058
|
+
if (useCache) {
|
|
11059
|
+
cacheSet("linkedin-accounts", cacheKey, data, ACCOUNTS_TTL_MS);
|
|
11060
|
+
}
|
|
11061
|
+
const fmt = csvOrJson2(args);
|
|
11062
|
+
if (fmt !== "json") {
|
|
11063
|
+
writeAdsOutput(data, fmt);
|
|
11064
|
+
return;
|
|
11065
|
+
}
|
|
11066
|
+
writeJsonEnvelope({ ok: true, data, hints: accountHints2(data, includeAll) });
|
|
11368
11067
|
} catch (err) {
|
|
11369
11068
|
handleLinkedinError(err);
|
|
11370
11069
|
}
|
|
11371
11070
|
}
|
|
11372
11071
|
});
|
|
11373
11072
|
|
|
11374
|
-
// src/commands/ads/linkedin/
|
|
11375
|
-
import { defineCommand as
|
|
11376
|
-
|
|
11377
|
-
// src/commands/ads/linkedin/write-commands.ts
|
|
11378
|
-
import { defineCommand as defineCommand38 } from "citty";
|
|
11073
|
+
// src/commands/ads/linkedin/analytics.ts
|
|
11074
|
+
import { defineCommand as defineCommand34 } from "citty";
|
|
11379
11075
|
|
|
11380
|
-
// src/commands/ads/linkedin/
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11076
|
+
// src/commands/ads/linkedin/presets.ts
|
|
11077
|
+
var INTENTS = {
|
|
11078
|
+
baseline: {
|
|
11079
|
+
name: "baseline",
|
|
11080
|
+
description: "Spend, impressions, clicks. The default 'how is delivery?' question. Derives CTR, CPC, CPM.",
|
|
11081
|
+
metrics: ["impressions", "clicks", "costInUsd", "costInLocalCurrency", "approximateUniqueImpressions"],
|
|
11082
|
+
derived: ["ctr", "cpc", "cpm", "frequency"]
|
|
11083
|
+
},
|
|
11084
|
+
revenue: {
|
|
11085
|
+
name: "revenue",
|
|
11086
|
+
description: "Spend, conversions, conversion value. 'How much money are we making?'",
|
|
11087
|
+
metrics: [
|
|
11088
|
+
"costInUsd",
|
|
11089
|
+
"externalWebsiteConversions",
|
|
11090
|
+
"externalWebsitePostClickConversions",
|
|
11091
|
+
"externalWebsitePostViewConversions",
|
|
11092
|
+
"conversionValueInLocalCurrency",
|
|
11093
|
+
"oneClickLeads"
|
|
11094
|
+
]
|
|
11095
|
+
},
|
|
11096
|
+
funnel: {
|
|
11097
|
+
name: "funnel",
|
|
11098
|
+
description: "Impression \u2192 click \u2192 landing \u2192 form open \u2192 lead \u2192 conversion. 'Where do users drop off?'",
|
|
11099
|
+
metrics: [
|
|
11100
|
+
"impressions",
|
|
11101
|
+
"clicks",
|
|
11102
|
+
"landingPageClicks",
|
|
11103
|
+
"oneClickLeadFormOpens",
|
|
11104
|
+
"oneClickLeads",
|
|
11105
|
+
"externalWebsiteConversions",
|
|
11106
|
+
"costInUsd"
|
|
11107
|
+
],
|
|
11108
|
+
derived: ["ctr", "leadCompletionRate"]
|
|
11109
|
+
},
|
|
11110
|
+
engagement: {
|
|
11111
|
+
name: "engagement",
|
|
11112
|
+
description: "Likes, comments, shares, follows + viral spillover. TLA-aware \u2014 measures organic reach effect.",
|
|
11113
|
+
metrics: [
|
|
11114
|
+
"impressions",
|
|
11115
|
+
"likes",
|
|
11116
|
+
"comments",
|
|
11117
|
+
"shares",
|
|
11118
|
+
"reactions",
|
|
11119
|
+
"follows",
|
|
11120
|
+
"totalEngagements",
|
|
11121
|
+
"viralImpressions",
|
|
11122
|
+
"viralLikes",
|
|
11123
|
+
"viralShares",
|
|
11124
|
+
"viralOneClickLeads",
|
|
11125
|
+
"costInUsd"
|
|
11126
|
+
]
|
|
11127
|
+
},
|
|
11128
|
+
video: {
|
|
11129
|
+
name: "video",
|
|
11130
|
+
description: "Video watch quartiles + cost. 'How are video creatives holding attention?'",
|
|
11131
|
+
metrics: [
|
|
11132
|
+
"videoStarts",
|
|
11133
|
+
"videoFirstQuartileCompletions",
|
|
11134
|
+
"videoMidpointCompletions",
|
|
11135
|
+
"videoThirdQuartileCompletions",
|
|
11136
|
+
"videoCompletions",
|
|
11137
|
+
"videoViews",
|
|
11138
|
+
"fullScreenPlays",
|
|
11139
|
+
"costInUsd",
|
|
11140
|
+
"impressions"
|
|
11141
|
+
]
|
|
11142
|
+
},
|
|
11143
|
+
"lead-gen": {
|
|
11144
|
+
name: "lead-gen",
|
|
11145
|
+
description: "Lead Gen Form opens + completions. CLI derives form-completion-rate.",
|
|
11146
|
+
metrics: [
|
|
11147
|
+
"oneClickLeadFormOpens",
|
|
11148
|
+
"oneClickLeads",
|
|
11149
|
+
"viralOneClickLeads",
|
|
11150
|
+
"viralOneClickLeadFormOpens",
|
|
11151
|
+
"costInUsd"
|
|
11152
|
+
],
|
|
11153
|
+
derived: ["leadCompletionRate"]
|
|
11154
|
+
},
|
|
11155
|
+
inmail: {
|
|
11156
|
+
name: "inmail",
|
|
11157
|
+
description: "Sponsored Message / Conversation Ads \u2014 sends, opens, clicks, lead-gen taps.",
|
|
11158
|
+
metrics: [
|
|
11159
|
+
"sends",
|
|
11160
|
+
"opens",
|
|
11161
|
+
"clicks",
|
|
11162
|
+
"costInUsd",
|
|
11163
|
+
"leadGenerationMailContactInfoShares",
|
|
11164
|
+
"leadGenerationMailInterestedClicks"
|
|
11165
|
+
]
|
|
11166
|
+
},
|
|
11167
|
+
document: {
|
|
11168
|
+
name: "document",
|
|
11169
|
+
description: "Document Ads \u2014 quartile read-through, completion, downloads.",
|
|
11170
|
+
metrics: [
|
|
11171
|
+
"documentFirstQuartileCompletions",
|
|
11172
|
+
"documentMidpointCompletions",
|
|
11173
|
+
"documentThirdQuartileCompletions",
|
|
11174
|
+
"documentCompletions",
|
|
11175
|
+
"downloadClicks",
|
|
11176
|
+
"costInUsd",
|
|
11177
|
+
"impressions"
|
|
11178
|
+
]
|
|
11179
|
+
},
|
|
11180
|
+
ranking: {
|
|
11181
|
+
name: "ranking",
|
|
11182
|
+
description: "Frequency + reach metrics for fatigue detection. CLI derives frequency = impressions/uniqueImpressions.",
|
|
11183
|
+
metrics: ["impressions", "clicks", "costInUsd", "approximateUniqueImpressions"],
|
|
11184
|
+
derived: ["frequency", "ctr"]
|
|
11185
|
+
},
|
|
11186
|
+
identity: {
|
|
11187
|
+
name: "identity",
|
|
11188
|
+
description: "Just spend + impressions. Cheap roll-up.",
|
|
11189
|
+
metrics: ["costInUsd", "impressions"]
|
|
11190
|
+
}
|
|
11191
|
+
};
|
|
11192
|
+
var MEMBER_PIVOTS = /* @__PURE__ */ new Set([
|
|
11193
|
+
"company",
|
|
11194
|
+
"job-title",
|
|
11195
|
+
"job-function",
|
|
11196
|
+
"seniority",
|
|
11197
|
+
"industry",
|
|
11198
|
+
"company-size",
|
|
11199
|
+
"country",
|
|
11200
|
+
"region"
|
|
11201
|
+
]);
|
|
11202
|
+
var ALL_PIVOTS = [
|
|
11203
|
+
"none",
|
|
11204
|
+
"campaign",
|
|
11205
|
+
"campaign-group",
|
|
11206
|
+
"creative",
|
|
11207
|
+
"company",
|
|
11208
|
+
"account",
|
|
11209
|
+
"conversion",
|
|
11210
|
+
"job-title",
|
|
11211
|
+
"job-function",
|
|
11212
|
+
"seniority",
|
|
11213
|
+
"industry",
|
|
11214
|
+
"company-size",
|
|
11215
|
+
"country",
|
|
11216
|
+
"region",
|
|
11217
|
+
"device",
|
|
11218
|
+
"placement",
|
|
11219
|
+
"serving-location",
|
|
11220
|
+
"card-index",
|
|
11221
|
+
"objective",
|
|
11222
|
+
"conversation-node",
|
|
11223
|
+
"conversation-node-button"
|
|
11224
|
+
];
|
|
11225
|
+
var ALL_LEVELS = ["account", "campaign-group", "campaign", "creative"];
|
|
11226
|
+
function listIntents() {
|
|
11227
|
+
return Object.values(INTENTS);
|
|
11386
11228
|
}
|
|
11387
|
-
function
|
|
11388
|
-
|
|
11389
|
-
process.exit(1);
|
|
11229
|
+
function isPivot(slug) {
|
|
11230
|
+
return ALL_PIVOTS.includes(slug);
|
|
11390
11231
|
}
|
|
11391
|
-
function
|
|
11392
|
-
|
|
11393
|
-
return {};
|
|
11394
|
-
}
|
|
11395
|
-
try {
|
|
11396
|
-
const parsed = JSON.parse(readFileSync6(path28, "utf8"));
|
|
11397
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11398
|
-
failWriteValidation2(`${path28} must contain a JSON object`);
|
|
11399
|
-
}
|
|
11400
|
-
return parsed;
|
|
11401
|
-
} catch (err) {
|
|
11402
|
-
if (err instanceof SyntaxError) {
|
|
11403
|
-
failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
|
|
11404
|
-
}
|
|
11405
|
-
throw err;
|
|
11406
|
-
}
|
|
11232
|
+
function isLevel(slug) {
|
|
11233
|
+
return ALL_LEVELS.includes(slug);
|
|
11407
11234
|
}
|
|
11408
|
-
function
|
|
11409
|
-
const
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11235
|
+
function composeFields(intent, _level, metricsOverride) {
|
|
11236
|
+
const base = metricsOverride && metricsOverride.length > 0 ? metricsOverride : INTENTS[intent].metrics;
|
|
11237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11238
|
+
const out = [];
|
|
11239
|
+
for (const f of ["pivotValues", "dateRange", ...base]) {
|
|
11240
|
+
if (!seen.has(f)) {
|
|
11241
|
+
seen.add(f);
|
|
11242
|
+
out.push(f);
|
|
11413
11243
|
}
|
|
11414
11244
|
}
|
|
11415
|
-
return
|
|
11245
|
+
return out;
|
|
11416
11246
|
}
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
if (
|
|
11421
|
-
|
|
11422
|
-
}
|
|
11423
|
-
if (file) {
|
|
11424
|
-
return loadJsonFileArg2(file);
|
|
11425
|
-
}
|
|
11426
|
-
try {
|
|
11427
|
-
const parsed = JSON.parse(inline);
|
|
11428
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11429
|
-
failWriteValidation2("--patch must be a JSON object");
|
|
11430
|
-
}
|
|
11431
|
-
return parsed;
|
|
11432
|
-
} catch (err) {
|
|
11433
|
-
if (err instanceof SyntaxError) {
|
|
11434
|
-
failWriteValidation2(`--patch is not valid JSON: ${err.message}`);
|
|
11435
|
-
}
|
|
11436
|
-
throw err;
|
|
11437
|
-
}
|
|
11247
|
+
|
|
11248
|
+
// src/commands/ads/linkedin/analytics.ts
|
|
11249
|
+
function commaSplit(input) {
|
|
11250
|
+
if (!input) return void 0;
|
|
11251
|
+
return input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11438
11252
|
}
|
|
11439
|
-
function
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
if (currency === void 0 || currency === null || currency === "") {
|
|
11444
|
-
return { amount: String(amount) };
|
|
11445
|
-
}
|
|
11446
|
-
if (typeof currency !== "string" || currency.length !== 3) {
|
|
11447
|
-
failWriteValidation2(`${flag} got an invalid --currency (expected a 3-letter code like EUR)`);
|
|
11253
|
+
function parseGranularity(raw) {
|
|
11254
|
+
const v = (raw ?? "DAILY").toUpperCase();
|
|
11255
|
+
if (v === "DAILY" || v === "MONTHLY" || v === "YEARLY" || v === "ALL") {
|
|
11256
|
+
return v;
|
|
11448
11257
|
}
|
|
11449
|
-
|
|
11258
|
+
handleLinkedinError(new Error(`Invalid --granularity "${raw}". Use DAILY | MONTHLY | YEARLY | ALL.`));
|
|
11450
11259
|
}
|
|
11451
|
-
function
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
const raw = String(value);
|
|
11456
|
-
if (/^\d{10,}$/.test(raw)) {
|
|
11457
|
-
return Number(raw);
|
|
11458
|
-
}
|
|
11459
|
-
const parsed = Date.parse(raw);
|
|
11460
|
-
if (Number.isNaN(parsed)) {
|
|
11461
|
-
failWriteValidation2(`${flag} must be an ISO date (2026-07-10) or epoch milliseconds`);
|
|
11260
|
+
function parseLevel(raw) {
|
|
11261
|
+
const v = raw ?? "account";
|
|
11262
|
+
if (!isLevel(v)) {
|
|
11263
|
+
handleLinkedinError(new Error(`Invalid --level "${raw}". Use one of: ${ALL_LEVELS.join(", ")}.`));
|
|
11462
11264
|
}
|
|
11463
|
-
return
|
|
11265
|
+
return v;
|
|
11464
11266
|
}
|
|
11465
|
-
function
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
const raw = String(value).toLowerCase();
|
|
11470
|
-
if (raw === "on" || raw === "true") {
|
|
11471
|
-
return true;
|
|
11472
|
-
}
|
|
11473
|
-
if (raw === "off" || raw === "false") {
|
|
11474
|
-
return false;
|
|
11267
|
+
function parsePivot(raw) {
|
|
11268
|
+
const v = raw ?? "none";
|
|
11269
|
+
if (!isPivot(v)) {
|
|
11270
|
+
handleLinkedinError(new Error(`Invalid --pivot "${raw}". Use one of: ${ALL_PIVOTS.join(", ")}.`));
|
|
11475
11271
|
}
|
|
11476
|
-
|
|
11272
|
+
return v;
|
|
11477
11273
|
}
|
|
11478
|
-
function
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11274
|
+
function parseIntent(raw) {
|
|
11275
|
+
const v = raw ?? "baseline";
|
|
11276
|
+
const known = [
|
|
11277
|
+
"baseline",
|
|
11278
|
+
"revenue",
|
|
11279
|
+
"funnel",
|
|
11280
|
+
"engagement",
|
|
11281
|
+
"video",
|
|
11282
|
+
"lead-gen",
|
|
11283
|
+
"inmail",
|
|
11284
|
+
"document",
|
|
11285
|
+
"ranking",
|
|
11286
|
+
"identity"
|
|
11287
|
+
];
|
|
11288
|
+
if (!known.includes(v)) {
|
|
11289
|
+
handleLinkedinError(new Error(`Invalid --intent "${raw}". Run --list-intents to see options.`));
|
|
11485
11290
|
}
|
|
11486
|
-
return
|
|
11291
|
+
return v;
|
|
11487
11292
|
}
|
|
11488
|
-
function
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
const criteria = parsed.targetingCriteria ?? parsed;
|
|
11494
|
-
if (!criteria.include) {
|
|
11495
|
-
failWriteValidation2(
|
|
11496
|
-
`${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
11497
|
-
);
|
|
11293
|
+
function resolveDateRange(args) {
|
|
11294
|
+
const start = args.start;
|
|
11295
|
+
const end = args.end;
|
|
11296
|
+
if (start) {
|
|
11297
|
+
return { start, end };
|
|
11498
11298
|
}
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
}
|
|
11504
|
-
function parseCsvLine(line) {
|
|
11505
|
-
const cells = [];
|
|
11506
|
-
let current = "";
|
|
11507
|
-
let inQuotes = false;
|
|
11508
|
-
for (let i = 0; i < line.length; i++) {
|
|
11509
|
-
const char = line[i];
|
|
11510
|
-
if (inQuotes) {
|
|
11511
|
-
if (char === '"' && line[i + 1] === '"') {
|
|
11512
|
-
current += '"';
|
|
11513
|
-
i++;
|
|
11514
|
-
} else if (char === '"') {
|
|
11515
|
-
inQuotes = false;
|
|
11516
|
-
} else {
|
|
11517
|
-
current += char;
|
|
11518
|
-
}
|
|
11519
|
-
} else if (char === '"') {
|
|
11520
|
-
inQuotes = true;
|
|
11521
|
-
} else if (char === ",") {
|
|
11522
|
-
cells.push(current);
|
|
11523
|
-
current = "";
|
|
11524
|
-
} else {
|
|
11525
|
-
current += char;
|
|
11526
|
-
}
|
|
11299
|
+
const lastDaysRaw = args["last-days"];
|
|
11300
|
+
const days = lastDaysRaw ? Number(lastDaysRaw) : 7;
|
|
11301
|
+
if (!Number.isFinite(days) || days < 1 || days > 730) {
|
|
11302
|
+
handleLinkedinError(new Error(`Invalid --last-days "${lastDaysRaw}". Use a positive number \u2264 730.`));
|
|
11527
11303
|
}
|
|
11528
|
-
|
|
11529
|
-
return cells.map((cell) => cell.trim());
|
|
11304
|
+
return { start: daysAgoIso2(days), end: todayIso2() };
|
|
11530
11305
|
}
|
|
11531
|
-
function
|
|
11532
|
-
if (
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
11541
|
-
const rows = [];
|
|
11542
|
-
for (const line of lines.slice(1)) {
|
|
11543
|
-
const cells = parseCsvLine(line);
|
|
11544
|
-
const row = {};
|
|
11545
|
-
columns.forEach((column, index) => {
|
|
11546
|
-
const value = cells[index] ?? "";
|
|
11547
|
-
if (!value) {
|
|
11548
|
-
return;
|
|
11549
|
-
}
|
|
11550
|
-
row[column] = column.toLowerCase() === "email" ? sha256Lower(value) : value;
|
|
11551
|
-
});
|
|
11552
|
-
if (Object.keys(row).length > 0) {
|
|
11553
|
-
rows.push(row);
|
|
11306
|
+
function resolveScopeIds(args, level) {
|
|
11307
|
+
if (level === "account") {
|
|
11308
|
+
const id = resolveAccountIdArg2(args);
|
|
11309
|
+
return [id];
|
|
11310
|
+
}
|
|
11311
|
+
if (level === "campaign-group") {
|
|
11312
|
+
const raw2 = args["campaign-group-id"] ?? args.ids;
|
|
11313
|
+
if (!raw2) {
|
|
11314
|
+
handleLinkedinError(new Error("Pass --campaign-group-id (CSV for multi) when --level campaign-group."));
|
|
11554
11315
|
}
|
|
11316
|
+
return commaSplit(raw2) ?? [];
|
|
11555
11317
|
}
|
|
11556
|
-
if (
|
|
11557
|
-
|
|
11318
|
+
if (level === "campaign") {
|
|
11319
|
+
const raw2 = args["campaign-id"] ?? args.ids;
|
|
11320
|
+
if (!raw2) {
|
|
11321
|
+
handleLinkedinError(new Error("Pass --campaign-id (CSV for multi) when --level campaign."));
|
|
11322
|
+
}
|
|
11323
|
+
return commaSplit(raw2) ?? [];
|
|
11558
11324
|
}
|
|
11559
|
-
|
|
11325
|
+
const raw = args["creative-id"] ?? args.ids;
|
|
11326
|
+
if (!raw) {
|
|
11327
|
+
handleLinkedinError(new Error("Pass --creative-id (CSV for multi) when --level creative."));
|
|
11328
|
+
}
|
|
11329
|
+
return commaSplit(raw) ?? [];
|
|
11560
11330
|
}
|
|
11561
|
-
function
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
message: fields.map((field) => field.path ? `${field.path}: ${field.message}` : field.message).join("; "),
|
|
11567
|
-
fields,
|
|
11568
|
-
fix: capabilityFix("linkedin-ads")
|
|
11569
|
-
}
|
|
11331
|
+
function sortRows(rows) {
|
|
11332
|
+
return [...rows].sort((a, b) => {
|
|
11333
|
+
const cs = numberOf(b.costInUsd) - numberOf(a.costInUsd);
|
|
11334
|
+
if (cs !== 0) return cs;
|
|
11335
|
+
return numberOf(b.impressions) - numberOf(a.impressions);
|
|
11570
11336
|
});
|
|
11571
|
-
process.exit(1);
|
|
11572
|
-
}
|
|
11573
|
-
var BATCH_NUDGE = "This command takes several ids at once \u2014 pass them comma-separated or with --ids-file instead of one call per id.";
|
|
11574
|
-
function batchNudge(data) {
|
|
11575
|
-
return data.sameKindStaged === void 0 ? [] : [BATCH_NUDGE];
|
|
11576
11337
|
}
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
if (
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
try {
|
|
11583
|
-
const chatId = requireChatId();
|
|
11584
|
-
const response = await apiPost("/api/ads/linkedin/draft/stage", {
|
|
11585
|
-
chatId,
|
|
11586
|
-
op: preflight.data
|
|
11587
|
-
});
|
|
11588
|
-
const allHints = [...batchNudge(response.data), ...hints ?? [], ...adCopyHints(preflight.data)];
|
|
11589
|
-
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
11590
|
-
} catch (err) {
|
|
11591
|
-
handleLinkedinError(err);
|
|
11338
|
+
function numberOf(v) {
|
|
11339
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : 0;
|
|
11340
|
+
if (typeof v === "string") {
|
|
11341
|
+
const n = Number(v);
|
|
11342
|
+
return Number.isFinite(n) ? n : 0;
|
|
11592
11343
|
}
|
|
11344
|
+
return 0;
|
|
11593
11345
|
}
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11346
|
+
var analyticsCommand = defineCommand34({
|
|
11347
|
+
meta: {
|
|
11348
|
+
name: "analytics",
|
|
11349
|
+
description: `Performance reporting \u2014 the workhorse for AI agents.
|
|
11350
|
+
|
|
11351
|
+
Three-axis design (LinkedIn's superpower over Meta/Google):
|
|
11352
|
+
--level account | campaign-group | campaign | creative
|
|
11353
|
+
--intent baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity
|
|
11354
|
+
--pivot none | job-title | company | industry | seniority | function | company-size | country | region
|
|
11355
|
+
| device | placement | serving-location | card-index | objective
|
|
11356
|
+
| conversation-node | conversation-node-button
|
|
11357
|
+
|
|
11358
|
+
Smart defaults:
|
|
11359
|
+
--level account, --intent baseline, --pivot none, --granularity DAILY, last 7 days
|
|
11360
|
+
Pivot on a MEMBER_* dim \u2192 granularity auto-forced to ALL (LinkedIn rejects DAILY+demographic)
|
|
11361
|
+
Demographic data delayed 12-24h with \u22653-event privacy floor \u2014 the CLI flags this in warnings.
|
|
11362
|
+
Derived metrics injected client-side: ctr, cpc, cpm, frequency, leadCompletionRate
|
|
11363
|
+
|
|
11364
|
+
Examples \u2014 common AI questions:
|
|
11365
|
+
# "How is this account doing this week?"
|
|
11366
|
+
baker ads linkedin analytics
|
|
11367
|
+
|
|
11368
|
+
# "Who are we reaching, by job title?" (the LinkedIn killer)
|
|
11369
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot job-title --intent baseline --last-days 30
|
|
11370
|
+
|
|
11371
|
+
# "Top companies seeing my ads" (ABM feedback loop)
|
|
11372
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot company --last-days 30
|
|
11373
|
+
|
|
11374
|
+
# "Revenue by campaign over Q1"
|
|
11375
|
+
baker ads linkedin analytics --level campaign --campaign-id 1,2,3 --intent revenue --start 2026-01-01 --end 2026-03-31 --granularity MONTHLY
|
|
11376
|
+
|
|
11377
|
+
# "How is the lead form converting?"
|
|
11378
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --intent lead-gen
|
|
11379
|
+
|
|
11380
|
+
# Custom field set (escape hatch)
|
|
11381
|
+
baker ads linkedin analytics --metrics impressions,clicks,oneClickLeads --intent identity`
|
|
11382
|
+
},
|
|
11383
|
+
args: {
|
|
11384
|
+
level: { type: "string", description: "Object scope (default: account)" },
|
|
11385
|
+
"account-id": { type: "string", description: "Account ID \u2014 numeric or urn:li:sponsoredAccount:N (level=account)" },
|
|
11386
|
+
"account-urn": { type: "string", description: "Alias for --account-id (URN form)" },
|
|
11387
|
+
"campaign-group-id": { type: "string", description: "Comma-separated IDs (level=campaign-group)" },
|
|
11388
|
+
"campaign-id": { type: "string", description: "Comma-separated IDs (level=campaign)" },
|
|
11389
|
+
"creative-id": { type: "string", description: "Comma-separated IDs (level=creative)" },
|
|
11390
|
+
ids: { type: "string", description: "Generic CSV of IDs at the chosen level (alternative to per-level flags)" },
|
|
11391
|
+
intent: {
|
|
11392
|
+
type: "string",
|
|
11393
|
+
description: "baseline|revenue|funnel|engagement|video|lead-gen|inmail|document|ranking|identity"
|
|
11394
|
+
},
|
|
11395
|
+
pivot: { type: "string", description: "Pivot dim. Default: none. See `--list-pivots`." },
|
|
11396
|
+
metrics: { type: "string", description: "CSV metric override \u2014 bypass --intent's bundle" },
|
|
11397
|
+
"list-intents": { type: "boolean", description: "Print intent definitions and exit" },
|
|
11398
|
+
"list-pivots": { type: "boolean", description: "Print pivot slugs and exit" },
|
|
11399
|
+
start: { type: "string", description: "Start date YYYY-MM-DD (overrides --last-days)" },
|
|
11400
|
+
end: { type: "string", description: "End date YYYY-MM-DD" },
|
|
11401
|
+
"last-days": { type: "string", description: "Window relative to today (default: 7)" },
|
|
11402
|
+
granularity: { type: "string", description: "DAILY|MONTHLY|YEARLY|ALL (default: DAILY)" },
|
|
11403
|
+
limit: { type: "string", description: "Max rows (default: 1000)" },
|
|
11404
|
+
"no-sort": { type: "boolean", description: "Skip default sort by costInUsd desc" },
|
|
11405
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11406
|
+
output: { type: "string", description: "Output format json|csv|jsonl|md", default: "json" }
|
|
11407
|
+
},
|
|
11408
|
+
run: async ({ args }) => {
|
|
11409
|
+
if (args["list-intents"]) {
|
|
11410
|
+
writeAdsJson({ ok: true, data: listIntents() });
|
|
11411
|
+
return;
|
|
11412
|
+
}
|
|
11413
|
+
if (args["list-pivots"]) {
|
|
11414
|
+
writeAdsJson({ ok: true, data: ALL_PIVOTS });
|
|
11415
|
+
return;
|
|
11416
|
+
}
|
|
11417
|
+
const level = parseLevel(args.level);
|
|
11418
|
+
const intent = parseIntent(args.intent);
|
|
11419
|
+
const pivot = parsePivot(args.pivot);
|
|
11420
|
+
let granularity = parseGranularity(args.granularity);
|
|
11421
|
+
if (MEMBER_PIVOTS.has(pivot) && granularity === "DAILY") {
|
|
11422
|
+
process.stderr.write(`note: pivot=${pivot} forces --granularity ALL (LinkedIn rejects DAILY + demographic).
|
|
11423
|
+
`);
|
|
11424
|
+
granularity = "ALL";
|
|
11425
|
+
}
|
|
11426
|
+
const ids = resolveScopeIds(args, level);
|
|
11427
|
+
if (ids.length === 0) {
|
|
11428
|
+
handleLinkedinError(new Error(`No IDs resolved for --level ${level}`));
|
|
11429
|
+
}
|
|
11430
|
+
const range = resolveDateRange(args);
|
|
11431
|
+
const metrics = commaSplit(args.metrics);
|
|
11432
|
+
const limit = args.limit ? Number(args.limit) : 1e3;
|
|
11433
|
+
const request = {
|
|
11434
|
+
level,
|
|
11435
|
+
ids,
|
|
11436
|
+
intent,
|
|
11437
|
+
pivot,
|
|
11438
|
+
metrics,
|
|
11439
|
+
start: range.start,
|
|
11440
|
+
end: range.end,
|
|
11441
|
+
granularity,
|
|
11442
|
+
limit
|
|
11443
|
+
};
|
|
11444
|
+
try {
|
|
11445
|
+
const data = await apiPost("/api/ads/linkedin/analytics", {
|
|
11446
|
+
request,
|
|
11447
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11448
|
+
});
|
|
11449
|
+
const rows = args["no-sort"] ? data.rows : sortRows(data.rows);
|
|
11450
|
+
const fmt = csvOrJson2(args);
|
|
11451
|
+
if (fmt !== "json") {
|
|
11452
|
+
writeAdsOutput(rows, fmt, data.fields);
|
|
11453
|
+
return;
|
|
11454
|
+
}
|
|
11455
|
+
writeAdsJson({
|
|
11456
|
+
ok: true,
|
|
11457
|
+
data: {
|
|
11458
|
+
rows,
|
|
11459
|
+
fields: data.fields,
|
|
11460
|
+
warnings: data.warnings,
|
|
11461
|
+
query: data.query,
|
|
11462
|
+
// Echo composed fields so an offline agent can pre-flight w/o this network call.
|
|
11463
|
+
composedFields: composeFields(intent, level, metrics)
|
|
11464
|
+
}
|
|
11465
|
+
});
|
|
11466
|
+
} catch (err) {
|
|
11467
|
+
handleLinkedinError(err);
|
|
11468
|
+
}
|
|
11598
11469
|
}
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11470
|
+
});
|
|
11471
|
+
|
|
11472
|
+
// src/commands/ads/linkedin/audience-size.ts
|
|
11473
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
11474
|
+
import { defineCommand as defineCommand35 } from "citty";
|
|
11475
|
+
function loadTargeting(args) {
|
|
11476
|
+
const inline = args.targeting;
|
|
11477
|
+
if (inline) {
|
|
11478
|
+
try {
|
|
11479
|
+
return JSON.parse(inline);
|
|
11480
|
+
} catch {
|
|
11481
|
+
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
const file = args["targeting-file"];
|
|
11485
|
+
if (file) {
|
|
11486
|
+
try {
|
|
11487
|
+
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
11488
|
+
} catch (e) {
|
|
11489
|
+
handleLinkedinError(
|
|
11490
|
+
new Error(`Failed to read --targeting-file: ${e instanceof Error ? e.message : "I/O error"}`)
|
|
11608
11491
|
);
|
|
11609
11492
|
}
|
|
11610
|
-
ops.push(preflight.data);
|
|
11611
|
-
}
|
|
11612
|
-
try {
|
|
11613
|
-
const chatId = requireChatId();
|
|
11614
|
-
const response = await apiPost("/api/ads/linkedin/draft/stage-batch", { chatId, ops });
|
|
11615
|
-
const allHints = [...hints ?? [], ...adCopyHints(ops)];
|
|
11616
|
-
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
11617
|
-
} catch (err) {
|
|
11618
|
-
handleLinkedinError(err);
|
|
11619
11493
|
}
|
|
11494
|
+
handleLinkedinError(new Error("Pass --targeting-file <path> or --targeting '{...JSON...}'"));
|
|
11495
|
+
}
|
|
11496
|
+
var audienceSizeCommand = defineCommand35({
|
|
11497
|
+
meta: {
|
|
11498
|
+
name: "audience-size",
|
|
11499
|
+
description: `Estimate audience size for a targeting payload \u2014 pre-launch sanity check.
|
|
11500
|
+
|
|
11501
|
+
Returns total + active counts plus playbook \xA704 warnings:
|
|
11502
|
+
size <20K \u2192 too narrow (will not deliver)
|
|
11503
|
+
size <50K \u2192 outside the 50K\u2013500K sweet spot
|
|
11504
|
+
size >500K \u2192 outside the sweet spot
|
|
11505
|
+
size >1M \u2192 too broad, refine ICP
|
|
11506
|
+
|
|
11507
|
+
The targeting payload must follow LinkedIn's 'targetingCriteria' shape \u2014 an
|
|
11508
|
+
include/exclude tree of and/or boolean operators with adTargetingFacet keys.
|
|
11509
|
+
The simplest form:
|
|
11510
|
+
{ "include": { "and": [{ "or": { "urn:li:adTargetingFacet:locations": ["urn:li:geo:103644278"] }}]}}
|
|
11511
|
+
|
|
11512
|
+
Examples:
|
|
11513
|
+
baker ads linkedin audience-size --account-id 503001492 --targeting-file targeting.json
|
|
11514
|
+
baker ads linkedin audience-size --account-id 503001492 --targeting '{"include":{"and":[]}}'`
|
|
11515
|
+
},
|
|
11516
|
+
args: {
|
|
11517
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11518
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11519
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria payload" },
|
|
11520
|
+
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11521
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11522
|
+
output: { type: "string", description: "json", default: "json" }
|
|
11523
|
+
},
|
|
11524
|
+
run: async ({ args }) => {
|
|
11525
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11526
|
+
const targetingCriteria = loadTargeting(args);
|
|
11527
|
+
try {
|
|
11528
|
+
const data = await apiPost("/api/ads/linkedin/audience-size", {
|
|
11529
|
+
accountId,
|
|
11530
|
+
targetingCriteria,
|
|
11531
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11532
|
+
});
|
|
11533
|
+
writeAdsJson({ ok: true, data });
|
|
11534
|
+
} catch (err) {
|
|
11535
|
+
handleLinkedinError(err);
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
11538
|
+
});
|
|
11539
|
+
|
|
11540
|
+
// src/commands/ads/linkedin/audit.ts
|
|
11541
|
+
import { defineCommand as defineCommand36 } from "citty";
|
|
11542
|
+
var SEVERITY_RANK = {
|
|
11543
|
+
critical: 0,
|
|
11544
|
+
high: 1,
|
|
11545
|
+
medium: 2,
|
|
11546
|
+
low: 3
|
|
11547
|
+
};
|
|
11548
|
+
var STATUS_RANK = {
|
|
11549
|
+
fail: 0,
|
|
11550
|
+
warn: 1,
|
|
11551
|
+
partial: 2,
|
|
11552
|
+
pass: 3,
|
|
11553
|
+
n_a: 4
|
|
11554
|
+
};
|
|
11555
|
+
function filterFindings(findings, severity, area) {
|
|
11556
|
+
return findings.filter((f) => {
|
|
11557
|
+
if (severity && !severity.includes(f.severity)) return false;
|
|
11558
|
+
if (area && !area.includes(f.area)) return false;
|
|
11559
|
+
return true;
|
|
11560
|
+
});
|
|
11620
11561
|
}
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11562
|
+
function sortFindings(findings) {
|
|
11563
|
+
return [...findings].sort((a, b) => {
|
|
11564
|
+
const sa = STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
11565
|
+
if (sa !== 0) return sa;
|
|
11566
|
+
return SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
11567
|
+
});
|
|
11626
11568
|
}
|
|
11627
|
-
function
|
|
11628
|
-
const
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11569
|
+
function renderMarkdown(result) {
|
|
11570
|
+
const lines = [];
|
|
11571
|
+
lines.push(`# LinkedIn Ads Audit \u2014 ${result.account.name} (${result.account.id})`);
|
|
11572
|
+
lines.push("");
|
|
11573
|
+
lines.push(
|
|
11574
|
+
`**Scope:** ${result.scope.campaigns} campaigns, ${result.scope.creatives} creatives, last ${result.scope.windowDays}d`
|
|
11575
|
+
);
|
|
11576
|
+
lines.push("");
|
|
11577
|
+
lines.push(
|
|
11578
|
+
`**Summary:** ${result.summary.pass} pass \u2022 ${result.summary.critical} critical \u2022 ${result.summary.high} high \u2022 ${result.summary.medium} medium \u2022 ${result.summary.low} low \u2022 ${result.summary.n_a} n/a (${result.summary.totalChecks} total)`
|
|
11579
|
+
);
|
|
11580
|
+
lines.push("");
|
|
11581
|
+
lines.push("| # | Area | Check | Status | Severity | Notes |");
|
|
11582
|
+
lines.push("|---|------|-------|--------|----------|-------|");
|
|
11583
|
+
result.findings.forEach((f, idx) => {
|
|
11584
|
+
const note = noteOf(f);
|
|
11585
|
+
lines.push(
|
|
11586
|
+
`| ${idx + 1} | ${f.area} | ${escapeMd(f.check)} | ${f.status.toUpperCase()} | ${f.severity} | ${escapeMd(note)} |`
|
|
11640
11587
|
);
|
|
11641
|
-
}
|
|
11642
|
-
return
|
|
11588
|
+
});
|
|
11589
|
+
return lines.join("\n");
|
|
11643
11590
|
}
|
|
11644
|
-
function
|
|
11645
|
-
|
|
11646
|
-
return [];
|
|
11647
|
-
}
|
|
11648
|
-
return args._.filter((value) => typeof value === "string" && value.length > 0);
|
|
11591
|
+
function escapeMd(text) {
|
|
11592
|
+
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
11649
11593
|
}
|
|
11650
|
-
function
|
|
11651
|
-
|
|
11594
|
+
function noteOf(f) {
|
|
11595
|
+
if (f.status === "pass") return "";
|
|
11596
|
+
const ev = f.evidence ? JSON.stringify(f.evidence) : "";
|
|
11597
|
+
const fix = f.fix?.explanation ?? "";
|
|
11598
|
+
return [fix, ev].filter(Boolean).join(" \u2014 ");
|
|
11652
11599
|
}
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11600
|
+
var auditCommand = defineCommand36({
|
|
11601
|
+
meta: {
|
|
11602
|
+
name: "audit",
|
|
11603
|
+
description: `Run a LinkedIn Ads playbook audit \u2014 30+ checks across Settings, Tracking,
|
|
11604
|
+
Audience, Campaigns, Creative, Performance, Bidding, and Compliance.
|
|
11605
|
+
|
|
11606
|
+
Each finding has {id, area, check, status, severity, evidence, fix} fields with
|
|
11607
|
+
playbook citations. The default JSON output is agent-friendly; --format md
|
|
11608
|
+
renders a deliverable-ready table that mirrors the google-ads-playbook
|
|
11609
|
+
10-audit-summary.md style.
|
|
11610
|
+
|
|
11611
|
+
Exit code 0 always. Use the JSON 'summary' counts to decide if action is needed.
|
|
11612
|
+
|
|
11613
|
+
Examples:
|
|
11614
|
+
baker ads linkedin audit --account-id 503001492
|
|
11615
|
+
baker ads linkedin audit --account-id 503001492 --campaign-id 1234 # narrow scope
|
|
11616
|
+
baker ads linkedin audit --account-id 503001492 --window-days 90
|
|
11617
|
+
baker ads linkedin audit --account-id 503001492 --severity critical,high
|
|
11618
|
+
baker ads linkedin audit --account-id 503001492 --area Settings,Tracking
|
|
11619
|
+
baker ads linkedin audit --account-id 503001492 --format md`
|
|
11620
|
+
},
|
|
11621
|
+
args: {
|
|
11622
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11623
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11624
|
+
"campaign-id": { type: "string", description: "Narrow scope to a single campaign" },
|
|
11625
|
+
"window-days": { type: "string", description: "Lookback window for performance checks (default: 30)" },
|
|
11626
|
+
severity: { type: "string", description: "CSV severity filter: critical,high,medium,low" },
|
|
11627
|
+
area: {
|
|
11628
|
+
type: "string",
|
|
11629
|
+
description: "CSV area filter: Settings,Tracking,Audience,Campaigns,Creative,Performance,Bidding,Compliance,Hygiene"
|
|
11630
|
+
},
|
|
11631
|
+
format: { type: "string", description: "Output format: json | md (default: json)" },
|
|
11632
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" }
|
|
11633
|
+
},
|
|
11634
|
+
run: async ({ args }) => {
|
|
11635
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11636
|
+
const windowDays = args["window-days"] ? Number(args["window-days"]) : 30;
|
|
11637
|
+
if (!Number.isFinite(windowDays) || windowDays < 1 || windowDays > 365) {
|
|
11638
|
+
handleLinkedinError(new Error("--window-days must be 1..365"));
|
|
11639
|
+
}
|
|
11640
|
+
try {
|
|
11641
|
+
const data = await apiPost("/api/ads/linkedin/audit", {
|
|
11642
|
+
accountId,
|
|
11643
|
+
campaignId: args["campaign-id"] ? String(args["campaign-id"]) : void 0,
|
|
11644
|
+
windowDays,
|
|
11645
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11646
|
+
});
|
|
11647
|
+
const severityFilter = args.severity?.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
11648
|
+
const areaFilter = args.area?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11649
|
+
const filtered = filterFindings(data.findings, severityFilter, areaFilter);
|
|
11650
|
+
const sorted = sortFindings(filtered);
|
|
11651
|
+
const result = { ...data, findings: sorted };
|
|
11652
|
+
const fmt = args.format ?? "json";
|
|
11653
|
+
if (fmt === "md") {
|
|
11654
|
+
process.stdout.write(`${renderMarkdown(result)}
|
|
11655
|
+
`);
|
|
11656
|
+
return;
|
|
11657
|
+
}
|
|
11658
|
+
writeAdsJson({ ok: true, data: result });
|
|
11659
|
+
} catch (err) {
|
|
11660
|
+
handleLinkedinError(err);
|
|
11661
|
+
}
|
|
11656
11662
|
}
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11663
|
+
});
|
|
11664
|
+
|
|
11665
|
+
// src/commands/ads/linkedin/bid-pricing.ts
|
|
11666
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
11667
|
+
import { defineCommand as defineCommand37 } from "citty";
|
|
11668
|
+
function loadTargeting2(args) {
|
|
11669
|
+
const inline = args.targeting;
|
|
11670
|
+
if (inline) {
|
|
11671
|
+
try {
|
|
11672
|
+
return JSON.parse(inline);
|
|
11673
|
+
} catch {
|
|
11674
|
+
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11675
|
+
}
|
|
11670
11676
|
}
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11677
|
+
const file = args["targeting-file"];
|
|
11678
|
+
if (file) {
|
|
11679
|
+
try {
|
|
11680
|
+
return JSON.parse(readFileSync6(file, "utf-8"));
|
|
11681
|
+
} catch (e) {
|
|
11682
|
+
handleLinkedinError(
|
|
11683
|
+
new Error(`Failed to read --targeting-file: ${e instanceof Error ? e.message : "I/O error"}`)
|
|
11684
|
+
);
|
|
11685
|
+
}
|
|
11675
11686
|
}
|
|
11676
|
-
|
|
11687
|
+
handleLinkedinError(new Error("Pass --targeting-file <path> or --targeting '{...JSON...}'"));
|
|
11677
11688
|
}
|
|
11689
|
+
var bidPricingCommand = defineCommand37({
|
|
11690
|
+
meta: {
|
|
11691
|
+
name: "bid-pricing",
|
|
11692
|
+
description: `Get LinkedIn's suggested bid range for a targeting + objective + cost type.
|
|
11693
|
+
|
|
11694
|
+
Returns LinkedIn's min / suggested / max bid plus the playbook \xA706 floor
|
|
11695
|
+
(2/3 of suggested \u2014 the recommended manual CPC starting point).
|
|
11696
|
+
|
|
11697
|
+
Objective types: BRAND_AWARENESS | WEBSITE_TRAFFIC | WEBSITE_VISIT |
|
|
11698
|
+
ENGAGEMENT | WEBSITE_CONVERSION | LEAD_GENERATION |
|
|
11699
|
+
JOB_APPLICANT | VIDEO_VIEW
|
|
11700
|
+
Cost types: CPC | CPM | CPV | CPS
|
|
11701
|
+
|
|
11702
|
+
Examples:
|
|
11703
|
+
baker ads linkedin bid-pricing --account-id 503001492 --objective WEBSITE_CONVERSION --cost-type CPC --targeting-file targeting.json
|
|
11704
|
+
baker ads linkedin bid-pricing --account-id 503001492 --objective LEAD_GENERATION --cost-type CPM --targeting '{"include":...}'`
|
|
11705
|
+
},
|
|
11706
|
+
args: {
|
|
11707
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11708
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11709
|
+
objective: {
|
|
11710
|
+
type: "string",
|
|
11711
|
+
description: "Objective type (e.g. WEBSITE_CONVERSION, LEAD_GENERATION)",
|
|
11712
|
+
required: true
|
|
11713
|
+
},
|
|
11714
|
+
"cost-type": { type: "string", description: "CPC | CPM | CPV | CPS", required: true },
|
|
11715
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria" },
|
|
11716
|
+
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11717
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11718
|
+
output: { type: "string", description: "json", default: "json" }
|
|
11719
|
+
},
|
|
11720
|
+
run: async ({ args }) => {
|
|
11721
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11722
|
+
const targetingCriteria = loadTargeting2(args);
|
|
11723
|
+
try {
|
|
11724
|
+
const data = await apiPost("/api/ads/linkedin/bid-pricing", {
|
|
11725
|
+
accountId,
|
|
11726
|
+
objectiveType: String(args.objective),
|
|
11727
|
+
costType: String(args["cost-type"]).toUpperCase(),
|
|
11728
|
+
targetingCriteria,
|
|
11729
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11730
|
+
});
|
|
11731
|
+
writeAdsJson({ ok: true, data });
|
|
11732
|
+
} catch (err) {
|
|
11733
|
+
handleLinkedinError(err);
|
|
11734
|
+
}
|
|
11735
|
+
}
|
|
11736
|
+
});
|
|
11737
|
+
|
|
11738
|
+
// src/commands/ads/linkedin/campaign-groups.ts
|
|
11739
|
+
import { defineCommand as defineCommand39 } from "citty";
|
|
11678
11740
|
|
|
11679
11741
|
// src/commands/ads/linkedin/write-commands.ts
|
|
11742
|
+
import { defineCommand as defineCommand38 } from "citty";
|
|
11680
11743
|
var STAGED_NOTE = "Staged until publish \u2014 review with `baker ads linkedin draft`.";
|
|
11681
11744
|
var accountArgs = {
|
|
11682
11745
|
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
@@ -11701,7 +11764,7 @@ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-
|
|
|
11701
11764
|
name: { type: "string", description: "Campaign group name (\u2264100 chars)" },
|
|
11702
11765
|
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
|
|
11703
11766
|
end: { type: "string", description: "Run schedule end" },
|
|
11704
|
-
"total-budget": { type: "string", description:
|
|
11767
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11705
11768
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11706
11769
|
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
|
|
11707
11770
|
file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
|
|
@@ -11730,7 +11793,7 @@ Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000
|
|
|
11730
11793
|
name: { type: "string", description: "New name" },
|
|
11731
11794
|
start: { type: "string", description: "New run schedule start" },
|
|
11732
11795
|
end: { type: "string", description: "New run schedule end" },
|
|
11733
|
-
"total-budget": { type: "string", description:
|
|
11796
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11734
11797
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11735
11798
|
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|\u2026" },
|
|
11736
11799
|
file: { type: "string", description: "JSON file with fields to change" }
|
|
@@ -11781,7 +11844,7 @@ var campaignFlagArgs = {
|
|
|
11781
11844
|
"cost-type": { type: "string", description: "CPC|CPM|CPV" },
|
|
11782
11845
|
bid: { type: "string", description: "Manual bid (unitCost) \u2014 playbook mandates manual bidding" },
|
|
11783
11846
|
"daily-budget": { type: "string", description: "Daily budget amount" },
|
|
11784
|
-
"total-budget": { type: "string", description:
|
|
11847
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11785
11848
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11786
11849
|
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
|
|
11787
11850
|
end: { type: "string", description: "Run schedule end" },
|