@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 +4 -0
- package/dist/main.js +135 -5
- package/package.json +1 -1
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
|