@koda-sl/baker-cli 0.384.0 → 0.385.0-dev.ec7b0e42b

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.
@@ -13,6 +13,14 @@ import {
13
13
  // src/commands/ads/meta/shared.ts
14
14
  var DAY_MS = 864e5;
15
15
  function metaForbiddenFix(message) {
16
+ if (message.includes("has not accepted Meta's Lead Generation Terms")) {
17
+ return {
18
+ action: "reject",
19
+ explanation: `${message}
20
+
21
+ Meta confirmed the Page's terms status using its Page access. Give the Page owner the terms link in the error and ask them to accept it before trying the form or lead ad set again. Do not suggest reconnecting Meta for this refusal; continue with other work that does not require the form or ad set.`
22
+ };
23
+ }
16
24
  return {
17
25
  action: "reject",
18
26
  explanation: [
@@ -125,4 +133,4 @@ export {
125
133
  resolveEffectiveStatus,
126
134
  statusFilterHints
127
135
  };
128
- //# sourceMappingURL=chunk-VLUGGVJL.js.map
136
+ //# sourceMappingURL=chunk-WLDM2TRF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/ads/meta/shared.ts"],"sourcesContent":["import { ApiError } from \"../../../client.ts\";\nimport { getEnv } from \"../../../env.ts\";\nimport { handleConnectionError, needsConnectionFix } from \"../../../error-handler.ts\";\nimport { writeAdsJson } from \"../output.ts\";\nimport type { AdsErrorEnvelope, QueryFix } from \"../types.ts\";\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Meta answers a permission refusal (Graph codes 200/10/294) with a bare\n * \"Permissions error\" that covers two causes needing opposite remedies: a\n * consent grant the connection never got — nothing validates Meta's granted\n * scopes after connect, unlike HubSpot's `requireCapability` — or a role the\n * user lacks on one asset, since a Pixel/Dataset, Page or catalog lives in a\n * Business and is assigned separately from the ad account. Only the blast radius\n * separates them, so hand the agent that test rather than a verdict: guessing\n * \"reconnect\" wastes the user's time on an unassigned asset, and ruling it out\n * strands a connection that really is short a scope.\n */\nexport function metaForbiddenFix(message: string): QueryFix {\n if (message.includes(\"has not accepted Meta's Lead Generation Terms\")) {\n return {\n action: \"reject\",\n explanation: `${message}\\n\\nMeta confirmed the Page's terms status using its Page access. Give the Page owner the terms link in the error and ask them to accept it before trying the form or lead ad set again. Do not suggest reconnecting Meta for this refusal; continue with other work that does not require the form or ad set.`,\n };\n }\n return {\n action: \"reject\",\n explanation: [\n message,\n \"This is a permission refusal, not a dead connection, and Meta returns the same code for two causes with opposite remedies. Tell them apart by how much else works before you answer.\",\n \"If other reads on this account are ALSO refused (campaigns, ad sets, insights), the connection is short a permission it was never granted. Only the user can widen it, by reconnecting Meta from the dashboard.\",\n \"If those reads work and only this object is refused, the asset (pixel/dataset, Page, or catalog) belongs to a Business the connected user holds no role in. Ad-account access does not carry asset access and reconnecting cannot grant it — the client assigns that asset to the connected user or partner in Meta Business settings, under the asset's own section (Data sources → Datasets for a pixel).\",\n \"Either way do not retry the same call, and do not abort the wider job. Continue with what you can read, and state in your answer that this specific check could not run and why — never present the task as verified when this step was skipped.\",\n ].join(\"\\n\\n\"),\n };\n}\n\nexport function handleMetaError(err: unknown): never {\n if (err instanceof ApiError) {\n if (needsConnectionFix(err.code, err.message)) {\n handleConnectionError(\"meta_ads\", err.message);\n }\n const envelope: AdsErrorEnvelope = {\n ok: false,\n error: {\n code: err.code,\n message: err.message,\n fix: err.code === \"FORBIDDEN\" ? metaForbiddenFix(err.message) : { action: \"reject\", explanation: err.message },\n retryable: err.code === \"RATE_LIMITED\" || err.code === \"UNAVAILABLE\" || err.code === \"INTERNAL_ERROR\",\n },\n };\n writeAdsJson(envelope);\n process.exit(1);\n }\n const message = err instanceof Error ? err.message : \"Unexpected error\";\n writeAdsJson({ ok: false, error: { code: \"NETWORK_ERROR\", message } });\n process.exit(1);\n}\n\nconst ACCOUNT_ID_RE = /^(act_)?[0-9]+$/;\n\n/** Pull the default Meta ad account ID from `--account-id` arg or `BAKER_META_AD_ACCOUNT_ID` env. */\nexport function resolveAccountIdArg(args: Record<string, unknown>): string {\n const fromArgs = args[\"account-id\"] as string | undefined;\n const id = fromArgs || getEnv().BAKER_META_AD_ACCOUNT_ID;\n if (!id) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"MISSING_ACCOUNT_ID\",\n message: \"Pass --account-id or set BAKER_META_AD_ACCOUNT_ID. Run `baker ads meta accounts` to find IDs.\",\n },\n });\n process.exit(1);\n }\n if (!ACCOUNT_ID_RE.test(id)) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"INVALID_ACCOUNT_ID\",\n message: `Invalid account ID \"${id}\". Expected numeric ID, optionally prefixed with \"act_\".`,\n },\n });\n process.exit(1);\n }\n return id.startsWith(\"act_\") ? id : `act_${id}`;\n}\n\nexport function getDateRange(args: Record<string, unknown>): { datePreset?: string; since?: string; until?: string } {\n const datePreset = args[\"date-preset\"] as string | undefined;\n const since = args.since as string | undefined;\n const until = args.until as string | undefined;\n if (since && until) return { since, until };\n return { datePreset: datePreset ?? \"last_7d\" };\n}\n\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nexport function daysAgoIso(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function csvOrJson(args: Record<string, unknown>): string {\n return (args.output as string | undefined) ?? \"json\";\n}\n\n/**\n * Matched by Meta itself, so finding one entity by name reads only the rows that\n * match instead of the whole account.\n */\nexport const NAME_CONTAINS_ARG = {\n type: \"string\",\n description:\n \"Only rows whose name contains this text, matched by Meta before anything is returned. Start here to find one entity by name: --name-contains octubre --all-statuses\",\n} as const;\n\n/**\n * Default to ACTIVE-only on listings — that's almost always what an AI agent or\n * a human glance wants. Pass `--effective-status ACTIVE,PAUSED` to override, or\n * `--all-statuses` to drop the filter entirely.\n */\nexport function resolveEffectiveStatus(args: Record<string, unknown>): string | undefined {\n if (args[\"all-statuses\"]) {\n return undefined;\n }\n const explicit = args[\"effective-status\"] as string | undefined;\n if (explicit) {\n return explicit;\n }\n return \"ACTIVE\";\n}\n\n/**\n * An empty listing under a status filter reads exactly like \"nothing exists\":\n * a paused campaign's ad sets are CAMPAIGN_PAUSED, so the ACTIVE default hides\n * every one of them. Say which filter produced the empty list and how to widen it.\n */\nexport function statusFilterHints(\n rows: ReadonlyArray<unknown>,\n effectiveStatus: string | undefined,\n noun: string,\n): string[] {\n if (rows.length > 0 || effectiveStatus === undefined) return [];\n if (effectiveStatus === \"ACTIVE\") {\n return [\n `No ACTIVE ${noun}. The ACTIVE filter hides paused, archived and in-review ones (under a paused campaign or ad set that is all of them), so this does NOT mean none exist. Re-run with --all-statuses before concluding there are none.`,\n ];\n }\n return [\n `No ${noun} with effective_status ${effectiveStatus}. Re-run with --all-statuses to see every status before concluding there are none.`,\n ];\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -70,7 +70,7 @@ import {
70
70
  validateCanvasDeep,
71
71
  withoutCastName,
72
72
  ytDlpBlockSignal
73
- } from "./chunk-FGHMWBUD.js";
73
+ } from "./chunk-JHKW6IDX.js";
74
74
  import {
75
75
  NAME_CONTAINS_ARG,
76
76
  csvOrJson,
@@ -81,7 +81,7 @@ import {
81
81
  resolveEffectiveStatus,
82
82
  statusFilterHints,
83
83
  todayIso
84
- } from "./chunk-VLUGGVJL.js";
84
+ } from "./chunk-WLDM2TRF.js";
85
85
  import {
86
86
  buildQueryCacheKey,
87
87
  cacheGet,
@@ -5124,6 +5124,16 @@ var FLOW_SIDE_EFFECT_CATALOG = [
5124
5124
  },
5125
5125
  "`advancedMatching` is how Meta re-identifies the visitor, and it is the difference between a matched conversion and a discarded one. Fill at least `em` and `ph` from the form: `baker flows map <slug> --map em=email --map ph=phone`."
5126
5126
  ),
5127
+ eventSideEffect(
5128
+ "chatgptAds",
5129
+ "Fires a ChatGPT Ads pixel conversion and mirrors it through the Conversions API when a key is configured.",
5130
+ {
5131
+ eventType: "standard",
5132
+ eventParams: [],
5133
+ advancedMatching: matchingSlots("em", "ph", "fn", "ln", "external_id", "ct", "st", "zp", "country")
5134
+ },
5135
+ "Use ChatGPT Ads event names such as lead_created, order_created or appointment_scheduled. For a custom event use a lowercase name of 1\u201364 letters, digits, underscores or dashes. Only value (major currency units) and currency are supported in eventParams. Map em from the form to improve conversion matching."
5136
+ ),
5127
5137
  eventSideEffect("googleAnalytics", "Fires a GA4 event.", { eventType: "custom", eventParams: [] }),
5128
5138
  {
5129
5139
  kind: "side-effect",
@@ -8036,6 +8046,7 @@ var studioImproveResponseSchema = z27.object({ prompt: z27.string(), original: z
8036
8046
  import { z as z28 } from "zod";
8037
8047
  var TAG_TYPES = [
8038
8048
  "meta",
8049
+ "chatgptAds",
8039
8050
  "amplitude",
8040
8051
  "googleAds",
8041
8052
  "tiktok",
@@ -8057,6 +8068,7 @@ var TAG_TYPES = [
8057
8068
  var tagTypeSchema = z28.enum(TAG_TYPES);
8058
8069
  var TAG_IDENTIFYING_FIELD = {
8059
8070
  meta: "pixelId",
8071
+ chatgptAds: "pixelId",
8060
8072
  googleAds: "conversionID",
8061
8073
  tiktok: "pixelCode",
8062
8074
  pinterest: "tagId",
@@ -15027,7 +15039,14 @@ function parseEventKey(key) {
15027
15039
  }
15028
15040
 
15029
15041
  // ../api/src/analytics/sends.ts
15030
- var SEND_PLATFORMS = ["meta", "googleAds", "linkedinInsightTag", "twitterAds", "googleAnalytics"];
15042
+ var SEND_PLATFORMS = [
15043
+ "meta",
15044
+ "chatgptAds",
15045
+ "googleAds",
15046
+ "linkedinInsightTag",
15047
+ "twitterAds",
15048
+ "googleAnalytics"
15049
+ ];
15031
15050
  function isSendPlatform(value) {
15032
15051
  return SEND_PLATFORMS.includes(value);
15033
15052
  }
@@ -15039,6 +15058,13 @@ var SEND_PLATFORM_SPECS = {
15039
15058
  credentialFields: ["accessToken"],
15040
15059
  setupHint: "Add the Conversions API access token to the Meta pixel in Tracking"
15041
15060
  },
15061
+ chatgptAds: {
15062
+ title: "ChatGPT Ads",
15063
+ eventMeans: "A ChatGPT Ads event \u2014 lead_created, order_created, or a custom name",
15064
+ eventKind: "name",
15065
+ credentialFields: ["apiKey"],
15066
+ setupHint: "Add the Conversions API key to the ChatGPT Ads pixel in Tracking"
15067
+ },
15042
15068
  googleAds: {
15043
15069
  title: "Google Ads",
15044
15070
  eventMeans: "One of the account's conversion actions, picked from its list",
@@ -15089,6 +15115,7 @@ function isConversionSend(type) {
15089
15115
  }
15090
15116
  var SERVER_SIDE_SENDER_TAG = {
15091
15117
  metaCapi: "meta",
15118
+ chatgptAdsCapi: "chatgptAds",
15092
15119
  googleAdsCapi: "googleAds",
15093
15120
  linkedinCapi: "linkedinInsightTag",
15094
15121
  twitterAdsCapi: "twitterAds",
@@ -20545,9 +20572,8 @@ function requireTargets(args5, entity) {
20545
20572
  var CHAT_READ_ARG_DESCRIPTION = CHAT_READ_ARG.description;
20546
20573
  registerSchema({
20547
20574
  command: "ads.linkedin.accounts",
20548
- description: "List LinkedIn ad accounts in the company's connected scope. Pass --include-all to list every account the OAuth token can see.",
20575
+ description: "List LinkedIn ad accounts in the company's connected scope.",
20549
20576
  args: {
20550
- "include-all": { type: "boolean", description: "Ignore picker scope", required: false },
20551
20577
  "no-cache": NO_CACHE_ARG,
20552
20578
  output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
20553
20579
  }
@@ -22258,11 +22284,7 @@ Examples:
22258
22284
 
22259
22285
  // src/commands/ads/linkedin/accounts.ts
22260
22286
  import { defineCommand as defineCommand35 } from "citty";
22261
- var ACCOUNTS_TTL_MS = 60 * 60 * 1e3;
22262
- function accountHints2(accounts, includeAll) {
22263
- if (includeAll) {
22264
- return [];
22265
- }
22287
+ function accountHints2(accounts) {
22266
22288
  return connectedResourceHints({
22267
22289
  plural: "LinkedIn ad accounts",
22268
22290
  singular: "LinkedIn ad account",
@@ -22277,44 +22299,23 @@ var accountsCommand2 = defineCommand35({
22277
22299
 
22278
22300
  Examples:
22279
22301
  baker ads linkedin accounts
22280
- baker ads linkedin accounts --include-all # ignore picker scope, list every account the token can see
22281
22302
  baker ads linkedin accounts --output csv`
22282
22303
  },
22283
22304
  args: {
22284
- "include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
22285
22305
  "no-cache": NO_CACHE_ARG,
22286
22306
  output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
22287
22307
  },
22288
22308
  run: async ({ args: args5 }) => {
22289
- const includeAll = args5["include-all"] === true;
22290
- const useCache = !args5["no-cache"];
22291
- const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
22292
- if (useCache) {
22293
- const cached = cacheGet("linkedin-accounts", cacheKey);
22294
- if (cached) {
22295
- writeJsonEnvelope({
22296
- ok: true,
22297
- data: cached.data,
22298
- cached: true,
22299
- hints: accountHints2(cached.data, includeAll)
22300
- });
22301
- return;
22302
- }
22303
- }
22304
22309
  try {
22305
22310
  const params = {};
22306
- if (includeAll) params["include-all"] = "true";
22307
- if (!useCache) params["skip-cache"] = "true";
22311
+ if (args5["no-cache"]) params["skip-cache"] = "true";
22308
22312
  const data = await apiGet("/api/ads/linkedin/accounts", params);
22309
- if (useCache) {
22310
- cacheSet("linkedin-accounts", cacheKey, data, ACCOUNTS_TTL_MS);
22311
- }
22312
22313
  const fmt = csvOrJson2(args5);
22313
22314
  if (fmt !== "json") {
22314
22315
  writeAdsOutput(data, fmt);
22315
22316
  return;
22316
22317
  }
22317
- writeJsonEnvelope({ ok: true, data, hints: accountHints2(data, includeAll) });
22318
+ writeJsonEnvelope({ ok: true, data, hints: accountHints2(data) });
22318
22319
  } catch (err) {
22319
22320
  handleLinkedinError(err);
22320
22321
  }
@@ -24313,7 +24314,6 @@ var linkedinCommand = defineCommand53({
24313
24314
 
24314
24315
  Discovery:
24315
24316
  baker ads linkedin accounts \u2014 accounts in this company's connected scope
24316
- baker ads linkedin accounts --include-all \u2014 every account the token can see
24317
24317
  baker ads linkedin account --account-id 503001492 \u2014 single-account detail (currency, type, status)
24318
24318
  baker ads linkedin url-params [--ad-sets] \u2014 UTMs in effect today, and keys set at two levels
24319
24319
 
@@ -24408,10 +24408,7 @@ Examples:
24408
24408
 
24409
24409
  // src/commands/ads/meta/accounts.ts
24410
24410
  import { defineCommand as defineCommand55 } from "citty";
24411
- function accountHints3(accounts, includeAll) {
24412
- if (includeAll) {
24413
- return [];
24414
- }
24411
+ function accountHints3(accounts) {
24415
24412
  return connectedResourceHints({
24416
24413
  plural: "Meta ad accounts",
24417
24414
  singular: "Meta ad account",
@@ -24429,44 +24426,23 @@ var accountsCommand3 = defineCommand55({
24429
24426
 
24430
24427
  Examples:
24431
24428
  baker ads meta accounts
24432
- baker ads meta accounts --include-all # ignore picker scope and list every account the token can see
24433
24429
  baker ads meta accounts --output csv`
24434
24430
  },
24435
24431
  args: {
24436
- "include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
24437
24432
  "no-cache": NO_CACHE_ARG,
24438
24433
  output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
24439
24434
  },
24440
24435
  run: async ({ args: args5 }) => {
24441
- const includeAll = args5["include-all"] === true;
24442
- const useCache = !args5["no-cache"];
24443
- const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
24444
- if (useCache) {
24445
- const cached = cacheGet("meta-accounts", cacheKey);
24446
- if (cached) {
24447
- writeJsonEnvelope({
24448
- ok: true,
24449
- data: cached.data,
24450
- cached: true,
24451
- hints: accountHints3(cached.data, includeAll)
24452
- });
24453
- return;
24454
- }
24455
- }
24456
24436
  try {
24457
24437
  const params = {};
24458
- if (includeAll) params["include-all"] = "true";
24459
- if (!useCache) params["skip-cache"] = "true";
24438
+ if (args5["no-cache"]) params["skip-cache"] = "true";
24460
24439
  const data = await apiGet("/api/ads/meta/accounts", params);
24461
- if (useCache) {
24462
- cacheSet("meta-accounts", cacheKey, data, 60 * 60 * 1e3);
24463
- }
24464
24440
  const fmt = csvOrJson(args5);
24465
24441
  if (fmt !== "json") {
24466
24442
  writeAdsOutput(data, fmt);
24467
24443
  return;
24468
24444
  }
24469
- writeJsonEnvelope({ ok: true, data, hints: accountHints3(data, includeAll) });
24445
+ writeJsonEnvelope({ ok: true, data, hints: accountHints3(data) });
24470
24446
  } catch (err) {
24471
24447
  handleMetaError(err);
24472
24448
  }
@@ -24767,7 +24743,7 @@ function leafRefusal(field, condition, path48) {
24767
24743
  }
24768
24744
 
24769
24745
  // ../api/src/ads-meta/leadAds.ts
24770
- var META_LEAD_ADS_NOT_ENABLED_MESSAGE = "Reading Meta instant forms and their leads is not switched on for this company. Everything else on Meta keeps working. Tell the user this needs switching on by the Baker team, and carry on with the rest of the job.";
24746
+ var META_LEAD_ADS_NOT_ENABLED_MESSAGE = "Using Meta instant forms and their leads is not switched on for this company. Everything else on Meta keeps working. Tell the user this needs switching on by the Baker team, and carry on with the rest of the job.";
24771
24747
  var META_LEAD_ADS_RECONNECT_MESSAGE = "This Meta connection was made before lead ads were switched on for this company, so it cannot work with instant forms or read their leads. Ask the user to reconnect Meta in the dashboard \u2014 that grants the extra permissions \u2014 then ask for this again. Everything else on Meta keeps working meanwhile.";
24772
24748
  function isMetaLeadAdsRefusal(message) {
24773
24749
  return message === META_LEAD_ADS_NOT_ENABLED_MESSAGE || message === META_LEAD_ADS_RECONNECT_MESSAGE;
@@ -25853,7 +25829,7 @@ var callToActionSchema = z37.strictObject({
25853
25829
  * Without it Meta refuses every ad in that ad set with subcode 3390001,
25854
25830
  * "Choose or create an instant form for your leads campaign".
25855
25831
  */
25856
- lead_gen_form_id: z37.string().regex(NUMERIC_ID_REGEX3).optional()
25832
+ lead_gen_form_id: parentRefSchema3.optional()
25857
25833
  }).superRefine((p, ctx) => {
25858
25834
  if (p.lead_gen_form_id && !LEAD_FORM_CTA_TYPES.some((allowed) => allowed === p.type)) {
25859
25835
  ctx.addIssue({
@@ -26176,6 +26152,50 @@ var mediaUploadSchema = z37.strictObject({
26176
26152
  ctx.addIssue({ code: "custom", path: ["bakerVideoId"], message: "video uploads need a bakerVideoId" });
26177
26153
  }
26178
26154
  });
26155
+ var leadFormCreateSchema2 = z37.strictObject({
26156
+ page_id: z37.string().regex(NUMERIC_ID_REGEX3),
26157
+ name: z37.string().min(1).max(255),
26158
+ locale: z37.enum(["EN_US", "EN_GB", "ES_ES", "ES_LA", "FR_FR", "DE_DE", "PT_BR", "PT_PT"]).default("EN_US"),
26159
+ questionPageHeadline: z37.string().min(1).max(255).optional(),
26160
+ questions: z37.array(
26161
+ z37.strictObject({
26162
+ type: z37.enum([
26163
+ "FULL_NAME",
26164
+ "FIRST_NAME",
26165
+ "LAST_NAME",
26166
+ "EMAIL",
26167
+ "PHONE",
26168
+ "CITY",
26169
+ "STATE",
26170
+ "COUNTRY",
26171
+ "ZIP_CODE",
26172
+ "COMPANY_NAME",
26173
+ "JOB_TITLE",
26174
+ "CUSTOM"
26175
+ ]),
26176
+ label: z37.string().min(1).max(255).optional()
26177
+ })
26178
+ ).min(1).max(15),
26179
+ privacyPolicyUrl: httpsUrlSchema3,
26180
+ privacyPolicyLinkText: z37.string().min(1).max(70).default("Privacy Policy"),
26181
+ followUpActionUrl: httpsUrlSchema3,
26182
+ optimizedForQuality: z37.boolean().default(false),
26183
+ thankYou: z37.strictObject({
26184
+ title: z37.string().min(1).max(255),
26185
+ body: z37.string().min(1).max(1e3),
26186
+ buttonText: z37.string().min(1).max(70).default("View website")
26187
+ }).optional()
26188
+ }).superRefine((form, ctx) => {
26189
+ const seen = /* @__PURE__ */ new Set();
26190
+ for (const [index, question] of form.questions.entries()) {
26191
+ if (question.type === "CUSTOM" && !question.label) {
26192
+ ctx.addIssue({ code: "custom", path: ["questions", index, "label"], message: "custom questions need a label" });
26193
+ }
26194
+ const key = `${question.type}:${question.label ?? ""}`;
26195
+ if (seen.has(key)) ctx.addIssue({ code: "custom", path: ["questions", index], message: "duplicate question" });
26196
+ seen.add(key);
26197
+ }
26198
+ });
26179
26199
  var META_DRAFT_OP_KINDS = [
26180
26200
  "campaign.create",
26181
26201
  "campaign.update",
@@ -26188,6 +26208,7 @@ var META_DRAFT_OP_KINDS = [
26188
26208
  "customAudience.create",
26189
26209
  "customAudience.update",
26190
26210
  "customConversion.create",
26211
+ "leadForm.create",
26191
26212
  "media.upload"
26192
26213
  ];
26193
26214
  var metaDraftOpKindSchema = z37.enum(META_DRAFT_OP_KINDS);
@@ -26211,6 +26232,7 @@ var metaDraftOpInputSchema = z37.discriminatedUnion("kind", [
26211
26232
  createOp4("customAudience.create", customAudienceCreateSchema),
26212
26233
  updateOp4("customAudience.update", customAudienceUpdateSchema),
26213
26234
  createOp4("customConversion.create", customConversionCreateSchema),
26235
+ createOp4("leadForm.create", leadFormCreateSchema2),
26214
26236
  createOp4("media.upload", mediaUploadSchema)
26215
26237
  ]);
26216
26238
 
@@ -26801,7 +26823,7 @@ Example: baker ads meta creatives create --page 555 --message "Save now" --link
26801
26823
  cta: { type: "string", description: "Call-to-action type (SHOP_NOW|LEARN_MORE|SIGN_UP|\u2026)" },
26802
26824
  "lead-form": {
26803
26825
  type: "string",
26804
- description: "Instant form id the button opens \u2014 REQUIRED for an ad set that collects leads on the ad. List them with `baker ads meta lead-forms`. With a form, --cta must be APPLY_NOW|DOWNLOAD|GET_QUOTE|LEARN_MORE|SIGN_UP|SUBSCRIBE"
26826
+ description: "Instant form id or meta_temp_* ref from `baker ads meta lead-forms create` \u2014 REQUIRED for an ad set that collects leads on the ad. With a form, --cta must be APPLY_NOW|DOWNLOAD|GET_QUOTE|LEARN_MORE|SIGN_UP|SUBSCRIBE"
26805
26827
  },
26806
26828
  "image-hash": { type: "string", description: "Meta ad-image hash (already uploaded)" },
26807
26829
  "image-ref": { type: "string", description: "meta_temp_* ref of a staged media upload" },
@@ -26945,7 +26967,7 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
26945
26967
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
26946
26968
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
26947
26969
  const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-NMQXZF3Z.js");
26948
- const { handleMetaError: handleMetaError2 } = await import("./shared-J7DQZ2ED.js");
26970
+ const { handleMetaError: handleMetaError2 } = await import("./shared-6762QE6R.js");
26949
26971
  try {
26950
26972
  const accountId = bareAccountId2(args5);
26951
26973
  const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
@@ -27099,7 +27121,7 @@ function duplicateCommand2(entity, label) {
27099
27121
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
27100
27122
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
27101
27123
  const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-NMQXZF3Z.js");
27102
- const { handleMetaError: handleMetaError2 } = await import("./shared-J7DQZ2ED.js");
27124
+ const { handleMetaError: handleMetaError2 } = await import("./shared-6762QE6R.js");
27103
27125
  try {
27104
27126
  const accountId = bareAccountId2(args5);
27105
27127
  const chatId = requireChatId2();
@@ -27352,7 +27374,7 @@ import { defineCommand as defineCommand61 } from "citty";
27352
27374
  var businessesCommand = defineCommand61({
27353
27375
  meta: {
27354
27376
  name: "businesses",
27355
- description: `List Meta Business Manager accounts the connected user has access to. Required for ad-studies and product-catalogs commands.
27377
+ description: `List Meta Business Manager accounts linked to this company's selected ad accounts. Required for ad-studies and product-catalogs commands.
27356
27378
 
27357
27379
  Examples:
27358
27380
  baker ads meta businesses`
@@ -28222,13 +28244,47 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
28222
28244
  import { defineCommand as defineCommand67 } from "citty";
28223
28245
  registerSchema({
28224
28246
  command: "ads.meta.lead-forms",
28225
- description: "List the instant (lead) forms on a Facebook Page, with how many leads each has collected. Start here: a lead is fetched by form id, and `baker ads meta pages` gives the page id.",
28247
+ description: "List the instant (lead) forms on a Facebook Page, with how many leads each has collected. Start here: a lead is fetched by form id. Use a Page from `baker ads meta pages` with hasPageAccessToken: true.",
28226
28248
  args: {
28227
- "page-id": { type: "string", description: "Facebook Page ID \u2014 from `baker ads meta pages`", required: true },
28249
+ "page-id": {
28250
+ type: "string",
28251
+ description: "Facebook Page ID \u2014 from `baker ads meta pages` with hasPageAccessToken: true",
28252
+ required: true
28253
+ },
28228
28254
  full: { type: "boolean", description: "Include each form's questions", required: false },
28229
28255
  output: { type: "string", description: "json|csv", required: false, default: "json" }
28230
28256
  }
28231
28257
  });
28258
+ registerSchema({
28259
+ command: "ads.meta.lead-forms.create",
28260
+ description: "Start here to stage a Page-owned Meta Instant Form. The form is created only when this Session publishes. Use the returned meta_temp_* ref as --lead-form on a creative in the same Session.",
28261
+ args: {
28262
+ "account-id": { type: "string", description: "Selected Meta ad account, act_<id>", required: true },
28263
+ "page-id": {
28264
+ type: "string",
28265
+ description: "Facebook Page that owns the form \u2014 choose one with this account in promotingAccountIds and hasPageAccessToken: true from `baker ads meta pages`",
28266
+ required: true
28267
+ },
28268
+ name: { type: "string", description: "Form name", required: true },
28269
+ questions: {
28270
+ type: "string",
28271
+ description: "Comma-separated Meta fields, e.g. FULL_NAME,EMAIL,PHONE",
28272
+ required: true
28273
+ },
28274
+ "privacy-policy-url": { type: "string", description: "Public HTTPS privacy policy", required: true },
28275
+ "follow-up-url": { type: "string", description: "Public HTTPS link after submission", required: true },
28276
+ "optimized-for-quality": {
28277
+ type: "boolean",
28278
+ description: "Ask Meta to optimize form leads for quality",
28279
+ required: false
28280
+ },
28281
+ file: {
28282
+ type: "string",
28283
+ description: "JSON file with full form payload, including thankYou and custom questions",
28284
+ required: false
28285
+ }
28286
+ }
28287
+ });
28232
28288
  registerSchema({
28233
28289
  command: "ads.meta.leads",
28234
28290
  description: "Read the leads an instant form collected. Contact details (email, phone) come back masked; pass --full only when the raw values are genuinely needed, e.g. handing them to a CRM.",
@@ -28294,25 +28350,56 @@ function leadPageHints(page, full) {
28294
28350
  }
28295
28351
  return hints2;
28296
28352
  }
28297
- var leadFormsCommand2 = defineCommand67({
28353
+ var leadFormsCreateCommand2 = defineCommand67({
28298
28354
  meta: {
28299
- name: "lead-forms",
28300
- description: `List the instant (lead) forms on a Facebook Page, with each form's lead count. A lead is read by form id, so start here \u2014 get the Page id from \`baker ads meta pages\`.
28301
-
28302
- Examples:
28303
- baker ads meta lead-forms --page-id 100635183117758
28304
- baker ads meta lead-forms --page-id 100635183117758 --full # also lists each form's questions`
28355
+ name: "create",
28356
+ description: `Stage a Meta Instant Form on a Facebook Page. The form appears in review and is created only when the Session publishes.
28357
+ Choose a Page with this account in promotingAccountIds and hasPageAccessToken: true from \`baker ads meta pages\`.
28358
+ Example: baker ads meta lead-forms create --account-id act_123 --page-id 456 --name "Free audit" --questions FULL_NAME,EMAIL,PHONE --privacy-policy-url https://example.com/privacy --follow-up-url https://example.com`
28305
28359
  },
28306
28360
  args: {
28361
+ "account-id": { type: "string", description: "Selected Meta ad account" },
28307
28362
  "page-id": { type: "string", description: "Facebook Page ID" },
28308
- full: { type: "boolean", description: "Include each form's questions" },
28309
- output: { type: "string", description: "Output format", default: "json" }
28363
+ name: { type: "string", description: "Form name" },
28364
+ questions: { type: "string", description: "FULL_NAME,EMAIL,PHONE or other Meta fields" },
28365
+ "privacy-policy-url": { type: "string", description: "HTTPS privacy policy" },
28366
+ "follow-up-url": { type: "string", description: "HTTPS thank-you link" },
28367
+ "optimized-for-quality": { type: "boolean", description: "Optimize form leads for quality" },
28368
+ file: { type: "string", description: "JSON form payload; flags override file keys" }
28310
28369
  },
28370
+ run: async ({ args: args5 }) => {
28371
+ const accountId = bareAccountId2(args5);
28372
+ const payload = mergePayload3(loadJsonFileArg3(args5.file), {
28373
+ page_id: args5["page-id"],
28374
+ name: args5.name,
28375
+ questions: typeof args5.questions === "string" ? args5.questions.split(",").map((type) => ({ type: type.trim().toUpperCase() })) : void 0,
28376
+ privacyPolicyUrl: args5["privacy-policy-url"],
28377
+ followUpActionUrl: args5["follow-up-url"],
28378
+ optimizedForQuality: args5["optimized-for-quality"]
28379
+ });
28380
+ await stageOp2({ kind: "leadForm.create", accountId, payload });
28381
+ }
28382
+ });
28383
+ var leadFormsListArgs = {
28384
+ "page-id": { type: "string", description: "Facebook Page ID" },
28385
+ full: { type: "boolean", description: "Include each form's questions" },
28386
+ output: { type: "string", description: "Output format", default: "json" }
28387
+ };
28388
+ var leadFormsListCommand = defineCommand67({
28389
+ meta: {
28390
+ name: "list",
28391
+ description: "List the Instant Forms on a Facebook Page, with lead counts. Choose a Page with hasPageAccessToken: true from `baker ads meta pages`. Pass --full to include questions."
28392
+ },
28393
+ args: leadFormsListArgs,
28311
28394
  run: async ({ args: args5 }) => {
28312
28395
  try {
28313
28396
  const pageId = args5["page-id"];
28314
28397
  if (!pageId) {
28315
- handleLeadsError(new Error("--page-id is required. Run `baker ads meta pages` to find it."));
28398
+ handleLeadsError(
28399
+ new Error(
28400
+ "--page-id is required. Run `baker ads meta pages` and choose a Page with hasPageAccessToken: true."
28401
+ )
28402
+ );
28316
28403
  }
28317
28404
  const params = { "page-id": pageId };
28318
28405
  if (args5.full) params.full = "true";
@@ -28329,6 +28416,19 @@ Examples:
28329
28416
  }
28330
28417
  }
28331
28418
  });
28419
+ var leadFormsCommand2 = defineCommand67({
28420
+ meta: {
28421
+ name: "lead-forms",
28422
+ description: `List Instant Forms on a Facebook Page or stage a new one.
28423
+ Examples:
28424
+ baker ads meta lead-forms --page-id 100635183117758
28425
+ baker ads meta lead-forms --page-id 100635183117758 --full
28426
+ baker ads meta lead-forms create --account-id act_123 --page-id 555 --file form.json`
28427
+ },
28428
+ args: leadFormsListArgs,
28429
+ subCommands: { list: leadFormsListCommand, create: leadFormsCreateCommand2 },
28430
+ default: "list"
28431
+ });
28332
28432
  var leadsCommand2 = defineCommand67({
28333
28433
  meta: {
28334
28434
  name: "leads",
@@ -28398,27 +28498,21 @@ var metaMediaCommand = defineCommand68({
28398
28498
 
28399
28499
  // src/commands/ads/meta/pages.ts
28400
28500
  import { defineCommand as defineCommand69 } from "citty";
28401
- function advertisablePages(data) {
28402
- return data.filter((p) => Array.isArray(p.tasks) && p.tasks.includes("ADVERTISE"));
28403
- }
28404
28501
  function pageSelectionHints(data) {
28405
- const advertisable = advertisablePages(data);
28406
- if (advertisable.length > 1) {
28502
+ if (data.length === 0) return [];
28503
+ const accountHint = "Use a Page only with an ad account listed in its promotingAccountIds. hasPageAccessToken must be true to read instant forms; null means Meta could not check it. Form reads also require the company's lead-ads permissions.";
28504
+ if (data.length > 1) {
28407
28505
  return [
28408
- "MULTIPLE Pages can run ads \u2014 do NOT guess. Confirm with the user which Page an ad should run from before passing --page to `creatives create`; a wrong Page publishes the ad under the wrong brand identity and staging will not catch it."
28506
+ accountHint,
28507
+ "MULTIPLE Pages can run ads from the selected Meta ad accounts \u2014 do NOT guess. Confirm with the user which Page an ad should run from before passing --page to `creatives create`; a wrong Page publishes the ad under the wrong brand identity and staging will not catch it."
28409
28508
  ];
28410
28509
  }
28411
- if (data.length > 0 && advertisable.length === 0) {
28412
- return [
28413
- "NO Page can run ads \u2014 none of the connected user's Pages carry the ADVERTISE task. Ask the user to grant Advertise access to the right Page before creating an ad; a readable-but-non-advertising Page still passes staging and would fail or run under the wrong identity."
28414
- ];
28415
- }
28416
- return [];
28510
+ return [accountHint];
28417
28511
  }
28418
28512
  var pagesCommand = defineCommand69({
28419
28513
  meta: {
28420
28514
  name: "pages",
28421
- description: `List the Facebook Pages the connected user manages. Every Meta ad publishes from a Page, so use this to identify the Page an ad should run from. The 'tasks' field shows what the user can do on each Page (ADVERTISE means it can back ads). If exactly one Page can ADVERTISE, use it; if more than one can, confirm with the user which Page to use; if none can, ask the user to grant Advertise access \u2014 a wrong or non-advertising Page publishes under the wrong brand identity.
28515
+ description: `List the Facebook Pages that the selected Meta ad accounts can promote, showing which accounts can promote each Page. Every Meta ad publishes from a Page, so match its promotingAccountIds to the target account. hasPageAccessToken shows whether the connected person can also read that Page's instant forms. If more than one Page is listed, confirm the brand with the user.
28422
28516
 
28423
28517
  Examples:
28424
28518
  baker ads meta pages`
@@ -28677,10 +28771,10 @@ var metaCommand = defineCommand72({
28677
28771
 
28678
28772
  Discovery:
28679
28773
  baker ads meta accounts \u2014 accounts in this company's connected scope
28680
- baker ads meta accounts --include-all \u2014 every account the token can see
28681
28774
  baker ads meta businesses \u2014 Business Manager IDs
28682
- baker ads meta pages \u2014 Facebook Pages the user manages (an ad's identity)
28775
+ baker ads meta pages \u2014 Facebook Pages the selected ad accounts can promote (an ad's identity)
28683
28776
  baker ads meta lead-forms --page-id 100635183117758 \u2014 instant (lead) forms on a Page, with lead counts
28777
+ baker ads meta lead-forms create --account-id act_123 --page-id 555 --file form.json \u2014 stage a new form
28684
28778
  baker ads meta leads --form-id 987654321 \u2014 the leads one form collected (contact details masked)
28685
28779
 
28686
28780
  Snapshot a running account (default = ACTIVE only):
@@ -49569,6 +49663,14 @@ var MAPPING_SLOTS = {
49569
49663
  code: [PAYLOAD("eventProperties")],
49570
49664
  datafast: [PAYLOAD("goalParams")],
49571
49665
  meta: [PAYLOAD("eventParams"), { kind: "record", key: "advancedMatching", allowed: META_MATCHING }],
49666
+ chatgptAds: [
49667
+ PAYLOAD("eventParams"),
49668
+ {
49669
+ kind: "record",
49670
+ key: "advancedMatching",
49671
+ allowed: ["em", "ph", "fn", "ln", "external_id", "ct", "st", "zp", "country"]
49672
+ }
49673
+ ],
49572
49674
  tiktok: [
49573
49675
  PAYLOAD("eventParams"),
49574
49676
  { kind: "record", key: "identify", allowed: ["email", "phone_number", "external_id"] }
@@ -66965,7 +67067,7 @@ var draftCommand5 = defineCommand218({
66965
67067
  import { defineCommand as defineCommand219 } from "citty";
66966
67068
  registerSchema({
66967
67069
  command: "tagManager.containers",
66968
- description: "List the Google Tag Manager containers this company's connection can reach. Every container the company connected is flagged `connected: true` \u2014 there can be several, and Baker may read and change all of them. Start here to confirm which containers you are managing.",
67070
+ description: "List the Google Tag Manager containers this company selected. There can be several, and Baker may read and change all of them. Start here to confirm which containers you are managing.",
66969
67071
  args: {}
66970
67072
  });
66971
67073
  registerSchema({
@@ -66986,27 +67088,34 @@ registerSchema({
66986
67088
  full: { type: "boolean", description: "Include the raw Tag Manager object for each entity", required: false }
66987
67089
  }
66988
67090
  });
66989
- function containersHints(containers) {
66990
- const connected = containers.filter((container) => container.connected);
66991
- if (connected.length === 0) {
66992
- return [
66993
- "None of the containers this company connected came back from Tag Manager. The connected Google account may have lost access to them \u2014 ask the user to reconnect or pick a container again."
66994
- ];
66995
- }
66996
- return connectedResourceHints({
67091
+ function containersHints(containers, unavailableContainers = []) {
67092
+ const hints2 = connectedResourceHints({
66997
67093
  plural: "Tag Manager containers",
66998
67094
  singular: "Tag Manager container",
66999
67095
  flag: "--container",
67000
- resources: connected.map((container) => ({
67096
+ resources: containers.map((container) => ({
67001
67097
  id: container.containerId,
67002
67098
  label: `${container.publicId} \xB7 ${container.name}`
67003
67099
  }))
67004
67100
  });
67101
+ if (unavailableContainers.length > 0) {
67102
+ const [available] = containers;
67103
+ if (containers.length === 1 && available) {
67104
+ hints2.length = 0;
67105
+ hints2.push(
67106
+ `Use --container ${available.containerId} for ${available.publicId} (${available.name}); another selected container cannot be opened.`
67107
+ );
67108
+ }
67109
+ hints2.push(
67110
+ `Tag Manager cannot open ${unavailableContainers.map((container) => `${container.publicId} (${container.name})`).join(", ")}. Ask the user to reconnect with a Google account that has access to these containers.`
67111
+ );
67112
+ }
67113
+ return hints2;
67005
67114
  }
67006
67115
  var containersCommand = defineCommand219({
67007
67116
  meta: {
67008
67117
  name: "containers",
67009
- description: `List Tag Manager containers reachable by this company's connection.
67118
+ description: `List Tag Manager containers selected by this company.
67010
67119
 
67011
67120
  Start here:
67012
67121
  baker tag-manager containers`
@@ -67014,7 +67123,7 @@ Start here:
67014
67123
  run: async () => {
67015
67124
  try {
67016
67125
  const data = await apiGet("/api/tag-manager/containers");
67017
- writeJsonEnvelope({ ok: true, data, hints: containersHints(data.containers) });
67126
+ writeJsonEnvelope({ ok: true, data, hints: containersHints(data.containers, data.unavailableContainers) });
67018
67127
  } catch (err) {
67019
67128
  handleError8(err);
67020
67129
  }