@wrangled/cli 0.3.147 → 0.3.149

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.
package/dist/guide.js CHANGED
@@ -528,6 +528,9 @@ ${cli} meta ads cancel-turn-on <ad-id> --reason "<why>" cancel a paused ad's s
528
528
  ${cli} meta audience create --account act_<id> --name "<name>" --kind website|video|page|instagram --retention-days <n> (--pixel <id> | --video <id> --watched 50% | --page <id> --engagement engaged | --instagram <id> --engagement engaged) --idempotency-key <key> make a custom audience in an account on the control list
529
529
  ${cli} meta audience lookalike --account act_<id> --name "<name>" --seed <audience-id> --country US --percent 1 --idempotency-key <key> make a lookalike of a custom audience there; customer-list upload is not offered
530
530
  ${cli} meta ad preview <ad-id> [--placement <ad_format>]... | meta ad review <ad-id> Meta's preview links of an ad, or whether Meta approved it and why not, asked now
531
+ ${cli} meta ad status <ad-id> [--name <expected>] the ad's status, id and name from Meta now; with --name, exits 1 unless it is active and its id and name match
532
+ ${cli} meta ad pause <ad-id> --idempotency-key <key> --reason "<why>" [--name "<expected>"] [--run <run-id>] pause one ad (it can only pause) and read it from Meta again; exits 0 only when it reads PAUSED; the same key never pauses twice
533
+ ${cli} meta ad pauses [<ad-id>] [--since <YYYY-MM-DD>] every pause attempt with its result, refusals too
531
534
  ${cli} meta lead-forms <page-id> | meta targeting search "<words>" [--account act_<id>] a Page's lead forms, or interest targeting ids, read from Meta
532
535
  ${cli} meta adset budget <id> | meta campaign budget <id> (--percent <n> | --daily <n>) --reason "<why>" change a daily budget within the workspace's limits
533
536
  ${cli} meta adset edit <id> (--name "<name>" | --targeting @spec.json | --bid-strategy <s> [--bid-amount <n>] | --optimization-goal <goal>) --reason "<why>" edit an ad set, never turning it on or off
@@ -564,6 +567,7 @@ ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--j
564
567
  ${cli} number-alerts [--since <date>] the key numbers that swung far from normal, day by day
565
568
  ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
566
569
  ${cli} lessons [--status active|retired] [--about ads|pages|funnel|messages|cross_channel] [--dimension <d>] | lessons add --file <lesson.json> | lessons retire <id> --note "<why>" lessons from winners and flops, writing one, retiring one
570
+ ${cli} tests p1 this brand's P1 test ads by verdict, judged on its own sales against its P1 thresholds, with Meta's count beside
567
571
  ${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] | tests results <id> | tests decide <id> --decision <d> [--note "<why>"] funnel tests, one change each
568
572
  ${cli} ads tags [--ad <id>] [--status <status>] [--limit <n>] | ads tag --file <tags.json> the marketing tags of ads not made here, and suggesting them
569
573
  ${cli} ads names [--unparsed] Meta ad names and the tags each gives; --unparsed lists those that fit no pattern
package/dist/main.js CHANGED
@@ -30,6 +30,7 @@ import { SKILL_NAME_PATTERN } from "./skill-zip.js";
30
30
  import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
31
31
  import { versionAtLeast } from "./version.js";
32
32
  const DEFAULT_WAIT_SECONDS = 300;
33
+ const META_AD_PAUSE_USAGE = 'meta ad pause <ad-id> --idempotency-key <key> --reason "<why>" [--name "<expected>"] [--run <run-id>]';
33
34
  const META_CHANGE_COLUMNS = ["id", "level", "object", "change", "status", "when", "by", "reason"];
34
35
  const GOOGLE_ADS_CHANGE_COLUMNS = ["id", "account", "level", "object", "change", "status", "when", "by", "reason"];
35
36
  const AD_CHANGE_COLUMNS = ["platform", ...GOOGLE_ADS_CHANGE_COLUMNS];
@@ -1997,6 +1998,83 @@ const commands = {
1997
1998
  console.log(`Issue${issue.level === null ? "" : ` (${issue.level.toLowerCase()})`}: ${[issue.summary, issue.message].filter(Boolean).join(". ")}`);
1998
1999
  },
1999
2000
  },
2001
+ "meta ad status": {
2002
+ usage: "meta ad status <ad-id> [--name <expected>] the ad's status, id and name from Meta now; with --name, exits 1 unless it is safe to pause",
2003
+ async run(args, options) {
2004
+ if (args.length !== 1)
2005
+ throw new CliError("Usage: meta ad status <ad-id> [--name <expected>]", exitCodes.usage);
2006
+ const query = options.name === undefined ? "" : `?${new URLSearchParams({ name: options.name })}`;
2007
+ const live = await api(await resolveConnection(options), `/api/v1/meta/ads/${encodeURIComponent(args[0])}/status${query}`);
2008
+ const failed = live.check && [!live.check.active && "it is not active", !live.check.idMatches && "Meta's id differs", !live.check.nameMatches && "its name differs"].filter((f) => f !== false);
2009
+ if (options.format === "json")
2010
+ printJson(live);
2011
+ else {
2012
+ printFields({
2013
+ ad: live.adId,
2014
+ id: live.id,
2015
+ name: live.name,
2016
+ status: live.status,
2017
+ "effective status": live.effectiveStatus,
2018
+ "read at": live.readAt,
2019
+ check: failed && (failed.length === 0 ? "ok" : `failed: ${failed.join(", ")}`),
2020
+ });
2021
+ }
2022
+ if (failed && failed.length > 0) {
2023
+ throw new CliError(`Ad ${live.adId} is not safe to pause as "${live.check.expectedName}": ${failed.join(", ")} (Meta says ${live.effectiveStatus}, id ${live.id ?? "none"}, name "${live.name ?? ""}").`, exitCodes.error);
2024
+ }
2025
+ },
2026
+ },
2027
+ "meta ad pause": {
2028
+ usage: `${META_AD_PAUSE_USAGE} pause one ad, never anything else, and read it from Meta again to confirm; exits 1 unless it reads PAUSED`,
2029
+ async run(args, options) {
2030
+ if (options.status !== undefined && options.status.toLowerCase() !== "paused") {
2031
+ throw new CliError(`meta ad pause only pauses; it cannot set status ${options.status}. Usage: ${META_AD_PAUSE_USAGE}`, exitCodes.usage);
2032
+ }
2033
+ if (args.length !== 1 || !options.idempotencyKey || !options.reason)
2034
+ throw new CliError(`Usage: ${META_AD_PAUSE_USAGE}`, exitCodes.usage);
2035
+ const attempt = await api(await resolveConnection(options), `/api/v1/meta/ads/${encodeURIComponent(args[0])}/pause`, {
2036
+ idempotencyKey: options.idempotencyKey,
2037
+ reason: options.reason,
2038
+ ...(options.name === undefined ? {} : { name: options.name }),
2039
+ ...(options.run === undefined ? {} : { runId: options.run }),
2040
+ });
2041
+ if (options.format === "json")
2042
+ printJson(attempt);
2043
+ else {
2044
+ printFields({
2045
+ ad: attempt.adId,
2046
+ result: attempt.result,
2047
+ before: attempt.beforeStatus,
2048
+ after: attempt.afterStatus,
2049
+ "effective status": attempt.effectiveStatus,
2050
+ "read at": attempt.readAt,
2051
+ change: attempt.changeId,
2052
+ attempt: attempt.id,
2053
+ error: attempt.error,
2054
+ });
2055
+ }
2056
+ if (attempt.result !== "paused" && attempt.result !== "already_paused") {
2057
+ throw new CliError(`Ad ${attempt.adId} is not confirmed paused (${attempt.result})${attempt.error === null ? "" : `: ${attempt.error}`}`, exitCodes.error);
2058
+ }
2059
+ },
2060
+ },
2061
+ "meta ad pauses": {
2062
+ usage: "meta ad pauses [<ad-id>] [--since <YYYY-MM-DD>] every meta ad pause attempt, newest first, refusals too",
2063
+ async run(args, options) {
2064
+ if (args.length > 1)
2065
+ throw new CliError("Usage: meta ad pauses [<ad-id>] [--since <YYYY-MM-DD>]", exitCodes.usage);
2066
+ const query = new URLSearchParams();
2067
+ if (args[0] !== undefined)
2068
+ query.set("adId", args[0]);
2069
+ if (options.since)
2070
+ query.set("since", options.since);
2071
+ const { attempts } = await api(await resolveConnection(options), `/api/v1/meta/pause-attempts${query.size ? `?${query}` : ""}`);
2072
+ if (options.format === "json")
2073
+ return printJson({ attempts });
2074
+ const rows = attempts.map((a) => ({ when: a.createdAt, ad: a.adId, result: a.result, before: a.beforeStatus, after: a.afterStatus, key: a.idempotencyKey, by: a.requestedBy, error: a.error }));
2075
+ console.log(rows.length ? renderTable(["when", "ad", "result", "before", "after", "key", "by", "error"], rows) : "No pause attempts yet.");
2076
+ },
2077
+ },
2000
2078
  "meta lead-forms": {
2001
2079
  usage: "meta lead-forms <page-id> a Facebook Page's lead forms, for a lead ad",
2002
2080
  async run(args, options) {
@@ -3076,6 +3154,17 @@ const commands = {
3076
3154
  console.log(renderTable(TEST_COLUMNS, tests.map(testRow)));
3077
3155
  },
3078
3156
  },
3157
+ "tests p1": {
3158
+ usage: "tests p1 this brand's P1 test ads by verdict (working, not working, paused, ready for P3, still testing), judged on its own sales against its P1 thresholds",
3159
+ async run(args, options) {
3160
+ if (args.length > 0)
3161
+ throw new CliError("Usage: tests p1", exitCodes.usage);
3162
+ const report = await api(await resolveConnection(options), "/api/v1/p1-tests");
3163
+ if (options.format === "json")
3164
+ return printJson(report);
3165
+ process.stdout.write(p1TestsText(report));
3166
+ },
3167
+ },
3079
3168
  "tests show": {
3080
3169
  usage: "tests show <id|test_id> one funnel test in full",
3081
3170
  async run(args, options) {
@@ -4169,6 +4258,45 @@ const FIRST_TOUCH_SALE_RULE = "Each sale goes to the Facebook ad that first brou
4169
4258
  * The winners report as plain Markdown: which goal the ads are judged on and what one means, then the dates asked for and,
4170
4259
  * unless they are the same dates, the last 30 days, each currency apart.
4171
4260
  */
4261
+ const P1_GROUPS = [
4262
+ ["working", "Working"],
4263
+ ["not_working", "Not working"],
4264
+ ["paused", "Paused"],
4265
+ ["ready_for_p3", "Ready for P3"],
4266
+ ["testing", "Still testing"],
4267
+ [null, "No verdict"],
4268
+ ];
4269
+ function p1TestsText(report) {
4270
+ const { window, thresholds: t, judgedOn } = report;
4271
+ const lines = [`P1 tests for ${report.brand}, ${window.from} to ${window.to} (${window.days} days), judged on ${judgedOn.label}`];
4272
+ const amount = (n, currency) => (n === null ? "" : currency === null ? n.toFixed(2) : money(n, currency));
4273
+ if (t !== null) {
4274
+ const currency = report.ads.find((ad) => ad.currency !== null)?.currency ?? null;
4275
+ lines.push(`Thresholds: not working after ${amount(t.minSpend, currency)} spent; working at ${amount(t.maxCostPerResult, currency)} or less per ${judgedOn.one}; ` +
4276
+ `ready for P3 at ${t.p3MinResults} or more at ${amount(t.p3MaxCostPerResult, currency)} or less each`);
4277
+ }
4278
+ if (report.missing !== null)
4279
+ lines.push(report.missing);
4280
+ const columns = ["ad", "campaign", "spend", `results (${judgedOn.label})`, "Meta's count", "cost per result", "why"];
4281
+ for (const [verdict, title] of P1_GROUPS) {
4282
+ const ads = report.ads.filter((ad) => ad.verdict === verdict);
4283
+ if (ads.length === 0)
4284
+ continue;
4285
+ const rows = ads.map((ad) => ({
4286
+ ad: `${ad.name} (${ad.adId})`,
4287
+ campaign: ad.campaignName,
4288
+ spend: amount(ad.spend, ad.currency),
4289
+ [columns[3]]: String(ad.results),
4290
+ "Meta's count": ad.metaResults === null ? "" : `${ad.metaResults}${ad.metaDisagrees ? " (differs)" : ""}`,
4291
+ "cost per result": amount(ad.costPerResult, ad.currency),
4292
+ why: ad.because,
4293
+ }));
4294
+ lines.push("", `${title} (${ads.length})`, renderTable(columns, rows));
4295
+ }
4296
+ if (report.ads.length === 0)
4297
+ lines.push("", "No live ads in the P1 campaigns.");
4298
+ return `${lines.join("\n")}\n`;
4299
+ }
4172
4300
  function winnersText(report) {
4173
4301
  const { week, last30Days } = report;
4174
4302
  const words = winnersWords(report);
@@ -4953,9 +5081,9 @@ Options:
4953
5081
  --sort-detail <label> content collections create: the detail whose date orders the collection, newest first, such as "Check date"
4954
5082
  --added content collections sort: order the collection by when items were added, newest first
4955
5083
  --tool-run <id> a succeeded tool run content create and edit link; repeat for several
4956
- --run <id> the agent run making the content (content) or the Meta change (meta ads|adset), the tool run to
5084
+ --run <id> the agent run making the content (content) or the Meta change (meta ads|adset, meta ad pause), the tool run to
4957
5085
  record (creatives create), or the tool run whose outputs media list shows
4958
- --reason <why> why: what meta ads|adset pause|resume, meta ads move, meta ad|adset copy, meta adset|campaign budget, meta adset edit, meta ad edit, meta ad rename, meta campaign create, meta audience create|lookalike, meta changes undo, meta ad create --status active and
5086
+ --reason <why> why: what meta ads|adset pause|resume, meta ad pause, meta ads move, meta ad|adset copy, meta adset|campaign budget, meta adset edit, meta ad edit, meta ad rename, meta campaign create, meta audience create|lookalike, meta changes undo, meta ad create --status active and
4959
5087
  the google-ads commands record in the change log
4960
5088
  --percent <n> with meta adset|campaign budget or google-ads campaign budget, the whole-number percent change from the daily budget now (--percent=-10 lowers it);
4961
5089
  with meta audience lookalike, how wide, 1 (the closest 1%) to 10
@@ -4970,7 +5098,8 @@ Options:
4970
5098
  --roas-floor <n> meta adset edit, with --bid-strategy LOWEST_COST_WITH_MIN_ROAS: the least return on ad spend times 10000 (15000 is 1.5)
4971
5099
  --targeting <json> meta adset edit: the ad set's whole new targeting spec as Graph takes it, as JSON, @file.json, or - to read it from stdin
4972
5100
  --optimization-goal <g> meta adset edit: what Meta optimizes the ad set for, such as OFFSITE_CONVERSIONS or LINK_CLICKS
4973
- --name <name> meta ad rename and meta adset edit: the new name; an ad's name that starts with a SKU must keep it at the front
5101
+ --name <name> meta ad rename and meta adset edit: the new name; an ad's name that starts with a SKU must keep it at the front;
5102
+ meta ad status and meta ad pause: the name you expect the ad to have, compared exactly
4974
5103
  --special <category> meta campaign create: a special ad category, such as HOUSING or CREDIT; repeat for several
4975
5104
  --paused meta campaign create and meta ad|adset copy: make it paused instead of on from the next midnight in the account's
4976
5105
  time zone; it is also made paused, saying why, when that start would be refused
@@ -4995,12 +5124,13 @@ Options:
4995
5124
  --slot square|tall meta ad edit: for an ad with a square and a tall (9:16) version, which one --file replaces
4996
5125
  --link <https url> meta ad create: where the ad links; the SKU's tracking tags are added
4997
5126
  --cta <type> meta ad create: the button, such as SHOP_NOW
4998
- --status scheduled|paused meta ad create and meta ad promote: how the new ad starts (default scheduled: the next midnight in the account's time zone)
5127
+ --status scheduled|paused meta ad create and meta ad promote: how the new ad starts (default scheduled: the next midnight in the account's time zone);
5128
+ meta ad pause takes only paused and refuses any other
4999
5129
  --sku <sku> with media list, only the output that became this creative
5000
5130
  --tool <tool> with media list, only this tool's outputs, such as kie:nano-banana
5001
5131
  --kind image|video with media list, only images or only videos
5002
5132
  --since, --until <YYYY-MM-DD | ISO time> with media list, made at or after --since and before --until (a day: through
5003
- its end, in UTC); with changes, meta changes or google-ads changes, only changes from --since on
5133
+ its end, in UTC); with changes, meta changes or google-ads changes, only changes from --since on; with meta ad pauses, only attempts from --since on
5004
5134
  --cursor <media-id> with media list, the next page: the id it printed after "More"
5005
5135
  --platform, --external-id where content status launched says it was published, and that platform's id for it
5006
5136
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.147",
3
+ "version": "0.3.149",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {