cli-meta-ads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +188 -0
  2. package/AI_CONTEXT.md +144 -0
  3. package/CLAUDE.md +183 -0
  4. package/README.md +590 -0
  5. package/REQUIREMENTS.md +148 -0
  6. package/dist/auth/constants.d.ts +1 -0
  7. package/dist/auth/constants.js +1 -0
  8. package/dist/auth/guards.d.ts +5 -0
  9. package/dist/auth/guards.js +16 -0
  10. package/dist/auth/login.d.ts +28 -0
  11. package/dist/auth/login.js +222 -0
  12. package/dist/cli/action.d.ts +11 -0
  13. package/dist/cli/action.js +77 -0
  14. package/dist/cli/build-cli.d.ts +2 -0
  15. package/dist/cli/build-cli.js +110 -0
  16. package/dist/cli/context.d.ts +24 -0
  17. package/dist/cli/context.js +19 -0
  18. package/dist/client/meta-api-client.d.ts +50 -0
  19. package/dist/client/meta-api-client.js +258 -0
  20. package/dist/client/meta-discovery.d.ts +13 -0
  21. package/dist/client/meta-discovery.js +88 -0
  22. package/dist/commands/accounts.d.ts +4 -0
  23. package/dist/commands/accounts.js +42 -0
  24. package/dist/commands/ads.d.ts +4 -0
  25. package/dist/commands/ads.js +148 -0
  26. package/dist/commands/adsets.d.ts +4 -0
  27. package/dist/commands/adsets.js +49 -0
  28. package/dist/commands/anomalies.d.ts +4 -0
  29. package/dist/commands/anomalies.js +44 -0
  30. package/dist/commands/assets.d.ts +4 -0
  31. package/dist/commands/assets.js +116 -0
  32. package/dist/commands/audiences.d.ts +4 -0
  33. package/dist/commands/audiences.js +40 -0
  34. package/dist/commands/auth.d.ts +4 -0
  35. package/dist/commands/auth.js +139 -0
  36. package/dist/commands/campaigns.d.ts +4 -0
  37. package/dist/commands/campaigns.js +273 -0
  38. package/dist/commands/capi.d.ts +4 -0
  39. package/dist/commands/capi.js +64 -0
  40. package/dist/commands/creatives.d.ts +4 -0
  41. package/dist/commands/creatives.js +49 -0
  42. package/dist/commands/diagnostics.d.ts +4 -0
  43. package/dist/commands/diagnostics.js +88 -0
  44. package/dist/commands/helpers.d.ts +13 -0
  45. package/dist/commands/helpers.js +50 -0
  46. package/dist/commands/launch.d.ts +4 -0
  47. package/dist/commands/launch.js +109 -0
  48. package/dist/commands/performance.d.ts +4 -0
  49. package/dist/commands/performance.js +55 -0
  50. package/dist/commands/pixel.d.ts +4 -0
  51. package/dist/commands/pixel.js +68 -0
  52. package/dist/commands/report.d.ts +4 -0
  53. package/dist/commands/report.js +30 -0
  54. package/dist/config/file-config.d.ts +6 -0
  55. package/dist/config/file-config.js +174 -0
  56. package/dist/config/types.d.ts +32 -0
  57. package/dist/config/types.js +1 -0
  58. package/dist/domain/account-scope.d.ts +7 -0
  59. package/dist/domain/account-scope.js +28 -0
  60. package/dist/domain/analytics.d.ts +52 -0
  61. package/dist/domain/analytics.js +125 -0
  62. package/dist/domain/approval-service.d.ts +10 -0
  63. package/dist/domain/approval-service.js +48 -0
  64. package/dist/domain/asset-feed-compiler.d.ts +43 -0
  65. package/dist/domain/asset-feed-compiler.js +104 -0
  66. package/dist/domain/launch-service.d.ts +200 -0
  67. package/dist/domain/launch-service.js +558 -0
  68. package/dist/domain/meta-ads-service.d.ts +620 -0
  69. package/dist/domain/meta-ads-service.js +841 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +9 -0
  72. package/dist/output/render.d.ts +3 -0
  73. package/dist/output/render.js +103 -0
  74. package/dist/types.d.ts +42 -0
  75. package/dist/types.js +1 -0
  76. package/dist/utils/currency.d.ts +4 -0
  77. package/dist/utils/currency.js +40 -0
  78. package/dist/utils/date-range.d.ts +20 -0
  79. package/dist/utils/date-range.js +115 -0
  80. package/dist/utils/errors.d.ts +35 -0
  81. package/dist/utils/errors.js +68 -0
  82. package/dist/utils/ids.d.ts +4 -0
  83. package/dist/utils/ids.js +23 -0
  84. package/dist/utils/meta-placement-assets.d.ts +44 -0
  85. package/dist/utils/meta-placement-assets.js +315 -0
  86. package/dist/utils/security.d.ts +5 -0
  87. package/dist/utils/security.js +104 -0
  88. package/dist/validators/common.d.ts +10 -0
  89. package/dist/validators/common.js +56 -0
  90. package/dist/validators/create-spec.d.ts +373 -0
  91. package/dist/validators/create-spec.js +394 -0
  92. package/dist/validators/launch-spec.d.ts +229 -0
  93. package/dist/validators/launch-spec.js +371 -0
  94. package/docs/TECHNICAL.md +480 -0
  95. package/examples/README.md +29 -0
  96. package/examples/launch/assets/feed4x5.png +0 -0
  97. package/examples/launch/assets/story9x16.png +0 -0
  98. package/examples/launch/multi-format-launch.json +90 -0
  99. package/examples/single-object/ad.json +6 -0
  100. package/examples/single-object/adset.json +30 -0
  101. package/examples/single-object/campaign.json +6 -0
  102. package/examples/single-object/creative.json +19 -0
  103. package/package.json +62 -0
  104. package/skills/meta-cli-operator/SKILL.md +105 -0
  105. package/skills/meta-cli-operator/agents/openai.yaml +4 -0
  106. package/skills/meta-cli-operator/references/update-matrix.md +117 -0
@@ -0,0 +1,841 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { discoverManagedAccounts } from "../client/meta-discovery.js";
5
+ import { buildAssetFeedCreativeSpec as compileAssetFeedCreativeSpec } from "./asset-feed-compiler.js";
6
+ import { buildRecentDateWindow, toInsightsTimeRange } from "../utils/date-range.js";
7
+ import { AppError, ExitCode } from "../utils/errors.js";
8
+ import { defaultCreativeFormatPlacementTargets, hasPlacementFormats, launchCreativeFormatKeys } from "../utils/meta-placement-assets.js";
9
+ import { fromMinorUnits } from "../utils/currency.js";
10
+ import { toAdAccountNodeId } from "../utils/ids.js";
11
+ const actionStatSchema = z.object({
12
+ action_type: z.string().optional(),
13
+ value: z.string().optional()
14
+ });
15
+ const insightsRowSchema = z
16
+ .object({
17
+ account_currency: z.string().optional(),
18
+ account_id: z.string().optional(),
19
+ account_name: z.string().optional(),
20
+ actions: z.array(actionStatSchema).optional(),
21
+ ad_id: z.string().optional(),
22
+ ad_name: z.string().optional(),
23
+ campaign_id: z.string().optional(),
24
+ campaign_name: z.string().optional(),
25
+ clicks: z.string().optional(),
26
+ cpc: z.string().optional(),
27
+ cpm: z.string().optional(),
28
+ ctr: z.string().optional(),
29
+ date_start: z.string().optional(),
30
+ date_stop: z.string().optional(),
31
+ frequency: z.string().optional(),
32
+ impressions: z.string().optional(),
33
+ reach: z.string().optional(),
34
+ spend: z.string().optional()
35
+ })
36
+ .passthrough();
37
+ const campaignSchema = z
38
+ .object({
39
+ account_id: z.string().optional(),
40
+ buying_type: z.string().optional(),
41
+ created_time: z.string().optional(),
42
+ daily_budget: z.union([z.string(), z.number()]).optional(),
43
+ effective_status: z.string().optional(),
44
+ id: z.string(),
45
+ name: z.string().optional(),
46
+ objective: z.string().optional(),
47
+ spend_cap: z.union([z.string(), z.number()]).optional(),
48
+ start_time: z.string().optional(),
49
+ status: z.string().optional(),
50
+ stop_time: z.string().optional(),
51
+ updated_time: z.string().optional()
52
+ })
53
+ .passthrough();
54
+ const adSchema = z
55
+ .object({
56
+ adset_id: z.string().optional(),
57
+ creative: z
58
+ .object({
59
+ id: z.string().optional(),
60
+ name: z.string().optional()
61
+ })
62
+ .passthrough()
63
+ .optional(),
64
+ effective_status: z.string().optional(),
65
+ id: z.string(),
66
+ name: z.string().optional(),
67
+ status: z.string().optional()
68
+ })
69
+ .passthrough();
70
+ const pixelSchema = z
71
+ .object({
72
+ creation_time: z.string().optional(),
73
+ enable_automatic_matching: z.boolean().optional(),
74
+ event_stats: z.union([z.string(), z.array(z.unknown()), z.record(z.string(), z.unknown())]).optional(),
75
+ event_time_max: z.union([z.number(), z.string()]).optional(),
76
+ first_party_cookie_status: z.string().optional(),
77
+ id: z.string(),
78
+ is_unavailable: z.boolean().optional(),
79
+ last_fired_time: z.string().optional(),
80
+ name: z.string().optional(),
81
+ server_events_business_ids: z.array(z.string()).optional()
82
+ })
83
+ .passthrough();
84
+ const audienceSchema = z
85
+ .object({
86
+ approximate_count: z.number().optional(),
87
+ approximate_count_lower_bound: z.number().optional(),
88
+ approximate_count_upper_bound: z.number().optional(),
89
+ delivery_status: z
90
+ .object({
91
+ code: z.number().optional(),
92
+ description: z.string().optional()
93
+ })
94
+ .passthrough()
95
+ .optional(),
96
+ id: z.string(),
97
+ name: z.string().optional(),
98
+ operation_status: z
99
+ .object({
100
+ code: z.number().optional(),
101
+ description: z.string().optional()
102
+ })
103
+ .passthrough()
104
+ .optional(),
105
+ retention_days: z.number().optional(),
106
+ subtype: z.string().optional(),
107
+ time_updated: z.union([z.number(), z.string()]).optional()
108
+ })
109
+ .passthrough();
110
+ const whoAmISchema = z
111
+ .object({
112
+ id: z.string(),
113
+ name: z.string().optional()
114
+ })
115
+ .passthrough();
116
+ const accountSchema = z
117
+ .object({
118
+ account_id: z.string().optional(),
119
+ account_status: z.union([z.number(), z.string()]).optional(),
120
+ business: z
121
+ .object({
122
+ id: z.string().optional(),
123
+ name: z.string().optional()
124
+ })
125
+ .passthrough()
126
+ .optional(),
127
+ currency: z.string().optional(),
128
+ id: z.string().optional(),
129
+ name: z.string().optional(),
130
+ timezone_name: z.string().optional()
131
+ })
132
+ .passthrough();
133
+ const previewSchema = z
134
+ .object({
135
+ body: z.string().optional(),
136
+ html: z.string().optional(),
137
+ preview_shareable_link: z.string().optional(),
138
+ render_type: z.string().optional()
139
+ })
140
+ .passthrough();
141
+ const createdEntitySchema = z.object({
142
+ id: z.string()
143
+ }).passthrough();
144
+ const adImageUploadSchema = z.object({
145
+ images: z.record(z.string(), z.object({
146
+ hash: z.string().optional(),
147
+ url: z.string().optional()
148
+ }).passthrough())
149
+ }).passthrough();
150
+ const adVideoUploadSchema = z.object({
151
+ id: z.string(),
152
+ success: z.boolean().optional()
153
+ }).passthrough();
154
+ const adVideoStatusSchema = z.object({
155
+ id: z.string(),
156
+ processing_progress: z.union([z.number(), z.string()]).optional(),
157
+ status: z.union([
158
+ z.string(),
159
+ z.object({
160
+ processing_phase: z.object({
161
+ progress: z.union([z.number(), z.string()]).optional(),
162
+ status: z.string().optional()
163
+ }).passthrough().optional(),
164
+ video_status: z.string().optional()
165
+ }).passthrough()
166
+ ]).optional(),
167
+ title: z.string().optional()
168
+ }).passthrough();
169
+ function toNumber(value) {
170
+ if (value === undefined) {
171
+ return 0;
172
+ }
173
+ return typeof value === "number" ? value : Number(value);
174
+ }
175
+ function normalizeInsightsRow(row) {
176
+ return {
177
+ accountCurrency: row.account_currency,
178
+ accountId: row.account_id,
179
+ accountName: row.account_name,
180
+ actions: row.actions?.map((entry) => ({
181
+ actionType: entry.action_type,
182
+ value: toNumber(entry.value)
183
+ })) ?? [],
184
+ adId: row.ad_id,
185
+ adName: row.ad_name,
186
+ campaignId: row.campaign_id,
187
+ campaignName: row.campaign_name,
188
+ clicks: toNumber(row.clicks),
189
+ cpc: toNumber(row.cpc),
190
+ cpm: toNumber(row.cpm),
191
+ ctr: toNumber(row.ctr),
192
+ dateStart: row.date_start,
193
+ dateStop: row.date_stop,
194
+ frequency: toNumber(row.frequency),
195
+ impressions: toNumber(row.impressions),
196
+ reach: toNumber(row.reach),
197
+ spend: toNumber(row.spend),
198
+ raw: row
199
+ };
200
+ }
201
+ function normalizeCampaign(row) {
202
+ return {
203
+ accountId: row.account_id,
204
+ buyingType: row.buying_type,
205
+ createdTime: row.created_time,
206
+ dailyBudgetMinorUnits: row.daily_budget !== undefined ? toNumber(row.daily_budget) : undefined,
207
+ effectiveStatus: row.effective_status,
208
+ id: row.id,
209
+ name: row.name,
210
+ objective: row.objective,
211
+ spendCapMinorUnits: row.spend_cap !== undefined ? toNumber(row.spend_cap) : undefined,
212
+ startTime: row.start_time,
213
+ status: row.status,
214
+ stopTime: row.stop_time,
215
+ updatedTime: row.updated_time,
216
+ raw: row
217
+ };
218
+ }
219
+ function normalizeAd(row) {
220
+ return {
221
+ adsetId: row.adset_id,
222
+ creativeId: row.creative?.id,
223
+ creativeName: row.creative?.name,
224
+ effectiveStatus: row.effective_status,
225
+ id: row.id,
226
+ name: row.name,
227
+ status: row.status,
228
+ raw: row
229
+ };
230
+ }
231
+ function normalizePixel(row) {
232
+ return {
233
+ createdAt: row.creation_time,
234
+ enableAutomaticMatching: row.enable_automatic_matching ?? false,
235
+ eventStats: row.event_stats,
236
+ eventTimeMax: row.event_time_max ? new Date(Number(row.event_time_max) * 1000).toISOString() : undefined,
237
+ firstPartyCookieStatus: row.first_party_cookie_status,
238
+ id: row.id,
239
+ isUnavailable: row.is_unavailable ?? false,
240
+ lastFiredTime: row.last_fired_time,
241
+ name: row.name,
242
+ serverEventsBusinessIds: row.server_events_business_ids ?? [],
243
+ raw: row
244
+ };
245
+ }
246
+ function normalizeAudience(row) {
247
+ return {
248
+ approximateCount: row.approximate_count,
249
+ approximateCountLowerBound: row.approximate_count_lower_bound,
250
+ approximateCountUpperBound: row.approximate_count_upper_bound,
251
+ deliveryStatusCode: row.delivery_status?.code,
252
+ deliveryStatusDescription: row.delivery_status?.description,
253
+ id: row.id,
254
+ name: row.name,
255
+ operationStatusCode: row.operation_status?.code,
256
+ operationStatusDescription: row.operation_status?.description,
257
+ retentionDays: row.retention_days,
258
+ subtype: row.subtype,
259
+ timeUpdated: row.time_updated,
260
+ raw: row
261
+ };
262
+ }
263
+ function toCreateResult(row) {
264
+ return {
265
+ id: row.id,
266
+ raw: row
267
+ };
268
+ }
269
+ function buildPromotedObject(spec) {
270
+ return {
271
+ application_id: spec.applicationId,
272
+ custom_conversion_id: spec.customConversionId,
273
+ custom_event_type: spec.customEventType,
274
+ event_id: spec.eventId,
275
+ instagram_actor_id: spec.instagramActorId,
276
+ object_store_url: spec.objectStoreUrl,
277
+ offline_conversion_data_set_id: spec.offlineConversionDataSetId,
278
+ page_id: spec.pageId,
279
+ pixel_id: spec.pixelId,
280
+ product_set_id: spec.productSetId,
281
+ whatsapp_phone_number: spec.whatsappPhoneNumber
282
+ };
283
+ }
284
+ function buildCreativePlatformCustomizations(spec) {
285
+ const instagram = spec?.instagram;
286
+ if (!instagram) {
287
+ return undefined;
288
+ }
289
+ return {
290
+ instagram: {
291
+ caption_ids: instagram.captionIds,
292
+ image_crops: instagram.imageCrops,
293
+ image_hash: instagram.imageHash,
294
+ image_url: instagram.imageUrl,
295
+ video_id: instagram.videoId
296
+ }
297
+ };
298
+ }
299
+ function resolveCreativeMediaBinding(source, fallbackId) {
300
+ if (source.videoId) {
301
+ return {
302
+ captionIds: source.captionIds,
303
+ id: `video:${source.videoId}`,
304
+ kind: "video",
305
+ videoId: source.videoId
306
+ };
307
+ }
308
+ return {
309
+ id: source.imageHash ? `image_hash:${source.imageHash}` : `image_url:${source.imageUrl ?? fallbackId}`,
310
+ imageCrops: source.imageCrops,
311
+ imageHash: source.imageHash,
312
+ imageUrl: source.imageUrl,
313
+ kind: "image"
314
+ };
315
+ }
316
+ function hasImageUrlFormatOverride(formats) {
317
+ return Boolean(formats && launchCreativeFormatKeys.some((key) => formats[key]?.imageUrl));
318
+ }
319
+ function normalizeVideoStatus(row) {
320
+ const status = typeof row.status === "string"
321
+ ? row.status
322
+ : row.status?.video_status ?? row.status?.processing_phase?.status;
323
+ const rawProgress = row.processing_progress
324
+ ?? (typeof row.status === "object" ? row.status?.processing_phase?.progress : undefined);
325
+ return {
326
+ id: row.id,
327
+ processingProgress: rawProgress !== undefined ? Number(rawProgress) : undefined,
328
+ status,
329
+ title: row.title,
330
+ raw: row
331
+ };
332
+ }
333
+ function isReadyVideoStatus(status) {
334
+ const normalizedStatus = status.status?.toLowerCase();
335
+ return normalizedStatus === "ready"
336
+ || normalizedStatus === "active"
337
+ || normalizedStatus === "available"
338
+ || status.processingProgress === 100;
339
+ }
340
+ function isFailedVideoStatus(status) {
341
+ const normalizedStatus = status.status?.toLowerCase();
342
+ return normalizedStatus === "error"
343
+ || normalizedStatus === "failed"
344
+ || normalizedStatus === "failure";
345
+ }
346
+ export class MetaAdsService {
347
+ client;
348
+ constructor(client) {
349
+ this.client = client;
350
+ }
351
+ async whoAmI() {
352
+ const response = await this.client.get("/me", {
353
+ fields: "id,name"
354
+ });
355
+ return whoAmISchema.parse(response.data);
356
+ }
357
+ async listManagedAccounts(config) {
358
+ return discoverManagedAccounts(config, this.client);
359
+ }
360
+ async getAccount(accountId) {
361
+ const response = await this.client.get(`/${toAdAccountNodeId(accountId)}`, {
362
+ fields: "id,account_id,name,account_status,currency,timezone_name,business{id,name}"
363
+ });
364
+ return accountSchema.parse(response.data);
365
+ }
366
+ async createCampaign(accountId, spec) {
367
+ const response = await this.client.post(`/${toAdAccountNodeId(accountId)}/campaigns`, {
368
+ bid_strategy: spec.bidStrategy,
369
+ daily_budget: spec.dailyBudget,
370
+ is_adset_budget_sharing_enabled: false,
371
+ lifetime_budget: spec.lifetimeBudget,
372
+ name: spec.name,
373
+ objective: spec.objective,
374
+ special_ad_categories: JSON.stringify(spec.specialAdCategories ?? []),
375
+ status: spec.status ?? "PAUSED"
376
+ });
377
+ return toCreateResult(createdEntitySchema.parse(response.data));
378
+ }
379
+ async createAdSet(accountId, spec) {
380
+ const response = await this.client.post(`/${toAdAccountNodeId(accountId)}/adsets`, {
381
+ bid_amount: spec.bidAmount,
382
+ bid_strategy: spec.bidStrategy
383
+ ?? (spec.bidAmount !== undefined ? "LOWEST_COST_WITH_BID_CAP" : "LOWEST_COST_WITHOUT_CAP"),
384
+ billing_event: spec.billingEvent,
385
+ campaign_id: spec.campaignId,
386
+ daily_budget: spec.dailyBudget,
387
+ end_time: spec.endTime,
388
+ lifetime_budget: spec.lifetimeBudget,
389
+ name: spec.name,
390
+ optimization_goal: spec.optimizationGoal,
391
+ promoted_object: JSON.stringify(buildPromotedObject(spec.promotedObject)),
392
+ start_time: spec.startTime,
393
+ status: spec.status ?? "PAUSED",
394
+ targeting: JSON.stringify(spec.targeting)
395
+ });
396
+ return toCreateResult(createdEntitySchema.parse(response.data));
397
+ }
398
+ async uploadAdImage(accountId, filePath, name) {
399
+ const fileBuffer = await readFile(filePath);
400
+ const response = await this.client.postMultipart(`/${toAdAccountNodeId(accountId)}/adimages`, {
401
+ filename: {
402
+ blob: new Blob([fileBuffer]),
403
+ filename: path.basename(filePath)
404
+ },
405
+ name
406
+ });
407
+ const payload = adImageUploadSchema.parse(response.data);
408
+ const [uploadedName, uploadedImage] = Object.entries(payload.images)[0] ?? [];
409
+ if (!uploadedName || !uploadedImage) {
410
+ throw new AppError("Meta image upload did not return an uploaded image record.", ExitCode.Provider);
411
+ }
412
+ return {
413
+ fileName: uploadedName,
414
+ hash: uploadedImage.hash,
415
+ name: name ?? uploadedName,
416
+ url: uploadedImage.url,
417
+ raw: uploadedImage
418
+ };
419
+ }
420
+ async uploadAdVideo(accountId, filePath, name) {
421
+ const fileBuffer = await readFile(filePath);
422
+ const response = await this.client.postMultipart(`/${toAdAccountNodeId(accountId)}/advideos`, {
423
+ name,
424
+ source: {
425
+ blob: new Blob([fileBuffer]),
426
+ filename: path.basename(filePath)
427
+ }
428
+ });
429
+ const payload = adVideoUploadSchema.parse(response.data);
430
+ return {
431
+ id: payload.id,
432
+ name,
433
+ raw: payload
434
+ };
435
+ }
436
+ async getAdVideoStatus(videoId) {
437
+ const response = await this.client.get(`/${videoId}`, {
438
+ fields: "id,title,status,processing_progress"
439
+ });
440
+ return normalizeVideoStatus(adVideoStatusSchema.parse(response.data));
441
+ }
442
+ async waitForVideoReady(videoId, options = {}) {
443
+ const pollMs = options.pollMs ?? 3000;
444
+ const timeoutMs = options.timeoutMs ?? 90000;
445
+ const deadline = Date.now() + timeoutMs;
446
+ let lastStatus = await this.getAdVideoStatus(videoId);
447
+ while (!isReadyVideoStatus(lastStatus)) {
448
+ if (isFailedVideoStatus(lastStatus)) {
449
+ throw new AppError(`Meta video ${videoId} failed during processing.`, ExitCode.Provider, {
450
+ status: lastStatus.status,
451
+ videoId
452
+ });
453
+ }
454
+ if (Date.now() >= deadline) {
455
+ throw new AppError(`Timed out waiting for Meta video ${videoId} to finish processing.`, ExitCode.VerificationFailed, {
456
+ status: lastStatus.status,
457
+ timeoutMs,
458
+ videoId
459
+ });
460
+ }
461
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
462
+ lastStatus = await this.getAdVideoStatus(videoId);
463
+ }
464
+ return lastStatus;
465
+ }
466
+ async createCreative(accountId, spec) {
467
+ const includeMatchingFormatRules = spec.kind === "link-image" && hasImageUrlFormatOverride(spec.formats);
468
+ const fallbackBinding = hasPlacementFormats(spec.formats)
469
+ ? spec.kind === "link-image"
470
+ ? resolveCreativeMediaBinding({
471
+ imageHash: spec.imageHash ?? spec.formats?.feed4x5?.imageHash ?? spec.formats?.square1x1?.imageHash ?? spec.formats?.story9x16?.imageHash,
472
+ imageUrl: spec.formats?.feed4x5?.imageUrl ?? spec.formats?.square1x1?.imageUrl ?? spec.formats?.story9x16?.imageUrl
473
+ }, spec.name ?? spec.kind)
474
+ : resolveCreativeMediaBinding({
475
+ videoId: spec.videoId ?? spec.formats?.feed4x5?.videoId ?? spec.formats?.square1x1?.videoId ?? spec.formats?.story9x16?.videoId
476
+ }, spec.name ?? spec.kind)
477
+ : undefined;
478
+ if (fallbackBinding && hasPlacementFormats(spec.formats)) {
479
+ const assetFeedSpec = spec.kind === "link-image"
480
+ ? compileAssetFeedCreativeSpec({
481
+ fallback: fallbackBinding,
482
+ formatBindings: {
483
+ feed4x5: spec.formats?.feed4x5
484
+ ? resolveCreativeMediaBinding(spec.formats.feed4x5, `${spec.name ?? spec.kind}:feed4x5`)
485
+ : undefined,
486
+ square1x1: spec.formats?.square1x1
487
+ ? resolveCreativeMediaBinding(spec.formats.square1x1, `${spec.name ?? spec.kind}:square1x1`)
488
+ : undefined,
489
+ story9x16: spec.formats?.story9x16
490
+ ? resolveCreativeMediaBinding(spec.formats.story9x16, `${spec.name ?? spec.kind}:story9x16`)
491
+ : undefined
492
+ },
493
+ kind: spec.kind,
494
+ linkData: spec.linkData,
495
+ includeMatchingFormatRules,
496
+ name: spec.name,
497
+ pageId: spec.pageId,
498
+ placementTargets: defaultCreativeFormatPlacementTargets
499
+ })
500
+ : compileAssetFeedCreativeSpec({
501
+ fallback: fallbackBinding,
502
+ formatBindings: {
503
+ feed4x5: spec.formats?.feed4x5
504
+ ? resolveCreativeMediaBinding(spec.formats.feed4x5, `${spec.name ?? spec.kind}:feed4x5`)
505
+ : undefined,
506
+ square1x1: spec.formats?.square1x1
507
+ ? resolveCreativeMediaBinding(spec.formats.square1x1, `${spec.name ?? spec.kind}:square1x1`)
508
+ : undefined,
509
+ story9x16: spec.formats?.story9x16
510
+ ? resolveCreativeMediaBinding(spec.formats.story9x16, `${spec.name ?? spec.kind}:story9x16`)
511
+ : undefined
512
+ },
513
+ kind: spec.kind,
514
+ includeMatchingFormatRules,
515
+ name: spec.name,
516
+ pageId: spec.pageId,
517
+ placementTargets: defaultCreativeFormatPlacementTargets,
518
+ videoData: spec.videoData
519
+ });
520
+ if (assetFeedSpec) {
521
+ return this.createAssetFeedCreative(accountId, assetFeedSpec);
522
+ }
523
+ }
524
+ const platformCustomizations = buildCreativePlatformCustomizations(spec.platformCustomizations);
525
+ const objectStorySpec = spec.kind === "link-image"
526
+ ? {
527
+ link_data: {
528
+ call_to_action: spec.linkData.callToAction
529
+ ? {
530
+ type: spec.linkData.callToAction,
531
+ value: {
532
+ link: spec.linkData.link
533
+ }
534
+ }
535
+ : undefined,
536
+ description: spec.linkData.description,
537
+ image_hash: spec.imageHash ?? fallbackBinding?.imageHash,
538
+ link: spec.linkData.link,
539
+ message: spec.linkData.message,
540
+ name: spec.linkData.headline
541
+ },
542
+ page_id: spec.pageId
543
+ }
544
+ : {
545
+ page_id: spec.pageId,
546
+ video_data: {
547
+ call_to_action: spec.videoData.callToAction
548
+ ? {
549
+ type: spec.videoData.callToAction,
550
+ value: {
551
+ link: spec.videoData.link
552
+ }
553
+ }
554
+ : undefined,
555
+ description: spec.videoData.description,
556
+ link: spec.videoData.link,
557
+ message: spec.videoData.message,
558
+ title: spec.videoData.title,
559
+ video_id: spec.videoId ?? fallbackBinding?.videoId
560
+ }
561
+ };
562
+ const response = await this.client.post(`/${toAdAccountNodeId(accountId)}/adcreatives`, {
563
+ name: spec.name ?? `${spec.kind}-${Date.now()}`,
564
+ object_story_spec: JSON.stringify(objectStorySpec),
565
+ platform_customizations: platformCustomizations
566
+ ? JSON.stringify(platformCustomizations)
567
+ : undefined
568
+ });
569
+ return toCreateResult(createdEntitySchema.parse(response.data));
570
+ }
571
+ async createAssetFeedCreative(accountId, spec) {
572
+ const response = await this.client.post(`/${toAdAccountNodeId(accountId)}/adcreatives`, {
573
+ asset_feed_spec: JSON.stringify(spec.assetFeedSpec),
574
+ name: spec.name ?? `${spec.kind}-placement-${Date.now()}`,
575
+ object_story_spec: JSON.stringify({
576
+ page_id: spec.pageId
577
+ })
578
+ });
579
+ return toCreateResult(createdEntitySchema.parse(response.data));
580
+ }
581
+ async createAd(accountId, spec) {
582
+ const response = await this.client.post(`/${toAdAccountNodeId(accountId)}/ads`, {
583
+ adset_id: spec.adSetId,
584
+ creative: JSON.stringify({
585
+ creative_id: spec.creativeId
586
+ }),
587
+ name: spec.name,
588
+ status: spec.status ?? "PAUSED"
589
+ });
590
+ return toCreateResult(createdEntitySchema.parse(response.data));
591
+ }
592
+ async listCampaigns(accountId) {
593
+ const response = await this.client.paginate(`/${toAdAccountNodeId(accountId)}/campaigns`, {
594
+ fields: "id,account_id,name,objective,status,effective_status,daily_budget,spend_cap,start_time,stop_time,created_time,updated_time",
595
+ limit: 500
596
+ });
597
+ return response.data.data.map((entry) => normalizeCampaign(campaignSchema.parse(entry)));
598
+ }
599
+ async getCampaign(campaignId) {
600
+ const response = await this.client.get(`/${campaignId}`, {
601
+ fields: "id,account_id,name,objective,status,effective_status,daily_budget,spend_cap,start_time,stop_time,created_time,updated_time,buying_type"
602
+ });
603
+ return normalizeCampaign(campaignSchema.parse(response.data));
604
+ }
605
+ async updateCampaignStatus(campaignId, status) {
606
+ return this.client.post(`/${campaignId}`, {
607
+ status
608
+ });
609
+ }
610
+ async updateCampaignDailyBudget(campaignId, dailyBudgetMinorUnits) {
611
+ return this.client.post(`/${campaignId}`, {
612
+ daily_budget: dailyBudgetMinorUnits
613
+ });
614
+ }
615
+ async listAdsByCampaign(campaignId) {
616
+ const response = await this.client.paginate(`/${campaignId}/ads`, {
617
+ fields: "id,name,status,effective_status,adset_id,creative{id,name}",
618
+ limit: 500
619
+ });
620
+ return response.data.data.map((entry) => normalizeAd(adSchema.parse(entry)));
621
+ }
622
+ async getAdPreview(adId, adFormat) {
623
+ const response = await this.client.paginate(`/${adId}/previews`, {
624
+ ...(adFormat ? { ad_format: adFormat } : {}),
625
+ limit: 50
626
+ });
627
+ return response.data.data.map((entry) => previewSchema.parse(entry));
628
+ }
629
+ async listPixels(accountId) {
630
+ const response = await this.client.paginate(`/${toAdAccountNodeId(accountId)}/adspixels`, {
631
+ fields: [
632
+ "id",
633
+ "name",
634
+ "creation_time",
635
+ "last_fired_time",
636
+ "event_time_max",
637
+ "event_stats",
638
+ "is_unavailable",
639
+ "enable_automatic_matching",
640
+ "first_party_cookie_status",
641
+ "server_events_business_ids"
642
+ ].join(","),
643
+ limit: 200
644
+ });
645
+ return response.data.data.map((entry) => normalizePixel(pixelSchema.parse(entry)));
646
+ }
647
+ async listAudiences(accountId) {
648
+ const response = await this.client.paginate(`/${toAdAccountNodeId(accountId)}/customaudiences`, {
649
+ fields: [
650
+ "id",
651
+ "name",
652
+ "subtype",
653
+ "delivery_status",
654
+ "approximate_count",
655
+ "approximate_count_lower_bound",
656
+ "approximate_count_upper_bound",
657
+ "operation_status",
658
+ "retention_days",
659
+ "time_updated"
660
+ ].join(","),
661
+ limit: 500
662
+ });
663
+ return response.data.data.map((entry) => normalizeAudience(audienceSchema.parse(entry)));
664
+ }
665
+ async getAudience(audienceId) {
666
+ const response = await this.client.get(`/${audienceId}`, {
667
+ fields: [
668
+ "id",
669
+ "name",
670
+ "subtype",
671
+ "delivery_status",
672
+ "approximate_count",
673
+ "approximate_count_lower_bound",
674
+ "approximate_count_upper_bound",
675
+ "operation_status",
676
+ "retention_days",
677
+ "time_updated"
678
+ ].join(",")
679
+ });
680
+ return normalizeAudience(audienceSchema.parse(response.data));
681
+ }
682
+ async getInsights(query) {
683
+ const response = await this.client.paginate(`/${query.targetId}/insights`, {
684
+ breakdowns: query.breakdowns && query.breakdowns.length > 0 ? query.breakdowns.join(",") : undefined,
685
+ fields: query.fields.join(","),
686
+ level: query.level,
687
+ limit: 500,
688
+ sort: query.sort ? JSON.stringify([query.sort]) : undefined,
689
+ time_increment: query.timeIncrement,
690
+ time_range: toInsightsTimeRange(query.window)
691
+ });
692
+ return response.data.data.map((entry) => normalizeInsightsRow(insightsRowSchema.parse(entry)));
693
+ }
694
+ async getPerformanceByAccount(accountId, window, breakdowns, campaignName, sort) {
695
+ const fields = [
696
+ "account_id",
697
+ "account_name",
698
+ "account_currency",
699
+ "campaign_id",
700
+ "campaign_name",
701
+ "impressions",
702
+ "reach",
703
+ "clicks",
704
+ "ctr",
705
+ "cpc",
706
+ "cpm",
707
+ "spend",
708
+ "frequency",
709
+ "actions",
710
+ "date_start",
711
+ "date_stop"
712
+ ];
713
+ if (!campaignName) {
714
+ return this.getInsights({
715
+ breakdowns,
716
+ fields,
717
+ level: "campaign",
718
+ sort,
719
+ targetId: toAdAccountNodeId(accountId),
720
+ window
721
+ });
722
+ }
723
+ const campaigns = await this.listCampaigns(accountId);
724
+ const matches = campaigns.filter((campaign) => (campaign.name ?? "").toLowerCase().includes(campaignName.toLowerCase()));
725
+ if (matches.length === 0) {
726
+ return [];
727
+ }
728
+ const rows = await Promise.all(matches.map((campaign) => this.getInsights({
729
+ breakdowns,
730
+ fields,
731
+ sort,
732
+ targetId: campaign.id,
733
+ window
734
+ })));
735
+ return rows.flat();
736
+ }
737
+ async getAdPerformanceByCampaign(campaignId, window, sort) {
738
+ return this.getInsights({
739
+ fields: [
740
+ "account_id",
741
+ "account_name",
742
+ "campaign_id",
743
+ "campaign_name",
744
+ "ad_id",
745
+ "ad_name",
746
+ "impressions",
747
+ "reach",
748
+ "clicks",
749
+ "ctr",
750
+ "cpc",
751
+ "cpm",
752
+ "spend",
753
+ "frequency",
754
+ "actions",
755
+ "date_start",
756
+ "date_stop"
757
+ ],
758
+ level: "ad",
759
+ sort,
760
+ targetId: campaignId,
761
+ window
762
+ });
763
+ }
764
+ async getDailyCampaignInsights(accountId, window) {
765
+ return this.getInsights({
766
+ fields: [
767
+ "account_id",
768
+ "account_name",
769
+ "campaign_id",
770
+ "campaign_name",
771
+ "impressions",
772
+ "clicks",
773
+ "ctr",
774
+ "cpc",
775
+ "spend",
776
+ "date_start",
777
+ "date_stop"
778
+ ],
779
+ level: "campaign",
780
+ targetId: toAdAccountNodeId(accountId),
781
+ timeIncrement: 1,
782
+ window
783
+ });
784
+ }
785
+ async getDailyAdInsights(campaignId, window) {
786
+ return this.getInsights({
787
+ fields: [
788
+ "ad_id",
789
+ "ad_name",
790
+ "campaign_id",
791
+ "campaign_name",
792
+ "impressions",
793
+ "clicks",
794
+ "ctr",
795
+ "cpc",
796
+ "cpm",
797
+ "spend",
798
+ "frequency",
799
+ "date_start",
800
+ "date_stop"
801
+ ],
802
+ level: "ad",
803
+ targetId: campaignId,
804
+ timeIncrement: 1,
805
+ window
806
+ });
807
+ }
808
+ async verifyApi(accountId) {
809
+ const whoAmI = await this.whoAmI();
810
+ const verification = {
811
+ apiVersion: this.client.apiVersion,
812
+ whoAmI
813
+ };
814
+ if (accountId) {
815
+ const account = await this.getAccount(accountId);
816
+ const campaigns = await this.listCampaigns(accountId);
817
+ const insights = await this.getInsights({
818
+ fields: ["account_id", "campaign_id", "campaign_name", "impressions", "clicks", "spend"],
819
+ level: "campaign",
820
+ targetId: toAdAccountNodeId(accountId),
821
+ window: buildRecentDateWindow(7)
822
+ });
823
+ verification.account = account;
824
+ verification.campaignCount = campaigns.length;
825
+ verification.insightSampleCount = insights.length;
826
+ }
827
+ return verification;
828
+ }
829
+ requireCampaignBudgetMinorUnits(campaign) {
830
+ if (campaign.dailyBudgetMinorUnits === undefined) {
831
+ throw new AppError("This campaign does not expose a campaign-level daily budget. Meta allows budget at campaign or ad set level; this CLI only updates campaign-level budgets via this command.", ExitCode.UnsafeBlocked);
832
+ }
833
+ return campaign.dailyBudgetMinorUnits;
834
+ }
835
+ expandCampaignBudget(campaign, currency) {
836
+ if (campaign.dailyBudgetMinorUnits === undefined) {
837
+ return undefined;
838
+ }
839
+ return fromMinorUnits(campaign.dailyBudgetMinorUnits, currency);
840
+ }
841
+ }