@koda-sl/baker-cli 0.384.0-dev.4699b7308 → 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-CHJOTJTZ.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,
@@ -299,6 +299,7 @@ var chatActionDraftOpViewSchema = z.object({
299
299
  targetActionId: z.string().optional(),
300
300
  tempId: z.string().optional(),
301
301
  tags: z.array(z.string()).optional(),
302
+ priority: actionPrioritySchema.nullable().optional(),
302
303
  reason: z.string().optional(),
303
304
  note: z.string().optional(),
304
305
  name: z.string().optional(),
@@ -20571,9 +20572,8 @@ function requireTargets(args5, entity) {
20571
20572
  var CHAT_READ_ARG_DESCRIPTION = CHAT_READ_ARG.description;
20572
20573
  registerSchema({
20573
20574
  command: "ads.linkedin.accounts",
20574
- 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.",
20575
20576
  args: {
20576
- "include-all": { type: "boolean", description: "Ignore picker scope", required: false },
20577
20577
  "no-cache": NO_CACHE_ARG,
20578
20578
  output: { type: "string", description: "json|csv|jsonl|md", required: false, default: "json" }
20579
20579
  }
@@ -22284,11 +22284,7 @@ Examples:
22284
22284
 
22285
22285
  // src/commands/ads/linkedin/accounts.ts
22286
22286
  import { defineCommand as defineCommand35 } from "citty";
22287
- var ACCOUNTS_TTL_MS = 60 * 60 * 1e3;
22288
- function accountHints2(accounts, includeAll) {
22289
- if (includeAll) {
22290
- return [];
22291
- }
22287
+ function accountHints2(accounts) {
22292
22288
  return connectedResourceHints({
22293
22289
  plural: "LinkedIn ad accounts",
22294
22290
  singular: "LinkedIn ad account",
@@ -22303,44 +22299,23 @@ var accountsCommand2 = defineCommand35({
22303
22299
 
22304
22300
  Examples:
22305
22301
  baker ads linkedin accounts
22306
- baker ads linkedin accounts --include-all # ignore picker scope, list every account the token can see
22307
22302
  baker ads linkedin accounts --output csv`
22308
22303
  },
22309
22304
  args: {
22310
- "include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
22311
22305
  "no-cache": NO_CACHE_ARG,
22312
22306
  output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
22313
22307
  },
22314
22308
  run: async ({ args: args5 }) => {
22315
- const includeAll = args5["include-all"] === true;
22316
- const useCache = !args5["no-cache"];
22317
- const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
22318
- if (useCache) {
22319
- const cached = cacheGet("linkedin-accounts", cacheKey);
22320
- if (cached) {
22321
- writeJsonEnvelope({
22322
- ok: true,
22323
- data: cached.data,
22324
- cached: true,
22325
- hints: accountHints2(cached.data, includeAll)
22326
- });
22327
- return;
22328
- }
22329
- }
22330
22309
  try {
22331
22310
  const params = {};
22332
- if (includeAll) params["include-all"] = "true";
22333
- if (!useCache) params["skip-cache"] = "true";
22311
+ if (args5["no-cache"]) params["skip-cache"] = "true";
22334
22312
  const data = await apiGet("/api/ads/linkedin/accounts", params);
22335
- if (useCache) {
22336
- cacheSet("linkedin-accounts", cacheKey, data, ACCOUNTS_TTL_MS);
22337
- }
22338
22313
  const fmt = csvOrJson2(args5);
22339
22314
  if (fmt !== "json") {
22340
22315
  writeAdsOutput(data, fmt);
22341
22316
  return;
22342
22317
  }
22343
- writeJsonEnvelope({ ok: true, data, hints: accountHints2(data, includeAll) });
22318
+ writeJsonEnvelope({ ok: true, data, hints: accountHints2(data) });
22344
22319
  } catch (err) {
22345
22320
  handleLinkedinError(err);
22346
22321
  }
@@ -24339,7 +24314,6 @@ var linkedinCommand = defineCommand53({
24339
24314
 
24340
24315
  Discovery:
24341
24316
  baker ads linkedin accounts \u2014 accounts in this company's connected scope
24342
- baker ads linkedin accounts --include-all \u2014 every account the token can see
24343
24317
  baker ads linkedin account --account-id 503001492 \u2014 single-account detail (currency, type, status)
24344
24318
  baker ads linkedin url-params [--ad-sets] \u2014 UTMs in effect today, and keys set at two levels
24345
24319
 
@@ -24434,10 +24408,7 @@ Examples:
24434
24408
 
24435
24409
  // src/commands/ads/meta/accounts.ts
24436
24410
  import { defineCommand as defineCommand55 } from "citty";
24437
- function accountHints3(accounts, includeAll) {
24438
- if (includeAll) {
24439
- return [];
24440
- }
24411
+ function accountHints3(accounts) {
24441
24412
  return connectedResourceHints({
24442
24413
  plural: "Meta ad accounts",
24443
24414
  singular: "Meta ad account",
@@ -24455,44 +24426,23 @@ var accountsCommand3 = defineCommand55({
24455
24426
 
24456
24427
  Examples:
24457
24428
  baker ads meta accounts
24458
- baker ads meta accounts --include-all # ignore picker scope and list every account the token can see
24459
24429
  baker ads meta accounts --output csv`
24460
24430
  },
24461
24431
  args: {
24462
- "include-all": { type: "boolean", description: "List all accessible accounts, ignoring picker scope" },
24463
24432
  "no-cache": NO_CACHE_ARG,
24464
24433
  output: { type: "string", description: "Output format: json|csv|jsonl|md", default: "json" }
24465
24434
  },
24466
24435
  run: async ({ args: args5 }) => {
24467
- const includeAll = args5["include-all"] === true;
24468
- const useCache = !args5["no-cache"];
24469
- const cacheKey = `accounts:${includeAll ? "all" : "scoped"}`;
24470
- if (useCache) {
24471
- const cached = cacheGet("meta-accounts", cacheKey);
24472
- if (cached) {
24473
- writeJsonEnvelope({
24474
- ok: true,
24475
- data: cached.data,
24476
- cached: true,
24477
- hints: accountHints3(cached.data, includeAll)
24478
- });
24479
- return;
24480
- }
24481
- }
24482
24436
  try {
24483
24437
  const params = {};
24484
- if (includeAll) params["include-all"] = "true";
24485
- if (!useCache) params["skip-cache"] = "true";
24438
+ if (args5["no-cache"]) params["skip-cache"] = "true";
24486
24439
  const data = await apiGet("/api/ads/meta/accounts", params);
24487
- if (useCache) {
24488
- cacheSet("meta-accounts", cacheKey, data, 60 * 60 * 1e3);
24489
- }
24490
24440
  const fmt = csvOrJson(args5);
24491
24441
  if (fmt !== "json") {
24492
24442
  writeAdsOutput(data, fmt);
24493
24443
  return;
24494
24444
  }
24495
- writeJsonEnvelope({ ok: true, data, hints: accountHints3(data, includeAll) });
24445
+ writeJsonEnvelope({ ok: true, data, hints: accountHints3(data) });
24496
24446
  } catch (err) {
24497
24447
  handleMetaError(err);
24498
24448
  }
@@ -24793,7 +24743,7 @@ function leafRefusal(field, condition, path48) {
24793
24743
  }
24794
24744
 
24795
24745
  // ../api/src/ads-meta/leadAds.ts
24796
- 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.";
24797
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.";
24798
24748
  function isMetaLeadAdsRefusal(message) {
24799
24749
  return message === META_LEAD_ADS_NOT_ENABLED_MESSAGE || message === META_LEAD_ADS_RECONNECT_MESSAGE;
@@ -25879,7 +25829,7 @@ var callToActionSchema = z37.strictObject({
25879
25829
  * Without it Meta refuses every ad in that ad set with subcode 3390001,
25880
25830
  * "Choose or create an instant form for your leads campaign".
25881
25831
  */
25882
- lead_gen_form_id: z37.string().regex(NUMERIC_ID_REGEX3).optional()
25832
+ lead_gen_form_id: parentRefSchema3.optional()
25883
25833
  }).superRefine((p, ctx) => {
25884
25834
  if (p.lead_gen_form_id && !LEAD_FORM_CTA_TYPES.some((allowed) => allowed === p.type)) {
25885
25835
  ctx.addIssue({
@@ -26202,6 +26152,50 @@ var mediaUploadSchema = z37.strictObject({
26202
26152
  ctx.addIssue({ code: "custom", path: ["bakerVideoId"], message: "video uploads need a bakerVideoId" });
26203
26153
  }
26204
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
+ });
26205
26199
  var META_DRAFT_OP_KINDS = [
26206
26200
  "campaign.create",
26207
26201
  "campaign.update",
@@ -26214,6 +26208,7 @@ var META_DRAFT_OP_KINDS = [
26214
26208
  "customAudience.create",
26215
26209
  "customAudience.update",
26216
26210
  "customConversion.create",
26211
+ "leadForm.create",
26217
26212
  "media.upload"
26218
26213
  ];
26219
26214
  var metaDraftOpKindSchema = z37.enum(META_DRAFT_OP_KINDS);
@@ -26237,6 +26232,7 @@ var metaDraftOpInputSchema = z37.discriminatedUnion("kind", [
26237
26232
  createOp4("customAudience.create", customAudienceCreateSchema),
26238
26233
  updateOp4("customAudience.update", customAudienceUpdateSchema),
26239
26234
  createOp4("customConversion.create", customConversionCreateSchema),
26235
+ createOp4("leadForm.create", leadFormCreateSchema2),
26240
26236
  createOp4("media.upload", mediaUploadSchema)
26241
26237
  ]);
26242
26238
 
@@ -26827,7 +26823,7 @@ Example: baker ads meta creatives create --page 555 --message "Save now" --link
26827
26823
  cta: { type: "string", description: "Call-to-action type (SHOP_NOW|LEARN_MORE|SIGN_UP|\u2026)" },
26828
26824
  "lead-form": {
26829
26825
  type: "string",
26830
- 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"
26831
26827
  },
26832
26828
  "image-hash": { type: "string", description: "Meta ad-image hash (already uploaded)" },
26833
26829
  "image-ref": { type: "string", description: "meta_temp_* ref of a staged media upload" },
@@ -26971,7 +26967,7 @@ Example: baker ads meta ads retag 120252158038400247 --url-tags "utm_source={{si
26971
26967
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
26972
26968
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
26973
26969
  const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-NMQXZF3Z.js");
26974
- const { handleMetaError: handleMetaError2 } = await import("./shared-J7DQZ2ED.js");
26970
+ const { handleMetaError: handleMetaError2 } = await import("./shared-6762QE6R.js");
26975
26971
  try {
26976
26972
  const accountId = bareAccountId2(args5);
26977
26973
  const urlTags = typeof args5["url-tags"] === "string" ? args5["url-tags"] : void 0;
@@ -27125,7 +27121,7 @@ function duplicateCommand2(entity, label) {
27125
27121
  const { apiPost: apiPost2 } = await import("./client-GR6YL3YX.js");
27126
27122
  const { requireChatId: requireChatId2 } = await import("./env-4P5HOGNA.js");
27127
27123
  const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-NMQXZF3Z.js");
27128
- const { handleMetaError: handleMetaError2 } = await import("./shared-J7DQZ2ED.js");
27124
+ const { handleMetaError: handleMetaError2 } = await import("./shared-6762QE6R.js");
27129
27125
  try {
27130
27126
  const accountId = bareAccountId2(args5);
27131
27127
  const chatId = requireChatId2();
@@ -27378,7 +27374,7 @@ import { defineCommand as defineCommand61 } from "citty";
27378
27374
  var businessesCommand = defineCommand61({
27379
27375
  meta: {
27380
27376
  name: "businesses",
27381
- 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.
27382
27378
 
27383
27379
  Examples:
27384
27380
  baker ads meta businesses`
@@ -28248,13 +28244,47 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
28248
28244
  import { defineCommand as defineCommand67 } from "citty";
28249
28245
  registerSchema({
28250
28246
  command: "ads.meta.lead-forms",
28251
- 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.",
28252
28248
  args: {
28253
- "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
+ },
28254
28254
  full: { type: "boolean", description: "Include each form's questions", required: false },
28255
28255
  output: { type: "string", description: "json|csv", required: false, default: "json" }
28256
28256
  }
28257
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
+ });
28258
28288
  registerSchema({
28259
28289
  command: "ads.meta.leads",
28260
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.",
@@ -28320,25 +28350,56 @@ function leadPageHints(page, full) {
28320
28350
  }
28321
28351
  return hints2;
28322
28352
  }
28323
- var leadFormsCommand2 = defineCommand67({
28353
+ var leadFormsCreateCommand2 = defineCommand67({
28324
28354
  meta: {
28325
- name: "lead-forms",
28326
- 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\`.
28327
-
28328
- Examples:
28329
- baker ads meta lead-forms --page-id 100635183117758
28330
- 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`
28331
28359
  },
28332
28360
  args: {
28361
+ "account-id": { type: "string", description: "Selected Meta ad account" },
28333
28362
  "page-id": { type: "string", description: "Facebook Page ID" },
28334
- full: { type: "boolean", description: "Include each form's questions" },
28335
- 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" }
28336
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,
28337
28394
  run: async ({ args: args5 }) => {
28338
28395
  try {
28339
28396
  const pageId = args5["page-id"];
28340
28397
  if (!pageId) {
28341
- 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
+ );
28342
28403
  }
28343
28404
  const params = { "page-id": pageId };
28344
28405
  if (args5.full) params.full = "true";
@@ -28355,6 +28416,19 @@ Examples:
28355
28416
  }
28356
28417
  }
28357
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
+ });
28358
28432
  var leadsCommand2 = defineCommand67({
28359
28433
  meta: {
28360
28434
  name: "leads",
@@ -28424,27 +28498,21 @@ var metaMediaCommand = defineCommand68({
28424
28498
 
28425
28499
  // src/commands/ads/meta/pages.ts
28426
28500
  import { defineCommand as defineCommand69 } from "citty";
28427
- function advertisablePages(data) {
28428
- return data.filter((p) => Array.isArray(p.tasks) && p.tasks.includes("ADVERTISE"));
28429
- }
28430
28501
  function pageSelectionHints(data) {
28431
- const advertisable = advertisablePages(data);
28432
- if (advertisable.length > 1) {
28433
- return [
28434
- "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."
28435
- ];
28436
- }
28437
- if (data.length > 0 && advertisable.length === 0) {
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) {
28438
28505
  return [
28439
- "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."
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."
28440
28508
  ];
28441
28509
  }
28442
- return [];
28510
+ return [accountHint];
28443
28511
  }
28444
28512
  var pagesCommand = defineCommand69({
28445
28513
  meta: {
28446
28514
  name: "pages",
28447
- 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.
28448
28516
 
28449
28517
  Examples:
28450
28518
  baker ads meta pages`
@@ -28703,10 +28771,10 @@ var metaCommand = defineCommand72({
28703
28771
 
28704
28772
  Discovery:
28705
28773
  baker ads meta accounts \u2014 accounts in this company's connected scope
28706
- baker ads meta accounts --include-all \u2014 every account the token can see
28707
28774
  baker ads meta businesses \u2014 Business Manager IDs
28708
- 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)
28709
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
28710
28778
  baker ads meta leads --form-id 987654321 \u2014 the leads one form collected (contact details masked)
28711
28779
 
28712
28780
  Snapshot a running account (default = ACTIVE only):
@@ -66999,7 +67067,7 @@ var draftCommand5 = defineCommand218({
66999
67067
  import { defineCommand as defineCommand219 } from "citty";
67000
67068
  registerSchema({
67001
67069
  command: "tagManager.containers",
67002
- 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.",
67003
67071
  args: {}
67004
67072
  });
67005
67073
  registerSchema({
@@ -67020,27 +67088,34 @@ registerSchema({
67020
67088
  full: { type: "boolean", description: "Include the raw Tag Manager object for each entity", required: false }
67021
67089
  }
67022
67090
  });
67023
- function containersHints(containers) {
67024
- const connected = containers.filter((container) => container.connected);
67025
- if (connected.length === 0) {
67026
- return [
67027
- "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."
67028
- ];
67029
- }
67030
- return connectedResourceHints({
67091
+ function containersHints(containers, unavailableContainers = []) {
67092
+ const hints2 = connectedResourceHints({
67031
67093
  plural: "Tag Manager containers",
67032
67094
  singular: "Tag Manager container",
67033
67095
  flag: "--container",
67034
- resources: connected.map((container) => ({
67096
+ resources: containers.map((container) => ({
67035
67097
  id: container.containerId,
67036
67098
  label: `${container.publicId} \xB7 ${container.name}`
67037
67099
  }))
67038
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;
67039
67114
  }
67040
67115
  var containersCommand = defineCommand219({
67041
67116
  meta: {
67042
67117
  name: "containers",
67043
- description: `List Tag Manager containers reachable by this company's connection.
67118
+ description: `List Tag Manager containers selected by this company.
67044
67119
 
67045
67120
  Start here:
67046
67121
  baker tag-manager containers`
@@ -67048,7 +67123,7 @@ Start here:
67048
67123
  run: async () => {
67049
67124
  try {
67050
67125
  const data = await apiGet("/api/tag-manager/containers");
67051
- writeJsonEnvelope({ ok: true, data, hints: containersHints(data.containers) });
67126
+ writeJsonEnvelope({ ok: true, data, hints: containersHints(data.containers, data.unavailableContainers) });
67052
67127
  } catch (err) {
67053
67128
  handleError8(err);
67054
67129
  }