@soku-ai/cli 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (92) hide show
  1. package/dist/auth/device.d.ts +9 -0
  2. package/dist/auth/device.d.ts.map +1 -1
  3. package/dist/auth/device.js +6 -1
  4. package/dist/auth/device.js.map +1 -1
  5. package/dist/commands/ads.d.ts +32 -0
  6. package/dist/commands/ads.d.ts.map +1 -0
  7. package/dist/commands/ads.js +752 -0
  8. package/dist/commands/ads.js.map +1 -0
  9. package/dist/commands/auth.d.ts +2 -0
  10. package/dist/commands/auth.d.ts.map +1 -1
  11. package/dist/commands/auth.js +28 -7
  12. package/dist/commands/auth.js.map +1 -1
  13. package/dist/commands/automation.d.ts +80 -0
  14. package/dist/commands/automation.d.ts.map +1 -0
  15. package/dist/commands/automation.js +213 -0
  16. package/dist/commands/automation.js.map +1 -0
  17. package/dist/commands/brand-skill.d.ts +72 -0
  18. package/dist/commands/brand-skill.d.ts.map +1 -0
  19. package/dist/commands/brand-skill.js +351 -0
  20. package/dist/commands/brand-skill.js.map +1 -0
  21. package/dist/commands/brand.d.ts.map +1 -1
  22. package/dist/commands/brand.js +59 -1
  23. package/dist/commands/brand.js.map +1 -1
  24. package/dist/commands/call.d.ts.map +1 -1
  25. package/dist/commands/call.js +6 -3
  26. package/dist/commands/call.js.map +1 -1
  27. package/dist/commands/context.d.ts +23 -0
  28. package/dist/commands/context.d.ts.map +1 -0
  29. package/dist/commands/context.js +291 -0
  30. package/dist/commands/context.js.map +1 -0
  31. package/dist/commands/files.d.ts +10 -0
  32. package/dist/commands/files.d.ts.map +1 -0
  33. package/dist/commands/files.js +48 -0
  34. package/dist/commands/files.js.map +1 -0
  35. package/dist/commands/generated.d.ts +24 -3
  36. package/dist/commands/generated.d.ts.map +1 -1
  37. package/dist/commands/generated.js +97 -10
  38. package/dist/commands/generated.js.map +1 -1
  39. package/dist/commands/memory.d.ts +18 -0
  40. package/dist/commands/memory.d.ts.map +1 -0
  41. package/dist/commands/memory.js +70 -0
  42. package/dist/commands/memory.js.map +1 -0
  43. package/dist/commands/review.d.ts.map +1 -1
  44. package/dist/commands/review.js +46 -2
  45. package/dist/commands/review.js.map +1 -1
  46. package/dist/commands/seo-hosting.d.ts +107 -0
  47. package/dist/commands/seo-hosting.d.ts.map +1 -0
  48. package/dist/commands/seo-hosting.js +499 -0
  49. package/dist/commands/seo-hosting.js.map +1 -0
  50. package/dist/commands/skill.d.ts +50 -6
  51. package/dist/commands/skill.d.ts.map +1 -1
  52. package/dist/commands/skill.js +120 -56
  53. package/dist/commands/skill.js.map +1 -1
  54. package/dist/commands/update.d.ts +23 -0
  55. package/dist/commands/update.d.ts.map +1 -0
  56. package/dist/commands/update.js +345 -0
  57. package/dist/commands/update.js.map +1 -0
  58. package/dist/commands/workspace.d.ts +4 -0
  59. package/dist/commands/workspace.d.ts.map +1 -0
  60. package/dist/commands/workspace.js +132 -0
  61. package/dist/commands/workspace.js.map +1 -0
  62. package/dist/config.d.ts +5 -4
  63. package/dist/config.d.ts.map +1 -1
  64. package/dist/config.js +5 -7
  65. package/dist/config.js.map +1 -1
  66. package/dist/generated/capabilities.json +291 -30
  67. package/dist/http/client.d.ts.map +1 -1
  68. package/dist/http/client.js +74 -16
  69. package/dist/http/client.js.map +1 -1
  70. package/dist/index.js +35 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/output/envelope.d.ts +5 -0
  73. package/dist/output/envelope.d.ts.map +1 -1
  74. package/dist/output/envelope.js +101 -2
  75. package/dist/output/envelope.js.map +1 -1
  76. package/dist/output/unwrap.d.ts.map +1 -1
  77. package/dist/output/unwrap.js +18 -1
  78. package/dist/output/unwrap.js.map +1 -1
  79. package/dist/skills/unzip.d.ts +1 -1
  80. package/dist/skills/unzip.js +1 -1
  81. package/dist/update-check.d.ts +0 -1
  82. package/dist/update-check.d.ts.map +1 -1
  83. package/dist/update-check.js +4 -54
  84. package/dist/update-check.js.map +1 -1
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.d.ts.map +1 -1
  87. package/dist/version.js +1 -1
  88. package/dist/version.js.map +1 -1
  89. package/package.json +7 -4
  90. package/postinstall.cjs +85 -0
  91. package/skills/soku/SKILL.md +254 -18
  92. package/README.md +0 -91
@@ -0,0 +1,752 @@
1
+ /** `soku ads <platform> <entity> <verb>` — Tier-2 ergonomic commands for the
2
+ * highest-traffic ads-ops write entities (campaign / adset / ad-group / ad /
3
+ * keyword).
4
+ *
5
+ * These wrap the same review-gated `/api/cli/call/ads/<action>` path the Tier-1
6
+ * generated commands use, but expose platform-specific explicit flags, do light
7
+ * validation up front, and always require `--summary`. Anything not surfaced as
8
+ * a flag can still be passed with `-p key=value`. Exotic actions stay on the raw
9
+ * `soku call ads <action>` escape hatch.
10
+ *
11
+ * Validation here is intentionally light (obvious missing-field guards only) —
12
+ * the backend (`packages/ads_ops`) owns the authoritative validation, so we
13
+ * don't duplicate its rules and risk drift.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { basename } from 'node:path';
17
+ import { emitError, ExitCode } from '../output/envelope.js';
18
+ import { buildGeneratedCommands, callTypedAction, loadManifest } from './generated.js';
19
+ /** Collect repeatable `-p key=value` into a payload object; values are parsed as
20
+ * JSON when possible (so numbers/booleans/arrays work), else kept as strings. */
21
+ function collectParam(entry, acc) {
22
+ const eq = entry.indexOf('=');
23
+ if (eq === -1) {
24
+ emitError('usage', `-p must be key=value, got: ${entry}`, ExitCode.USAGE);
25
+ }
26
+ const key = entry.slice(0, eq);
27
+ const raw = entry.slice(eq + 1);
28
+ try {
29
+ acc[key] = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ acc[key] = raw;
33
+ }
34
+ return acc;
35
+ }
36
+ function collectString(value, acc) {
37
+ acc.push(value);
38
+ return acc;
39
+ }
40
+ function parseIntFlag(flag, raw) {
41
+ const n = Number(raw);
42
+ if (!Number.isInteger(n)) {
43
+ emitError('usage', `${flag} must be an integer, got: ${raw}`, ExitCode.USAGE);
44
+ }
45
+ return n;
46
+ }
47
+ function parseJsonValue(flag, raw) {
48
+ try {
49
+ return JSON.parse(raw);
50
+ }
51
+ catch {
52
+ emitError('usage', `${flag} must be valid JSON.`, ExitCode.USAGE);
53
+ }
54
+ }
55
+ function readItemsFile(file) {
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
59
+ }
60
+ catch (err) {
61
+ return emitError('usage', `--items-file must be readable JSON: ${file}`, ExitCode.USAGE, err.message);
62
+ }
63
+ if (!Array.isArray(parsed) || parsed.length === 0) {
64
+ emitError('usage', '--items-file must contain a non-empty JSON array.', ExitCode.USAGE);
65
+ }
66
+ const seen = new Set();
67
+ parsed.forEach((item, index) => {
68
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
69
+ emitError('usage', `items[${index}] must be an object.`, ExitCode.USAGE);
70
+ }
71
+ const clientRef = item.client_ref;
72
+ if (typeof clientRef !== 'string' || clientRef.trim() === '') {
73
+ emitError('usage', `items[${index}].client_ref is required.`, ExitCode.USAGE);
74
+ }
75
+ if (seen.has(clientRef)) {
76
+ emitError('usage', `items[${index}].client_ref is duplicated: ${clientRef}`, ExitCode.USAGE);
77
+ }
78
+ seen.add(clientRef);
79
+ });
80
+ return parsed;
81
+ }
82
+ /** Resolve a daily budget to micros from either human-units USD or raw micros.
83
+ * `--budget-daily-micros` wins when both are given. Micros = USD * 1_000_000. */
84
+ export function resolveBudgetMicros(usd, micros) {
85
+ if (micros !== undefined)
86
+ return parseIntFlag('--budget-daily-micros', micros);
87
+ if (usd !== undefined) {
88
+ const n = Number(usd);
89
+ if (!Number.isFinite(n) || n < 0) {
90
+ emitError('usage', `--budget-daily must be a non-negative number, got: ${usd}`, ExitCode.USAGE);
91
+ }
92
+ return Math.round(n * 1_000_000);
93
+ }
94
+ return undefined;
95
+ }
96
+ /** Merge explicit fields + `-p` passthrough + `_summary`, then dispatch. */
97
+ function runAdsWrite(platform, action, fields, opts) {
98
+ const payload = { ...opts.param };
99
+ payload.platform = platform;
100
+ for (const [k, v] of Object.entries(fields)) {
101
+ if (v !== undefined)
102
+ payload[k] = v;
103
+ }
104
+ if (opts.accountId && payload.account_id === undefined) {
105
+ payload.account_id = opts.accountId;
106
+ }
107
+ payload._summary = opts.summary;
108
+ return callTypedAction('ads', action, payload);
109
+ }
110
+ function runMetaBulkCreate(action, opts) {
111
+ return runAdsWrite('meta', action, {
112
+ account_id: opts.accountId,
113
+ items: readItemsFile(opts.itemsFile),
114
+ }, opts);
115
+ }
116
+ function addMetaBulkCreate(group, action, entity) {
117
+ group
118
+ .command('bulk-create')
119
+ .description(`Bulk-create Meta ${entity} from a JSON items file`)
120
+ .requiredOption('--account-id <id>', 'Meta ad account id')
121
+ .requiredOption('--items-file <json>', 'JSON array; each item requires client_ref')
122
+ .requiredOption(...SUMMARY_OPT)
123
+ .option(...PARAM_OPT, collectParam, {})
124
+ .action(async (opts) => {
125
+ await runMetaBulkCreate(action, opts);
126
+ });
127
+ }
128
+ const PARAM_OPT = [
129
+ '-p, --param <key=value>',
130
+ 'Set an extra payload field (repeatable); JSON-parsed when possible',
131
+ ];
132
+ const SUMMARY_OPT = [
133
+ '--summary <text>',
134
+ 'Human-readable description of this write; becomes the review approval card header',
135
+ ];
136
+ /** Optional account id for update/remove: the backend needs it to resolve the
137
+ * account when the parent entity has not yet synced into the local table, so
138
+ * the common "create → immediately update/remove" flow does not 400. */
139
+ const ACCOUNT_ID_OPT = [
140
+ '--account-id <id>',
141
+ 'Ad account id (aids resolution before the target entity syncs locally)',
142
+ ];
143
+ function nameFromUrl(rawUrl, index) {
144
+ try {
145
+ const pathName = new URL(rawUrl).pathname;
146
+ const fileName = basename(pathName);
147
+ return fileName || `image-${index + 1}.jpg`;
148
+ }
149
+ catch {
150
+ return `image-${index + 1}.jpg`;
151
+ }
152
+ }
153
+ function readLocalImageBytes(file) {
154
+ try {
155
+ return readFileSync(file);
156
+ }
157
+ catch (err) {
158
+ return emitError('usage', `Could not read file: ${file}`, ExitCode.USAGE, err.message);
159
+ }
160
+ }
161
+ export async function buildUploadImages(files, urls, namePrefix) {
162
+ const items = [];
163
+ for (const file of files) {
164
+ const bytes = readLocalImageBytes(file);
165
+ const fileName = basename(file);
166
+ items.push({
167
+ client_ref: file,
168
+ bytes_base64: bytes.toString('base64'),
169
+ name: namePrefix ? `${namePrefix}-${fileName}` : fileName,
170
+ });
171
+ }
172
+ urls.forEach((url, index) => {
173
+ const urlName = nameFromUrl(url, index);
174
+ items.push({
175
+ client_ref: url,
176
+ image_url: url,
177
+ name: namePrefix ? `${namePrefix}-${urlName}` : urlName,
178
+ });
179
+ });
180
+ return items;
181
+ }
182
+ /** Add `activate` / `pause` convenience verbs to a platform-specific entity
183
+ * group. The public command tree already determines the platform; the helper
184
+ * only injects the right active status and payload id field. */
185
+ function addActivatePause(group, platform, idFlag, optionKey, payloadKey, action, entity, activeStatus) {
186
+ group
187
+ .command('activate')
188
+ .description(`Start serving the ${entity} (sets status ${activeStatus})`)
189
+ .requiredOption(`${idFlag} <id>`, `${entity} id`)
190
+ .option(...ACCOUNT_ID_OPT)
191
+ .requiredOption(...SUMMARY_OPT)
192
+ .option(...PARAM_OPT, collectParam, {})
193
+ .action(async (opts) => {
194
+ await runAdsWrite(platform, action, { [payloadKey]: opts[optionKey], status: activeStatus }, { ...opts, accountId: opts.accountId });
195
+ });
196
+ group
197
+ .command('pause')
198
+ .description(`Stop serving the ${entity} (sets status PAUSED)`)
199
+ .requiredOption(`${idFlag} <id>`, `${entity} id`)
200
+ .option(...ACCOUNT_ID_OPT)
201
+ .requiredOption(...SUMMARY_OPT)
202
+ .option(...PARAM_OPT, collectParam, {})
203
+ .action(async (opts) => {
204
+ await runAdsWrite(platform, action, { [payloadKey]: opts[optionKey], status: 'PAUSED' }, { ...opts, accountId: opts.accountId });
205
+ });
206
+ }
207
+ function splitCommaList(value) {
208
+ return value ? value.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
209
+ }
210
+ function registerMetaAssetCommands(meta) {
211
+ const asset = meta.command('asset').description('Upload and manage Meta ad assets');
212
+ asset
213
+ .command('upload-images [files...]')
214
+ .description('Upload local files or public image URLs to the Meta asset library')
215
+ .requiredOption('--account-id <id>', 'Meta ad account id')
216
+ .option('--url <url>', 'Public https image URL (repeatable)', collectString, [])
217
+ .option('--concurrency <n>', 'Bulk upload concurrency (server clamps to 1-10)')
218
+ .option('--name-prefix <prefix>', 'Prefix uploaded asset names')
219
+ .action(async (files, opts) => {
220
+ const urls = opts.url ?? [];
221
+ if (files.length === 0 && urls.length === 0) {
222
+ emitError('usage', 'upload-images requires at least one local file argument or --url.', ExitCode.USAGE);
223
+ }
224
+ const images = await buildUploadImages(files, urls, opts.namePrefix);
225
+ await callTypedAction('ads', 'upload_images', {
226
+ platform: 'meta',
227
+ account_id: opts.accountId,
228
+ images,
229
+ concurrency: opts.concurrency
230
+ ? parseIntFlag('--concurrency', opts.concurrency)
231
+ : undefined,
232
+ });
233
+ });
234
+ }
235
+ function registerMetaAccountCommands(meta) {
236
+ const account = meta.command('account').description('Read Meta ad account resources');
237
+ account
238
+ .command('pages')
239
+ .description('List Facebook Pages promotable from a Meta ad account')
240
+ .requiredOption('--account-id <id>', 'Meta ad account id')
241
+ .option(...PARAM_OPT, collectParam, {})
242
+ .action(async (opts) => {
243
+ await callTypedAction('ads', 'get_account_pages', {
244
+ ...opts.param,
245
+ platform: 'meta',
246
+ account_id: opts.accountId,
247
+ });
248
+ });
249
+ }
250
+ function registerMetaCampaignCommands(meta) {
251
+ const campaign = meta.command('campaign').description('Create and manage Meta campaigns');
252
+ campaign
253
+ .command('get')
254
+ .description('Read one Meta campaign by id')
255
+ .requiredOption('--account-id <id>', 'Meta ad account id')
256
+ .requiredOption('--campaign-id <id>', 'Campaign id')
257
+ .option(...PARAM_OPT, collectParam, {})
258
+ .action(async (opts) => {
259
+ await callTypedAction('ads', 'get_campaign', {
260
+ ...opts.param,
261
+ platform: 'meta',
262
+ account_id: opts.accountId,
263
+ campaign_id: opts.campaignId,
264
+ });
265
+ });
266
+ campaign
267
+ .command('create')
268
+ .description('Create a Meta campaign (created paused)')
269
+ .requiredOption('--account-id <id>', 'Meta ad account id')
270
+ .requiredOption('--name <name>', 'Campaign name')
271
+ .requiredOption('--objective <objective>', 'Meta objective, e.g. OUTCOME_TRAFFIC')
272
+ .option('--bidding-strategy <strategy>', 'Meta campaign-level bid strategy')
273
+ .option('--special-ad-categories <list>', 'Comma-separated regulated categories')
274
+ .option('--special-ad-category-country <list>', 'Comma-separated ISO country codes')
275
+ .option('--start-time <time>', 'Meta start_time')
276
+ .option('--stop-time <time>', 'Meta stop_time')
277
+ .option('--lifetime-budget <amount>', 'Meta lifetime budget in account-currency cents')
278
+ .option('--spend-cap <amount>', 'Meta spend cap in account-currency cents')
279
+ .option('--buying-type <type>', 'Meta buying type')
280
+ .requiredOption(...SUMMARY_OPT)
281
+ .option(...PARAM_OPT, collectParam, {})
282
+ .action(async (opts) => {
283
+ const categories = splitCommaList(opts.specialAdCategories);
284
+ const regulated = categories?.some((c) => c && c !== 'NONE');
285
+ if (regulated && !opts.specialAdCategoryCountry) {
286
+ emitError('usage', 'meta regulated categories require --special-ad-category-country', ExitCode.USAGE);
287
+ }
288
+ await runAdsWrite('meta', 'create_campaign', {
289
+ account_id: opts.accountId,
290
+ name: opts.name,
291
+ objective: opts.objective,
292
+ bidding_strategy: opts.biddingStrategy,
293
+ special_ad_categories: categories,
294
+ special_ad_category_country: splitCommaList(opts.specialAdCategoryCountry),
295
+ start_time: opts.startTime,
296
+ stop_time: opts.stopTime,
297
+ lifetime_budget: opts.lifetimeBudget
298
+ ? parseIntFlag('--lifetime-budget', opts.lifetimeBudget)
299
+ : undefined,
300
+ spend_cap: opts.spendCap ? parseIntFlag('--spend-cap', opts.spendCap) : undefined,
301
+ buying_type: opts.buyingType,
302
+ }, opts);
303
+ });
304
+ addMetaBulkCreate(campaign, 'bulk_create_campaigns', 'campaigns');
305
+ campaign
306
+ .command('update')
307
+ .description('Update a Meta campaign')
308
+ .requiredOption('--campaign-id <id>', 'Campaign id')
309
+ .option('--name <name>', 'New campaign name')
310
+ .option('--status <status>', 'ACTIVE | PAUSED')
311
+ .requiredOption(...SUMMARY_OPT)
312
+ .option(...ACCOUNT_ID_OPT)
313
+ .option(...PARAM_OPT, collectParam, {})
314
+ .action(async (opts) => {
315
+ await runAdsWrite('meta', 'update_campaign', { campaign_id: opts.campaignId, name: opts.name, status: opts.status }, opts);
316
+ });
317
+ campaign
318
+ .command('remove')
319
+ .description('Remove a Meta campaign')
320
+ .requiredOption('--campaign-id <id>', 'Campaign id')
321
+ .requiredOption(...SUMMARY_OPT)
322
+ .option(...ACCOUNT_ID_OPT)
323
+ .option(...PARAM_OPT, collectParam, {})
324
+ .action(async (opts) => {
325
+ await runAdsWrite('meta', 'remove_campaign', { campaign_id: opts.campaignId }, opts);
326
+ });
327
+ addActivatePause(campaign, 'meta', '--campaign-id', 'campaignId', 'campaign_id', 'update_campaign', 'campaign', 'ACTIVE');
328
+ }
329
+ function registerMetaAdsetCommands(meta) {
330
+ const adset = meta.command('adset').description('Create and manage Meta ad sets');
331
+ adset
332
+ .command('create')
333
+ .description('Create a Meta ad set')
334
+ .requiredOption('--campaign-id <id>', 'Parent campaign id')
335
+ .requiredOption('--name <name>', 'Ad set name')
336
+ .requiredOption('--optimization-goal <goal>', 'Meta optimization goal')
337
+ .requiredOption('--billing-event <event>', 'Meta billing event')
338
+ .option('--account-id <id>', 'Meta ad account id (aids resolution before campaign syncs)')
339
+ .requiredOption(...SUMMARY_OPT)
340
+ .option(...PARAM_OPT, collectParam, {})
341
+ .action(async (opts) => {
342
+ if (opts.param.targeting === undefined) {
343
+ emitError('usage', "meta ad sets require targeting — pass it with -p targeting='{...}'", ExitCode.USAGE);
344
+ }
345
+ await runAdsWrite('meta', 'create_adset', {
346
+ campaign_id: opts.campaignId,
347
+ name: opts.name,
348
+ optimization_goal: opts.optimizationGoal,
349
+ billing_event: opts.billingEvent,
350
+ account_id: opts.accountId,
351
+ }, opts);
352
+ });
353
+ addMetaBulkCreate(adset, 'bulk_create_adsets', 'ad sets');
354
+ adset
355
+ .command('update')
356
+ .description('Update a Meta ad set')
357
+ .requiredOption('--adset-id <id>', 'Ad set id')
358
+ .option('--name <name>', 'New name')
359
+ .option('--status <status>', 'ACTIVE | PAUSED')
360
+ .requiredOption(...SUMMARY_OPT)
361
+ .option(...ACCOUNT_ID_OPT)
362
+ .option(...PARAM_OPT, collectParam, {})
363
+ .action(async (opts) => {
364
+ await runAdsWrite('meta', 'update_adset', { adset_id: opts.adsetId, name: opts.name, status: opts.status }, opts);
365
+ });
366
+ adset
367
+ .command('remove')
368
+ .description('Remove a Meta ad set')
369
+ .requiredOption('--adset-id <id>', 'Ad set id')
370
+ .requiredOption(...SUMMARY_OPT)
371
+ .option(...ACCOUNT_ID_OPT)
372
+ .option(...PARAM_OPT, collectParam, {})
373
+ .action(async (opts) => {
374
+ await runAdsWrite('meta', 'remove_adset', { adset_id: opts.adsetId }, opts);
375
+ });
376
+ addActivatePause(adset, 'meta', '--adset-id', 'adsetId', 'adset_id', 'update_adset', 'ad set', 'ACTIVE');
377
+ }
378
+ function registerMetaCreativeCommands(meta) {
379
+ const creative = meta.command('creative').description('Create and manage Meta ad creatives');
380
+ creative
381
+ .command('create')
382
+ .description('Create a Meta ad creative for later ad creation')
383
+ .requiredOption('--account-id <id>', 'Meta ad account id')
384
+ .requiredOption('--name <name>', 'Creative name')
385
+ .requiredOption('--page-id <id>', 'Facebook Page id')
386
+ .option('--image-hash <hash>', 'Image hash from `soku ads meta asset upload-images`')
387
+ .option('--image-url <url>', 'Public https image URL (alternative to image hash)')
388
+ .option('--video-id <id>', 'Meta video id')
389
+ .option('--thumbnail-url <url>', 'Public https thumbnail URL for video creatives')
390
+ .option('--thumbnail-image-hash <hash>', 'Image hash for video thumbnail')
391
+ .option('--child-attachments <json>', 'Carousel child attachments JSON array')
392
+ .option('--object-story-id <id>', 'Existing Page post id to boost')
393
+ .option('--message <text>', 'Primary text')
394
+ .option('--headline <text>', 'Headline')
395
+ .option('--description <text>', 'Description')
396
+ .option('--link <url>', 'Destination URL')
397
+ .option('--caption <text>', 'Display URL caption')
398
+ .option('--call-to-action-type <type>', 'CTA type, e.g. LEARN_MORE or SIGN_UP')
399
+ .option('--url-tags <text>', 'Meta url_tags query string')
400
+ .option('--lead-gen-form-id <id>', 'Lead form id for lead creatives')
401
+ .option('--instagram-user-id <id>', 'Instagram user id')
402
+ .option('--instagram-actor-id <id>', 'Instagram actor id')
403
+ .requiredOption(...SUMMARY_OPT)
404
+ .option(...PARAM_OPT, collectParam, {})
405
+ .action(async (opts) => {
406
+ const primaryMedia = [
407
+ opts.imageHash,
408
+ opts.imageUrl,
409
+ opts.videoId,
410
+ opts.childAttachments,
411
+ opts.objectStoryId,
412
+ ].filter((value) => value !== undefined);
413
+ if (primaryMedia.length === 0) {
414
+ emitError('usage', 'creative create requires one media source: --image-hash, --image-url, --video-id, --child-attachments, or --object-story-id.', ExitCode.USAGE);
415
+ }
416
+ if (primaryMedia.length > 1) {
417
+ emitError('usage', 'creative create accepts only one primary media source.', ExitCode.USAGE);
418
+ }
419
+ await runAdsWrite('meta', 'create_ad_creative', {
420
+ account_id: opts.accountId,
421
+ name: opts.name,
422
+ page_id: opts.pageId,
423
+ image_hash: opts.imageHash,
424
+ image_url: opts.imageUrl,
425
+ video_id: opts.videoId,
426
+ thumbnail_url: opts.thumbnailUrl,
427
+ thumbnail_image_hash: opts.thumbnailImageHash,
428
+ child_attachments: opts.childAttachments
429
+ ? parseJsonValue('--child-attachments', opts.childAttachments)
430
+ : undefined,
431
+ object_story_id: opts.objectStoryId,
432
+ message: opts.message,
433
+ headline: opts.headline,
434
+ description: opts.description,
435
+ link: opts.link,
436
+ caption: opts.caption,
437
+ call_to_action_type: opts.callToActionType,
438
+ url_tags: opts.urlTags,
439
+ lead_gen_form_id: opts.leadGenFormId,
440
+ instagram_user_id: opts.instagramUserId,
441
+ instagram_actor_id: opts.instagramActorId,
442
+ }, opts);
443
+ });
444
+ addMetaBulkCreate(creative, 'bulk_create_ad_creatives', 'ad creatives');
445
+ }
446
+ function registerMetaAdCommands(meta) {
447
+ const ad = meta.command('ad').description('Create and manage Meta ads');
448
+ ad
449
+ .command('get')
450
+ .description('Read one Meta ad by id')
451
+ .requiredOption('--account-id <id>', 'Meta ad account id')
452
+ .requiredOption('--ad-id <id>', 'Meta ad id')
453
+ .option(...PARAM_OPT, collectParam, {})
454
+ .action(async (opts) => {
455
+ await callTypedAction('ads', 'get_ad', {
456
+ ...opts.param,
457
+ platform: 'meta',
458
+ account_id: opts.accountId,
459
+ ad_id: opts.adId,
460
+ });
461
+ });
462
+ ad
463
+ .command('create')
464
+ .description('Create a Meta ad under an ad set')
465
+ .requiredOption('--adset-id <id>', 'Parent Meta ad set id')
466
+ .requiredOption('--name <name>', 'Ad name')
467
+ .option('--account-id <id>', 'Meta ad account id (aids resolution before parent syncs)')
468
+ .option('--creative-id <id>', 'Existing Meta creative id')
469
+ .option('--creative <json>', 'Inline Meta creative spec JSON', (v) => parseJsonValue('--creative', v))
470
+ .requiredOption(...SUMMARY_OPT)
471
+ .option(...PARAM_OPT, collectParam, {})
472
+ .action(async (opts) => {
473
+ const hasCreative = opts.creativeId !== undefined ||
474
+ opts.creative !== undefined ||
475
+ opts.param.creative_id !== undefined ||
476
+ opts.param.creative !== undefined;
477
+ if (!hasCreative) {
478
+ emitError('usage', 'meta ad create requires --creative-id, --creative, or -p creative_id=...', ExitCode.USAGE);
479
+ }
480
+ await runAdsWrite('meta', 'create_ad', {
481
+ adset_id: opts.adsetId,
482
+ name: opts.name,
483
+ account_id: opts.accountId,
484
+ creative_id: opts.creativeId,
485
+ creative: opts.creative,
486
+ }, opts);
487
+ });
488
+ addMetaBulkCreate(ad, 'bulk_create_ads', 'ads');
489
+ ad
490
+ .command('update')
491
+ .description('Update a Meta ad')
492
+ .requiredOption('--ad-id <id>', 'Ad id')
493
+ .option('--status <status>', 'ACTIVE | PAUSED')
494
+ .requiredOption(...SUMMARY_OPT)
495
+ .option(...ACCOUNT_ID_OPT)
496
+ .option(...PARAM_OPT, collectParam, {})
497
+ .action(async (opts) => {
498
+ await runAdsWrite('meta', 'update_ad', { ad_id: opts.adId, status: opts.status }, opts);
499
+ });
500
+ ad
501
+ .command('remove')
502
+ .description('Remove a Meta ad')
503
+ .requiredOption('--ad-id <id>', 'Ad id')
504
+ .requiredOption(...SUMMARY_OPT)
505
+ .option(...ACCOUNT_ID_OPT)
506
+ .option(...PARAM_OPT, collectParam, {})
507
+ .action(async (opts) => {
508
+ await runAdsWrite('meta', 'remove_ad', { ad_id: opts.adId }, opts);
509
+ });
510
+ addActivatePause(ad, 'meta', '--ad-id', 'adId', 'ad_id', 'update_ad', 'ad', 'ACTIVE');
511
+ }
512
+ function registerGoogleCampaignCommands(google) {
513
+ const campaign = google.command('campaign').description('Create and manage Google campaigns');
514
+ campaign
515
+ .command('create')
516
+ .description('Create a Google campaign (created paused)')
517
+ .requiredOption('--account-id <id>', 'Google ad account id')
518
+ .requiredOption('--name <name>', 'Campaign name')
519
+ .requiredOption('--campaign-type <type>', 'SEARCH | DISPLAY | SHOPPING | VIDEO')
520
+ .option('--budget-daily-micros <micros>', 'Daily budget in micros')
521
+ .option('--budget-daily <amount>', 'Daily budget in account currency')
522
+ .option('--bidding-strategy <strategy>', 'Google bidding strategy')
523
+ .option('--start-date <date>', 'YYYY-MM-DD')
524
+ .option('--end-date <date>', 'YYYY-MM-DD')
525
+ .requiredOption(...SUMMARY_OPT)
526
+ .option(...PARAM_OPT, collectParam, {})
527
+ .action(async (opts) => {
528
+ const budgetMicros = resolveBudgetMicros(opts.budgetDaily, opts.budgetDailyMicros);
529
+ if (budgetMicros === undefined) {
530
+ emitError('usage', 'google campaigns require --budget-daily-micros (or --budget-daily)', ExitCode.USAGE);
531
+ }
532
+ await runAdsWrite('google', 'create_campaign', {
533
+ account_id: opts.accountId,
534
+ name: opts.name,
535
+ campaign_type: opts.campaignType,
536
+ budget_daily_micros: budgetMicros,
537
+ bidding_strategy: opts.biddingStrategy,
538
+ start_date: opts.startDate,
539
+ end_date: opts.endDate,
540
+ }, opts);
541
+ });
542
+ campaign
543
+ .command('update')
544
+ .description('Update a Google campaign')
545
+ .requiredOption('--campaign-id <id>', 'Campaign id')
546
+ .option('--name <name>', 'New campaign name')
547
+ .option('--status <status>', 'ENABLED | PAUSED')
548
+ .option('--budget-daily-micros <micros>', 'Daily budget in micros')
549
+ .option('--budget-daily <amount>', 'Daily budget in account currency')
550
+ .option('--start-date <date>', 'YYYY-MM-DD')
551
+ .option('--end-date <date>', 'YYYY-MM-DD')
552
+ .requiredOption(...SUMMARY_OPT)
553
+ .option(...ACCOUNT_ID_OPT)
554
+ .option(...PARAM_OPT, collectParam, {})
555
+ .action(async (opts) => {
556
+ await runAdsWrite('google', 'update_campaign', {
557
+ campaign_id: opts.campaignId,
558
+ name: opts.name,
559
+ status: opts.status,
560
+ budget_daily_micros: resolveBudgetMicros(opts.budgetDaily, opts.budgetDailyMicros),
561
+ start_date: opts.startDate,
562
+ end_date: opts.endDate,
563
+ }, opts);
564
+ });
565
+ campaign
566
+ .command('remove')
567
+ .description('Remove a Google campaign')
568
+ .requiredOption('--campaign-id <id>', 'Campaign id')
569
+ .requiredOption(...SUMMARY_OPT)
570
+ .option(...ACCOUNT_ID_OPT)
571
+ .option(...PARAM_OPT, collectParam, {})
572
+ .action(async (opts) => {
573
+ await runAdsWrite('google', 'remove_campaign', { campaign_id: opts.campaignId }, opts);
574
+ });
575
+ campaign
576
+ .command('pmax-create')
577
+ .description('Create a Google Performance Max campaign')
578
+ .requiredOption('--account-id <id>', 'Google ad account id')
579
+ .requiredOption('--name <name>', 'Campaign name')
580
+ .requiredOption('--budget-amount-micros <micros>', 'Daily budget in micros')
581
+ .requiredOption('--bidding-strategy <strategy>', 'Google bidding strategy')
582
+ .requiredOption('--asset-group <json>', 'PMax asset group payload JSON', (v) => parseJsonValue('--asset-group', v))
583
+ .requiredOption(...SUMMARY_OPT)
584
+ .option(...PARAM_OPT, collectParam, {})
585
+ .action(async (opts) => {
586
+ await runAdsWrite('google', 'create_pmax_campaign', {
587
+ account_id: opts.accountId,
588
+ name: opts.name,
589
+ budget_amount_micros: parseIntFlag('--budget-amount-micros', opts.budgetAmountMicros),
590
+ bidding_strategy: opts.biddingStrategy,
591
+ asset_group: opts.assetGroup,
592
+ }, opts);
593
+ });
594
+ addActivatePause(campaign, 'google', '--campaign-id', 'campaignId', 'campaign_id', 'update_campaign', 'campaign', 'ENABLED');
595
+ }
596
+ function registerGoogleAdGroupCommands(google) {
597
+ const adGroup = google.command('ad-group').description('Create and manage Google ad groups');
598
+ adGroup
599
+ .command('create')
600
+ .description('Create a Google ad group')
601
+ .requiredOption('--campaign-id <id>', 'Parent campaign id')
602
+ .requiredOption('--name <name>', 'Ad group name')
603
+ .option('--account-id <id>', 'Google ad account id (aids resolution before campaign syncs)')
604
+ .requiredOption(...SUMMARY_OPT)
605
+ .option(...PARAM_OPT, collectParam, {})
606
+ .action(async (opts) => {
607
+ await runAdsWrite('google', 'create_ad_group', { campaign_id: opts.campaignId, name: opts.name, account_id: opts.accountId }, opts);
608
+ });
609
+ adGroup
610
+ .command('update')
611
+ .description('Update a Google ad group')
612
+ .requiredOption('--ad-group-id <id>', 'Ad group id')
613
+ .option('--name <name>', 'New name')
614
+ .option('--status <status>', 'ENABLED | PAUSED')
615
+ .requiredOption(...SUMMARY_OPT)
616
+ .option(...ACCOUNT_ID_OPT)
617
+ .option(...PARAM_OPT, collectParam, {})
618
+ .action(async (opts) => {
619
+ await runAdsWrite('google', 'update_ad_group', { ad_group_id: opts.adGroupId, name: opts.name, status: opts.status }, opts);
620
+ });
621
+ adGroup
622
+ .command('remove')
623
+ .description('Remove a Google ad group')
624
+ .requiredOption('--ad-group-id <id>', 'Ad group id')
625
+ .requiredOption(...SUMMARY_OPT)
626
+ .option(...ACCOUNT_ID_OPT)
627
+ .option(...PARAM_OPT, collectParam, {})
628
+ .action(async (opts) => {
629
+ await runAdsWrite('google', 'remove_ad_group', { ad_group_id: opts.adGroupId }, opts);
630
+ });
631
+ addActivatePause(adGroup, 'google', '--ad-group-id', 'adGroupId', 'ad_group_id', 'update_ad_group', 'ad group', 'ENABLED');
632
+ }
633
+ function registerGoogleAdCommands(google) {
634
+ const ad = google.command('ad').description('Create and manage Google ads');
635
+ ad
636
+ .command('create')
637
+ .description('Create a Google responsive search ad under an ad group')
638
+ .requiredOption('--ad-group-id <id>', 'Parent Google ad group id')
639
+ .requiredOption('--final-urls <json>', 'JSON array of final URLs', (v) => parseJsonValue('--final-urls', v))
640
+ .requiredOption('--headlines <json>', 'JSON array of headlines', (v) => parseJsonValue('--headlines', v))
641
+ .requiredOption('--descriptions <json>', 'JSON array of descriptions', (v) => parseJsonValue('--descriptions', v))
642
+ .option('--account-id <id>', 'Google ad account id (aids resolution before parent syncs)')
643
+ .option('--path1 <text>', 'Google display path 1')
644
+ .option('--path2 <text>', 'Google display path 2')
645
+ .option('--tracking-url-template <text>', 'Google tracking URL template')
646
+ .option('--final-url-suffix <text>', 'Google final URL suffix')
647
+ .requiredOption(...SUMMARY_OPT)
648
+ .option(...PARAM_OPT, collectParam, {})
649
+ .action(async (opts) => {
650
+ await runAdsWrite('google', 'create_ad', {
651
+ adset_id: opts.adGroupId,
652
+ final_urls: opts.finalUrls,
653
+ headlines: opts.headlines,
654
+ descriptions: opts.descriptions,
655
+ account_id: opts.accountId,
656
+ path1: opts.path1,
657
+ path2: opts.path2,
658
+ tracking_url_template: opts.trackingUrlTemplate,
659
+ final_url_suffix: opts.finalUrlSuffix,
660
+ }, opts);
661
+ });
662
+ ad
663
+ .command('update')
664
+ .description('Update a Google ad')
665
+ .requiredOption('--ad-id <id>', 'Ad id')
666
+ .option('--status <status>', 'ENABLED | PAUSED')
667
+ .requiredOption(...SUMMARY_OPT)
668
+ .option(...ACCOUNT_ID_OPT)
669
+ .option(...PARAM_OPT, collectParam, {})
670
+ .action(async (opts) => {
671
+ await runAdsWrite('google', 'update_ad', { ad_id: opts.adId, status: opts.status }, opts);
672
+ });
673
+ ad
674
+ .command('remove')
675
+ .description('Remove a Google ad')
676
+ .requiredOption('--ad-id <id>', 'Ad id')
677
+ .requiredOption(...SUMMARY_OPT)
678
+ .option(...ACCOUNT_ID_OPT)
679
+ .option(...PARAM_OPT, collectParam, {})
680
+ .action(async (opts) => {
681
+ await runAdsWrite('google', 'remove_ad', { ad_id: opts.adId }, opts);
682
+ });
683
+ addActivatePause(ad, 'google', '--ad-id', 'adId', 'ad_id', 'update_ad', 'ad', 'ENABLED');
684
+ }
685
+ function registerGoogleKeywordCommands(google) {
686
+ const keyword = google.command('keyword').description('Manage Google ad group keywords');
687
+ keyword
688
+ .command('add')
689
+ .description('Add keywords to a Google ad group')
690
+ .requiredOption('--ad-group-id <id>', 'Ad group id')
691
+ .requiredOption('--keywords <list>', 'Comma-separated keyword texts')
692
+ .option('--account-id <id>', 'Google ad account id (aids resolution before ad group syncs)')
693
+ .requiredOption(...SUMMARY_OPT)
694
+ .option(...PARAM_OPT, collectParam, {})
695
+ .action(async (opts) => {
696
+ await runAdsWrite('google', 'add_keywords', {
697
+ ad_group_id: opts.adGroupId,
698
+ keywords: splitCommaList(opts.keywords),
699
+ account_id: opts.accountId,
700
+ }, opts);
701
+ });
702
+ keyword
703
+ .command('update')
704
+ .description('Update a Google keyword criterion')
705
+ .requiredOption('--ad-group-id <id>', 'Ad group id')
706
+ .requiredOption('--criterion-id <id>', 'Keyword criterion id')
707
+ .option('--status <status>', 'ENABLED | PAUSED')
708
+ .requiredOption(...SUMMARY_OPT)
709
+ .option(...ACCOUNT_ID_OPT)
710
+ .option(...PARAM_OPT, collectParam, {})
711
+ .action(async (opts) => {
712
+ await runAdsWrite('google', 'update_keyword', {
713
+ ad_group_id: opts.adGroupId,
714
+ criterion_id: opts.criterionId,
715
+ status: opts.status,
716
+ }, opts);
717
+ });
718
+ keyword
719
+ .command('remove')
720
+ .description('Remove a Google keyword criterion')
721
+ .requiredOption('--ad-group-id <id>', 'Ad group id')
722
+ .requiredOption('--criterion-id <id>', 'Keyword criterion id')
723
+ .requiredOption(...SUMMARY_OPT)
724
+ .option(...ACCOUNT_ID_OPT)
725
+ .option(...PARAM_OPT, collectParam, {})
726
+ .action(async (opts) => {
727
+ await runAdsWrite('google', 'remove_keyword', { ad_group_id: opts.adGroupId, criterion_id: opts.criterionId }, opts);
728
+ });
729
+ }
730
+ /** Attach platform-first Tier-2 entity sub-groups to the shared `ads` group. */
731
+ export function registerAdsEntities(ads) {
732
+ const meta = ads.command('meta').description('Meta Ads write commands');
733
+ registerMetaAccountCommands(meta);
734
+ registerMetaAssetCommands(meta);
735
+ registerMetaCampaignCommands(meta);
736
+ registerMetaAdsetCommands(meta);
737
+ registerMetaCreativeCommands(meta);
738
+ registerMetaAdCommands(meta);
739
+ const google = ads.command('google').description('Google Ads write commands');
740
+ registerGoogleCampaignCommands(google);
741
+ registerGoogleAdGroupCommands(google);
742
+ registerGoogleAdCommands(google);
743
+ registerGoogleKeywordCommands(google);
744
+ }
745
+ /** Register the `ads` command group: Tier-1 generated data actions (read +
746
+ * write) plus Tier-2 ergonomic entity commands, on a single shared group. */
747
+ export function registerAdsCommands(program) {
748
+ const groups = buildGeneratedCommands(program, loadManifest(), { namespaces: ['ads'] });
749
+ const ads = groups.get('ads') ?? program.command('ads').description('ads data capabilities');
750
+ registerAdsEntities(ads);
751
+ }
752
+ //# sourceMappingURL=ads.js.map