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,44 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { collectAcrossAccounts } from "./helpers.js";
3
+ import { detectAnomalies } from "../domain/analytics.js";
4
+ import { resolveAccountScope } from "../domain/account-scope.js";
5
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
6
+ import { parseDateWindow } from "../utils/date-range.js";
7
+ import { parseThreshold } from "../validators/common.js";
8
+ export function registerAnomaliesCommand(program, deps, state) {
9
+ program
10
+ .command("anomalies")
11
+ .description("Detect campaign-level anomalies from recent daily insights.")
12
+ .requiredOption("--account <id|all>", "Ad account id or all.")
13
+ .option("--threshold <percent>", "Percent change threshold.", "20")
14
+ .action(createAction("anomalies", deps, state, async (context, options) => {
15
+ const threshold = parseThreshold(options.threshold);
16
+ const scope = await resolveAccountScope(context, options.account);
17
+ const service = new MetaAdsService(context.client);
18
+ const window = parseDateWindow({
19
+ last: "14d"
20
+ });
21
+ const collected = await collectAcrossAccounts(scope.accounts, async (account) => {
22
+ const rows = await service.getDailyCampaignInsights(account.id, window);
23
+ const anomalies = detectAnomalies(rows, threshold);
24
+ return {
25
+ accountId: account.id,
26
+ accountName: account.name,
27
+ anomalies
28
+ };
29
+ });
30
+ return {
31
+ ok: true,
32
+ command: "anomalies",
33
+ data: {
34
+ accounts: collected.results,
35
+ threshold,
36
+ window
37
+ },
38
+ partialFailures: collected.failures.map((failure) => ({
39
+ message: failure.message,
40
+ scope: failure.accountId
41
+ }))
42
+ };
43
+ }));
44
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAssetCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,116 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { requirePermission } from "../auth/guards.js";
3
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
4
+ import { parsePositiveNumber, parseVideoId } from "../validators/common.js";
5
+ import { resolveExistingFilePath } from "../validators/create-spec.js";
6
+ import { resolveSingleAccount } from "./helpers.js";
7
+ export function registerAssetCommands(program, deps, state) {
8
+ const assets = program.command("assets").description("Asset uploads and video processing visibility.");
9
+ const images = assets.command("images").description("Image asset uploads.");
10
+ const videos = assets.command("videos").description("Video asset uploads and processing status.");
11
+ images
12
+ .command("upload")
13
+ .requiredOption("--account <id>", "Ad account id.")
14
+ .requiredOption("--file <path>", "Local image file path.")
15
+ .option("--name <name>", "Optional asset name.")
16
+ .description("Draft or upload an image asset to the ad account.")
17
+ .action(createAction("assets images upload", deps, state, async (context, options) => {
18
+ const account = await resolveSingleAccount(context, options.account, "assets images upload");
19
+ const absolutePath = await resolveExistingFilePath(options.file, "Image file");
20
+ const service = new MetaAdsService(context.client);
21
+ const plan = {
22
+ accountId: account.id,
23
+ action: "upload-image",
24
+ file: absolutePath,
25
+ name: options.name
26
+ };
27
+ if (!context.apply) {
28
+ return {
29
+ ok: true,
30
+ command: "assets images upload",
31
+ data: plan,
32
+ meta: {
33
+ mode: "draft"
34
+ }
35
+ };
36
+ }
37
+ requirePermission(context.config, "write", "Image upload");
38
+ const asset = await service.uploadAdImage(account.id, absolutePath, options.name);
39
+ return {
40
+ ok: true,
41
+ command: "assets images upload",
42
+ data: {
43
+ accountId: account.id,
44
+ applied: true,
45
+ asset
46
+ },
47
+ meta: {
48
+ mode: "write"
49
+ }
50
+ };
51
+ }));
52
+ videos
53
+ .command("upload")
54
+ .requiredOption("--account <id>", "Ad account id.")
55
+ .requiredOption("--file <path>", "Local video file path.")
56
+ .option("--name <name>", "Optional asset name.")
57
+ .option("--wait", "Wait until Meta reports the video as ready.")
58
+ .option("--poll-ms <ms>", "Polling interval in milliseconds while waiting.", "3000")
59
+ .option("--timeout-ms <ms>", "Wait timeout in milliseconds.", "90000")
60
+ .description("Draft or upload a video asset to the ad account.")
61
+ .action(createAction("assets videos upload", deps, state, async (context, options) => {
62
+ const account = await resolveSingleAccount(context, options.account, "assets videos upload");
63
+ const absolutePath = await resolveExistingFilePath(options.file, "Video file");
64
+ const pollMs = parsePositiveNumber(options.pollMs ?? "3000", "Poll interval");
65
+ const timeoutMs = parsePositiveNumber(options.timeoutMs ?? "90000", "Timeout");
66
+ const service = new MetaAdsService(context.client);
67
+ const plan = {
68
+ accountId: account.id,
69
+ action: "upload-video",
70
+ file: absolutePath,
71
+ name: options.name,
72
+ wait: Boolean(options.wait)
73
+ };
74
+ if (!context.apply) {
75
+ return {
76
+ ok: true,
77
+ command: "assets videos upload",
78
+ data: plan,
79
+ meta: {
80
+ mode: "draft"
81
+ }
82
+ };
83
+ }
84
+ requirePermission(context.config, "write", "Video upload");
85
+ const asset = await service.uploadAdVideo(account.id, absolutePath, options.name);
86
+ const status = options.wait
87
+ ? await service.waitForVideoReady(asset.id, { pollMs, timeoutMs })
88
+ : undefined;
89
+ return {
90
+ ok: true,
91
+ command: "assets videos upload",
92
+ data: {
93
+ accountId: account.id,
94
+ applied: true,
95
+ asset,
96
+ status
97
+ },
98
+ meta: {
99
+ mode: "write"
100
+ }
101
+ };
102
+ }));
103
+ videos
104
+ .command("status")
105
+ .argument("<video-id>", "Video id.")
106
+ .description("Show the current processing status of an uploaded video.")
107
+ .action(createAction("assets videos status", deps, state, async (context, videoId) => {
108
+ const service = new MetaAdsService(context.client);
109
+ const status = await service.getAdVideoStatus(parseVideoId(videoId));
110
+ return {
111
+ ok: true,
112
+ command: "assets videos status",
113
+ data: status
114
+ };
115
+ }));
116
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAudienceCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,40 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { resolveSingleAccount } from "./helpers.js";
3
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
4
+ import { parseAudienceId } from "../validators/common.js";
5
+ export function registerAudienceCommands(program, deps, state) {
6
+ const audiences = program.command("audiences").description("Audience discovery and size estimates.");
7
+ audiences
8
+ .command("list")
9
+ .requiredOption("--account <id>", "Ad account id.")
10
+ .description("List audiences for an account.")
11
+ .action(createAction("audiences list", deps, state, async (context, options) => {
12
+ const account = await resolveSingleAccount(context, options.account, "audiences list");
13
+ const service = new MetaAdsService(context.client);
14
+ const rows = await service.listAudiences(account.id);
15
+ return {
16
+ ok: true,
17
+ command: "audiences list",
18
+ data: {
19
+ accountId: account.id,
20
+ audiences: rows
21
+ },
22
+ meta: {
23
+ count: rows.length
24
+ }
25
+ };
26
+ }));
27
+ audiences
28
+ .command("size")
29
+ .requiredOption("--audience <id>", "Audience id.")
30
+ .description("Get audience size estimate details.")
31
+ .action(createAction("audiences size", deps, state, async (context, options) => {
32
+ const service = new MetaAdsService(context.client);
33
+ const audience = await service.getAudience(parseAudienceId(options.audience));
34
+ return {
35
+ ok: true,
36
+ command: "audiences size",
37
+ data: audience
38
+ };
39
+ }));
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerAuthCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,139 @@
1
+ import { DEFAULT_AUTH_REDIRECT_URI } from "../auth/constants.js";
2
+ import { createAction } from "../cli/action.js";
3
+ import { runInteractiveAuthLogin } from "../auth/login.js";
4
+ import { writeFileConfig } from "../config/file-config.js";
5
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
6
+ function resolveSettingSource(envName, resolvedValue, fallback = "none") {
7
+ if (process.env[envName]?.trim()) {
8
+ return "env";
9
+ }
10
+ if (resolvedValue) {
11
+ return fallback;
12
+ }
13
+ return "none";
14
+ }
15
+ function isExpired(value) {
16
+ if (!value) {
17
+ return undefined;
18
+ }
19
+ const expiresAt = new Date(value);
20
+ if (Number.isNaN(expiresAt.getTime())) {
21
+ return undefined;
22
+ }
23
+ return expiresAt.getTime() <= Date.now();
24
+ }
25
+ export function registerAuthCommands(program, deps, state) {
26
+ const auth = program.command("auth").description("Authentication and token visibility.");
27
+ auth
28
+ .command("status")
29
+ .description("Show resolved auth state, token source, and a lightweight token probe if possible.")
30
+ .action(createAction("auth status", deps, state, async (context) => {
31
+ const service = new MetaAdsService(context.client);
32
+ let tokenProbe = null;
33
+ let tokenProbeError;
34
+ if (context.config.accessToken) {
35
+ try {
36
+ tokenProbe = await service.whoAmI();
37
+ }
38
+ catch (error) {
39
+ tokenProbeError = error instanceof Error ? error.message : "Unknown auth probe failure.";
40
+ }
41
+ }
42
+ return {
43
+ ok: true,
44
+ command: "auth status",
45
+ data: {
46
+ accessTokenExpiresAt: context.config.accessTokenExpiresAt,
47
+ accessTokenExpired: isExpired(context.config.accessTokenExpiresAt),
48
+ accessTokenPresent: Boolean(context.config.accessToken),
49
+ accessTokenSource: resolveSettingSource("META_ACCESS_TOKEN", context.config.accessToken, "config"),
50
+ apiVersion: context.config.apiVersion,
51
+ appIdPresent: Boolean(context.config.appId),
52
+ appIdSource: resolveSettingSource("META_APP_ID", context.config.appId, "config"),
53
+ appSecretPresent: Boolean(context.config.appSecret),
54
+ approvalWebhookPresent: Boolean(context.config.approvalWebhook),
55
+ authConfigIdPresent: Boolean(context.config.authConfigId),
56
+ authConfigIdSource: resolveSettingSource("META_AUTH_CONFIG_ID", context.config.authConfigId, "config"),
57
+ authRedirectUri: context.config.authRedirectUri,
58
+ authRedirectUriSource: process.env.META_AUTH_REDIRECT_URI?.trim()
59
+ ? "env"
60
+ : context.config.authRedirectUri && context.config.authRedirectUri !== DEFAULT_AUTH_REDIRECT_URI
61
+ ? "config"
62
+ : "default",
63
+ configPath: context.config.configPath,
64
+ defaultAccountId: context.config.defaultAccountId,
65
+ permissionMode: context.config.permissionMode,
66
+ tokenProbe,
67
+ tokenProbeError
68
+ }
69
+ };
70
+ }));
71
+ auth
72
+ .command("login")
73
+ .description("Run Facebook Login for Business and persist the resulting user token locally.")
74
+ .option("--app-id <id>", "Override the Meta app ID used for the login flow.")
75
+ .option("--config-id <id>", "Override the Facebook Login for Business configuration ID.")
76
+ .option("--redirect-uri <url>", "Override the redirect URI used for the login flow.")
77
+ .option("--no-open-browser", "Do not open a browser automatically; print the login URL and wait for the pasted callback URL.")
78
+ .action(createAction("auth login", deps, state, async (context, options) => {
79
+ const login = await runInteractiveAuthLogin(context.config, {
80
+ appId: options.appId,
81
+ configId: options.configId,
82
+ openBrowser: options.openBrowser,
83
+ redirectUri: options.redirectUri
84
+ }, {
85
+ fetchImpl: context.fetchImpl
86
+ });
87
+ await writeFileConfig(context.config.configPath, {
88
+ accessToken: login.accessToken,
89
+ accessTokenExpiresAt: login.accessTokenExpiresAt,
90
+ appId: login.appId,
91
+ authConfigId: login.authConfigId,
92
+ authRedirectUri: login.authRedirectUri
93
+ });
94
+ const warnings = [];
95
+ if (process.env.META_ACCESS_TOKEN?.trim()) {
96
+ warnings.push("META_ACCESS_TOKEN is currently set in the environment and will continue to override the stored login token until it is unset.");
97
+ }
98
+ if ((options.openBrowser ?? true) && !login.browserOpened) {
99
+ warnings.push("The browser could not be opened automatically. Complete the login flow manually with the printed login URL.");
100
+ }
101
+ return {
102
+ ok: true,
103
+ command: "auth login",
104
+ data: {
105
+ accessTokenExpiresAt: login.accessTokenExpiresAt,
106
+ authConfigId: login.authConfigId,
107
+ authRedirectUri: login.authRedirectUri,
108
+ browserOpened: login.browserOpened,
109
+ codeExchangeUsed: login.codeExchangeUsed,
110
+ configPath: context.config.configPath,
111
+ tokenSource: "config"
112
+ },
113
+ warnings
114
+ };
115
+ }));
116
+ auth
117
+ .command("logout")
118
+ .description("Remove any locally stored access token from the CLI config.")
119
+ .action(createAction("auth logout", deps, state, async (context) => {
120
+ await writeFileConfig(context.config.configPath, {
121
+ accessToken: undefined,
122
+ accessTokenExpiresAt: undefined
123
+ });
124
+ const warnings = process.env.META_ACCESS_TOKEN?.trim()
125
+ ? [
126
+ "META_ACCESS_TOKEN is still set in the environment and will remain active for this shell until it is unset."
127
+ ]
128
+ : undefined;
129
+ return {
130
+ ok: true,
131
+ command: "auth logout",
132
+ data: {
133
+ configPath: context.config.configPath,
134
+ localAccessTokenCleared: true
135
+ },
136
+ warnings
137
+ };
138
+ }));
139
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerCampaignCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;
@@ -0,0 +1,273 @@
1
+ import { createAction } from "../cli/action.js";
2
+ import { requirePermission } from "../auth/guards.js";
3
+ import { resolveSingleAccount } from "./helpers.js";
4
+ import { decideBudgetApproval, decideCampaignStatusApproval, submitApprovalRequest } from "../domain/approval-service.js";
5
+ import { MetaAdsService } from "../domain/meta-ads-service.js";
6
+ import { fromMinorUnits, toMinorUnits } from "../utils/currency.js";
7
+ import { percentChange } from "../utils/date-range.js";
8
+ import { AppError, ExitCode } from "../utils/errors.js";
9
+ import { campaignCreateSpecSchema, readSpecFile } from "../validators/create-spec.js";
10
+ import { parseCampaignId, parsePositiveNumber } from "../validators/common.js";
11
+ function buildApprovalPayload(context, action, resourceId, accountId, changeSummary, reason) {
12
+ const approvalArgv = action === "budget"
13
+ ? ["campaigns", "budget", resourceId, "--daily", String(changeSummary.requestedBudget ?? "unknown")]
14
+ : ["campaigns", action, resourceId];
15
+ return {
16
+ action,
17
+ accountId,
18
+ approvalReason: reason,
19
+ argv: approvalArgv,
20
+ changeSummary,
21
+ command: approvalArgv.join(" "),
22
+ requestedAt: new Date().toISOString(),
23
+ requestedMode: context.config.permissionMode,
24
+ resourceId,
25
+ resourceType: "campaign"
26
+ };
27
+ }
28
+ export function registerCampaignCommands(program, deps, state) {
29
+ const campaigns = program.command("campaigns").description("Campaign reads, creation, and controlled writes.");
30
+ campaigns
31
+ .command("create")
32
+ .requiredOption("--account <id>", "Ad account id.")
33
+ .requiredOption("--spec <path>", "Path to a JSON campaign create spec.")
34
+ .description("Draft or create a campaign from a JSON spec.")
35
+ .action(createAction("campaigns create", deps, state, async (context, options) => {
36
+ const account = await resolveSingleAccount(context, options.account, "campaigns create");
37
+ const { spec, specPath } = await readSpecFile(options.spec, campaignCreateSpecSchema, "Campaign create");
38
+ const service = new MetaAdsService(context.client);
39
+ const plan = {
40
+ accountId: account.id,
41
+ action: "create-campaign",
42
+ spec,
43
+ specPath
44
+ };
45
+ if (!context.apply) {
46
+ return {
47
+ ok: true,
48
+ command: "campaigns create",
49
+ data: plan,
50
+ meta: {
51
+ mode: "draft"
52
+ }
53
+ };
54
+ }
55
+ requirePermission(context.config, "write", "Campaign create");
56
+ const campaign = await service.createCampaign(account.id, spec);
57
+ return {
58
+ ok: true,
59
+ command: "campaigns create",
60
+ data: {
61
+ accountId: account.id,
62
+ applied: true,
63
+ campaign,
64
+ specPath
65
+ },
66
+ meta: {
67
+ mode: "write"
68
+ }
69
+ };
70
+ }));
71
+ campaigns
72
+ .command("list")
73
+ .requiredOption("--account <id>", "Ad account id.")
74
+ .description("List campaigns for an ad account.")
75
+ .action(createAction("campaigns list", deps, state, async (context, options) => {
76
+ const account = await resolveSingleAccount(context, options.account, "campaigns list");
77
+ const service = new MetaAdsService(context.client);
78
+ const campaignsList = await service.listCampaigns(account.id);
79
+ return {
80
+ ok: true,
81
+ command: "campaigns list",
82
+ data: {
83
+ accountId: account.id,
84
+ campaigns: campaignsList
85
+ },
86
+ meta: {
87
+ count: campaignsList.length
88
+ }
89
+ };
90
+ }));
91
+ campaigns
92
+ .command("get")
93
+ .argument("<campaign-id>", "Campaign id.")
94
+ .description("Get campaign details.")
95
+ .action(createAction("campaigns get", deps, state, async (context, campaignId) => {
96
+ const service = new MetaAdsService(context.client);
97
+ const campaign = await service.getCampaign(parseCampaignId(campaignId));
98
+ return {
99
+ ok: true,
100
+ command: "campaigns get",
101
+ data: campaign
102
+ };
103
+ }));
104
+ campaigns
105
+ .command("pause")
106
+ .argument("<campaign-id>", "Campaign id.")
107
+ .description("Draft or submit approval request to pause a campaign.")
108
+ .action(createAction("campaigns pause", deps, state, async (context, campaignId) => {
109
+ const service = new MetaAdsService(context.client);
110
+ const normalizedId = parseCampaignId(campaignId);
111
+ const campaign = await service.getCampaign(normalizedId);
112
+ const decision = decideCampaignStatusApproval("pause");
113
+ const plan = {
114
+ action: "pause",
115
+ approvalRequired: decision.required,
116
+ campaign,
117
+ mutation: {
118
+ status: "PAUSED"
119
+ },
120
+ reason: decision.reason
121
+ };
122
+ if (!context.apply) {
123
+ return {
124
+ ok: true,
125
+ command: "campaigns pause",
126
+ data: plan,
127
+ meta: {
128
+ mode: "draft"
129
+ }
130
+ };
131
+ }
132
+ requirePermission(context.config, "write", "Campaign pause");
133
+ const approvalPayload = buildApprovalPayload(context, "pause", normalizedId, campaign.accountId, plan.mutation, decision.reason);
134
+ await submitApprovalRequest(context, approvalPayload);
135
+ return {
136
+ ok: true,
137
+ command: "campaigns pause",
138
+ data: {
139
+ approvalSubmitted: true,
140
+ payload: approvalPayload
141
+ },
142
+ meta: {
143
+ exitCode: ExitCode.ApprovalRequired,
144
+ mode: "approval"
145
+ }
146
+ };
147
+ }));
148
+ campaigns
149
+ .command("enable")
150
+ .argument("<campaign-id>", "Campaign id.")
151
+ .description("Draft or submit approval request to enable a campaign.")
152
+ .action(createAction("campaigns enable", deps, state, async (context, campaignId) => {
153
+ const service = new MetaAdsService(context.client);
154
+ const normalizedId = parseCampaignId(campaignId);
155
+ const campaign = await service.getCampaign(normalizedId);
156
+ const decision = decideCampaignStatusApproval("enable");
157
+ const plan = {
158
+ action: "enable",
159
+ approvalRequired: decision.required,
160
+ campaign,
161
+ mutation: {
162
+ status: "ACTIVE"
163
+ },
164
+ reason: decision.reason
165
+ };
166
+ if (!context.apply) {
167
+ return {
168
+ ok: true,
169
+ command: "campaigns enable",
170
+ data: plan,
171
+ meta: {
172
+ mode: "draft"
173
+ }
174
+ };
175
+ }
176
+ requirePermission(context.config, "write", "Campaign enable");
177
+ const approvalPayload = buildApprovalPayload(context, "enable", normalizedId, campaign.accountId, plan.mutation, decision.reason);
178
+ await submitApprovalRequest(context, approvalPayload);
179
+ return {
180
+ ok: true,
181
+ command: "campaigns enable",
182
+ data: {
183
+ approvalSubmitted: true,
184
+ payload: approvalPayload
185
+ },
186
+ meta: {
187
+ exitCode: ExitCode.ApprovalRequired,
188
+ mode: "approval"
189
+ }
190
+ };
191
+ }));
192
+ campaigns
193
+ .command("budget")
194
+ .argument("<campaign-id>", "Campaign id.")
195
+ .requiredOption("--daily <amount>", "Daily budget amount in account currency.")
196
+ .description("Draft or update a campaign-level daily budget.")
197
+ .action(createAction("campaigns budget", deps, state, async (context, campaignId, options) => {
198
+ const service = new MetaAdsService(context.client);
199
+ const normalizedId = parseCampaignId(campaignId);
200
+ const campaign = await service.getCampaign(normalizedId);
201
+ if (!campaign.accountId) {
202
+ throw new AppError("Campaign account id is missing from the provider response.", ExitCode.Provider);
203
+ }
204
+ const account = await service.getAccount(campaign.accountId);
205
+ const currency = account.currency ?? "USD";
206
+ const currentBudgetMinorUnits = service.requireCampaignBudgetMinorUnits(campaign);
207
+ const currentBudget = fromMinorUnits(currentBudgetMinorUnits, currency);
208
+ const requestedBudget = parsePositiveNumber(options.daily, "Daily budget");
209
+ const change = percentChange(currentBudget, requestedBudget);
210
+ const decision = decideBudgetApproval(change);
211
+ const minorUnits = toMinorUnits(requestedBudget, currency);
212
+ const plan = {
213
+ action: "budget",
214
+ approvalRequired: decision.required,
215
+ campaign,
216
+ changePercent: change,
217
+ currency,
218
+ currentBudget,
219
+ mutation: {
220
+ daily_budget: minorUnits
221
+ },
222
+ requestedBudget,
223
+ reason: decision.reason
224
+ };
225
+ if (!context.apply) {
226
+ return {
227
+ ok: true,
228
+ command: "campaigns budget",
229
+ data: plan,
230
+ meta: {
231
+ mode: "draft"
232
+ }
233
+ };
234
+ }
235
+ requirePermission(context.config, "write", "Campaign budget");
236
+ if (decision.required) {
237
+ const approvalPayload = buildApprovalPayload(context, "budget", normalizedId, campaign.accountId, {
238
+ currentBudget,
239
+ currency,
240
+ requestedBudget
241
+ }, decision.reason);
242
+ await submitApprovalRequest(context, approvalPayload);
243
+ return {
244
+ ok: true,
245
+ command: "campaigns budget",
246
+ data: {
247
+ approvalSubmitted: true,
248
+ payload: approvalPayload
249
+ },
250
+ meta: {
251
+ exitCode: ExitCode.ApprovalRequired,
252
+ mode: "approval"
253
+ }
254
+ };
255
+ }
256
+ await service.updateCampaignDailyBudget(normalizedId, minorUnits);
257
+ return {
258
+ ok: true,
259
+ command: "campaigns budget",
260
+ data: {
261
+ accountId: campaign.accountId,
262
+ applied: true,
263
+ campaignId: normalizedId,
264
+ currency,
265
+ currentBudget,
266
+ requestedBudget
267
+ },
268
+ meta: {
269
+ mode: "write"
270
+ }
271
+ };
272
+ }));
273
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ import { type CliRuntimeState } from "../cli/action.js";
3
+ import type { CliDeps } from "../cli/context.js";
4
+ export declare function registerCapiCommands(program: Command, deps: CliDeps, state: CliRuntimeState): void;