@koda-sl/baker-cli 0.169.2 → 0.173.0-dev.6ecd65d29
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 +18 -4
- package/dist/{chunk-CG6OTNUU.js → chunk-K424D3CY.js} +4 -4
- package/dist/chunk-K424D3CY.js.map +1 -0
- package/dist/cli.js +2147 -1746
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-CG6OTNUU.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
toModelSafeImage,
|
|
37
37
|
ulid,
|
|
38
38
|
validateCanvasDeep
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-K424D3CY.js";
|
|
40
40
|
import {
|
|
41
41
|
csvOrJson,
|
|
42
42
|
daysAgoIso,
|
|
@@ -2983,6 +2983,117 @@ var hubspotFormsViewResponseSchema = z10.object({
|
|
|
2983
2983
|
external: hubspotFormNodeExternalSchema.optional()
|
|
2984
2984
|
})
|
|
2985
2985
|
});
|
|
2986
|
+
var HUBSPOT_MISSING_GRANT_MARKER = "has not granted access to";
|
|
2987
|
+
var hubspotFormSubmissionsRequestSchema = z10.object({
|
|
2988
|
+
formId: z10.string().min(1),
|
|
2989
|
+
/** Window in days, counted back from now. */
|
|
2990
|
+
days: z10.number().int().min(1).max(365).default(30),
|
|
2991
|
+
/** Include each submission's field values — personal data, so opt-in. */
|
|
2992
|
+
full: z10.boolean().optional()
|
|
2993
|
+
});
|
|
2994
|
+
var hubspotSubmissionValueSchema = z10.object({ name: z10.string(), value: z10.string() });
|
|
2995
|
+
var hubspotFormSubmissionsResponseSchema = z10.object({
|
|
2996
|
+
ok: z10.literal(true),
|
|
2997
|
+
data: z10.object({
|
|
2998
|
+
/** Submissions read from HubSpot, across every page fetched. */
|
|
2999
|
+
total: z10.number().int(),
|
|
3000
|
+
/** Submissions inside the requested window. */
|
|
3001
|
+
inWindow: z10.number().int(),
|
|
3002
|
+
lastSubmittedAt: z10.number().nullable(),
|
|
3003
|
+
byDay: z10.array(z10.object({ day: z10.string(), count: z10.number().int() })),
|
|
3004
|
+
/**
|
|
3005
|
+
* True when the page budget ran out before the window was fully covered, so
|
|
3006
|
+
* `inWindow` is a floor rather than a count. Reported rather than hidden:
|
|
3007
|
+
* a truncated read must not be mistaken for a complete one.
|
|
3008
|
+
*/
|
|
3009
|
+
truncated: z10.boolean(),
|
|
3010
|
+
/** Present only when `full` was requested; null otherwise. */
|
|
3011
|
+
submissions: z10.array(
|
|
3012
|
+
z10.object({
|
|
3013
|
+
submittedAt: z10.number(),
|
|
3014
|
+
pageUrl: z10.string().nullable(),
|
|
3015
|
+
values: z10.array(hubspotSubmissionValueSchema)
|
|
3016
|
+
})
|
|
3017
|
+
).nullable()
|
|
3018
|
+
})
|
|
3019
|
+
});
|
|
3020
|
+
var hubspotWorkflowsListRequestSchema = z10.object({
|
|
3021
|
+
search: z10.string().min(1).max(200).optional(),
|
|
3022
|
+
/** Only workflows that are currently on. */
|
|
3023
|
+
enabledOnly: z10.boolean().optional()
|
|
3024
|
+
});
|
|
3025
|
+
var hubspotWorkflowSummarySchema = z10.object({
|
|
3026
|
+
id: z10.string(),
|
|
3027
|
+
name: z10.string(),
|
|
3028
|
+
enabled: z10.boolean(),
|
|
3029
|
+
type: z10.string().nullable(),
|
|
3030
|
+
updatedAt: z10.number().nullable()
|
|
3031
|
+
});
|
|
3032
|
+
var hubspotWorkflowsListResponseSchema = z10.object({
|
|
3033
|
+
ok: z10.literal(true),
|
|
3034
|
+
data: z10.object({ workflows: z10.array(hubspotWorkflowSummarySchema) })
|
|
3035
|
+
});
|
|
3036
|
+
var hubspotContactsSummaryRequestSchema = z10.object({
|
|
3037
|
+
/** Window in days, counted back from now, over contact creation date. */
|
|
3038
|
+
days: z10.number().int().min(1).max(365).default(30)
|
|
3039
|
+
});
|
|
3040
|
+
var hubspotContactsSummaryResponseSchema = z10.object({
|
|
3041
|
+
ok: z10.literal(true),
|
|
3042
|
+
data: z10.object({
|
|
3043
|
+
total: z10.number().int(),
|
|
3044
|
+
withOwner: z10.number().int(),
|
|
3045
|
+
withoutOwner: z10.number().int(),
|
|
3046
|
+
byStage: z10.array(z10.object({ stage: z10.string(), count: z10.number().int() })),
|
|
3047
|
+
/** True when the page budget ran out, so the counts are floors. */
|
|
3048
|
+
truncated: z10.boolean()
|
|
3049
|
+
})
|
|
3050
|
+
});
|
|
3051
|
+
var hubspotContactLookupRequestSchema = z10.object({
|
|
3052
|
+
email: z10.string().min(1).max(320)
|
|
3053
|
+
});
|
|
3054
|
+
var hubspotContactLookupResponseSchema = z10.object({
|
|
3055
|
+
ok: z10.literal(true),
|
|
3056
|
+
data: z10.object({
|
|
3057
|
+
/** False means the lead never reached the CRM — a finding, not an error. */
|
|
3058
|
+
found: z10.boolean(),
|
|
3059
|
+
contact: z10.object({
|
|
3060
|
+
id: z10.string(),
|
|
3061
|
+
lifecycleStage: z10.string().nullable(),
|
|
3062
|
+
ownerId: z10.string().nullable(),
|
|
3063
|
+
createdAt: z10.string().nullable()
|
|
3064
|
+
}).nullable(),
|
|
3065
|
+
deals: z10.array(z10.object({ id: z10.string(), stage: z10.string().nullable(), pipeline: z10.string().nullable() })),
|
|
3066
|
+
/**
|
|
3067
|
+
* The account the contact was filed under. In B2B a workflow usually
|
|
3068
|
+
* creates it on contact creation, so its absence is a finding — but only
|
|
3069
|
+
* when `companyReadable` is true.
|
|
3070
|
+
*/
|
|
3071
|
+
company: z10.object({ id: z10.string(), name: z10.string().nullable(), domain: z10.string().nullable() }).nullable(),
|
|
3072
|
+
/**
|
|
3073
|
+
* False when the connection never granted company access, which makes
|
|
3074
|
+
* `company: null` unknown rather than absent. Company access is granted
|
|
3075
|
+
* separately from contact access, so one can be present without the other.
|
|
3076
|
+
*/
|
|
3077
|
+
companyReadable: z10.boolean()
|
|
3078
|
+
})
|
|
3079
|
+
});
|
|
3080
|
+
var hubspotPipelinesListRequestSchema = z10.object({});
|
|
3081
|
+
var hubspotPipelineStageSchema = z10.object({
|
|
3082
|
+
id: z10.string(),
|
|
3083
|
+
label: z10.string(),
|
|
3084
|
+
/** HubSpot's win probability for the stage, as it returns it (a decimal string). */
|
|
3085
|
+
probability: z10.string().nullable()
|
|
3086
|
+
});
|
|
3087
|
+
var hubspotPipelineSchema = z10.object({
|
|
3088
|
+
id: z10.string(),
|
|
3089
|
+
label: z10.string(),
|
|
3090
|
+
/** Ordered as they appear in HubSpot, so the sequence is the real one. */
|
|
3091
|
+
stages: z10.array(hubspotPipelineStageSchema)
|
|
3092
|
+
});
|
|
3093
|
+
var hubspotPipelinesListResponseSchema = z10.object({
|
|
3094
|
+
ok: z10.literal(true),
|
|
3095
|
+
data: z10.object({ pipelines: z10.array(hubspotPipelineSchema) })
|
|
3096
|
+
});
|
|
2986
3097
|
var hubspotMeetingFieldSchema = z10.object({
|
|
2987
3098
|
name: z10.string(),
|
|
2988
3099
|
label: z10.string(),
|
|
@@ -9778,1394 +9889,1161 @@ Full guide: __tooling__/docs/tools/baker/ads-google.md`
|
|
|
9778
9889
|
// src/commands/ads/linkedin/index.ts
|
|
9779
9890
|
import { defineCommand as defineCommand51 } from "citty";
|
|
9780
9891
|
|
|
9781
|
-
// src/commands/ads/linkedin/
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
}
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9892
|
+
// src/commands/ads/linkedin/write-shared.ts
|
|
9893
|
+
import { createHash } from "crypto";
|
|
9894
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
9895
|
+
|
|
9896
|
+
// src/commands/ads/linkedin/shared.ts
|
|
9897
|
+
var DAY_MS2 = 864e5;
|
|
9898
|
+
function handleLinkedinError(err) {
|
|
9899
|
+
if (err instanceof ApiError) {
|
|
9900
|
+
if (err.code === "UNAUTHORIZED") {
|
|
9901
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9902
|
+
}
|
|
9903
|
+
if (err.code === "NOT_FOUND") {
|
|
9904
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9905
|
+
}
|
|
9906
|
+
if (isNoAccountsSelectedError(err.code, err.message)) {
|
|
9907
|
+
handleConnectionError("linkedin_ads", err.message);
|
|
9908
|
+
}
|
|
9909
|
+
const explanation = explainLinkedinError(err);
|
|
9910
|
+
const envelope = {
|
|
9911
|
+
ok: false,
|
|
9912
|
+
error: {
|
|
9913
|
+
code: err.code,
|
|
9914
|
+
message: err.message,
|
|
9915
|
+
fix: {
|
|
9916
|
+
action: explanation.action,
|
|
9917
|
+
explanation: explanation.message,
|
|
9918
|
+
correctedCommand: explanation.correctedCommand
|
|
9919
|
+
},
|
|
9920
|
+
retryable: err.code === "RATE_LIMITED" || err.code === "INTERNAL_ERROR"
|
|
9921
|
+
}
|
|
9922
|
+
};
|
|
9923
|
+
writeAdsJson(envelope);
|
|
9924
|
+
process.exit(1);
|
|
9800
9925
|
}
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9926
|
+
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
9927
|
+
writeAdsJson({ ok: false, error: { code: "NETWORK_ERROR", message } });
|
|
9928
|
+
process.exit(1);
|
|
9929
|
+
}
|
|
9930
|
+
function isNoAccountsSelectedError(code, message) {
|
|
9931
|
+
return code === "VALIDATION_ERROR" && /no linkedin ad accounts selected/i.test(message);
|
|
9932
|
+
}
|
|
9933
|
+
function explainLinkedinError(err) {
|
|
9934
|
+
const lower = err.message.toLowerCase();
|
|
9935
|
+
if (err.code === "VALIDATION_ERROR" && lower.includes("granularity") && lower.includes("daily")) {
|
|
9936
|
+
return {
|
|
9937
|
+
action: "retry_with_flag",
|
|
9938
|
+
message: "LinkedIn rejects DAILY granularity when pivoting on demographic dims (job-title, company, etc.). Re-run with --granularity ALL or MONTHLY.",
|
|
9939
|
+
correctedCommand: void 0
|
|
9940
|
+
};
|
|
9813
9941
|
}
|
|
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" }
|
|
9942
|
+
if (err.code === "RATE_LIMITED") {
|
|
9943
|
+
return {
|
|
9944
|
+
action: "wait_and_retry",
|
|
9945
|
+
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."
|
|
9946
|
+
};
|
|
9827
9947
|
}
|
|
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" }
|
|
9948
|
+
if (err.code === "INTERNAL_ERROR" && lower.includes("deprecat")) {
|
|
9949
|
+
return {
|
|
9950
|
+
action: "reject",
|
|
9951
|
+
message: "LinkedIn-Version header is deprecated. The backend constant LINKEDIN_API_VERSION needs to be bumped \u2014 file an issue or update the constant."
|
|
9952
|
+
};
|
|
9851
9953
|
}
|
|
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" }
|
|
9954
|
+
return { action: "reject", message: err.message };
|
|
9955
|
+
}
|
|
9956
|
+
var LI_ACCOUNT_ID_RE = /^(urn:li:sponsoredAccount:)?\d+$/;
|
|
9957
|
+
function resolveAccountIdArg2(args) {
|
|
9958
|
+
const fromArgs = args["account-id"] ?? args["account-urn"];
|
|
9959
|
+
const id = fromArgs || getEnv().BAKER_LINKEDIN_AD_ACCOUNT_ID;
|
|
9960
|
+
if (!id) {
|
|
9961
|
+
writeAdsJson({
|
|
9962
|
+
ok: false,
|
|
9963
|
+
error: {
|
|
9964
|
+
code: "MISSING_ACCOUNT_ID",
|
|
9965
|
+
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."
|
|
9966
|
+
}
|
|
9967
|
+
});
|
|
9968
|
+
process.exit(1);
|
|
9933
9969
|
}
|
|
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" }
|
|
9970
|
+
if (!LI_ACCOUNT_ID_RE.test(id)) {
|
|
9971
|
+
writeAdsJson({
|
|
9972
|
+
ok: false,
|
|
9973
|
+
error: {
|
|
9974
|
+
code: "INVALID_ACCOUNT_ID",
|
|
9975
|
+
message: `Invalid LinkedIn account ID "${id}". Expected numeric or urn:li:sponsoredAccount:N.`
|
|
9976
|
+
}
|
|
9977
|
+
});
|
|
9978
|
+
process.exit(1);
|
|
9948
9979
|
}
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9980
|
+
return id.startsWith("urn:") ? id.split(":").pop() ?? id : id;
|
|
9981
|
+
}
|
|
9982
|
+
function todayIso2() {
|
|
9983
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9984
|
+
}
|
|
9985
|
+
function daysAgoIso2(days) {
|
|
9986
|
+
return new Date(Date.now() - days * DAY_MS2).toISOString().slice(0, 10);
|
|
9987
|
+
}
|
|
9988
|
+
function csvOrJson2(args) {
|
|
9989
|
+
return args.output ?? "json";
|
|
9990
|
+
}
|
|
9991
|
+
function resolveStatusFilter(args) {
|
|
9992
|
+
if (args["all-statuses"]) {
|
|
9993
|
+
return void 0;
|
|
9962
9994
|
}
|
|
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" }
|
|
9995
|
+
const explicit = args.statuses;
|
|
9996
|
+
if (explicit) {
|
|
9997
|
+
return explicit.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
9970
9998
|
}
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9999
|
+
return ["ACTIVE"];
|
|
10000
|
+
}
|
|
10001
|
+
|
|
10002
|
+
// src/commands/ads/linkedin/write-shared.ts
|
|
10003
|
+
var TOTAL_BUDGET_HELP = "Lifetime budget \u2014 on something already running it must exceed spend to date, or LinkedIn rejects it";
|
|
10004
|
+
function bareAccountId(args) {
|
|
10005
|
+
const raw = resolveAccountIdArg2(args);
|
|
10006
|
+
return raw.replace(/^urn:li:sponsoredAccount:/, "").replace(/^sponsoredAccount:/, "");
|
|
10007
|
+
}
|
|
10008
|
+
function failWriteValidation2(message) {
|
|
10009
|
+
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
10010
|
+
process.exit(1);
|
|
10011
|
+
}
|
|
10012
|
+
function loadJsonFileArg2(path28) {
|
|
10013
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10014
|
+
return {};
|
|
9982
10015
|
}
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
10016
|
+
try {
|
|
10017
|
+
const parsed = JSON.parse(readFileSync4(path28, "utf8"));
|
|
10018
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10019
|
+
failWriteValidation2(`${path28} must contain a JSON object`);
|
|
10020
|
+
}
|
|
10021
|
+
return parsed;
|
|
10022
|
+
} catch (err) {
|
|
10023
|
+
if (err instanceof SyntaxError) {
|
|
10024
|
+
failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
|
|
10025
|
+
}
|
|
10026
|
+
throw err;
|
|
9994
10027
|
}
|
|
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" }
|
|
10028
|
+
}
|
|
10029
|
+
function mergePayload2(file, flags) {
|
|
10030
|
+
const merged = { ...file };
|
|
10031
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
10032
|
+
if (value !== void 0) {
|
|
10033
|
+
merged[key] = value;
|
|
10034
|
+
}
|
|
10012
10035
|
}
|
|
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" }
|
|
10036
|
+
return merged;
|
|
10037
|
+
}
|
|
10038
|
+
function loadPatchArg2(args) {
|
|
10039
|
+
const file = typeof args.file === "string" && args.file.length > 0 ? args.file : void 0;
|
|
10040
|
+
const inline = typeof args.patch === "string" && args.patch.length > 0 ? args.patch : void 0;
|
|
10041
|
+
if (Boolean(file) === Boolean(inline)) {
|
|
10042
|
+
failWriteValidation2("pass exactly one of --file <patch.json> or --patch '<json>'");
|
|
10029
10043
|
}
|
|
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" }
|
|
10044
|
+
if (file) {
|
|
10045
|
+
return loadJsonFileArg2(file);
|
|
10044
10046
|
}
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10047
|
+
try {
|
|
10048
|
+
const parsed = JSON.parse(inline);
|
|
10049
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10050
|
+
failWriteValidation2("--patch must be a JSON object");
|
|
10051
|
+
}
|
|
10052
|
+
return parsed;
|
|
10053
|
+
} catch (err) {
|
|
10054
|
+
if (err instanceof SyntaxError) {
|
|
10055
|
+
failWriteValidation2(`--patch is not valid JSON: ${err.message}`);
|
|
10056
|
+
}
|
|
10057
|
+
throw err;
|
|
10054
10058
|
}
|
|
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" }
|
|
10059
|
+
}
|
|
10060
|
+
function parseMoneyFlag(amount, currency, flag) {
|
|
10061
|
+
if (amount === void 0 || amount === null || amount === "") {
|
|
10062
|
+
return void 0;
|
|
10064
10063
|
}
|
|
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" }
|
|
10064
|
+
if (currency === void 0 || currency === null || currency === "") {
|
|
10065
|
+
return { amount: String(amount) };
|
|
10076
10066
|
}
|
|
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 }
|
|
10067
|
+
if (typeof currency !== "string" || currency.length !== 3) {
|
|
10068
|
+
failWriteValidation2(`${flag} got an invalid --currency (expected a 3-letter code like EUR)`);
|
|
10090
10069
|
}
|
|
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 }
|
|
10070
|
+
return { amount: String(amount), currencyCode: currency.toUpperCase() };
|
|
10071
|
+
}
|
|
10072
|
+
function parseDateFlag(value, flag) {
|
|
10073
|
+
if (value === void 0 || value === null || value === "") {
|
|
10074
|
+
return void 0;
|
|
10112
10075
|
}
|
|
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 }
|
|
10076
|
+
const raw = String(value);
|
|
10077
|
+
if (/^\d{10,}$/.test(raw)) {
|
|
10078
|
+
return Number(raw);
|
|
10135
10079
|
}
|
|
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 }
|
|
10080
|
+
const parsed = Date.parse(raw);
|
|
10081
|
+
if (Number.isNaN(parsed)) {
|
|
10082
|
+
failWriteValidation2(`${flag} must be an ISO date (2026-07-10) or epoch milliseconds`);
|
|
10170
10083
|
}
|
|
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 }
|
|
10084
|
+
return parsed;
|
|
10085
|
+
}
|
|
10086
|
+
function parseOnOffFlag(value, flag) {
|
|
10087
|
+
if (value === void 0 || value === null || value === "") {
|
|
10088
|
+
return void 0;
|
|
10198
10089
|
}
|
|
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 }
|
|
10090
|
+
const raw = String(value).toLowerCase();
|
|
10091
|
+
if (raw === "on" || raw === "true") {
|
|
10092
|
+
return true;
|
|
10218
10093
|
}
|
|
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 }
|
|
10094
|
+
if (raw === "off" || raw === "false") {
|
|
10095
|
+
return false;
|
|
10303
10096
|
}
|
|
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 }
|
|
10097
|
+
failWriteValidation2(`${flag} must be on|off`);
|
|
10098
|
+
}
|
|
10099
|
+
function parseLocaleFlag(value) {
|
|
10100
|
+
if (value === void 0 || value === null || value === "") {
|
|
10101
|
+
return void 0;
|
|
10347
10102
|
}
|
|
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 }
|
|
10103
|
+
const match = /^([a-z]{2})[_-]([A-Za-z]{2})$/.exec(String(value));
|
|
10104
|
+
if (!match) {
|
|
10105
|
+
failWriteValidation2("--locale must look like en_US (language_COUNTRY)");
|
|
10401
10106
|
}
|
|
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 }
|
|
10107
|
+
return { language: match[1], country: match[2].toUpperCase() };
|
|
10108
|
+
}
|
|
10109
|
+
function loadTargetingFileArg(path28) {
|
|
10110
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10111
|
+
return void 0;
|
|
10410
10112
|
}
|
|
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 }
|
|
10113
|
+
const parsed = loadJsonFileArg2(path28);
|
|
10114
|
+
const criteria = parsed.targetingCriteria ?? parsed;
|
|
10115
|
+
if (!criteria.include) {
|
|
10116
|
+
failWriteValidation2(
|
|
10117
|
+
`${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
10118
|
+
);
|
|
10437
10119
|
}
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10120
|
+
return criteria;
|
|
10121
|
+
}
|
|
10122
|
+
function sha256Lower(value) {
|
|
10123
|
+
return createHash("sha256").update(value.trim().toLowerCase()).digest("hex");
|
|
10124
|
+
}
|
|
10125
|
+
function parseCsvLine(line) {
|
|
10126
|
+
const cells = [];
|
|
10127
|
+
let current = "";
|
|
10128
|
+
let inQuotes = false;
|
|
10129
|
+
for (let i = 0; i < line.length; i++) {
|
|
10130
|
+
const char = line[i];
|
|
10131
|
+
if (inQuotes) {
|
|
10132
|
+
if (char === '"' && line[i + 1] === '"') {
|
|
10133
|
+
current += '"';
|
|
10134
|
+
i++;
|
|
10135
|
+
} else if (char === '"') {
|
|
10136
|
+
inQuotes = false;
|
|
10137
|
+
} else {
|
|
10138
|
+
current += char;
|
|
10139
|
+
}
|
|
10140
|
+
} else if (char === '"') {
|
|
10141
|
+
inQuotes = true;
|
|
10142
|
+
} else if (char === ",") {
|
|
10143
|
+
cells.push(current);
|
|
10144
|
+
current = "";
|
|
10145
|
+
} else {
|
|
10146
|
+
current += char;
|
|
10147
|
+
}
|
|
10451
10148
|
}
|
|
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 }
|
|
10149
|
+
cells.push(current);
|
|
10150
|
+
return cells.map((cell) => cell.trim());
|
|
10151
|
+
}
|
|
10152
|
+
function parseListFileArg(path28, maxRows) {
|
|
10153
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10154
|
+
return void 0;
|
|
10460
10155
|
}
|
|
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 }
|
|
10156
|
+
const raw = readFileSync4(path28, "utf8");
|
|
10157
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
10158
|
+
if (lines.length < 2) {
|
|
10159
|
+
failWriteValidation2(`${path28} needs a header row and at least one data row`);
|
|
10470
10160
|
}
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10161
|
+
const columns = parseCsvLine(lines[0]).map((column) => column.trim());
|
|
10162
|
+
const rows = [];
|
|
10163
|
+
for (const line of lines.slice(1)) {
|
|
10164
|
+
const cells = parseCsvLine(line);
|
|
10165
|
+
const row = {};
|
|
10166
|
+
columns.forEach((column, index) => {
|
|
10167
|
+
const value = cells[index] ?? "";
|
|
10168
|
+
if (!value) {
|
|
10169
|
+
return;
|
|
10170
|
+
}
|
|
10171
|
+
row[column] = column.toLowerCase() === "email" ? sha256Lower(value) : value;
|
|
10172
|
+
});
|
|
10173
|
+
if (Object.keys(row).length > 0) {
|
|
10174
|
+
rows.push(row);
|
|
10480
10175
|
}
|
|
10481
10176
|
}
|
|
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
|
-
}
|
|
10177
|
+
if (rows.length > maxRows) {
|
|
10178
|
+
failWriteValidation2(`${path28} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
|
|
10492
10179
|
}
|
|
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);
|
|
10180
|
+
return { columns, rows };
|
|
10181
|
+
}
|
|
10182
|
+
function failPreflight(fields) {
|
|
10183
|
+
writeJsonEnvelope({
|
|
10184
|
+
ok: false,
|
|
10185
|
+
error: {
|
|
10186
|
+
code: "VALIDATION_ERROR",
|
|
10187
|
+
message: fields.map((field) => field.path ? `${field.path}: ${field.message}` : field.message).join("; "),
|
|
10188
|
+
fields,
|
|
10189
|
+
fix: capabilityFix("linkedin-ads")
|
|
10515
10190
|
}
|
|
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 } });
|
|
10191
|
+
});
|
|
10535
10192
|
process.exit(1);
|
|
10536
10193
|
}
|
|
10537
|
-
|
|
10538
|
-
|
|
10194
|
+
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.";
|
|
10195
|
+
function batchNudge(data) {
|
|
10196
|
+
return data.sameKindStaged === void 0 ? [] : [BATCH_NUDGE];
|
|
10539
10197
|
}
|
|
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
|
-
};
|
|
10198
|
+
async function stageOp(raw, hints) {
|
|
10199
|
+
const preflight = linkedinDraftOpInputSchema.safeParse(raw);
|
|
10200
|
+
if (!preflight.success) {
|
|
10201
|
+
failPreflight(preflight.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message })));
|
|
10548
10202
|
}
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10203
|
+
try {
|
|
10204
|
+
const chatId = requireChatId();
|
|
10205
|
+
const response = await apiPost("/api/ads/linkedin/draft/stage", {
|
|
10206
|
+
chatId,
|
|
10207
|
+
op: preflight.data
|
|
10208
|
+
});
|
|
10209
|
+
const allHints = [...batchNudge(response.data), ...hints ?? [], ...adCopyHints(preflight.data)];
|
|
10210
|
+
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
10211
|
+
} catch (err) {
|
|
10212
|
+
handleLinkedinError(err);
|
|
10554
10213
|
}
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10214
|
+
}
|
|
10215
|
+
async function stageLinkedinOps(rawOps, hints) {
|
|
10216
|
+
if (rawOps.length === 1 && rawOps[0]) {
|
|
10217
|
+
await stageOp(rawOps[0], hints);
|
|
10218
|
+
return;
|
|
10219
|
+
}
|
|
10220
|
+
const ops = [];
|
|
10221
|
+
for (const [index, raw] of rawOps.entries()) {
|
|
10222
|
+
const preflight = linkedinDraftOpInputSchema.safeParse(raw);
|
|
10223
|
+
if (!preflight.success) {
|
|
10224
|
+
failPreflight(
|
|
10225
|
+
preflight.error.issues.map((issue) => ({
|
|
10226
|
+
path: `ops[${index}]${issue.path.length > 0 ? `.${issue.path.join(".")}` : ""}`,
|
|
10227
|
+
message: issue.message
|
|
10228
|
+
}))
|
|
10229
|
+
);
|
|
10230
|
+
}
|
|
10231
|
+
ops.push(preflight.data);
|
|
10232
|
+
}
|
|
10233
|
+
try {
|
|
10234
|
+
const chatId = requireChatId();
|
|
10235
|
+
const response = await apiPost("/api/ads/linkedin/draft/stage-batch", { chatId, ops });
|
|
10236
|
+
const allHints = [...hints ?? [], ...adCopyHints(ops)];
|
|
10237
|
+
writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
|
|
10238
|
+
} catch (err) {
|
|
10239
|
+
handleLinkedinError(err);
|
|
10560
10240
|
}
|
|
10561
|
-
return { action: "reject", message: err.message };
|
|
10562
10241
|
}
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
}
|
|
10574
|
-
});
|
|
10575
|
-
process.exit(1);
|
|
10242
|
+
async function stageStatusChange(input) {
|
|
10243
|
+
const payload = input.kind === "creative.update" ? { intendedStatus: input.status } : { status: input.status };
|
|
10244
|
+
await stageLinkedinOps(
|
|
10245
|
+
input.targets.map((target) => ({ kind: input.kind, accountId: input.accountId, target, payload }))
|
|
10246
|
+
);
|
|
10247
|
+
}
|
|
10248
|
+
function requireTarget2(args, entity) {
|
|
10249
|
+
const inverted = booleanWordPositionalError(args, entity);
|
|
10250
|
+
if (inverted !== void 0) {
|
|
10251
|
+
failWriteValidation2(inverted);
|
|
10576
10252
|
}
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
code: "INVALID_ACCOUNT_ID",
|
|
10582
|
-
message: `Invalid LinkedIn account ID "${id}". Expected numeric or urn:li:sponsoredAccount:N.`
|
|
10583
|
-
}
|
|
10584
|
-
});
|
|
10585
|
-
process.exit(1);
|
|
10253
|
+
const positionals = readPositionals(args);
|
|
10254
|
+
const target = args.id ?? args.target ?? args.ref ?? positionals[0];
|
|
10255
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
10256
|
+
failWriteValidation2(`pass the ${entity} id or URN as the positional argument`);
|
|
10586
10257
|
}
|
|
10587
|
-
|
|
10258
|
+
if (positionals.length > 1 || splitIdList(target).length > 1) {
|
|
10259
|
+
failWriteValidation2(
|
|
10260
|
+
`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.`
|
|
10261
|
+
);
|
|
10262
|
+
}
|
|
10263
|
+
return target;
|
|
10588
10264
|
}
|
|
10589
|
-
function
|
|
10590
|
-
|
|
10265
|
+
function readPositionals(args) {
|
|
10266
|
+
if (!Array.isArray(args._)) {
|
|
10267
|
+
return [];
|
|
10268
|
+
}
|
|
10269
|
+
return args._.filter((value) => typeof value === "string" && value.length > 0);
|
|
10591
10270
|
}
|
|
10592
|
-
function
|
|
10593
|
-
return
|
|
10271
|
+
function splitIdList(raw) {
|
|
10272
|
+
return raw.split(",").map((id) => id.trim()).filter(Boolean);
|
|
10594
10273
|
}
|
|
10595
|
-
function
|
|
10596
|
-
|
|
10274
|
+
function idsFileEntries(path28) {
|
|
10275
|
+
if (typeof path28 !== "string" || path28.length === 0) {
|
|
10276
|
+
return [];
|
|
10277
|
+
}
|
|
10278
|
+
return readFileSync4(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
|
|
10597
10279
|
}
|
|
10598
|
-
function
|
|
10599
|
-
|
|
10600
|
-
|
|
10280
|
+
function requireTargets(args, entity) {
|
|
10281
|
+
const positionals = readPositionals(args);
|
|
10282
|
+
const named = [args.id, args.target, args.ref].filter(
|
|
10283
|
+
(value) => typeof value === "string" && value.length > 0
|
|
10284
|
+
);
|
|
10285
|
+
const fromArgs = positionals.length > 0 ? positionals : named;
|
|
10286
|
+
const targets = [.../* @__PURE__ */ new Set([...fromArgs.flatMap(splitIdList), ...idsFileEntries(args["ids-file"])])];
|
|
10287
|
+
if (targets.length === 0) {
|
|
10288
|
+
failWriteValidation2(
|
|
10289
|
+
`pass the ${entity} id or URN as the positional argument (comma-separated for several), or --ids-file <path>`
|
|
10290
|
+
);
|
|
10601
10291
|
}
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10292
|
+
if (targets.length > LINKEDIN_DRAFT_BATCH_MAX) {
|
|
10293
|
+
failWriteValidation2(
|
|
10294
|
+
`${targets.length} ids exceeds the batch limit of ${LINKEDIN_DRAFT_BATCH_MAX} \u2014 split the list`
|
|
10295
|
+
);
|
|
10605
10296
|
}
|
|
10606
|
-
return
|
|
10297
|
+
return targets;
|
|
10607
10298
|
}
|
|
10608
10299
|
|
|
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
|
-
},
|
|
10300
|
+
// src/commands/ads/linkedin/schemas.ts
|
|
10301
|
+
registerSchema({
|
|
10302
|
+
command: "ads.linkedin.accounts",
|
|
10303
|
+
description: "List LinkedIn ad accounts in the company's connected scope. Pass --include-all to list every account the OAuth token can see.",
|
|
10619
10304
|
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
|
-
}
|
|
10305
|
+
"include-all": { type: "boolean", description: "Ignore picker scope", required: false },
|
|
10306
|
+
"no-cache": { type: "boolean", description: "Skip CLI-side cache", required: false },
|
|
10307
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache", required: false },
|
|
10308
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10640
10309
|
}
|
|
10641
10310
|
});
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10311
|
+
registerSchema({
|
|
10312
|
+
command: "ads.linkedin.account",
|
|
10313
|
+
description: "Single LinkedIn ad account detail (currency, status, type). Resolves the urn \u2192 {id, name, currency, status, type}.",
|
|
10314
|
+
args: {
|
|
10315
|
+
"account-id": { type: "string", description: "Numeric ID or urn:li:sponsoredAccount:N", required: false },
|
|
10316
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false },
|
|
10317
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10318
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10649
10319
|
}
|
|
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
|
-
},
|
|
10320
|
+
});
|
|
10321
|
+
registerSchema({
|
|
10322
|
+
command: "ads.linkedin.campaign-groups",
|
|
10323
|
+
description: "List LinkedIn campaign groups in an account. Default ACTIVE-only \u2014 pass --all-statuses to widen.",
|
|
10667
10324
|
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
|
-
}
|
|
10325
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10326
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false },
|
|
10327
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10328
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10329
|
+
limit: { type: "string", description: "Max rows (default 500)", required: false },
|
|
10330
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10331
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10706
10332
|
}
|
|
10707
10333
|
});
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
|
|
10714
|
-
|
|
10715
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
"
|
|
10738
|
-
"
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
"
|
|
10752
|
-
"
|
|
10753
|
-
|
|
10754
|
-
"
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
"
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10334
|
+
registerSchema({
|
|
10335
|
+
command: "ads.linkedin.campaigns",
|
|
10336
|
+
description: "List LinkedIn campaigns. Returns audit-relevant settings (audienceExpansionEnabled, offsiteDeliveryEnabled, optimizationTargetType, costType, budgets, runSchedule, targetingCriteria).",
|
|
10337
|
+
args: {
|
|
10338
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10339
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10340
|
+
"campaign-group-id": { type: "string", description: "Filter by campaign group", required: false },
|
|
10341
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10342
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10343
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10344
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10345
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10346
|
+
}
|
|
10347
|
+
});
|
|
10348
|
+
registerSchema({
|
|
10349
|
+
command: "ads.linkedin.creatives",
|
|
10350
|
+
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.",
|
|
10351
|
+
args: {
|
|
10352
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10353
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10354
|
+
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
10355
|
+
"all-statuses": { type: "boolean", description: "Drop the ACTIVE filter", required: false },
|
|
10356
|
+
statuses: { type: "string", description: "CSV of statuses", required: false },
|
|
10357
|
+
fields: {
|
|
10358
|
+
type: "string",
|
|
10359
|
+
description: "CSV of dotted paths to return instead of the compact row, e.g. id,content.textAd.landingPage",
|
|
10360
|
+
required: false
|
|
10361
|
+
},
|
|
10362
|
+
full: {
|
|
10363
|
+
type: "boolean",
|
|
10364
|
+
description: "Return the complete raw records, including the opaque 'content' block",
|
|
10365
|
+
required: false
|
|
10366
|
+
},
|
|
10367
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10368
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10369
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10370
|
+
}
|
|
10371
|
+
});
|
|
10372
|
+
registerSchema({
|
|
10373
|
+
command: "ads.linkedin.analytics",
|
|
10374
|
+
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.",
|
|
10375
|
+
args: {
|
|
10376
|
+
level: {
|
|
10377
|
+
type: "string",
|
|
10378
|
+
description: "account|campaign-group|campaign|creative",
|
|
10379
|
+
required: false,
|
|
10380
|
+
default: "account"
|
|
10381
|
+
},
|
|
10382
|
+
"account-id": { type: "string", description: "When level=account", required: false },
|
|
10383
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10384
|
+
"campaign-group-id": { type: "string", description: "CSV when level=campaign-group", required: false },
|
|
10385
|
+
"campaign-id": { type: "string", description: "CSV when level=campaign", required: false },
|
|
10386
|
+
"creative-id": { type: "string", description: "CSV when level=creative", required: false },
|
|
10387
|
+
ids: { type: "string", description: "Generic CSV alternative", required: false },
|
|
10388
|
+
intent: {
|
|
10389
|
+
type: "string",
|
|
10390
|
+
description: "Field bundle",
|
|
10391
|
+
required: false,
|
|
10392
|
+
default: "baseline",
|
|
10393
|
+
enum: [
|
|
10394
|
+
"baseline",
|
|
10395
|
+
"revenue",
|
|
10396
|
+
"funnel",
|
|
10397
|
+
"engagement",
|
|
10398
|
+
"video",
|
|
10399
|
+
"lead-gen",
|
|
10400
|
+
"inmail",
|
|
10401
|
+
"document",
|
|
10402
|
+
"ranking",
|
|
10403
|
+
"identity"
|
|
10404
|
+
]
|
|
10405
|
+
},
|
|
10406
|
+
pivot: {
|
|
10407
|
+
type: "string",
|
|
10408
|
+
description: "Demographic / firmographic dim",
|
|
10409
|
+
required: false,
|
|
10410
|
+
default: "none",
|
|
10411
|
+
enum: [
|
|
10412
|
+
"none",
|
|
10413
|
+
"campaign",
|
|
10414
|
+
"campaign-group",
|
|
10415
|
+
"creative",
|
|
10416
|
+
"company",
|
|
10417
|
+
"account",
|
|
10418
|
+
"conversion",
|
|
10419
|
+
"job-title",
|
|
10420
|
+
"job-function",
|
|
10421
|
+
"seniority",
|
|
10422
|
+
"industry",
|
|
10423
|
+
"company-size",
|
|
10424
|
+
"country",
|
|
10425
|
+
"region",
|
|
10426
|
+
"device",
|
|
10427
|
+
"placement",
|
|
10428
|
+
"serving-location",
|
|
10429
|
+
"card-index",
|
|
10430
|
+
"objective",
|
|
10431
|
+
"conversation-node",
|
|
10432
|
+
"conversation-node-button"
|
|
10433
|
+
]
|
|
10434
|
+
},
|
|
10435
|
+
metrics: { type: "string", description: "CSV metric override (escape hatch)", required: false },
|
|
10436
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10437
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10438
|
+
"last-days": { type: "string", description: "Window (default 7)", required: false },
|
|
10439
|
+
granularity: {
|
|
10440
|
+
type: "string",
|
|
10441
|
+
description: "DAILY|MONTHLY|YEARLY|ALL",
|
|
10442
|
+
required: false,
|
|
10443
|
+
default: "DAILY",
|
|
10444
|
+
enum: ["DAILY", "MONTHLY", "YEARLY", "ALL"]
|
|
10445
|
+
},
|
|
10446
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10447
|
+
"no-sort": { type: "boolean", description: "Skip default sort", required: false },
|
|
10448
|
+
"list-intents": { type: "boolean", description: "List intents and exit", required: false },
|
|
10449
|
+
"list-pivots": { type: "boolean", description: "List pivots and exit", required: false },
|
|
10450
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10451
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10452
|
+
}
|
|
10453
|
+
});
|
|
10454
|
+
registerSchema({
|
|
10455
|
+
command: "ads.linkedin.demographics",
|
|
10456
|
+
description: "Sweep all firmographic pivots (job-title, company, industry, seniority, function, company-size) in one call. Returns top-N rows per pivot.",
|
|
10457
|
+
args: {
|
|
10458
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10459
|
+
pivots: { type: "string", description: "CSV of pivots (default: all firmographic)", required: false },
|
|
10460
|
+
"top-n": { type: "string", description: "Top rows per pivot (default 10)", required: false },
|
|
10461
|
+
intent: { type: "string", description: "Field bundle", required: false, default: "baseline" },
|
|
10462
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10463
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10464
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10465
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10466
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10467
|
+
}
|
|
10468
|
+
});
|
|
10469
|
+
registerSchema({
|
|
10470
|
+
command: "ads.linkedin.top-companies",
|
|
10471
|
+
description: "Top companies whose employees saw / clicked / converted. Wrapper for analytics --pivot company. The ABM feedback loop.",
|
|
10472
|
+
args: {
|
|
10473
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10474
|
+
intent: { type: "string", description: "Field bundle (default baseline)", required: false },
|
|
10475
|
+
"top-n": { type: "string", description: "Top rows by impressions (default 25)", required: false },
|
|
10476
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10477
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10478
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10479
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10480
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10481
|
+
}
|
|
10482
|
+
});
|
|
10483
|
+
registerSchema({
|
|
10484
|
+
command: "ads.linkedin.facets.list",
|
|
10485
|
+
description: "List every targeting facet LinkedIn supports (industries, seniorities, titles, employers, growthRate, etc).",
|
|
10486
|
+
args: {
|
|
10487
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10488
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10489
|
+
}
|
|
10490
|
+
});
|
|
10491
|
+
registerSchema({
|
|
10492
|
+
command: "ads.linkedin.facets.values",
|
|
10493
|
+
description: "Look up entity values for a facet \u2014 full list (q=adTargetingFacet) or typeahead search (q=typeahead, with --query).",
|
|
10494
|
+
args: {
|
|
10495
|
+
facet: { type: "string", description: "Facet name (industries) or URN", required: true },
|
|
10496
|
+
query: { type: "string", description: "Typeahead query (auto-switches finder)", required: false },
|
|
10497
|
+
finder: { type: "string", description: "adTargetingFacet|typeahead|similarEntities", required: false },
|
|
10498
|
+
locale: { type: "string", description: "Locale (default en_US)", required: false },
|
|
10499
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10500
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10501
|
+
}
|
|
10502
|
+
});
|
|
10503
|
+
registerSchema({
|
|
10504
|
+
command: "ads.linkedin.audience-size",
|
|
10505
|
+
description: "Estimate audience size for a targetingCriteria payload. Returns total + active + playbook \xA704 sweet-spot warnings.",
|
|
10506
|
+
args: {
|
|
10507
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10508
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10509
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10510
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10511
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10512
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10513
|
+
}
|
|
10514
|
+
});
|
|
10515
|
+
registerSchema({
|
|
10516
|
+
command: "ads.linkedin.bid-pricing",
|
|
10517
|
+
description: "LinkedIn's suggested bid range + playbook \xA706 floor (2/3 of suggested). Inputs: targeting + objective + cost type.",
|
|
10518
|
+
args: {
|
|
10519
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10520
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10521
|
+
objective: {
|
|
10522
|
+
type: "string",
|
|
10523
|
+
description: "Objective type (WEBSITE_CONVERSION, LEAD_GENERATION, etc.)",
|
|
10524
|
+
required: true
|
|
10525
|
+
},
|
|
10526
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true, enum: ["CPC", "CPM", "CPV", "CPS"] },
|
|
10527
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10528
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10529
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10530
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10826
10531
|
}
|
|
10827
|
-
};
|
|
10828
|
-
|
|
10829
|
-
"
|
|
10830
|
-
"
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
"company",
|
|
10844
|
-
"account",
|
|
10845
|
-
"conversion",
|
|
10846
|
-
"job-title",
|
|
10847
|
-
"job-function",
|
|
10848
|
-
"seniority",
|
|
10849
|
-
"industry",
|
|
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
|
-
}
|
|
10532
|
+
});
|
|
10533
|
+
registerSchema({
|
|
10534
|
+
command: "ads.linkedin.forecast",
|
|
10535
|
+
description: "Forecast reach + impressions + clicks + spend for a hypothetical campaign. Useful for \xA714 greenfield planning.",
|
|
10536
|
+
args: {
|
|
10537
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10538
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10539
|
+
objective: { type: "string", description: "Objective type", required: true },
|
|
10540
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV|CPS", required: true },
|
|
10541
|
+
"daily-budget": { type: "string", description: "e.g. '200 USD'", required: false },
|
|
10542
|
+
"total-budget": { type: "string", description: "e.g. '6000 USD'", required: false },
|
|
10543
|
+
bid: { type: "string", description: "Bid e.g. '8 USD'", required: false },
|
|
10544
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria", required: false },
|
|
10545
|
+
"targeting-file": { type: "string", description: "Path to JSON file", required: false },
|
|
10546
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10547
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10880
10548
|
}
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
}
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10549
|
+
});
|
|
10550
|
+
registerSchema({
|
|
10551
|
+
command: "ads.linkedin.leads",
|
|
10552
|
+
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.",
|
|
10553
|
+
args: {
|
|
10554
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10555
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10556
|
+
"form-id": { type: "string", description: "Filter by form ID", required: false },
|
|
10557
|
+
"campaign-id": { type: "string", description: "Filter by campaign", required: false },
|
|
10558
|
+
"since-days": { type: "string", description: "Last N days", required: false },
|
|
10559
|
+
"since-ms": { type: "string", description: "Epoch ms (alternative to --since-days)", required: false },
|
|
10560
|
+
limit: { type: "string", description: "Max rows", required: false },
|
|
10561
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10562
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10893
10563
|
}
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10564
|
+
});
|
|
10565
|
+
registerSchema({
|
|
10566
|
+
command: "ads.linkedin.conversions.list",
|
|
10567
|
+
description: "List conversion rules (Insight Tag + Conversions API).",
|
|
10568
|
+
args: {
|
|
10569
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10570
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10571
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10572
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10900
10573
|
}
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10574
|
+
});
|
|
10575
|
+
registerSchema({
|
|
10576
|
+
command: "ads.linkedin.conversions.health",
|
|
10577
|
+
description: "Playbook \xA707 5-point health check: rules enabled? lead/purchase event? CAPI fired in 7d? view-through \u22647d? lead de-dup correct?",
|
|
10578
|
+
args: {
|
|
10579
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10580
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10581
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10582
|
+
output: { type: "string", description: "json", required: false, default: "json" }
|
|
10907
10583
|
}
|
|
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.`));
|
|
10584
|
+
});
|
|
10585
|
+
registerSchema({
|
|
10586
|
+
command: "ads.linkedin.conversation",
|
|
10587
|
+
description: "Per-button click rates inside Sponsored Messaging / Conversation Ads. Wraps analytics --pivot conversation-node-button.",
|
|
10588
|
+
args: {
|
|
10589
|
+
"campaign-id": { type: "string", description: "CSV campaign IDs", required: true },
|
|
10590
|
+
start: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10591
|
+
end: { type: "string", description: "YYYY-MM-DD", required: false },
|
|
10592
|
+
"last-days": { type: "string", description: "Window (default 30)", required: false },
|
|
10593
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false },
|
|
10594
|
+
output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
|
|
10926
10595
|
}
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10596
|
+
});
|
|
10597
|
+
registerSchema({
|
|
10598
|
+
command: "ads.linkedin.audit",
|
|
10599
|
+
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.",
|
|
10600
|
+
args: {
|
|
10601
|
+
"account-id": { type: "string", description: "Account ID", required: false },
|
|
10602
|
+
"account-urn": { type: "string", description: "Alias", required: false },
|
|
10603
|
+
"campaign-id": { type: "string", description: "Narrow to a single campaign", required: false },
|
|
10604
|
+
"window-days": { type: "string", description: "Performance lookback (default 30)", required: false },
|
|
10605
|
+
severity: { type: "string", description: "CSV severity filter (critical,high,medium,low)", required: false },
|
|
10606
|
+
area: { type: "string", description: "CSV area filter", required: false },
|
|
10607
|
+
format: { type: "string", description: "json | md", required: false, default: "json" },
|
|
10608
|
+
"skip-cache": { type: "boolean", description: "Bypass cache", required: false }
|
|
10934
10609
|
}
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10610
|
+
});
|
|
10611
|
+
var writeAccountArgs = {
|
|
10612
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N", required: false },
|
|
10613
|
+
"account-urn": { type: "string", description: "Alias for --account-id", required: false }
|
|
10614
|
+
};
|
|
10615
|
+
registerSchema({
|
|
10616
|
+
command: "ads.linkedin.campaign-groups.create",
|
|
10617
|
+
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.",
|
|
10618
|
+
args: {
|
|
10619
|
+
...writeAccountArgs,
|
|
10620
|
+
name: { type: "string", description: "Name (\u2264100 chars)", required: true },
|
|
10621
|
+
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)", required: true },
|
|
10622
|
+
end: { type: "string", description: "Run schedule end", required: false },
|
|
10623
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10624
|
+
currency: {
|
|
10625
|
+
type: "string",
|
|
10626
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10627
|
+
required: false
|
|
10628
|
+
},
|
|
10629
|
+
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10630
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10631
|
+
}
|
|
10632
|
+
});
|
|
10633
|
+
registerSchema({
|
|
10634
|
+
command: "ads.linkedin.campaign-groups.update",
|
|
10635
|
+
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.",
|
|
10636
|
+
args: {
|
|
10637
|
+
id: {
|
|
10638
|
+
type: "positional",
|
|
10639
|
+
description: "Campaign group id/URN, or li_temp_* ref staged in this chat",
|
|
10640
|
+
required: true
|
|
10641
|
+
},
|
|
10642
|
+
...writeAccountArgs,
|
|
10643
|
+
name: { type: "string", description: "New name", required: false },
|
|
10644
|
+
start: { type: "string", description: "New schedule start", required: false },
|
|
10645
|
+
end: { type: "string", description: "New schedule end", required: false },
|
|
10646
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10647
|
+
currency: {
|
|
10648
|
+
type: "string",
|
|
10649
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10650
|
+
required: false
|
|
10651
|
+
},
|
|
10652
|
+
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10653
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10654
|
+
}
|
|
10655
|
+
});
|
|
10656
|
+
registerSchema({
|
|
10657
|
+
command: "ads.linkedin.campaigns.create",
|
|
10658
|
+
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.",
|
|
10659
|
+
args: {
|
|
10660
|
+
...writeAccountArgs,
|
|
10661
|
+
name: { type: "string", description: "Name (\u2264255 chars)", required: true },
|
|
10662
|
+
group: { type: "string", description: "Campaign group id/URN or li_temp_* ref", required: true },
|
|
10663
|
+
type: { type: "string", description: "SPONSORED_UPDATES|TEXT_AD|SPONSORED_INMAILS|DYNAMIC", required: true },
|
|
10664
|
+
objective: { type: "string", description: "LEAD_GENERATION|WEBSITE_VISIT|BRAND_AWARENESS|\u2026", required: false },
|
|
10665
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: true },
|
|
10666
|
+
bid: { type: "string", description: "Manual bid amount (unitCost)", required: false },
|
|
10667
|
+
"daily-budget": { type: "string", description: "Daily budget amount", required: false },
|
|
10668
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10669
|
+
currency: {
|
|
10670
|
+
type: "string",
|
|
10671
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10672
|
+
required: false
|
|
10673
|
+
},
|
|
10674
|
+
start: { type: "string", description: "Run schedule start", required: false },
|
|
10675
|
+
end: { type: "string", description: "Run schedule end", required: false },
|
|
10676
|
+
locale: { type: "string", description: "Profile locale like en_US (default en_US)", required: false },
|
|
10677
|
+
"targeting-file": { type: "string", description: "JSON file with targetingCriteria", required: true },
|
|
10678
|
+
"audience-expansion": { type: "string", description: "on|off (playbook: off; default off)", required: false },
|
|
10679
|
+
lan: {
|
|
10680
|
+
type: "string",
|
|
10681
|
+
description: "LinkedIn Audience Network on|off (playbook: off; default off)",
|
|
10682
|
+
required: false
|
|
10683
|
+
},
|
|
10684
|
+
ctv: { type: "string", description: "Connected TV on|off (default off)", required: false },
|
|
10685
|
+
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10686
|
+
"associated-entity": { type: "string", description: "Organization URN", required: false },
|
|
10687
|
+
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10688
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10939
10689
|
}
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10690
|
+
});
|
|
10691
|
+
registerSchema({
|
|
10692
|
+
command: "ads.linkedin.campaigns.update",
|
|
10693
|
+
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.",
|
|
10694
|
+
args: {
|
|
10695
|
+
id: { type: "positional", description: "Campaign id/URN, or li_temp_* ref staged in this chat", required: true },
|
|
10696
|
+
...writeAccountArgs,
|
|
10697
|
+
name: { type: "string", description: "New name", required: false },
|
|
10698
|
+
objective: { type: "string", description: "New objective", required: false },
|
|
10699
|
+
"cost-type": { type: "string", description: "CPC|CPM|CPV", required: false },
|
|
10700
|
+
bid: { type: "string", description: "New manual bid", required: false },
|
|
10701
|
+
"daily-budget": { type: "string", description: "New daily budget", required: false },
|
|
10702
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP, required: false },
|
|
10703
|
+
currency: {
|
|
10704
|
+
type: "string",
|
|
10705
|
+
description: "3-letter currency code (defaults to the ad account currency)",
|
|
10706
|
+
required: false
|
|
10707
|
+
},
|
|
10708
|
+
start: { type: "string", description: "New schedule start", required: false },
|
|
10709
|
+
end: { type: "string", description: "New schedule end", required: false },
|
|
10710
|
+
"targeting-file": { type: "string", description: "JSON file with replacement targetingCriteria", required: false },
|
|
10711
|
+
"audience-expansion": { type: "string", description: "on|off", required: false },
|
|
10712
|
+
lan: { type: "string", description: "on|off", required: false },
|
|
10713
|
+
ctv: { type: "string", description: "on|off", required: false },
|
|
10714
|
+
"creative-selection": { type: "string", description: "OPTIMIZED|ROUND_ROBIN", required: false },
|
|
10715
|
+
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED", required: false },
|
|
10716
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10946
10717
|
}
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10718
|
+
});
|
|
10719
|
+
registerSchema({
|
|
10720
|
+
command: "ads.linkedin.campaigns.url-params",
|
|
10721
|
+
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.",
|
|
10722
|
+
args: {
|
|
10723
|
+
id: { type: "positional", description: "Ad set (campaign) id or URN", required: true },
|
|
10724
|
+
...writeAccountArgs,
|
|
10725
|
+
param: {
|
|
10726
|
+
type: "string",
|
|
10727
|
+
description: 'Fixed key=value, repeatable or &-joined \u2014 e.g. "utm_source=linkedin&utm_medium=paid-social"',
|
|
10728
|
+
required: false
|
|
10729
|
+
},
|
|
10730
|
+
dynamic: {
|
|
10731
|
+
type: "string",
|
|
10732
|
+
description: "key=PLACEHOLDER filled in per ad, repeatable \u2014 e.g. utm_campaign=CAMPAIGN_NAME",
|
|
10733
|
+
required: false
|
|
10734
|
+
},
|
|
10735
|
+
clear: { type: "boolean", description: "Remove every tracking parameter from this ad set", required: false },
|
|
10736
|
+
file: { type: "string", description: "JSON payload file; flags override file keys", required: false }
|
|
10953
10737
|
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10738
|
+
});
|
|
10739
|
+
var statusSugarArgs = {
|
|
10740
|
+
id: {
|
|
10741
|
+
type: "positional",
|
|
10742
|
+
description: "Entity id or URN \u2014 comma-separate several to change them all in one call",
|
|
10743
|
+
required: false
|
|
10744
|
+
},
|
|
10745
|
+
"ids-file": { type: "string", description: "File of ids, one per line or comma-separated", required: false },
|
|
10746
|
+
...writeAccountArgs
|
|
10747
|
+
};
|
|
10748
|
+
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.";
|
|
10749
|
+
registerSchema({
|
|
10750
|
+
command: "ads.linkedin.campaigns.pause",
|
|
10751
|
+
description: `Stage pausing a campaign (status \u2192 PAUSED on publish). ${MULTI_ID_NOTE}`,
|
|
10752
|
+
args: statusSugarArgs
|
|
10753
|
+
});
|
|
10754
|
+
registerSchema({
|
|
10755
|
+
command: "ads.linkedin.campaigns.resume",
|
|
10756
|
+
description: `Stage resuming a paused campaign (status \u2192 ACTIVE on publish). ${MULTI_ID_NOTE}`,
|
|
10757
|
+
args: statusSugarArgs
|
|
10758
|
+
});
|
|
10759
|
+
registerSchema({
|
|
10760
|
+
command: "ads.linkedin.campaigns.archive",
|
|
10761
|
+
description: `Stage archiving a campaign (status \u2192 ARCHIVED on publish). ${MULTI_ID_NOTE}`,
|
|
10762
|
+
args: statusSugarArgs
|
|
10763
|
+
});
|
|
10764
|
+
registerSchema({
|
|
10765
|
+
command: "ads.linkedin.campaign-groups.pause",
|
|
10766
|
+
description: `Stage pausing a campaign group. ${MULTI_ID_NOTE}`,
|
|
10767
|
+
args: statusSugarArgs
|
|
10768
|
+
});
|
|
10769
|
+
registerSchema({
|
|
10770
|
+
command: "ads.linkedin.campaign-groups.resume",
|
|
10771
|
+
description: `Stage resuming a campaign group. ${MULTI_ID_NOTE}`,
|
|
10772
|
+
args: statusSugarArgs
|
|
10773
|
+
});
|
|
10774
|
+
registerSchema({
|
|
10775
|
+
command: "ads.linkedin.creatives.pause",
|
|
10776
|
+
description: `Stage pausing a creative (intendedStatus \u2192 PAUSED on publish). Playbook early-kill rule: >30 impressions with 0 clicks. ${MULTI_ID_NOTE}`,
|
|
10777
|
+
args: statusSugarArgs
|
|
10778
|
+
});
|
|
10779
|
+
registerSchema({
|
|
10780
|
+
command: "ads.linkedin.creatives.resume",
|
|
10781
|
+
description: `Stage resuming a paused creative. ${MULTI_ID_NOTE}`,
|
|
10782
|
+
args: statusSugarArgs
|
|
10783
|
+
});
|
|
10784
|
+
var duplicateArgs = {
|
|
10785
|
+
id: { type: "positional", description: "Source id or URN", required: true },
|
|
10786
|
+
...writeAccountArgs,
|
|
10787
|
+
name: { type: "string", description: "Name for the copy", required: false },
|
|
10788
|
+
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10789
|
+
};
|
|
10790
|
+
registerSchema({
|
|
10791
|
+
command: "ads.linkedin.campaigns.duplicate",
|
|
10792
|
+
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.",
|
|
10793
|
+
args: duplicateArgs
|
|
10794
|
+
});
|
|
10795
|
+
registerSchema({
|
|
10796
|
+
command: "ads.linkedin.campaign-groups.duplicate",
|
|
10797
|
+
description: "Stage a copy of an existing campaign group as a new DRAFT create.",
|
|
10798
|
+
args: duplicateArgs
|
|
10799
|
+
});
|
|
10800
|
+
registerSchema({
|
|
10801
|
+
command: "ads.linkedin.creatives.duplicate",
|
|
10802
|
+
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.",
|
|
10803
|
+
args: {
|
|
10804
|
+
id: { type: "positional", description: "Source creative id or URN", required: true },
|
|
10805
|
+
...writeAccountArgs,
|
|
10806
|
+
campaign: { type: "string", description: "Stage the copy under a different ad set", required: false },
|
|
10807
|
+
"intended-status": { type: "string", description: "Status for the copy (default DRAFT)", required: false },
|
|
10808
|
+
replace: { type: "boolean", description: "Also pause the original once the new ad publishes", required: false },
|
|
10809
|
+
headline: { type: "string", description: "New headline", required: false },
|
|
10810
|
+
description: { type: "string", description: "New description (text/spotlight)", required: false },
|
|
10811
|
+
intro: { type: "string", description: "New commentary / intro text", required: false },
|
|
10812
|
+
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10813
|
+
cta: { type: "string", description: "New CTA", required: false },
|
|
10814
|
+
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10815
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10816
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10817
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10818
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10819
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10820
|
+
title: { type: "string", description: "Media title", required: false },
|
|
10821
|
+
file: { type: "string", description: "JSON file with field overrides for the copy", required: false }
|
|
10960
10822
|
}
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10823
|
+
});
|
|
10824
|
+
registerSchema({
|
|
10825
|
+
command: "ads.linkedin.creatives.create",
|
|
10826
|
+
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.",
|
|
10827
|
+
args: {
|
|
10828
|
+
...writeAccountArgs,
|
|
10829
|
+
campaign: { type: "string", description: "Campaign id/URN or li_temp_* ref", required: true },
|
|
10830
|
+
format: {
|
|
10831
|
+
type: "string",
|
|
10832
|
+
description: "image|video|text|spotlight|follower|document|carousel|conversation|tla|jobs|event|article",
|
|
10833
|
+
required: true
|
|
10834
|
+
},
|
|
10835
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10836
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10837
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10838
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10839
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10840
|
+
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10841
|
+
headline: { type: "string", description: "Headline", required: false },
|
|
10842
|
+
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10843
|
+
"landing-url": { type: "string", description: "https destination URL", required: false },
|
|
10844
|
+
cta: { type: "string", description: "LEARN_MORE|REQUEST_DEMO|SIGN_UP|REGISTER|DOWNLOAD|\u2026", required: false },
|
|
10845
|
+
"cta-label": { type: "string", description: "Spotlight CTA label (\u226418 chars)", required: false },
|
|
10846
|
+
"post-urn": { type: "string", description: "TLA: post to sponsor (urn:li:share|ugcPost:*)", required: false },
|
|
10847
|
+
"event-urn": {
|
|
10848
|
+
type: "string",
|
|
10849
|
+
description: "Event ads: the LinkedIn event to sponsor (urn:li:event:*)",
|
|
10850
|
+
required: false
|
|
10851
|
+
},
|
|
10852
|
+
"article-url": {
|
|
10853
|
+
type: "string",
|
|
10854
|
+
description: "Article ads: https URL of the article/newsletter to sponsor",
|
|
10855
|
+
required: false
|
|
10856
|
+
},
|
|
10857
|
+
"thumbnail-image-id": {
|
|
10858
|
+
type: "string",
|
|
10859
|
+
description: "Article ads: Baker image id for the link thumbnail",
|
|
10860
|
+
required: false
|
|
10861
|
+
},
|
|
10862
|
+
"thumbnail-urn": { type: "string", description: "Article ads: existing urn:li:image:* thumbnail", required: false },
|
|
10863
|
+
"intended-status": { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)", required: false },
|
|
10864
|
+
title: { type: "string", description: "Media title (image/video) or article title", required: false },
|
|
10865
|
+
file: { type: "string", description: "JSON file with the full content object", required: false }
|
|
10964
10866
|
}
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10867
|
+
});
|
|
10868
|
+
registerSchema({
|
|
10869
|
+
command: "ads.linkedin.creatives.update",
|
|
10870
|
+
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.`,
|
|
10871
|
+
args: {
|
|
10872
|
+
id: {
|
|
10873
|
+
type: "positional",
|
|
10874
|
+
description: "Creative id/URN \u2014 comma-separate several. A li_temp_* ref amends what this chat staged and must be passed on its own.",
|
|
10875
|
+
required: false
|
|
10876
|
+
},
|
|
10877
|
+
"ids-file": {
|
|
10878
|
+
type: "string",
|
|
10879
|
+
description: "File of creative ids, one per line or comma-separated",
|
|
10880
|
+
required: false
|
|
10881
|
+
},
|
|
10882
|
+
...writeAccountArgs,
|
|
10883
|
+
"intended-status": { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|DRAFT", required: false },
|
|
10884
|
+
name: { type: "string", description: "New creative name", required: false },
|
|
10885
|
+
campaign: {
|
|
10886
|
+
type: "string",
|
|
10887
|
+
description: "Re-parent to a different ad set (id/URN or li_temp_*) \u2014 staged creatives only",
|
|
10888
|
+
required: false
|
|
10889
|
+
},
|
|
10890
|
+
headline: { type: "string", description: "New headline", required: false },
|
|
10891
|
+
intro: { type: "string", description: "Commentary / intro text", required: false },
|
|
10892
|
+
description: { type: "string", description: "Description (text/spotlight)", required: false },
|
|
10893
|
+
"landing-url": { type: "string", description: "New https destination URL", required: false },
|
|
10894
|
+
cta: { type: "string", description: "New CTA", required: false },
|
|
10895
|
+
"cta-label": { type: "string", description: "Spotlight custom CTA label", required: false },
|
|
10896
|
+
"image-id": { type: "string", description: "Baker image library id", required: false },
|
|
10897
|
+
"image-urn": { type: "string", description: "Existing urn:li:image:*", required: false },
|
|
10898
|
+
"video-id": { type: "string", description: "Baker video library id", required: false },
|
|
10899
|
+
"video-urn": { type: "string", description: "Existing urn:li:video:*", required: false },
|
|
10900
|
+
"logo-image-id": { type: "string", description: "Baker image id for the logo", required: false },
|
|
10901
|
+
"thumbnail-image-id": {
|
|
10902
|
+
type: "string",
|
|
10903
|
+
description: "Baker image id for the article thumbnail",
|
|
10904
|
+
required: false
|
|
10905
|
+
},
|
|
10906
|
+
"thumbnail-urn": { type: "string", description: "Existing urn:li:image:* thumbnail", required: false },
|
|
10907
|
+
title: { type: "string", description: "Media or article title", required: false },
|
|
10908
|
+
file: { type: "string", description: "JSON file with fields to change; flags override file keys", required: false }
|
|
10979
10909
|
}
|
|
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
|
-
},
|
|
10910
|
+
});
|
|
10911
|
+
registerSchema({
|
|
10912
|
+
command: "ads.linkedin.audiences.create",
|
|
10913
|
+
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.",
|
|
11019
10914
|
args: {
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
"
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
10915
|
+
...writeAccountArgs,
|
|
10916
|
+
name: { type: "string", description: "Audience name (\u2264100 chars)", required: true },
|
|
10917
|
+
type: { type: "string", description: "company-list|user-list|retargeting|engagement", required: true },
|
|
10918
|
+
"list-file": { type: "string", description: "CSV file with the list rows", required: false },
|
|
10919
|
+
file: { type: "string", description: "JSON raw source config for retargeting/engagement", required: false }
|
|
10920
|
+
}
|
|
10921
|
+
});
|
|
10922
|
+
registerSchema({
|
|
10923
|
+
command: "ads.linkedin.audiences.upload",
|
|
10924
|
+
description: "Stage adding rows to an existing audience segment (or a li_temp_* segment staged in this chat). Same CSV rules as audiences.create.",
|
|
10925
|
+
args: {
|
|
10926
|
+
id: { type: "positional", description: "Segment URN/id or li_temp_* ref", required: true },
|
|
10927
|
+
...writeAccountArgs,
|
|
10928
|
+
"list-file": { type: "string", description: "CSV file with the rows to add", required: true }
|
|
10929
|
+
}
|
|
10930
|
+
});
|
|
10931
|
+
registerSchema({
|
|
10932
|
+
command: "ads.linkedin.conversions.create",
|
|
10933
|
+
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.",
|
|
10934
|
+
args: {
|
|
10935
|
+
...writeAccountArgs,
|
|
10936
|
+
name: { type: "string", description: "Rule name", required: true },
|
|
10937
|
+
type: {
|
|
11028
10938
|
type: "string",
|
|
11029
|
-
description: "
|
|
10939
|
+
description: "LEAD|SIGN_UP|DOWNLOAD|PURCHASE|KEY_PAGE_VIEW|ADD_TO_CART|INSTALL|OTHER",
|
|
10940
|
+
required: true
|
|
11030
10941
|
},
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
"
|
|
11034
|
-
"
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
if (args["list-intents"]) {
|
|
11046
|
-
writeAdsJson({ ok: true, data: listIntents() });
|
|
11047
|
-
return;
|
|
11048
|
-
}
|
|
11049
|
-
if (args["list-pivots"]) {
|
|
11050
|
-
writeAdsJson({ ok: true, data: ALL_PIVOTS });
|
|
11051
|
-
return;
|
|
11052
|
-
}
|
|
11053
|
-
const level = parseLevel(args.level);
|
|
11054
|
-
const intent = parseIntent(args.intent);
|
|
11055
|
-
const pivot = parsePivot(args.pivot);
|
|
11056
|
-
let granularity = parseGranularity(args.granularity);
|
|
11057
|
-
if (MEMBER_PIVOTS.has(pivot) && granularity === "DAILY") {
|
|
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
|
-
}
|
|
10942
|
+
method: { type: "string", description: "INSIGHT_TAG|CONVERSIONS_API", required: true },
|
|
10943
|
+
"post-click-window": { type: "string", description: "1|7|30|90 days", required: false },
|
|
10944
|
+
"view-window": { type: "string", description: "1|7|30 days", required: false },
|
|
10945
|
+
"attribution-type": {
|
|
10946
|
+
type: "string",
|
|
10947
|
+
description: "LAST_TOUCH_BY_CAMPAIGN|LAST_TOUCH_BY_CONVERSION",
|
|
10948
|
+
required: false
|
|
10949
|
+
},
|
|
10950
|
+
"associate-all-campaigns": {
|
|
10951
|
+
type: "boolean",
|
|
10952
|
+
description: "Auto-associate with active campaigns",
|
|
10953
|
+
required: false
|
|
10954
|
+
},
|
|
10955
|
+
file: { type: "string", description: "JSON payload file (e.g. urlMatchRuleExpression)", required: false }
|
|
11105
10956
|
}
|
|
11106
10957
|
});
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
10958
|
+
registerSchema({
|
|
10959
|
+
command: "ads.linkedin.conversions.update",
|
|
10960
|
+
description: "Stage changes to a conversion rule (windows, attribution, enabled, name). Applies on chat publish.",
|
|
10961
|
+
args: {
|
|
10962
|
+
id: { type: "positional", description: "Conversion rule id or URN", required: true },
|
|
10963
|
+
...writeAccountArgs,
|
|
10964
|
+
name: { type: "string", description: "New name", required: false },
|
|
10965
|
+
"post-click-window": { type: "string", description: "1|7|30|90 days", required: false },
|
|
10966
|
+
"view-window": { type: "string", description: "1|7|30 days", required: false },
|
|
10967
|
+
"attribution-type": { type: "string", description: "New attribution type", required: false },
|
|
10968
|
+
enabled: { type: "string", description: "on|off", required: false },
|
|
10969
|
+
file: { type: "string", description: "JSON file with fields to change", required: false }
|
|
10970
|
+
}
|
|
10971
|
+
});
|
|
10972
|
+
registerSchema({
|
|
10973
|
+
command: "ads.linkedin.lead-forms.create",
|
|
10974
|
+
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.",
|
|
10975
|
+
args: {
|
|
10976
|
+
...writeAccountArgs,
|
|
10977
|
+
file: { type: "string", description: "JSON form definition file", required: true },
|
|
10978
|
+
name: { type: "string", description: "Override the form name", required: false }
|
|
10979
|
+
}
|
|
10980
|
+
});
|
|
10981
|
+
registerSchema({
|
|
10982
|
+
command: "ads.linkedin.lead-forms.update",
|
|
10983
|
+
description: "Stage changes to a Lead Gen Form from a JSON file. Applies on chat publish.",
|
|
10984
|
+
args: {
|
|
10985
|
+
id: { type: "positional", description: "Lead form id or URN", required: true },
|
|
10986
|
+
...writeAccountArgs,
|
|
10987
|
+
file: { type: "string", description: "JSON file with the fields to change", required: true },
|
|
10988
|
+
name: { type: "string", description: "New form name", required: false }
|
|
10989
|
+
}
|
|
10990
|
+
});
|
|
10991
|
+
registerSchema({
|
|
10992
|
+
command: "ads.linkedin.draft.list",
|
|
10993
|
+
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.",
|
|
10994
|
+
args: {
|
|
10995
|
+
json: {
|
|
10996
|
+
type: "boolean",
|
|
10997
|
+
description: "Print the raw JSON envelope instead of the readable tree",
|
|
10998
|
+
required: false
|
|
11118
10999
|
}
|
|
11119
11000
|
}
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
)
|
|
11001
|
+
});
|
|
11002
|
+
registerSchema({
|
|
11003
|
+
command: "ads.linkedin.draft.remove",
|
|
11004
|
+
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).",
|
|
11005
|
+
args: {
|
|
11006
|
+
ref: {
|
|
11007
|
+
type: "positional",
|
|
11008
|
+
description: "Op ref (li_temp_* for creates, target id/URN for updates)",
|
|
11009
|
+
required: true
|
|
11128
11010
|
}
|
|
11129
11011
|
}
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
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
|
|
11012
|
+
});
|
|
11013
|
+
registerSchema({
|
|
11014
|
+
command: "ads.linkedin.draft.clear",
|
|
11015
|
+
description: "Discard ALL staged LinkedIn ops in this chat's draft. Nothing applies on publish.",
|
|
11016
|
+
args: {}
|
|
11017
|
+
});
|
|
11142
11018
|
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11019
|
+
// src/commands/ads/linkedin/account.ts
|
|
11020
|
+
import { defineCommand as defineCommand32 } from "citty";
|
|
11021
|
+
var accountCommand = defineCommand32({
|
|
11022
|
+
meta: {
|
|
11023
|
+
name: "account",
|
|
11024
|
+
description: `Single LinkedIn ad account detail (currency, status, type).
|
|
11147
11025
|
|
|
11148
11026
|
Examples:
|
|
11149
|
-
baker ads linkedin
|
|
11150
|
-
baker ads linkedin
|
|
11027
|
+
baker ads linkedin account --account-id 503001492
|
|
11028
|
+
baker ads linkedin account --account-urn urn:li:sponsoredAccount:503001492 --output md`
|
|
11151
11029
|
},
|
|
11152
11030
|
args: {
|
|
11153
11031
|
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11154
11032
|
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11155
|
-
targeting: { type: "string", description: "Inline JSON targetingCriteria payload" },
|
|
11156
|
-
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11157
11033
|
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11158
|
-
output: { type: "string", description: "json", default: "json" }
|
|
11034
|
+
output: { type: "string", description: "json|csv|jsonl|md", default: "json" }
|
|
11159
11035
|
},
|
|
11160
11036
|
run: async ({ args }) => {
|
|
11161
11037
|
const accountId = resolveAccountIdArg2(args);
|
|
11162
|
-
const targetingCriteria = loadTargeting(args);
|
|
11163
11038
|
try {
|
|
11164
|
-
const
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11039
|
+
const params = { "account-id": accountId };
|
|
11040
|
+
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
11041
|
+
const data = await apiGet("/api/ads/linkedin/account", params);
|
|
11042
|
+
const fmt = csvOrJson2(args);
|
|
11043
|
+
if (fmt !== "json") {
|
|
11044
|
+
writeAdsOutput([data], fmt);
|
|
11045
|
+
return;
|
|
11046
|
+
}
|
|
11169
11047
|
writeAdsJson({ ok: true, data });
|
|
11170
11048
|
} catch (err) {
|
|
11171
11049
|
handleLinkedinError(err);
|
|
@@ -11173,510 +11051,742 @@ Examples:
|
|
|
11173
11051
|
}
|
|
11174
11052
|
});
|
|
11175
11053
|
|
|
11176
|
-
// src/commands/ads/linkedin/
|
|
11177
|
-
import { defineCommand as
|
|
11178
|
-
var
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
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
|
-
);
|
|
11054
|
+
// src/commands/ads/linkedin/accounts.ts
|
|
11055
|
+
import { defineCommand as defineCommand33 } from "citty";
|
|
11056
|
+
var ACCOUNTS_TTL_MS = 60 * 60 * 1e3;
|
|
11057
|
+
function accountHints2(accounts, includeAll) {
|
|
11058
|
+
if (includeAll) {
|
|
11059
|
+
return [];
|
|
11060
|
+
}
|
|
11061
|
+
return connectedResourceHints({
|
|
11062
|
+
plural: "LinkedIn ad accounts",
|
|
11063
|
+
singular: "LinkedIn ad account",
|
|
11064
|
+
flag: "--account-id",
|
|
11065
|
+
resources: accounts.map((account) => ({ id: account.externalId, label: account.name }))
|
|
11224
11066
|
});
|
|
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
11067
|
}
|
|
11236
|
-
var
|
|
11068
|
+
var accountsCommand2 = defineCommand33({
|
|
11237
11069
|
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.
|
|
11070
|
+
name: "accounts",
|
|
11071
|
+
description: `List LinkedIn ad accounts in this company's connected scope.
|
|
11248
11072
|
|
|
11249
11073
|
Examples:
|
|
11250
|
-
baker ads linkedin
|
|
11251
|
-
baker ads linkedin
|
|
11252
|
-
baker ads linkedin
|
|
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`
|
|
11074
|
+
baker ads linkedin accounts
|
|
11075
|
+
baker ads linkedin accounts --include-all # ignore picker scope, list every account the token can see
|
|
11076
|
+
baker ads linkedin accounts --output csv`
|
|
11256
11077
|
},
|
|
11257
11078
|
args: {
|
|
11258
|
-
"
|
|
11259
|
-
"
|
|
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" }
|
|
11079
|
+
"include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
|
|
11080
|
+
"no-cache": { type: "boolean", description: "Skip CLI-side cache" },
|
|
11081
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache (force re-fetch)" },
|
|
11082
|
+
output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
|
|
11269
11083
|
},
|
|
11270
11084
|
run: async ({ args }) => {
|
|
11271
|
-
const
|
|
11272
|
-
const
|
|
11273
|
-
|
|
11274
|
-
|
|
11085
|
+
const includeAll = args["include-all"] === true;
|
|
11086
|
+
const useCache = !args["no-cache"];
|
|
11087
|
+
const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
|
|
11088
|
+
if (useCache) {
|
|
11089
|
+
const cached = cacheGet("linkedin-accounts", cacheKey);
|
|
11090
|
+
if (cached) {
|
|
11091
|
+
writeJsonEnvelope({
|
|
11092
|
+
ok: true,
|
|
11093
|
+
data: cached.data,
|
|
11094
|
+
cached: true,
|
|
11095
|
+
hints: accountHints2(cached.data, includeAll)
|
|
11096
|
+
});
|
|
11097
|
+
return;
|
|
11098
|
+
}
|
|
11275
11099
|
}
|
|
11276
11100
|
try {
|
|
11277
|
-
const
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
const
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
const result = { ...data, findings: sorted };
|
|
11288
|
-
const fmt = args.format ?? "json";
|
|
11289
|
-
if (fmt === "md") {
|
|
11290
|
-
process.stdout.write(`${renderMarkdown(result)}
|
|
11291
|
-
`);
|
|
11101
|
+
const params = {};
|
|
11102
|
+
if (includeAll) params["include-all"] = "true";
|
|
11103
|
+
if (args["skip-cache"]) params["skip-cache"] = "true";
|
|
11104
|
+
const data = await apiGet("/api/ads/linkedin/accounts", params);
|
|
11105
|
+
if (useCache) {
|
|
11106
|
+
cacheSet("linkedin-accounts", cacheKey, data, ACCOUNTS_TTL_MS);
|
|
11107
|
+
}
|
|
11108
|
+
const fmt = csvOrJson2(args);
|
|
11109
|
+
if (fmt !== "json") {
|
|
11110
|
+
writeAdsOutput(data, fmt);
|
|
11292
11111
|
return;
|
|
11293
11112
|
}
|
|
11294
|
-
|
|
11113
|
+
writeJsonEnvelope({ ok: true, data, hints: accountHints2(data, includeAll) });
|
|
11295
11114
|
} catch (err) {
|
|
11296
11115
|
handleLinkedinError(err);
|
|
11297
11116
|
}
|
|
11298
11117
|
}
|
|
11299
11118
|
});
|
|
11300
11119
|
|
|
11301
|
-
// src/commands/ads/linkedin/
|
|
11302
|
-
import {
|
|
11303
|
-
import { defineCommand as defineCommand37 } from "citty";
|
|
11304
|
-
function loadTargeting2(args) {
|
|
11305
|
-
const inline = args.targeting;
|
|
11306
|
-
if (inline) {
|
|
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
|
-
}
|
|
11322
|
-
}
|
|
11323
|
-
handleLinkedinError(new Error("Pass --targeting-file <path> or --targeting '{...JSON...}'"));
|
|
11324
|
-
}
|
|
11325
|
-
var bidPricingCommand = defineCommand37({
|
|
11326
|
-
meta: {
|
|
11327
|
-
name: "bid-pricing",
|
|
11328
|
-
description: `Get LinkedIn's suggested bid range for a targeting + objective + cost type.
|
|
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
|
|
11120
|
+
// src/commands/ads/linkedin/analytics.ts
|
|
11121
|
+
import { defineCommand as defineCommand34 } from "citty";
|
|
11337
11122
|
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11123
|
+
// src/commands/ads/linkedin/presets.ts
|
|
11124
|
+
var INTENTS = {
|
|
11125
|
+
baseline: {
|
|
11126
|
+
name: "baseline",
|
|
11127
|
+
description: "Spend, impressions, clicks. The default 'how is delivery?' question. Derives CTR, CPC, CPM.",
|
|
11128
|
+
metrics: ["impressions", "clicks", "costInUsd", "costInLocalCurrency", "approximateUniqueImpressions"],
|
|
11129
|
+
derived: ["ctr", "cpc", "cpm", "frequency"]
|
|
11130
|
+
},
|
|
11131
|
+
revenue: {
|
|
11132
|
+
name: "revenue",
|
|
11133
|
+
description: "Spend, conversions, conversion value. 'How much money are we making?'",
|
|
11134
|
+
metrics: [
|
|
11135
|
+
"costInUsd",
|
|
11136
|
+
"externalWebsiteConversions",
|
|
11137
|
+
"externalWebsitePostClickConversions",
|
|
11138
|
+
"externalWebsitePostViewConversions",
|
|
11139
|
+
"conversionValueInLocalCurrency",
|
|
11140
|
+
"oneClickLeads"
|
|
11141
|
+
]
|
|
11142
|
+
},
|
|
11143
|
+
funnel: {
|
|
11144
|
+
name: "funnel",
|
|
11145
|
+
description: "Impression \u2192 click \u2192 landing \u2192 form open \u2192 lead \u2192 conversion. 'Where do users drop off?'",
|
|
11146
|
+
metrics: [
|
|
11147
|
+
"impressions",
|
|
11148
|
+
"clicks",
|
|
11149
|
+
"landingPageClicks",
|
|
11150
|
+
"oneClickLeadFormOpens",
|
|
11151
|
+
"oneClickLeads",
|
|
11152
|
+
"externalWebsiteConversions",
|
|
11153
|
+
"costInUsd"
|
|
11154
|
+
],
|
|
11155
|
+
derived: ["ctr", "leadCompletionRate"]
|
|
11156
|
+
},
|
|
11157
|
+
engagement: {
|
|
11158
|
+
name: "engagement",
|
|
11159
|
+
description: "Likes, comments, shares, follows + viral spillover. TLA-aware \u2014 measures organic reach effect.",
|
|
11160
|
+
metrics: [
|
|
11161
|
+
"impressions",
|
|
11162
|
+
"likes",
|
|
11163
|
+
"comments",
|
|
11164
|
+
"shares",
|
|
11165
|
+
"reactions",
|
|
11166
|
+
"follows",
|
|
11167
|
+
"totalEngagements",
|
|
11168
|
+
"viralImpressions",
|
|
11169
|
+
"viralLikes",
|
|
11170
|
+
"viralShares",
|
|
11171
|
+
"viralOneClickLeads",
|
|
11172
|
+
"costInUsd"
|
|
11173
|
+
]
|
|
11341
11174
|
},
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11175
|
+
video: {
|
|
11176
|
+
name: "video",
|
|
11177
|
+
description: "Video watch quartiles + cost. 'How are video creatives holding attention?'",
|
|
11178
|
+
metrics: [
|
|
11179
|
+
"videoStarts",
|
|
11180
|
+
"videoFirstQuartileCompletions",
|
|
11181
|
+
"videoMidpointCompletions",
|
|
11182
|
+
"videoThirdQuartileCompletions",
|
|
11183
|
+
"videoCompletions",
|
|
11184
|
+
"videoViews",
|
|
11185
|
+
"fullScreenPlays",
|
|
11186
|
+
"costInUsd",
|
|
11187
|
+
"impressions"
|
|
11188
|
+
]
|
|
11355
11189
|
},
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11190
|
+
"lead-gen": {
|
|
11191
|
+
name: "lead-gen",
|
|
11192
|
+
description: "Lead Gen Form opens + completions. CLI derives form-completion-rate.",
|
|
11193
|
+
metrics: [
|
|
11194
|
+
"oneClickLeadFormOpens",
|
|
11195
|
+
"oneClickLeads",
|
|
11196
|
+
"viralOneClickLeads",
|
|
11197
|
+
"viralOneClickLeadFormOpens",
|
|
11198
|
+
"costInUsd"
|
|
11199
|
+
],
|
|
11200
|
+
derived: ["leadCompletionRate"]
|
|
11201
|
+
},
|
|
11202
|
+
inmail: {
|
|
11203
|
+
name: "inmail",
|
|
11204
|
+
description: "Sponsored Message / Conversation Ads \u2014 sends, opens, clicks, lead-gen taps.",
|
|
11205
|
+
metrics: [
|
|
11206
|
+
"sends",
|
|
11207
|
+
"opens",
|
|
11208
|
+
"clicks",
|
|
11209
|
+
"costInUsd",
|
|
11210
|
+
"leadGenerationMailContactInfoShares",
|
|
11211
|
+
"leadGenerationMailInterestedClicks"
|
|
11212
|
+
]
|
|
11213
|
+
},
|
|
11214
|
+
document: {
|
|
11215
|
+
name: "document",
|
|
11216
|
+
description: "Document Ads \u2014 quartile read-through, completion, downloads.",
|
|
11217
|
+
metrics: [
|
|
11218
|
+
"documentFirstQuartileCompletions",
|
|
11219
|
+
"documentMidpointCompletions",
|
|
11220
|
+
"documentThirdQuartileCompletions",
|
|
11221
|
+
"documentCompletions",
|
|
11222
|
+
"downloadClicks",
|
|
11223
|
+
"costInUsd",
|
|
11224
|
+
"impressions"
|
|
11225
|
+
]
|
|
11226
|
+
},
|
|
11227
|
+
ranking: {
|
|
11228
|
+
name: "ranking",
|
|
11229
|
+
description: "Frequency + reach metrics for fatigue detection. CLI derives frequency = impressions/uniqueImpressions.",
|
|
11230
|
+
metrics: ["impressions", "clicks", "costInUsd", "approximateUniqueImpressions"],
|
|
11231
|
+
derived: ["frequency", "ctr"]
|
|
11232
|
+
},
|
|
11233
|
+
identity: {
|
|
11234
|
+
name: "identity",
|
|
11235
|
+
description: "Just spend + impressions. Cheap roll-up.",
|
|
11236
|
+
metrics: ["costInUsd", "impressions"]
|
|
11371
11237
|
}
|
|
11372
|
-
}
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11238
|
+
};
|
|
11239
|
+
var MEMBER_PIVOTS = /* @__PURE__ */ new Set([
|
|
11240
|
+
"company",
|
|
11241
|
+
"job-title",
|
|
11242
|
+
"job-function",
|
|
11243
|
+
"seniority",
|
|
11244
|
+
"industry",
|
|
11245
|
+
"company-size",
|
|
11246
|
+
"country",
|
|
11247
|
+
"region"
|
|
11248
|
+
]);
|
|
11249
|
+
var ALL_PIVOTS = [
|
|
11250
|
+
"none",
|
|
11251
|
+
"campaign",
|
|
11252
|
+
"campaign-group",
|
|
11253
|
+
"creative",
|
|
11254
|
+
"company",
|
|
11255
|
+
"account",
|
|
11256
|
+
"conversion",
|
|
11257
|
+
"job-title",
|
|
11258
|
+
"job-function",
|
|
11259
|
+
"seniority",
|
|
11260
|
+
"industry",
|
|
11261
|
+
"company-size",
|
|
11262
|
+
"country",
|
|
11263
|
+
"region",
|
|
11264
|
+
"device",
|
|
11265
|
+
"placement",
|
|
11266
|
+
"serving-location",
|
|
11267
|
+
"card-index",
|
|
11268
|
+
"objective",
|
|
11269
|
+
"conversation-node",
|
|
11270
|
+
"conversation-node-button"
|
|
11271
|
+
];
|
|
11272
|
+
var ALL_LEVELS = ["account", "campaign-group", "campaign", "creative"];
|
|
11273
|
+
function listIntents() {
|
|
11274
|
+
return Object.values(INTENTS);
|
|
11386
11275
|
}
|
|
11387
|
-
function
|
|
11388
|
-
|
|
11389
|
-
process.exit(1);
|
|
11276
|
+
function isPivot(slug) {
|
|
11277
|
+
return ALL_PIVOTS.includes(slug);
|
|
11390
11278
|
}
|
|
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
|
-
}
|
|
11279
|
+
function isLevel(slug) {
|
|
11280
|
+
return ALL_LEVELS.includes(slug);
|
|
11407
11281
|
}
|
|
11408
|
-
function
|
|
11409
|
-
const
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11282
|
+
function composeFields(intent, _level, metricsOverride) {
|
|
11283
|
+
const base = metricsOverride && metricsOverride.length > 0 ? metricsOverride : INTENTS[intent].metrics;
|
|
11284
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11285
|
+
const out = [];
|
|
11286
|
+
for (const f of ["pivotValues", "dateRange", ...base]) {
|
|
11287
|
+
if (!seen.has(f)) {
|
|
11288
|
+
seen.add(f);
|
|
11289
|
+
out.push(f);
|
|
11413
11290
|
}
|
|
11414
11291
|
}
|
|
11415
|
-
return
|
|
11292
|
+
return out;
|
|
11416
11293
|
}
|
|
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
|
-
}
|
|
11294
|
+
|
|
11295
|
+
// src/commands/ads/linkedin/analytics.ts
|
|
11296
|
+
function commaSplit(input) {
|
|
11297
|
+
if (!input) return void 0;
|
|
11298
|
+
return input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11438
11299
|
}
|
|
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)`);
|
|
11300
|
+
function parseGranularity(raw) {
|
|
11301
|
+
const v = (raw ?? "DAILY").toUpperCase();
|
|
11302
|
+
if (v === "DAILY" || v === "MONTHLY" || v === "YEARLY" || v === "ALL") {
|
|
11303
|
+
return v;
|
|
11448
11304
|
}
|
|
11449
|
-
|
|
11305
|
+
handleLinkedinError(new Error(`Invalid --granularity "${raw}". Use DAILY | MONTHLY | YEARLY | ALL.`));
|
|
11450
11306
|
}
|
|
11451
|
-
function
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
const raw = String(value);
|
|
11456
|
-
if (/^\d{10,}$/.test(raw)) {
|
|
11457
|
-
return Number(raw);
|
|
11307
|
+
function parseLevel(raw) {
|
|
11308
|
+
const v = raw ?? "account";
|
|
11309
|
+
if (!isLevel(v)) {
|
|
11310
|
+
handleLinkedinError(new Error(`Invalid --level "${raw}". Use one of: ${ALL_LEVELS.join(", ")}.`));
|
|
11458
11311
|
}
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11312
|
+
return v;
|
|
11313
|
+
}
|
|
11314
|
+
function parsePivot(raw) {
|
|
11315
|
+
const v = raw ?? "none";
|
|
11316
|
+
if (!isPivot(v)) {
|
|
11317
|
+
handleLinkedinError(new Error(`Invalid --pivot "${raw}". Use one of: ${ALL_PIVOTS.join(", ")}.`));
|
|
11462
11318
|
}
|
|
11463
|
-
return
|
|
11319
|
+
return v;
|
|
11464
11320
|
}
|
|
11465
|
-
function
|
|
11466
|
-
|
|
11467
|
-
|
|
11321
|
+
function parseIntent(raw) {
|
|
11322
|
+
const v = raw ?? "baseline";
|
|
11323
|
+
const known = [
|
|
11324
|
+
"baseline",
|
|
11325
|
+
"revenue",
|
|
11326
|
+
"funnel",
|
|
11327
|
+
"engagement",
|
|
11328
|
+
"video",
|
|
11329
|
+
"lead-gen",
|
|
11330
|
+
"inmail",
|
|
11331
|
+
"document",
|
|
11332
|
+
"ranking",
|
|
11333
|
+
"identity"
|
|
11334
|
+
];
|
|
11335
|
+
if (!known.includes(v)) {
|
|
11336
|
+
handleLinkedinError(new Error(`Invalid --intent "${raw}". Run --list-intents to see options.`));
|
|
11468
11337
|
}
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11338
|
+
return v;
|
|
11339
|
+
}
|
|
11340
|
+
function resolveDateRange(args) {
|
|
11341
|
+
const start = args.start;
|
|
11342
|
+
const end = args.end;
|
|
11343
|
+
if (start) {
|
|
11344
|
+
return { start, end };
|
|
11472
11345
|
}
|
|
11473
|
-
|
|
11474
|
-
|
|
11346
|
+
const lastDaysRaw = args["last-days"];
|
|
11347
|
+
const days = lastDaysRaw ? Number(lastDaysRaw) : 7;
|
|
11348
|
+
if (!Number.isFinite(days) || days < 1 || days > 730) {
|
|
11349
|
+
handleLinkedinError(new Error(`Invalid --last-days "${lastDaysRaw}". Use a positive number \u2264 730.`));
|
|
11475
11350
|
}
|
|
11476
|
-
|
|
11351
|
+
return { start: daysAgoIso2(days), end: todayIso2() };
|
|
11477
11352
|
}
|
|
11478
|
-
function
|
|
11479
|
-
if (
|
|
11480
|
-
|
|
11353
|
+
function resolveScopeIds(args, level) {
|
|
11354
|
+
if (level === "account") {
|
|
11355
|
+
const id = resolveAccountIdArg2(args);
|
|
11356
|
+
return [id];
|
|
11481
11357
|
}
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11358
|
+
if (level === "campaign-group") {
|
|
11359
|
+
const raw2 = args["campaign-group-id"] ?? args.ids;
|
|
11360
|
+
if (!raw2) {
|
|
11361
|
+
handleLinkedinError(new Error("Pass --campaign-group-id (CSV for multi) when --level campaign-group."));
|
|
11362
|
+
}
|
|
11363
|
+
return commaSplit(raw2) ?? [];
|
|
11485
11364
|
}
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11365
|
+
if (level === "campaign") {
|
|
11366
|
+
const raw2 = args["campaign-id"] ?? args.ids;
|
|
11367
|
+
if (!raw2) {
|
|
11368
|
+
handleLinkedinError(new Error("Pass --campaign-id (CSV for multi) when --level campaign."));
|
|
11369
|
+
}
|
|
11370
|
+
return commaSplit(raw2) ?? [];
|
|
11491
11371
|
}
|
|
11492
|
-
const
|
|
11493
|
-
|
|
11494
|
-
|
|
11495
|
-
failWriteValidation2(
|
|
11496
|
-
`${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
|
|
11497
|
-
);
|
|
11372
|
+
const raw = args["creative-id"] ?? args.ids;
|
|
11373
|
+
if (!raw) {
|
|
11374
|
+
handleLinkedinError(new Error("Pass --creative-id (CSV for multi) when --level creative."));
|
|
11498
11375
|
}
|
|
11499
|
-
return
|
|
11376
|
+
return commaSplit(raw) ?? [];
|
|
11500
11377
|
}
|
|
11501
|
-
function
|
|
11502
|
-
return
|
|
11378
|
+
function sortRows(rows) {
|
|
11379
|
+
return [...rows].sort((a, b) => {
|
|
11380
|
+
const cs = numberOf(b.costInUsd) - numberOf(a.costInUsd);
|
|
11381
|
+
if (cs !== 0) return cs;
|
|
11382
|
+
return numberOf(b.impressions) - numberOf(a.impressions);
|
|
11383
|
+
});
|
|
11503
11384
|
}
|
|
11504
|
-
function
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
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
|
-
}
|
|
11385
|
+
function numberOf(v) {
|
|
11386
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : 0;
|
|
11387
|
+
if (typeof v === "string") {
|
|
11388
|
+
const n = Number(v);
|
|
11389
|
+
return Number.isFinite(n) ? n : 0;
|
|
11527
11390
|
}
|
|
11528
|
-
|
|
11529
|
-
return cells.map((cell) => cell.trim());
|
|
11391
|
+
return 0;
|
|
11530
11392
|
}
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11393
|
+
var analyticsCommand = defineCommand34({
|
|
11394
|
+
meta: {
|
|
11395
|
+
name: "analytics",
|
|
11396
|
+
description: `Performance reporting \u2014 the workhorse for AI agents.
|
|
11397
|
+
|
|
11398
|
+
Three-axis design (LinkedIn's superpower over Meta/Google):
|
|
11399
|
+
--level account | campaign-group | campaign | creative
|
|
11400
|
+
--intent baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity
|
|
11401
|
+
--pivot none | job-title | company | industry | seniority | function | company-size | country | region
|
|
11402
|
+
| device | placement | serving-location | card-index | objective
|
|
11403
|
+
| conversation-node | conversation-node-button
|
|
11404
|
+
|
|
11405
|
+
Smart defaults:
|
|
11406
|
+
--level account, --intent baseline, --pivot none, --granularity DAILY, last 7 days
|
|
11407
|
+
Pivot on a MEMBER_* dim \u2192 granularity auto-forced to ALL (LinkedIn rejects DAILY+demographic)
|
|
11408
|
+
Demographic data delayed 12-24h with \u22653-event privacy floor \u2014 the CLI flags this in warnings.
|
|
11409
|
+
Derived metrics injected client-side: ctr, cpc, cpm, frequency, leadCompletionRate
|
|
11410
|
+
|
|
11411
|
+
Examples \u2014 common AI questions:
|
|
11412
|
+
# "How is this account doing this week?"
|
|
11413
|
+
baker ads linkedin analytics
|
|
11414
|
+
|
|
11415
|
+
# "Who are we reaching, by job title?" (the LinkedIn killer)
|
|
11416
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot job-title --intent baseline --last-days 30
|
|
11417
|
+
|
|
11418
|
+
# "Top companies seeing my ads" (ABM feedback loop)
|
|
11419
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --pivot company --last-days 30
|
|
11420
|
+
|
|
11421
|
+
# "Revenue by campaign over Q1"
|
|
11422
|
+
baker ads linkedin analytics --level campaign --campaign-id 1,2,3 --intent revenue --start 2026-01-01 --end 2026-03-31 --granularity MONTHLY
|
|
11423
|
+
|
|
11424
|
+
# "How is the lead form converting?"
|
|
11425
|
+
baker ads linkedin analytics --level campaign --campaign-id 1234 --intent lead-gen
|
|
11426
|
+
|
|
11427
|
+
# Custom field set (escape hatch)
|
|
11428
|
+
baker ads linkedin analytics --metrics impressions,clicks,oneClickLeads --intent identity`
|
|
11429
|
+
},
|
|
11430
|
+
args: {
|
|
11431
|
+
level: { type: "string", description: "Object scope (default: account)" },
|
|
11432
|
+
"account-id": { type: "string", description: "Account ID \u2014 numeric or urn:li:sponsoredAccount:N (level=account)" },
|
|
11433
|
+
"account-urn": { type: "string", description: "Alias for --account-id (URN form)" },
|
|
11434
|
+
"campaign-group-id": { type: "string", description: "Comma-separated IDs (level=campaign-group)" },
|
|
11435
|
+
"campaign-id": { type: "string", description: "Comma-separated IDs (level=campaign)" },
|
|
11436
|
+
"creative-id": { type: "string", description: "Comma-separated IDs (level=creative)" },
|
|
11437
|
+
ids: { type: "string", description: "Generic CSV of IDs at the chosen level (alternative to per-level flags)" },
|
|
11438
|
+
intent: {
|
|
11439
|
+
type: "string",
|
|
11440
|
+
description: "baseline|revenue|funnel|engagement|video|lead-gen|inmail|document|ranking|identity"
|
|
11441
|
+
},
|
|
11442
|
+
pivot: { type: "string", description: "Pivot dim. Default: none. See `--list-pivots`." },
|
|
11443
|
+
metrics: { type: "string", description: "CSV metric override \u2014 bypass --intent's bundle" },
|
|
11444
|
+
"list-intents": { type: "boolean", description: "Print intent definitions and exit" },
|
|
11445
|
+
"list-pivots": { type: "boolean", description: "Print pivot slugs and exit" },
|
|
11446
|
+
start: { type: "string", description: "Start date YYYY-MM-DD (overrides --last-days)" },
|
|
11447
|
+
end: { type: "string", description: "End date YYYY-MM-DD" },
|
|
11448
|
+
"last-days": { type: "string", description: "Window relative to today (default: 7)" },
|
|
11449
|
+
granularity: { type: "string", description: "DAILY|MONTHLY|YEARLY|ALL (default: DAILY)" },
|
|
11450
|
+
limit: { type: "string", description: "Max rows (default: 1000)" },
|
|
11451
|
+
"no-sort": { type: "boolean", description: "Skip default sort by costInUsd desc" },
|
|
11452
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11453
|
+
output: { type: "string", description: "Output format json|csv|jsonl|md", default: "json" }
|
|
11454
|
+
},
|
|
11455
|
+
run: async ({ args }) => {
|
|
11456
|
+
if (args["list-intents"]) {
|
|
11457
|
+
writeAdsJson({ ok: true, data: listIntents() });
|
|
11458
|
+
return;
|
|
11459
|
+
}
|
|
11460
|
+
if (args["list-pivots"]) {
|
|
11461
|
+
writeAdsJson({ ok: true, data: ALL_PIVOTS });
|
|
11462
|
+
return;
|
|
11463
|
+
}
|
|
11464
|
+
const level = parseLevel(args.level);
|
|
11465
|
+
const intent = parseIntent(args.intent);
|
|
11466
|
+
const pivot = parsePivot(args.pivot);
|
|
11467
|
+
let granularity = parseGranularity(args.granularity);
|
|
11468
|
+
if (MEMBER_PIVOTS.has(pivot) && granularity === "DAILY") {
|
|
11469
|
+
process.stderr.write(`note: pivot=${pivot} forces --granularity ALL (LinkedIn rejects DAILY + demographic).
|
|
11470
|
+
`);
|
|
11471
|
+
granularity = "ALL";
|
|
11472
|
+
}
|
|
11473
|
+
const ids = resolveScopeIds(args, level);
|
|
11474
|
+
if (ids.length === 0) {
|
|
11475
|
+
handleLinkedinError(new Error(`No IDs resolved for --level ${level}`));
|
|
11476
|
+
}
|
|
11477
|
+
const range = resolveDateRange(args);
|
|
11478
|
+
const metrics = commaSplit(args.metrics);
|
|
11479
|
+
const limit = args.limit ? Number(args.limit) : 1e3;
|
|
11480
|
+
const request = {
|
|
11481
|
+
level,
|
|
11482
|
+
ids,
|
|
11483
|
+
intent,
|
|
11484
|
+
pivot,
|
|
11485
|
+
metrics,
|
|
11486
|
+
start: range.start,
|
|
11487
|
+
end: range.end,
|
|
11488
|
+
granularity,
|
|
11489
|
+
limit
|
|
11490
|
+
};
|
|
11491
|
+
try {
|
|
11492
|
+
const data = await apiPost("/api/ads/linkedin/analytics", {
|
|
11493
|
+
request,
|
|
11494
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11495
|
+
});
|
|
11496
|
+
const rows = args["no-sort"] ? data.rows : sortRows(data.rows);
|
|
11497
|
+
const fmt = csvOrJson2(args);
|
|
11498
|
+
if (fmt !== "json") {
|
|
11499
|
+
writeAdsOutput(rows, fmt, data.fields);
|
|
11548
11500
|
return;
|
|
11549
11501
|
}
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11502
|
+
writeAdsJson({
|
|
11503
|
+
ok: true,
|
|
11504
|
+
data: {
|
|
11505
|
+
rows,
|
|
11506
|
+
fields: data.fields,
|
|
11507
|
+
warnings: data.warnings,
|
|
11508
|
+
query: data.query,
|
|
11509
|
+
// Echo composed fields so an offline agent can pre-flight w/o this network call.
|
|
11510
|
+
composedFields: composeFields(intent, level, metrics)
|
|
11511
|
+
}
|
|
11512
|
+
});
|
|
11513
|
+
} catch (err) {
|
|
11514
|
+
handleLinkedinError(err);
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
});
|
|
11518
|
+
|
|
11519
|
+
// src/commands/ads/linkedin/audience-size.ts
|
|
11520
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
11521
|
+
import { defineCommand as defineCommand35 } from "citty";
|
|
11522
|
+
function loadTargeting(args) {
|
|
11523
|
+
const inline = args.targeting;
|
|
11524
|
+
if (inline) {
|
|
11525
|
+
try {
|
|
11526
|
+
return JSON.parse(inline);
|
|
11527
|
+
} catch {
|
|
11528
|
+
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11554
11529
|
}
|
|
11555
11530
|
}
|
|
11556
|
-
|
|
11557
|
-
|
|
11531
|
+
const file = args["targeting-file"];
|
|
11532
|
+
if (file) {
|
|
11533
|
+
try {
|
|
11534
|
+
return JSON.parse(readFileSync5(file, "utf-8"));
|
|
11535
|
+
} catch (e) {
|
|
11536
|
+
handleLinkedinError(
|
|
11537
|
+
new Error(`Failed to read --targeting-file: ${e instanceof Error ? e.message : "I/O error"}`)
|
|
11538
|
+
);
|
|
11539
|
+
}
|
|
11558
11540
|
}
|
|
11559
|
-
|
|
11541
|
+
handleLinkedinError(new Error("Pass --targeting-file <path> or --targeting '{...JSON...}'"));
|
|
11560
11542
|
}
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11543
|
+
var audienceSizeCommand = defineCommand35({
|
|
11544
|
+
meta: {
|
|
11545
|
+
name: "audience-size",
|
|
11546
|
+
description: `Estimate audience size for a targeting payload \u2014 pre-launch sanity check.
|
|
11547
|
+
|
|
11548
|
+
Returns total + active counts plus playbook \xA704 warnings:
|
|
11549
|
+
size <20K \u2192 too narrow (will not deliver)
|
|
11550
|
+
size <50K \u2192 outside the 50K\u2013500K sweet spot
|
|
11551
|
+
size >500K \u2192 outside the sweet spot
|
|
11552
|
+
size >1M \u2192 too broad, refine ICP
|
|
11553
|
+
|
|
11554
|
+
The targeting payload must follow LinkedIn's 'targetingCriteria' shape \u2014 an
|
|
11555
|
+
include/exclude tree of and/or boolean operators with adTargetingFacet keys.
|
|
11556
|
+
The simplest form:
|
|
11557
|
+
{ "include": { "and": [{ "or": { "urn:li:adTargetingFacet:locations": ["urn:li:geo:103644278"] }}]}}
|
|
11558
|
+
|
|
11559
|
+
Examples:
|
|
11560
|
+
baker ads linkedin audience-size --account-id 503001492 --targeting-file targeting.json
|
|
11561
|
+
baker ads linkedin audience-size --account-id 503001492 --targeting '{"include":{"and":[]}}'`
|
|
11562
|
+
},
|
|
11563
|
+
args: {
|
|
11564
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11565
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11566
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria payload" },
|
|
11567
|
+
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11568
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11569
|
+
output: { type: "string", description: "json", default: "json" }
|
|
11570
|
+
},
|
|
11571
|
+
run: async ({ args }) => {
|
|
11572
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11573
|
+
const targetingCriteria = loadTargeting(args);
|
|
11574
|
+
try {
|
|
11575
|
+
const data = await apiPost("/api/ads/linkedin/audience-size", {
|
|
11576
|
+
accountId,
|
|
11577
|
+
targetingCriteria,
|
|
11578
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11579
|
+
});
|
|
11580
|
+
writeAdsJson({ ok: true, data });
|
|
11581
|
+
} catch (err) {
|
|
11582
|
+
handleLinkedinError(err);
|
|
11569
11583
|
}
|
|
11584
|
+
}
|
|
11585
|
+
});
|
|
11586
|
+
|
|
11587
|
+
// src/commands/ads/linkedin/audit.ts
|
|
11588
|
+
import { defineCommand as defineCommand36 } from "citty";
|
|
11589
|
+
var SEVERITY_RANK = {
|
|
11590
|
+
critical: 0,
|
|
11591
|
+
high: 1,
|
|
11592
|
+
medium: 2,
|
|
11593
|
+
low: 3
|
|
11594
|
+
};
|
|
11595
|
+
var STATUS_RANK = {
|
|
11596
|
+
fail: 0,
|
|
11597
|
+
warn: 1,
|
|
11598
|
+
partial: 2,
|
|
11599
|
+
pass: 3,
|
|
11600
|
+
n_a: 4
|
|
11601
|
+
};
|
|
11602
|
+
function filterFindings(findings, severity, area) {
|
|
11603
|
+
return findings.filter((f) => {
|
|
11604
|
+
if (severity && !severity.includes(f.severity)) return false;
|
|
11605
|
+
if (area && !area.includes(f.area)) return false;
|
|
11606
|
+
return true;
|
|
11570
11607
|
});
|
|
11571
|
-
process.exit(1);
|
|
11572
11608
|
}
|
|
11573
|
-
|
|
11574
|
-
|
|
11575
|
-
|
|
11609
|
+
function sortFindings(findings) {
|
|
11610
|
+
return [...findings].sort((a, b) => {
|
|
11611
|
+
const sa = STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
11612
|
+
if (sa !== 0) return sa;
|
|
11613
|
+
return SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
11614
|
+
});
|
|
11576
11615
|
}
|
|
11577
|
-
|
|
11578
|
-
const
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11616
|
+
function renderMarkdown(result) {
|
|
11617
|
+
const lines = [];
|
|
11618
|
+
lines.push(`# LinkedIn Ads Audit \u2014 ${result.account.name} (${result.account.id})`);
|
|
11619
|
+
lines.push("");
|
|
11620
|
+
lines.push(
|
|
11621
|
+
`**Scope:** ${result.scope.campaigns} campaigns, ${result.scope.creatives} creatives, last ${result.scope.windowDays}d`
|
|
11622
|
+
);
|
|
11623
|
+
lines.push("");
|
|
11624
|
+
lines.push(
|
|
11625
|
+
`**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)`
|
|
11626
|
+
);
|
|
11627
|
+
lines.push("");
|
|
11628
|
+
lines.push("| # | Area | Check | Status | Severity | Notes |");
|
|
11629
|
+
lines.push("|---|------|-------|--------|----------|-------|");
|
|
11630
|
+
result.findings.forEach((f, idx) => {
|
|
11631
|
+
const note = noteOf(f);
|
|
11632
|
+
lines.push(
|
|
11633
|
+
`| ${idx + 1} | ${f.area} | ${escapeMd(f.check)} | ${f.status.toUpperCase()} | ${f.severity} | ${escapeMd(note)} |`
|
|
11634
|
+
);
|
|
11635
|
+
});
|
|
11636
|
+
return lines.join("\n");
|
|
11593
11637
|
}
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
await stageOp(rawOps[0], hints);
|
|
11597
|
-
return;
|
|
11598
|
-
}
|
|
11599
|
-
const ops = [];
|
|
11600
|
-
for (const [index, raw] of rawOps.entries()) {
|
|
11601
|
-
const preflight = linkedinDraftOpInputSchema.safeParse(raw);
|
|
11602
|
-
if (!preflight.success) {
|
|
11603
|
-
failPreflight(
|
|
11604
|
-
preflight.error.issues.map((issue) => ({
|
|
11605
|
-
path: `ops[${index}]${issue.path.length > 0 ? `.${issue.path.join(".")}` : ""}`,
|
|
11606
|
-
message: issue.message
|
|
11607
|
-
}))
|
|
11608
|
-
);
|
|
11609
|
-
}
|
|
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
|
-
}
|
|
11638
|
+
function escapeMd(text) {
|
|
11639
|
+
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
11620
11640
|
}
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
);
|
|
11641
|
+
function noteOf(f) {
|
|
11642
|
+
if (f.status === "pass") return "";
|
|
11643
|
+
const ev = f.evidence ? JSON.stringify(f.evidence) : "";
|
|
11644
|
+
const fix = f.fix?.explanation ?? "";
|
|
11645
|
+
return [fix, ev].filter(Boolean).join(" \u2014 ");
|
|
11626
11646
|
}
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11647
|
+
var auditCommand = defineCommand36({
|
|
11648
|
+
meta: {
|
|
11649
|
+
name: "audit",
|
|
11650
|
+
description: `Run a LinkedIn Ads playbook audit \u2014 30+ checks across Settings, Tracking,
|
|
11651
|
+
Audience, Campaigns, Creative, Performance, Bidding, and Compliance.
|
|
11652
|
+
|
|
11653
|
+
Each finding has {id, area, check, status, severity, evidence, fix} fields with
|
|
11654
|
+
playbook citations. The default JSON output is agent-friendly; --format md
|
|
11655
|
+
renders a deliverable-ready table that mirrors the google-ads-playbook
|
|
11656
|
+
10-audit-summary.md style.
|
|
11657
|
+
|
|
11658
|
+
Exit code 0 always. Use the JSON 'summary' counts to decide if action is needed.
|
|
11659
|
+
|
|
11660
|
+
Examples:
|
|
11661
|
+
baker ads linkedin audit --account-id 503001492
|
|
11662
|
+
baker ads linkedin audit --account-id 503001492 --campaign-id 1234 # narrow scope
|
|
11663
|
+
baker ads linkedin audit --account-id 503001492 --window-days 90
|
|
11664
|
+
baker ads linkedin audit --account-id 503001492 --severity critical,high
|
|
11665
|
+
baker ads linkedin audit --account-id 503001492 --area Settings,Tracking
|
|
11666
|
+
baker ads linkedin audit --account-id 503001492 --format md`
|
|
11667
|
+
},
|
|
11668
|
+
args: {
|
|
11669
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11670
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11671
|
+
"campaign-id": { type: "string", description: "Narrow scope to a single campaign" },
|
|
11672
|
+
"window-days": { type: "string", description: "Lookback window for performance checks (default: 30)" },
|
|
11673
|
+
severity: { type: "string", description: "CSV severity filter: critical,high,medium,low" },
|
|
11674
|
+
area: {
|
|
11675
|
+
type: "string",
|
|
11676
|
+
description: "CSV area filter: Settings,Tracking,Audience,Campaigns,Creative,Performance,Bidding,Compliance,Hygiene"
|
|
11677
|
+
},
|
|
11678
|
+
format: { type: "string", description: "Output format: json | md (default: json)" },
|
|
11679
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" }
|
|
11680
|
+
},
|
|
11681
|
+
run: async ({ args }) => {
|
|
11682
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11683
|
+
const windowDays = args["window-days"] ? Number(args["window-days"]) : 30;
|
|
11684
|
+
if (!Number.isFinite(windowDays) || windowDays < 1 || windowDays > 365) {
|
|
11685
|
+
handleLinkedinError(new Error("--window-days must be 1..365"));
|
|
11686
|
+
}
|
|
11687
|
+
try {
|
|
11688
|
+
const data = await apiPost("/api/ads/linkedin/audit", {
|
|
11689
|
+
accountId,
|
|
11690
|
+
campaignId: args["campaign-id"] ? String(args["campaign-id"]) : void 0,
|
|
11691
|
+
windowDays,
|
|
11692
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11693
|
+
});
|
|
11694
|
+
const severityFilter = args.severity?.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
11695
|
+
const areaFilter = args.area?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11696
|
+
const filtered = filterFindings(data.findings, severityFilter, areaFilter);
|
|
11697
|
+
const sorted = sortFindings(filtered);
|
|
11698
|
+
const result = { ...data, findings: sorted };
|
|
11699
|
+
const fmt = args.format ?? "json";
|
|
11700
|
+
if (fmt === "md") {
|
|
11701
|
+
process.stdout.write(`${renderMarkdown(result)}
|
|
11702
|
+
`);
|
|
11703
|
+
return;
|
|
11704
|
+
}
|
|
11705
|
+
writeAdsJson({ ok: true, data: result });
|
|
11706
|
+
} catch (err) {
|
|
11707
|
+
handleLinkedinError(err);
|
|
11708
|
+
}
|
|
11641
11709
|
}
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11710
|
+
});
|
|
11711
|
+
|
|
11712
|
+
// src/commands/ads/linkedin/bid-pricing.ts
|
|
11713
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
11714
|
+
import { defineCommand as defineCommand37 } from "citty";
|
|
11715
|
+
function loadTargeting2(args) {
|
|
11716
|
+
const inline = args.targeting;
|
|
11717
|
+
if (inline) {
|
|
11718
|
+
try {
|
|
11719
|
+
return JSON.parse(inline);
|
|
11720
|
+
} catch {
|
|
11721
|
+
handleLinkedinError(new Error("--targeting must be valid JSON. See LinkedIn 'targetingCriteria' shape."));
|
|
11722
|
+
}
|
|
11647
11723
|
}
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
}
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11724
|
+
const file = args["targeting-file"];
|
|
11725
|
+
if (file) {
|
|
11726
|
+
try {
|
|
11727
|
+
return JSON.parse(readFileSync6(file, "utf-8"));
|
|
11728
|
+
} catch (e) {
|
|
11729
|
+
handleLinkedinError(
|
|
11730
|
+
new Error(`Failed to read --targeting-file: ${e instanceof Error ? e.message : "I/O error"}`)
|
|
11731
|
+
);
|
|
11732
|
+
}
|
|
11656
11733
|
}
|
|
11657
|
-
|
|
11734
|
+
handleLinkedinError(new Error("Pass --targeting-file <path> or --targeting '{...JSON...}'"));
|
|
11658
11735
|
}
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11736
|
+
var bidPricingCommand = defineCommand37({
|
|
11737
|
+
meta: {
|
|
11738
|
+
name: "bid-pricing",
|
|
11739
|
+
description: `Get LinkedIn's suggested bid range for a targeting + objective + cost type.
|
|
11740
|
+
|
|
11741
|
+
Returns LinkedIn's min / suggested / max bid plus the playbook \xA706 floor
|
|
11742
|
+
(2/3 of suggested \u2014 the recommended manual CPC starting point).
|
|
11743
|
+
|
|
11744
|
+
Objective types: BRAND_AWARENESS | WEBSITE_TRAFFIC | WEBSITE_VISIT |
|
|
11745
|
+
ENGAGEMENT | WEBSITE_CONVERSION | LEAD_GENERATION |
|
|
11746
|
+
JOB_APPLICANT | VIDEO_VIEW
|
|
11747
|
+
Cost types: CPC | CPM | CPV | CPS
|
|
11748
|
+
|
|
11749
|
+
Examples:
|
|
11750
|
+
baker ads linkedin bid-pricing --account-id 503001492 --objective WEBSITE_CONVERSION --cost-type CPC --targeting-file targeting.json
|
|
11751
|
+
baker ads linkedin bid-pricing --account-id 503001492 --objective LEAD_GENERATION --cost-type CPM --targeting '{"include":...}'`
|
|
11752
|
+
},
|
|
11753
|
+
args: {
|
|
11754
|
+
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
11755
|
+
"account-urn": { type: "string", description: "Alias for --account-id" },
|
|
11756
|
+
objective: {
|
|
11757
|
+
type: "string",
|
|
11758
|
+
description: "Objective type (e.g. WEBSITE_CONVERSION, LEAD_GENERATION)",
|
|
11759
|
+
required: true
|
|
11760
|
+
},
|
|
11761
|
+
"cost-type": { type: "string", description: "CPC | CPM | CPV | CPS", required: true },
|
|
11762
|
+
targeting: { type: "string", description: "Inline JSON targetingCriteria" },
|
|
11763
|
+
"targeting-file": { type: "string", description: "Path to JSON file with targetingCriteria" },
|
|
11764
|
+
"skip-cache": { type: "boolean", description: "Bypass server-side cache" },
|
|
11765
|
+
output: { type: "string", description: "json", default: "json" }
|
|
11766
|
+
},
|
|
11767
|
+
run: async ({ args }) => {
|
|
11768
|
+
const accountId = resolveAccountIdArg2(args);
|
|
11769
|
+
const targetingCriteria = loadTargeting2(args);
|
|
11770
|
+
try {
|
|
11771
|
+
const data = await apiPost("/api/ads/linkedin/bid-pricing", {
|
|
11772
|
+
accountId,
|
|
11773
|
+
objectiveType: String(args.objective),
|
|
11774
|
+
costType: String(args["cost-type"]).toUpperCase(),
|
|
11775
|
+
targetingCriteria,
|
|
11776
|
+
skipCache: Boolean(args["skip-cache"])
|
|
11777
|
+
});
|
|
11778
|
+
writeAdsJson({ ok: true, data });
|
|
11779
|
+
} catch (err) {
|
|
11780
|
+
handleLinkedinError(err);
|
|
11781
|
+
}
|
|
11675
11782
|
}
|
|
11676
|
-
|
|
11677
|
-
|
|
11783
|
+
});
|
|
11784
|
+
|
|
11785
|
+
// src/commands/ads/linkedin/campaign-groups.ts
|
|
11786
|
+
import { defineCommand as defineCommand39 } from "citty";
|
|
11678
11787
|
|
|
11679
11788
|
// src/commands/ads/linkedin/write-commands.ts
|
|
11789
|
+
import { defineCommand as defineCommand38 } from "citty";
|
|
11680
11790
|
var STAGED_NOTE = "Staged until publish \u2014 review with `baker ads linkedin draft`.";
|
|
11681
11791
|
var accountArgs = {
|
|
11682
11792
|
"account-id": { type: "string", description: "Numeric account ID or urn:li:sponsoredAccount:N" },
|
|
@@ -11701,7 +11811,7 @@ Example: baker ads linkedin campaign-groups create --name "Q3 ABM" --start 2026-
|
|
|
11701
11811
|
name: { type: "string", description: "Campaign group name (\u2264100 chars)" },
|
|
11702
11812
|
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
|
|
11703
11813
|
end: { type: "string", description: "Run schedule end" },
|
|
11704
|
-
"total-budget": { type: "string", description:
|
|
11814
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11705
11815
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11706
11816
|
status: { type: "string", description: "DRAFT|ACTIVE|PAUSED (default DRAFT)" },
|
|
11707
11817
|
file: { type: "string", description: "JSON file with the full payload; flags override file keys" }
|
|
@@ -11730,7 +11840,7 @@ Example: baker ads linkedin campaign-groups update 635137195 --total-budget 8000
|
|
|
11730
11840
|
name: { type: "string", description: "New name" },
|
|
11731
11841
|
start: { type: "string", description: "New run schedule start" },
|
|
11732
11842
|
end: { type: "string", description: "New run schedule end" },
|
|
11733
|
-
"total-budget": { type: "string", description:
|
|
11843
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11734
11844
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11735
11845
|
status: { type: "string", description: "ACTIVE|PAUSED|ARCHIVED|\u2026" },
|
|
11736
11846
|
file: { type: "string", description: "JSON file with fields to change" }
|
|
@@ -11781,7 +11891,7 @@ var campaignFlagArgs = {
|
|
|
11781
11891
|
"cost-type": { type: "string", description: "CPC|CPM|CPV" },
|
|
11782
11892
|
bid: { type: "string", description: "Manual bid (unitCost) \u2014 playbook mandates manual bidding" },
|
|
11783
11893
|
"daily-budget": { type: "string", description: "Daily budget amount" },
|
|
11784
|
-
"total-budget": { type: "string", description:
|
|
11894
|
+
"total-budget": { type: "string", description: TOTAL_BUDGET_HELP },
|
|
11785
11895
|
currency: { type: "string", description: "3-letter currency code (defaults to the ad account currency)" },
|
|
11786
11896
|
start: { type: "string", description: "Run schedule start (ISO date or epoch ms)" },
|
|
11787
11897
|
end: { type: "string", description: "Run schedule end" },
|
|
@@ -26508,6 +26618,20 @@ Full guide: __tooling__/docs/tools/baker/history.md`
|
|
|
26508
26618
|
import { defineCommand as defineCommand114 } from "citty";
|
|
26509
26619
|
var EMBED_TYPES = ["legacy", "v4", "unknown"];
|
|
26510
26620
|
function failNotConnected(err) {
|
|
26621
|
+
if (err instanceof ApiError && err.code === "UNAUTHORIZED" && err.message.includes(HUBSPOT_MISSING_GRANT_MARKER)) {
|
|
26622
|
+
writeJson({
|
|
26623
|
+
ok: false,
|
|
26624
|
+
error: {
|
|
26625
|
+
code: "HUBSPOT_RECONNECT_REQUIRED",
|
|
26626
|
+
message: err.message,
|
|
26627
|
+
fix: {
|
|
26628
|
+
action: "reconnect",
|
|
26629
|
+
explanation: "AGENT INSTRUCTION: HubSpot IS connected \u2014 do not tell the user it is missing, and do not retry. This account was connected before this capability existed, so its access does not cover it. Only the user can widen it: tell them to open the dashboard, go to Brain -> Integrations -> Tools and reconnect HubSpot; it works from their next message. Do not abort the wider task: say which part you could not read, skip it, and finish everything else."
|
|
26630
|
+
}
|
|
26631
|
+
}
|
|
26632
|
+
});
|
|
26633
|
+
process.exit(1);
|
|
26634
|
+
}
|
|
26511
26635
|
if (err instanceof ApiError && err.code === "NOT_FOUND" && err.message.includes("No HubSpot account is connected")) {
|
|
26512
26636
|
writeJson({
|
|
26513
26637
|
ok: false,
|
|
@@ -26561,6 +26685,47 @@ registerSchema({
|
|
|
26561
26685
|
}
|
|
26562
26686
|
}
|
|
26563
26687
|
});
|
|
26688
|
+
registerSchema({
|
|
26689
|
+
command: "hubspot.forms.submissions",
|
|
26690
|
+
description: "How many leads a form actually received over a window, and on which days \u2014 the difference between 'this form is live' and 'this form is working'. Start here when asked whether a form or a campaign is producing leads. Returns counts and dates only; the leads' own answers need --full and are personal data, so do not put them in a report. Pair with `hubspot pipelines list` to see whether those leads were then worked.",
|
|
26691
|
+
args: {
|
|
26692
|
+
formId: { type: "positional", description: "HubSpot form id (from `baker hubspot forms list`)", required: true },
|
|
26693
|
+
days: { type: "string", description: "Window in days, 1-365 (default 30)", required: false },
|
|
26694
|
+
full: {
|
|
26695
|
+
type: "boolean",
|
|
26696
|
+
description: "Include each submission's field values \u2014 personal data, request only when actually needed",
|
|
26697
|
+
required: false,
|
|
26698
|
+
default: false
|
|
26699
|
+
}
|
|
26700
|
+
}
|
|
26701
|
+
});
|
|
26702
|
+
registerSchema({
|
|
26703
|
+
command: "hubspot.contacts.summary",
|
|
26704
|
+
description: "Whether the leads that arrived recently are actually being worked, as counts: how many contacts were created in the window, how many have someone assigned, and how they split across lifecycle stages. This is the 'have these leads been treated?' answer. Returns no names or emails \u2014 use `hubspot contacts lookup` for one specific person.",
|
|
26705
|
+
args: {
|
|
26706
|
+
days: { type: "string", description: "Window in days, 1-365 (default 30)", required: false }
|
|
26707
|
+
}
|
|
26708
|
+
});
|
|
26709
|
+
registerSchema({
|
|
26710
|
+
command: "hubspot.contacts.lookup",
|
|
26711
|
+
description: "One contact by email: its lifecycle stage, whether anyone is assigned, the account (company) it was filed under, and any deals it is attached to with their stage. Use it to settle whether a specific lead was picked up. `found: false` means the lead never reached the CRM at all \u2014 that is a finding, not an error. `companyReadable: false` means account details were not readable on this connection, so a null company is unknown rather than absent.",
|
|
26712
|
+
args: {
|
|
26713
|
+
email: { type: "positional", description: "The contact's email address", required: true }
|
|
26714
|
+
}
|
|
26715
|
+
});
|
|
26716
|
+
registerSchema({
|
|
26717
|
+
command: "hubspot.workflows.list",
|
|
26718
|
+
description: "Every workflow on the connected account, with whether it is switched on. Read it when auditing what happens to a lead after it is captured \u2014 a form wired to a workflow that is off looks connected and does nothing.",
|
|
26719
|
+
args: {
|
|
26720
|
+
search: { type: "string", description: "Case-insensitive substring match on the workflow name", required: false },
|
|
26721
|
+
"enabled-only": { type: "boolean", description: "Only workflows that are on", required: false, default: false }
|
|
26722
|
+
}
|
|
26723
|
+
});
|
|
26724
|
+
registerSchema({
|
|
26725
|
+
command: "hubspot.pipelines.list",
|
|
26726
|
+
description: "The account's deal pipelines and their stages, in the order they run. Use it to answer where a lead lands once captured and whether it moved \u2014 the 'have these leads been worked?' half of a form audit.",
|
|
26727
|
+
args: {}
|
|
26728
|
+
});
|
|
26564
26729
|
registerSchema({
|
|
26565
26730
|
command: "hubspot.meetings.list",
|
|
26566
26731
|
description: "Every HubSpot meeting link (calendar) on the connected account, with just enough to choose one: name, slug, link type (personal / group / round-robin), whether booking redirects somewhere, and how many booking fields it asks for (null = unknown, read it with `meetings view`). Next step is usually `hubspot meetings view <slug> --as-node`, which gives you the blob to write straight into a hubspotMeeting step.",
|
|
@@ -26770,9 +26935,207 @@ var meetingsViewCommand = defineCommand114({
|
|
|
26770
26935
|
}
|
|
26771
26936
|
}
|
|
26772
26937
|
});
|
|
26938
|
+
var formsSubmissionsCommand = defineCommand114({
|
|
26939
|
+
meta: {
|
|
26940
|
+
name: "submissions",
|
|
26941
|
+
description: "How many leads a form received, and when. Example: baker hubspot forms submissions <formId> --days 30"
|
|
26942
|
+
},
|
|
26943
|
+
args: {
|
|
26944
|
+
formId: { type: "positional", description: "HubSpot form id", required: true },
|
|
26945
|
+
days: { type: "string", description: "Window in days (default 30)", required: false },
|
|
26946
|
+
full: { type: "boolean", description: "Include each lead's answers", required: false }
|
|
26947
|
+
},
|
|
26948
|
+
run: async ({ args }) => {
|
|
26949
|
+
try {
|
|
26950
|
+
const days = args.days === void 0 ? 30 : Number(args.days);
|
|
26951
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
26952
|
+
throw new ApiError("VALIDATION_ERROR", "--days must be a whole number between 1 and 365.");
|
|
26953
|
+
}
|
|
26954
|
+
const response = await apiPost("/api/hubspot/forms/submissions", {
|
|
26955
|
+
formId: args.formId,
|
|
26956
|
+
days,
|
|
26957
|
+
...args.full ? { full: true } : {}
|
|
26958
|
+
});
|
|
26959
|
+
const { inWindow, total, truncated, lastSubmittedAt } = response.data;
|
|
26960
|
+
const hints = [];
|
|
26961
|
+
if (inWindow === 0) {
|
|
26962
|
+
hints.push(
|
|
26963
|
+
total === 0 ? "This form has received nothing at all \u2014 check it is the form actually embedded on the page before concluding the campaign is at fault." : `No leads in the last ${days} days, though the form has ${total} older submission(s). It worked once and has gone quiet.`
|
|
26964
|
+
);
|
|
26965
|
+
}
|
|
26966
|
+
if (truncated) {
|
|
26967
|
+
hints.push(
|
|
26968
|
+
"Stopped before reaching the end of the window, so `inWindow` is a floor, not a count. Say so rather than reporting it as exact."
|
|
26969
|
+
);
|
|
26970
|
+
}
|
|
26971
|
+
if (!args.full && inWindow > 0) {
|
|
26972
|
+
hints.push(
|
|
26973
|
+
"Counts only \u2014 the leads' own answers are not included. Add --full if you genuinely need the field values; it returns personal data, so do not paste it into a report or a chat message."
|
|
26974
|
+
);
|
|
26975
|
+
}
|
|
26976
|
+
if (lastSubmittedAt !== null) {
|
|
26977
|
+
hints.push(
|
|
26978
|
+
"To find out whether these leads were worked, cross-check the deal pipeline: `baker hubspot pipelines list`."
|
|
26979
|
+
);
|
|
26980
|
+
}
|
|
26981
|
+
writeJson({ ...response, meta: { count: inWindow }, ...hints.length > 0 ? { hints } : {} });
|
|
26982
|
+
} catch (err) {
|
|
26983
|
+
failNotConnected(err);
|
|
26984
|
+
}
|
|
26985
|
+
}
|
|
26986
|
+
});
|
|
26987
|
+
var workflowsListCommand = defineCommand114({
|
|
26988
|
+
meta: {
|
|
26989
|
+
name: "list",
|
|
26990
|
+
description: "List HubSpot workflows. Example: baker hubspot workflows list --enabled-only"
|
|
26991
|
+
},
|
|
26992
|
+
args: {
|
|
26993
|
+
search: { type: "string", description: "Match on workflow name", required: false },
|
|
26994
|
+
"enabled-only": { type: "boolean", description: "Only workflows that are on", required: false }
|
|
26995
|
+
},
|
|
26996
|
+
run: async ({ args }) => {
|
|
26997
|
+
try {
|
|
26998
|
+
const body = {};
|
|
26999
|
+
if (args.search) {
|
|
27000
|
+
body.search = args.search;
|
|
27001
|
+
}
|
|
27002
|
+
if (args["enabled-only"]) {
|
|
27003
|
+
body.enabledOnly = true;
|
|
27004
|
+
}
|
|
27005
|
+
const response = await apiPost("/api/hubspot/workflows/list", body);
|
|
27006
|
+
const { workflows } = response.data;
|
|
27007
|
+
const off = workflows.filter((workflow) => !workflow.enabled);
|
|
27008
|
+
const hints = [];
|
|
27009
|
+
if (workflows.length === 0) {
|
|
27010
|
+
hints.push("No workflows matched. Drop --search/--enabled-only, or the account genuinely has none.");
|
|
27011
|
+
}
|
|
27012
|
+
if (off.length > 0) {
|
|
27013
|
+
hints.push(
|
|
27014
|
+
`${off.length} of these are switched off. A form wired to a workflow that is off looks connected and does nothing \u2014 worth flagging to the user.`
|
|
27015
|
+
);
|
|
27016
|
+
}
|
|
27017
|
+
writeJson({ ...response, meta: { count: workflows.length }, ...hints.length > 0 ? { hints } : {} });
|
|
27018
|
+
} catch (err) {
|
|
27019
|
+
failNotConnected(err);
|
|
27020
|
+
}
|
|
27021
|
+
}
|
|
27022
|
+
});
|
|
27023
|
+
var pipelinesListCommand = defineCommand114({
|
|
27024
|
+
meta: {
|
|
27025
|
+
name: "list",
|
|
27026
|
+
description: "List HubSpot deal pipelines and their stages. Example: baker hubspot pipelines list"
|
|
27027
|
+
},
|
|
27028
|
+
args: {},
|
|
27029
|
+
run: async () => {
|
|
27030
|
+
try {
|
|
27031
|
+
const response = await apiPost("/api/hubspot/pipelines/list", {});
|
|
27032
|
+
const { pipelines } = response.data;
|
|
27033
|
+
const hints = pipelines.length === 0 ? ["This account has no deal pipelines, so there is nowhere for a lead to be worked."] : ["Stages are listed in the order they run in HubSpot, so the first stage is where a new lead lands."];
|
|
27034
|
+
writeJson({ ...response, meta: { count: pipelines.length }, hints });
|
|
27035
|
+
} catch (err) {
|
|
27036
|
+
failNotConnected(err);
|
|
27037
|
+
}
|
|
27038
|
+
}
|
|
27039
|
+
});
|
|
27040
|
+
var contactsSummaryCommand = defineCommand114({
|
|
27041
|
+
meta: {
|
|
27042
|
+
name: "summary",
|
|
27043
|
+
description: "Whether recent leads are being worked, as counts. Example: baker hubspot contacts summary --days 30"
|
|
27044
|
+
},
|
|
27045
|
+
args: {
|
|
27046
|
+
days: { type: "string", description: "Window in days (default 30)", required: false }
|
|
27047
|
+
},
|
|
27048
|
+
run: async ({ args }) => {
|
|
27049
|
+
try {
|
|
27050
|
+
const days = args.days === void 0 ? 30 : Number(args.days);
|
|
27051
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
27052
|
+
throw new ApiError("VALIDATION_ERROR", "--days must be a whole number between 1 and 365.");
|
|
27053
|
+
}
|
|
27054
|
+
const response = await apiPost("/api/hubspot/contacts/summary", { days });
|
|
27055
|
+
const { total, withOwner, withoutOwner, byStage, truncated } = response.data;
|
|
27056
|
+
const hints = [];
|
|
27057
|
+
if (total === 0) {
|
|
27058
|
+
hints.push(`No contacts were created in the last ${days} days \u2014 nothing reached the CRM in that window.`);
|
|
27059
|
+
} else if (withOwner === 0) {
|
|
27060
|
+
hints.push(
|
|
27061
|
+
`None of these ${total} contacts has anyone assigned. That is the strongest available signal that the leads are arriving and nobody is picking them up \u2014 worth raising with the user.`
|
|
27062
|
+
);
|
|
27063
|
+
} else if (withoutOwner > withOwner) {
|
|
27064
|
+
hints.push(`${withoutOwner} of ${total} contacts have nobody assigned \u2014 more unworked than worked.`);
|
|
27065
|
+
}
|
|
27066
|
+
const unset = byStage.find((entry) => entry.stage === "unset");
|
|
27067
|
+
if (unset && unset.count > 0) {
|
|
27068
|
+
hints.push(
|
|
27069
|
+
`${unset.count} contact(s) have no lifecycle stage set at all, so they were created and never staged.`
|
|
27070
|
+
);
|
|
27071
|
+
}
|
|
27072
|
+
if (truncated) {
|
|
27073
|
+
hints.push("Stopped before reading every contact in the window, so these counts are floors, not totals.");
|
|
27074
|
+
}
|
|
27075
|
+
writeJson({ ...response, meta: { count: total }, ...hints.length > 0 ? { hints } : {} });
|
|
27076
|
+
} catch (err) {
|
|
27077
|
+
failNotConnected(err);
|
|
27078
|
+
}
|
|
27079
|
+
}
|
|
27080
|
+
});
|
|
27081
|
+
var contactsLookupCommand = defineCommand114({
|
|
27082
|
+
meta: {
|
|
27083
|
+
name: "lookup",
|
|
27084
|
+
description: "Find one contact by email and see whether it was worked. Example: baker hubspot contacts lookup a@b.com"
|
|
27085
|
+
},
|
|
27086
|
+
args: {
|
|
27087
|
+
email: { type: "positional", description: "The contact's email address", required: true }
|
|
27088
|
+
},
|
|
27089
|
+
run: async ({ args }) => {
|
|
27090
|
+
try {
|
|
27091
|
+
const response = await apiPost("/api/hubspot/contacts/lookup", {
|
|
27092
|
+
email: args.email
|
|
27093
|
+
});
|
|
27094
|
+
const { found, contact, deals, company, companyReadable } = response.data;
|
|
27095
|
+
const hints = [];
|
|
27096
|
+
if (!found) {
|
|
27097
|
+
hints.push(
|
|
27098
|
+
"No contact with that email. The lead never reached the CRM \u2014 check the form's submissions before assuming the CRM lost it."
|
|
27099
|
+
);
|
|
27100
|
+
} else {
|
|
27101
|
+
if (contact?.ownerId === null) {
|
|
27102
|
+
hints.push("Nobody is assigned to this contact.");
|
|
27103
|
+
}
|
|
27104
|
+
if (deals.length === 0) {
|
|
27105
|
+
hints.push("No deal is associated, so this contact was never taken into the pipeline.");
|
|
27106
|
+
}
|
|
27107
|
+
if (companyReadable && company === null) {
|
|
27108
|
+
hints.push(
|
|
27109
|
+
"No account is associated either. Most B2B setups create one automatically when a contact arrives, so this usually means the lead was never processed at all \u2014 not that the account is missing."
|
|
27110
|
+
);
|
|
27111
|
+
}
|
|
27112
|
+
if (!companyReadable) {
|
|
27113
|
+
hints.push(
|
|
27114
|
+
"Account details were not read: this HubSpot connection does not cover them. Do not report the lead as having no account \u2014 you cannot tell. The user can enable it by reconnecting HubSpot."
|
|
27115
|
+
);
|
|
27116
|
+
}
|
|
27117
|
+
}
|
|
27118
|
+
writeJson({ ...response, meta: { count: found ? 1 : 0 }, ...hints.length > 0 ? { hints } : {} });
|
|
27119
|
+
} catch (err) {
|
|
27120
|
+
failNotConnected(err);
|
|
27121
|
+
}
|
|
27122
|
+
}
|
|
27123
|
+
});
|
|
27124
|
+
var contactsCommand = defineCommand114({
|
|
27125
|
+
meta: { name: "contacts", description: "Contacts on the connected HubSpot account." },
|
|
27126
|
+
subCommands: { summary: contactsSummaryCommand, lookup: contactsLookupCommand }
|
|
27127
|
+
});
|
|
26773
27128
|
var formsCommand = defineCommand114({
|
|
26774
27129
|
meta: { name: "forms", description: "HubSpot forms on the connected account." },
|
|
26775
|
-
subCommands: { list: formsListCommand, view: formsViewCommand }
|
|
27130
|
+
subCommands: { list: formsListCommand, view: formsViewCommand, submissions: formsSubmissionsCommand }
|
|
27131
|
+
});
|
|
27132
|
+
var workflowsCommand = defineCommand114({
|
|
27133
|
+
meta: { name: "workflows", description: "HubSpot workflows on the connected account." },
|
|
27134
|
+
subCommands: { list: workflowsListCommand }
|
|
27135
|
+
});
|
|
27136
|
+
var pipelinesCommand = defineCommand114({
|
|
27137
|
+
meta: { name: "pipelines", description: "HubSpot deal pipelines on the connected account." },
|
|
27138
|
+
subCommands: { list: pipelinesListCommand }
|
|
26776
27139
|
});
|
|
26777
27140
|
var meetingsCommand = defineCommand114({
|
|
26778
27141
|
meta: { name: "meetings", description: "HubSpot meeting links (calendars) on the connected account." },
|
|
@@ -26781,21 +27144,37 @@ var meetingsCommand = defineCommand114({
|
|
|
26781
27144
|
var hubspotCommand = defineCommand114({
|
|
26782
27145
|
meta: {
|
|
26783
27146
|
name: "hubspot",
|
|
26784
|
-
description: `Read the connected HubSpot account \u2014 forms
|
|
27147
|
+
description: `Read the connected HubSpot account \u2014 forms, the leads they received, workflows, deal pipelines and
|
|
27148
|
+
meeting links (calendars) \u2014 and get the blob to wire a form into a Form step yourself.
|
|
26785
27149
|
|
|
26786
27150
|
Examples:
|
|
26787
27151
|
baker hubspot forms list # every form: embed type + post-submit action
|
|
26788
27152
|
baker hubspot forms list --redirecting-only # which forms redirect away on submit
|
|
26789
27153
|
baker hubspot forms view <formId> # fields, consent mode, full configuration
|
|
26790
27154
|
baker hubspot forms view <formId> --as-node # the form.external blob -> write into the step
|
|
27155
|
+
baker hubspot forms submissions <formId> # how many leads it got, and when
|
|
27156
|
+
baker hubspot workflows list --enabled-only # what runs after a lead is captured
|
|
27157
|
+
baker hubspot pipelines list # where a lead lands, and the stages after it
|
|
27158
|
+
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
27159
|
+
baker hubspot contacts lookup a@b.com # was this one lead picked up?
|
|
26791
27160
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
26792
27161
|
baker hubspot meetings view <slug> --as-node # same, for a hubspotMeeting step
|
|
26793
27162
|
|
|
27163
|
+
An account connected before one of these existed cannot serve it \u2014 you get HUBSPOT_RECONNECT_REQUIRED, which
|
|
27164
|
+
means HubSpot IS connected and the user has to reconnect it to widen what Baker may read. Report that and
|
|
27165
|
+
carry on with the rest; never call it broken and never retry.
|
|
27166
|
+
|
|
26794
27167
|
Nothing here is secret, so HubSpot steps do not need request_flow_input: take --as-node and edit the
|
|
26795
27168
|
flow's _data.json. Use the approval form only when no account is connected or the right form is unclear.
|
|
26796
27169
|
Full guide: __tooling__/docs/tools/baker/hubspot.md`
|
|
26797
27170
|
},
|
|
26798
|
-
subCommands: {
|
|
27171
|
+
subCommands: {
|
|
27172
|
+
forms: formsCommand,
|
|
27173
|
+
workflows: workflowsCommand,
|
|
27174
|
+
pipelines: pipelinesCommand,
|
|
27175
|
+
contacts: contactsCommand,
|
|
27176
|
+
meetings: meetingsCommand
|
|
27177
|
+
}
|
|
26799
27178
|
});
|
|
26800
27179
|
|
|
26801
27180
|
// src/commands/images/index.ts
|
|
@@ -30883,6 +31262,21 @@ Subcommands:
|
|
|
30883
31262
|
|
|
30884
31263
|
// src/commands/mcp/index.ts
|
|
30885
31264
|
import { defineCommand as defineCommand141 } from "citty";
|
|
31265
|
+
|
|
31266
|
+
// src/commands/mcp/platforms.ts
|
|
31267
|
+
function readsKey(label) {
|
|
31268
|
+
const [first = "", ...rest] = label.split(" ");
|
|
31269
|
+
return [first.toLowerCase(), ...rest].join("");
|
|
31270
|
+
}
|
|
31271
|
+
function platformEntry(platform) {
|
|
31272
|
+
return {
|
|
31273
|
+
platform: platform.label,
|
|
31274
|
+
...platform.account ? { account: platform.account } : {},
|
|
31275
|
+
...platform.readsLabel && platform.reads.length > 0 ? { [readsKey(platform.readsLabel)]: platform.reads } : {}
|
|
31276
|
+
};
|
|
31277
|
+
}
|
|
31278
|
+
|
|
31279
|
+
// src/commands/mcp/index.ts
|
|
30886
31280
|
var SCOPES = ["user", "user_org", "company", "org"];
|
|
30887
31281
|
function parseScope(raw) {
|
|
30888
31282
|
const scope = raw === void 0 ? "company" : String(raw);
|
|
@@ -30930,18 +31324,21 @@ function fail6(err) {
|
|
|
30930
31324
|
writeJson({ ok: false, error: { code: "NETWORK_ERROR", message: "Unexpected error" } });
|
|
30931
31325
|
process.exit(1);
|
|
30932
31326
|
}
|
|
31327
|
+
var PLATFORMS_HINT = "`platforms` are connected accounts you reach through `baker` commands (HubSpot, Google Ads, GA4, Search Console, Tag Manager, \u2026), NOT mcp__* tools \u2014 there is no tool to call for them. A platform listed here IS connected: never tell the user to connect it, and never conclude it is missing because it has no mcp__* entry. Read what it holds with its own command family (`baker hubspot \u2026`, `baker ads google \u2026`, `baker ga4 \u2026`, and so on).";
|
|
30933
31328
|
registerSchema({
|
|
30934
31329
|
command: "mcp.connected",
|
|
30935
|
-
description: "List
|
|
31330
|
+
description: "List everything this chat can reach: managed integrations (Attio, Slack, Gmail, Google Sheets, \u2026), custom MCP servers, and the platforms the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager) which you read through their own `baker` commands. Start here when the user mentions an external tool or platform.",
|
|
30936
31331
|
args: {}
|
|
30937
31332
|
});
|
|
30938
31333
|
var connectedCommand = defineCommand141({
|
|
30939
31334
|
meta: {
|
|
30940
31335
|
name: "connected",
|
|
30941
|
-
description: `
|
|
31336
|
+
description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
|
|
30942
31337
|
|
|
30943
31338
|
Managed tools ride the composio servers (mcp__composio_<scope>__*); custom servers expose mcp__<name>__*.
|
|
30944
|
-
|
|
31339
|
+
Platforms (HubSpot, Google Ads, GA4, \u2026) have no mcp__* tools at all \u2014 you read them with their own
|
|
31340
|
+
\`baker\` command family, and one listed here is connected and ready to use.
|
|
31341
|
+
A tool or platform the user names that is NOT listed anywhere here is simply not connected yet.`
|
|
30945
31342
|
},
|
|
30946
31343
|
run: async () => {
|
|
30947
31344
|
try {
|
|
@@ -30955,7 +31352,11 @@ A tool the user names that is NOT listed here is simply not connected yet.`
|
|
|
30955
31352
|
...c.readOnly ? { readOnly: true } : {}
|
|
30956
31353
|
}));
|
|
30957
31354
|
const custom = data.custom.map(customEntry);
|
|
31355
|
+
const platforms = (data.platforms ?? []).map(platformEntry);
|
|
30958
31356
|
const hints = [];
|
|
31357
|
+
if (platforms.length > 0) {
|
|
31358
|
+
hints.push(PLATFORMS_HINT);
|
|
31359
|
+
}
|
|
30959
31360
|
if (data.custom.some((s) => s.health && !s.health.ok && !s.health.transient)) {
|
|
30960
31361
|
hints.push(BROKEN_SERVER_HINT);
|
|
30961
31362
|
}
|
|
@@ -30971,11 +31372,11 @@ A tool the user names that is NOT listed here is simply not connected yet.`
|
|
|
30971
31372
|
);
|
|
30972
31373
|
}
|
|
30973
31374
|
hints.push(
|
|
30974
|
-
"Not listed = not connected. Managed app (Slack, HubSpot,
|
|
31375
|
+
"Not listed anywhere = not connected. Managed app (Slack, Attio, \u2026) or platform (HubSpot, Google Ads, GA4, \u2026) \u2192 the user connects it in the dashboard: Brain \u2192 Integrations \u2192 Tools. Custom HTTPS MCP endpoint \u2192 `baker mcp add`."
|
|
30975
31376
|
);
|
|
30976
31377
|
writeJson({
|
|
30977
31378
|
ok: true,
|
|
30978
|
-
data: { ...data.managedDisabled ? { managedDisabled: true } : {}, managed, custom },
|
|
31379
|
+
data: { ...data.managedDisabled ? { managedDisabled: true } : {}, managed, custom, platforms },
|
|
30979
31380
|
hints
|
|
30980
31381
|
});
|
|
30981
31382
|
} catch (err) {
|
|
@@ -31108,7 +31509,7 @@ Tools from a registered server appear as mcp__<name>__* on the next message.
|
|
|
31108
31509
|
Managed here are **company-scoped** servers; user- and org-scoped servers are managed in the dashboard.
|
|
31109
31510
|
|
|
31110
31511
|
Start here:
|
|
31111
|
-
baker mcp connected # everything this chat can reach: managed integrations
|
|
31512
|
+
baker mcp connected # everything this chat can reach: managed integrations, custom servers, connected platforms
|
|
31112
31513
|
|
|
31113
31514
|
Examples:
|
|
31114
31515
|
baker mcp add --name weather --url https://mcp.example.com/mcp
|