@wrangled/cli 0.3.37 → 0.3.40
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/api.js +2 -0
- package/dist/guide.js +9 -8
- package/dist/main.js +94 -19
- package/dist/playbooks.js +6 -6
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -21,6 +21,8 @@ export class UnreadableCredentialsError extends CliError {
|
|
|
21
21
|
* in src/lib/knowledge/tags.ts. format's flag is --ad-format, because --format picks the output.
|
|
22
22
|
*/
|
|
23
23
|
export const entryTagFlags = { angle: "angle", hook_kind: "hook-kind", format: "ad-format", awareness: "awareness" };
|
|
24
|
+
/** The days before a sale in which a click or reply helps it, as the app counts them (MESSAGE_CREDIT_DAYS). */
|
|
25
|
+
export const MESSAGE_CREDIT_DAYS = 7;
|
|
24
26
|
/** Copies of the server's IMPORT_LIMITS and import name rule; src/lib/cli-consistency.test.ts keeps them in step. */
|
|
25
27
|
export const IMPORT_LIMITS = { bytes: 50 * 1024 * 1024, rows: 1_000_000, columns: 500 };
|
|
26
28
|
export const IMPORT_NAME_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
package/dist/guide.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IMPORT_LIMITS } from "./api.js";
|
|
1
|
+
import { IMPORT_LIMITS, MESSAGE_CREDIT_DAYS } from "./api.js";
|
|
2
2
|
import { brand, envPrefix } from "./brand.js";
|
|
3
3
|
import { BACK_TAG_SKILL_NAME, CRO_SKILL_NAME, GRAMMAR_SKILL_NAME, LEARN_NAMES_SKILL_NAME, LESSONS_SKILL_NAME, MAP_OFFERS_SKILL_NAME, PLAYBOOK_CHOICES, STORY_SKILL_NAME, WEEKLY_LOOP_SKILL_NAME } from "./playbooks.js";
|
|
4
4
|
import { PULL_STATE_FILE } from "./pull.js";
|
|
@@ -40,7 +40,7 @@ ${brand.name} does not build pages; it hands whoever builds one what is already
|
|
|
40
40
|
|
|
41
41
|
## Which Facebook ads and pages won: winners and flops
|
|
42
42
|
|
|
43
|
-
\`${cli} winners\` says which Facebook ads won and which flopped, for
|
|
43
|
+
\`${cli} winners\` says which Facebook ads won and which flopped, for the dates asked for and for the 30 days ending on their last day, judged on real sales: each sale goes to the Facebook ad that first brought the buyer in, and payments typed into GoHighLevel by hand never count toward an ad. An ad with no sales that has spent less than twice the account's cost per sale is too new to judge on sales, so it is judged on cost per sign-up and listed apart. Each ad says in a sentence why it is on its list. Each currency also says how many of its Facebook sales could be tied to a specific ad (\`adSales\` of \`facebookSales\` in the JSON): a sale whose link names no single ad counts toward Facebook but toward no ad. It judges the landing pages the ads sent people to the same way, and each ad on each page as an ad-and-page pair: a page or pair counts only once the page reader has read the page and it got at least 100 visits in the dates, and pairs are listed only for an ad that sent people to two or more pages, or a page that got people from two or more ads, each naming what the same ad cost on its other pages. \`--week <YYYY-MM-DD>\` picks any day of a Monday-to-Sunday week (default: the latest complete week), \`--days <n>\` the last n complete days ending yesterday, and \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` any dates; use one of them, at most 90 days. Check as often as suits the client, daily or every few days: a short window holds few sales, so more ads are judged on sign-ups, and the report says so. \`--top <n>\` how many ads, pages or pairs each list shows (default 5, at most 20), and \`--format json\` gives the report as data.
|
|
44
44
|
|
|
45
45
|
## What winners and flops taught: lessons
|
|
46
46
|
|
|
@@ -50,11 +50,11 @@ Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<
|
|
|
50
50
|
|
|
51
51
|
## Sending winners to Exodus
|
|
52
52
|
|
|
53
|
-
\`${cli} winners send-to-exodus\` sends the
|
|
53
|
+
\`${cli} winners send-to-exodus\` sends the winners to Exodus so it makes new ads from them: one package per Facebook page, each winner with its reason, its numbers, and a note of the lessons it proves, persona lessons and the freshest customer words of the dates. Winners whose page, words or format are unknown are left out and listed with why. \`--week <YYYY-MM-DD>\`, \`--days <n>\` or \`--from\` and \`--to\` pick the dates, as for \`${cli} winners\` (default: the latest complete week). \`--dry-run\` asks Exodus what it would create or update and writes nothing. Sending the same dates again is safe: an unchanged package is not sent twice, and Exodus updates each ad in place. A real send needs the workspace's Exodus brand and a Scrape Creators key on the Exodus account.
|
|
54
54
|
|
|
55
55
|
## The weekly loop: a sample for an orchestrator agent
|
|
56
56
|
|
|
57
|
-
The \`weekly-loop\` playbook (\`${cli} skill install weekly-loop\` installs it as the \`${WEEKLY_LOOP_SKILL_NAME}\` skill; \`${cli} docs weekly-loop\` prints it) is a sample
|
|
57
|
+
The \`weekly-loop\` playbook (\`${cli} skill install weekly-loop\` installs it as the \`${WEEKLY_LOOP_SKILL_NAME}\` skill; \`${cli} docs weekly-loop\` prints it) is a sample to borrow from, not a schedule, for a local agent that runs a client's Facebook ads with its owner: its steps can run daily, several times a week or weekly, and the winners report covers whatever dates are asked for. It names the command and the MCP tool for each step: the winners and flops report, lessons, fresh customer words, the brief, the offers and funnels map, making ads (Exodus, paid tools or the agent's own skills), and the results of what was published, read back by Adzara's labels. Judge winners by ${brand.name}'s real-sales report (\`${cli} winners\`); Adzara's results are Meta's count. Approvals and decisions happen in the agent's own session with the owner; ${brand.name} records none. Ads are published and changed through Adzara, never through ${brand.name}.
|
|
58
58
|
|
|
59
59
|
## Trying one change: funnel tests
|
|
60
60
|
|
|
@@ -113,7 +113,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
|
|
|
113
113
|
|
|
114
114
|
## Webhook addresses
|
|
115
115
|
|
|
116
|
-
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. Add \`--kind thrivecart_events\` for a ThriveCart account's webhook: affiliate commissions earned, taken back and paid out also land, typed, in \`thrivecart_affiliate_commissions\`, where they stay after their events pass the retention, and \`daily_spend\` counts them. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
116
|
+
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. Add \`--kind thrivecart_events\` for a ThriveCart account's webhook: affiliate commissions earned, taken back and paid out also land, typed, in \`thrivecart_affiliate_commissions\`, where they stay after their events pass the retention, and \`daily_spend\` counts them. Add \`--kind roezan\` once the workspace's Roezan source is connected: the server uses the source's key to ask Roezan to send each new broadcast to the link (\`subscription\` in the answer says whether that worked; if not, add the link in Roezan's Webhooks tab), each broadcast an event names lands in \`roezan_broadcast_events\`, and the Roezan source reads it on its next hourly sync. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
117
117
|
|
|
118
118
|
## Limits
|
|
119
119
|
|
|
@@ -298,8 +298,9 @@ ${cli} guide what this workspace's data holds, how fresh
|
|
|
298
298
|
${cli} brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>] the offer, voice, best and worst ads, pages, results and audience: start here before writing ads
|
|
299
299
|
${cli} pages brief --step <key> [--funnel <key>] | --page <ref> the offer, step, current page, best and worst pages of its kind, its ads and how to hand a page back: start here before writing a landing page
|
|
300
300
|
${cli} pages results [--from <date>] [--to <date>] [--page <ref>] each landing page's, and each ad and page pair's, real first-touch sales, cost per sale and sales per page view
|
|
301
|
-
${cli}
|
|
302
|
-
${cli} winners
|
|
301
|
+
${cli} messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <id>] real sales after each follow-up email, text and sequence: every message the buyer clicked or replied to in the ${MESSAGE_CREDIT_DAYS} days before buying (helped), and the latest one (last nudge)
|
|
302
|
+
${cli} winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] which Facebook ads and landing pages won and flopped in a week or any dates and over 30 days, judged on first-touch sales
|
|
303
|
+
${cli} winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run] send the winners, with their reasons, lessons and customer words, to Exodus; safe to send again
|
|
303
304
|
${cli} warehouse schema [--summary] [--table <name> ...] [--search <words>] list tables and columns; --summary for one row per table
|
|
304
305
|
${cli} warehouse query "<sql>" run one read-only query (or --file <path>, or stdin); --out <file.csv | file.ndjson | file.json> saves the rows; add --all for every row
|
|
305
306
|
${cli} funnel [--from <date>] [--to <date>] [--grain day|week|month] spend, leads, calls and sales per period, with totals
|
|
@@ -309,7 +310,7 @@ ${cli} sources list list data sources and their health
|
|
|
309
310
|
${cli} sources show <id> show one source's health, tables and last sync runs
|
|
310
311
|
${cli} sources settings <id> [<key>=<value> ...] show a key source's settings, or check them with its saved key and save them
|
|
311
312
|
${cli} intake list list webhook addresses and their health
|
|
312
|
-
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun|thrivecart_events] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
313
|
+
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun|thrivecart_events|roezan] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
313
314
|
${cli} intake rotate <id> replace an address's token; prints the new link and token once
|
|
314
315
|
${cli} intake signing-key <id> --signing-key-stdin replace the workspace's signing key for that address's kind (mailgun)
|
|
315
316
|
${cli} intake disable <id> | enable <id> turn an address off or back on
|
package/dist/main.js
CHANGED
|
@@ -669,13 +669,24 @@ const commands = {
|
|
|
669
669
|
printPageResults(results);
|
|
670
670
|
},
|
|
671
671
|
},
|
|
672
|
+
"messages results": {
|
|
673
|
+
usage: "messages results [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--channel email|text] [--sequence <workflow id>] (which follow-up emails, texts and sequences bring real sales)",
|
|
674
|
+
async run(args, options) {
|
|
675
|
+
if (args.length > 0)
|
|
676
|
+
throw new CliError("Usage: messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <workflow id>]", exitCodes.usage);
|
|
677
|
+
const results = await api(await resolveConnection(options.apiUrl), `/api/v1/messages/results${reportQuery(options, { channel: options.channel, sequence: options.sequence })}`);
|
|
678
|
+
if (options.format === "json")
|
|
679
|
+
return printJson(results);
|
|
680
|
+
printMessageResults(results);
|
|
681
|
+
},
|
|
682
|
+
},
|
|
672
683
|
winners: {
|
|
673
|
-
usage: "winners [--week <YYYY-MM-DD>] [--top <n>] (which Facebook ads and landing pages won and flopped, judged on real first-touch sales)",
|
|
684
|
+
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] (which Facebook ads and landing pages won and flopped, judged on real first-touch sales)",
|
|
674
685
|
async run(args, options) {
|
|
675
686
|
if (args.length > 0)
|
|
676
|
-
throw new CliError("Usage: winners [--week <date>] [--top <n>]", exitCodes.usage);
|
|
687
|
+
throw new CliError("Usage: winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>]", exitCodes.usage);
|
|
677
688
|
const query = new URLSearchParams();
|
|
678
|
-
for (const [name, value] of Object.entries({
|
|
689
|
+
for (const [name, value] of Object.entries({ ...winnersPeriod(options), top: options.top }))
|
|
679
690
|
if (value !== undefined)
|
|
680
691
|
query.set(name, value);
|
|
681
692
|
const report = await api(await resolveConnection(options.apiUrl), `/api/v1/winners${query.size ? `?${query}` : ""}`);
|
|
@@ -685,14 +696,12 @@ const commands = {
|
|
|
685
696
|
},
|
|
686
697
|
},
|
|
687
698
|
"winners send-to-exodus": {
|
|
688
|
-
usage: "winners send-to-exodus [--week <YYYY-MM-DD>] [--dry-run] send the
|
|
699
|
+
usage: "winners send-to-exodus [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--dry-run] send the winning ads, with their reasons, lessons and customer words, to Exodus; safe to send again",
|
|
689
700
|
async run(args, options) {
|
|
690
701
|
if (args.length > 0)
|
|
691
|
-
throw new CliError("Usage: winners send-to-exodus [--week <date>] [--dry-run]", exitCodes.usage);
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
dryRun: options.dryRun,
|
|
695
|
-
});
|
|
702
|
+
throw new CliError("Usage: winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run]", exitCodes.usage);
|
|
703
|
+
const period = Object.entries(winnersPeriod(options)).filter(([, value]) => value !== undefined);
|
|
704
|
+
const sent = await api(await resolveConnection(options.apiUrl), "/api/v1/winners/exodus", { ...Object.fromEntries(period), dryRun: options.dryRun });
|
|
696
705
|
if (options.format === "json")
|
|
697
706
|
printJson(sent);
|
|
698
707
|
else
|
|
@@ -2059,7 +2068,7 @@ function winnersPageLines(page, currency) {
|
|
|
2059
2068
|
return `- ${ad}${name}: ${counted(Math.round(n.visits), "visit")}, spent ${money(n.spend, currency)}, ${count ? `${count}, ` : ""}${cost}${bought}\n ${page.pickedBecause}\n`;
|
|
2060
2069
|
}
|
|
2061
2070
|
function winnersWindowText(title, window) {
|
|
2062
|
-
const head = `\n## ${title}
|
|
2071
|
+
const head = `\n## ${title}\n`;
|
|
2063
2072
|
if (window.sections.length === 0)
|
|
2064
2073
|
return `${head}\nNo Facebook ad spent in these dates.\n`;
|
|
2065
2074
|
return (head +
|
|
@@ -2095,8 +2104,19 @@ function winnersWindowText(title, window) {
|
|
|
2095
2104
|
})
|
|
2096
2105
|
.join(""));
|
|
2097
2106
|
}
|
|
2107
|
+
/** The window flags as the API takes them; the server refuses two windows at once or half a range. */
|
|
2108
|
+
function winnersPeriod(options) {
|
|
2109
|
+
return { week: options.week, days: options.days, from: options.from, to: options.to };
|
|
2110
|
+
}
|
|
2111
|
+
/** "This week, 2026-09-28 to 2026-10-04", "Last 3 days, 2026-10-04 to 2026-10-06" or "2026-10-01 to 2026-10-03". */
|
|
2112
|
+
function winnersDatesTitle(window) {
|
|
2113
|
+
const dates = `${window.from} to ${window.to}`;
|
|
2114
|
+
if (window.kind === "week")
|
|
2115
|
+
return `This week, ${dates}`;
|
|
2116
|
+
return window.kind === "days" ? `Last ${counted(window.days, "day")}, ${dates}` : dates;
|
|
2117
|
+
}
|
|
2098
2118
|
function winnersSendText(sent) {
|
|
2099
|
-
const lines = [
|
|
2119
|
+
const lines = [`${sent.window.kind === "week" ? "Week " : ""}${sent.window.from} to ${sent.window.to}${sent.dryRun ? ", dry run: nothing was written to Exodus" : ""}.`];
|
|
2100
2120
|
if (sent.pages.length === 0)
|
|
2101
2121
|
lines.push("No winners to send.");
|
|
2102
2122
|
for (const page of sent.pages) {
|
|
@@ -2123,12 +2143,12 @@ function winnersSendText(sent) {
|
|
|
2123
2143
|
lines.push("Left out:", ...sent.skipped.map((s) => ` ${s.adId}: ${s.reason}`));
|
|
2124
2144
|
return `${lines.join("\n")}\n`;
|
|
2125
2145
|
}
|
|
2126
|
-
/** The winners report as plain Markdown: what a sale means, then
|
|
2146
|
+
/** The winners report as plain Markdown: what a sale means, then the dates asked for and the last 30 days, each currency apart. */
|
|
2127
2147
|
function winnersText(report) {
|
|
2128
2148
|
return (`# Winners and flops for ${report.workspace}\n\n` +
|
|
2129
2149
|
`A sale means ${report.saleMeans}. Each sale goes to the Facebook ad that first brought the buyer in; payments typed into GoHighLevel by hand never count toward an ad.\n` +
|
|
2130
|
-
winnersWindowText(
|
|
2131
|
-
winnersWindowText(
|
|
2150
|
+
winnersWindowText(winnersDatesTitle(report.window), report.week) +
|
|
2151
|
+
winnersWindowText(`Last 30 days, ${report.last30Days.from} to ${report.last30Days.to}`, report.last30Days));
|
|
2132
2152
|
}
|
|
2133
2153
|
function reportQuery(options, extra) {
|
|
2134
2154
|
const query = new URLSearchParams();
|
|
@@ -2161,6 +2181,51 @@ function printPageResults(results) {
|
|
|
2161
2181
|
console.log(renderTable(["page", "ad", "ad name", "currency", ...PAGE_NUMBERS], results.adPages.map((r) => ({ ...pageNumbers(r), ad: r.adId ?? "(no Meta ad)", "ad name": r.adName === null ? "" : excerpt(r.adName, 30) }))));
|
|
2162
2182
|
console.log('\n"by ad link" counts sales whose buyer\'s landing page was not recorded, credited to the page the ad links to now.');
|
|
2163
2183
|
}
|
|
2184
|
+
const MESSAGE_NUMBERS = ["currency", "helped", "helped revenue", "last nudge", "last nudge revenue", "first from Facebook"];
|
|
2185
|
+
/** One row per currency the sales are in, or one row with no sales. */
|
|
2186
|
+
function salesRows(label, sales) {
|
|
2187
|
+
if (sales.length === 0)
|
|
2188
|
+
return [{ ...label, helped: 0, "last nudge": 0 }];
|
|
2189
|
+
return sales.map((s) => ({
|
|
2190
|
+
...label,
|
|
2191
|
+
currency: s.currency,
|
|
2192
|
+
helped: s.salesHelped,
|
|
2193
|
+
"helped revenue": s.revenueHelped,
|
|
2194
|
+
"last nudge": s.salesLastNudge,
|
|
2195
|
+
"last nudge revenue": s.revenueLastNudge,
|
|
2196
|
+
"first from Facebook": s.buyersFirstTouchMeta,
|
|
2197
|
+
}));
|
|
2198
|
+
}
|
|
2199
|
+
const messageLabel = (m) => excerpt(m.name ?? m.subject ?? (m.realSales === "available" ? m.messageKey : (m.itemId ?? "")), 40);
|
|
2200
|
+
function printMessageResults(results) {
|
|
2201
|
+
const real = results.messages.flatMap((m) => (m.realSales === "available" ? [m] : []));
|
|
2202
|
+
const claimed = results.messages.flatMap((m) => (m.realSales === "not_available" ? [m] : []));
|
|
2203
|
+
if (results.messages.length === 0 && results.sequences.length === 0)
|
|
2204
|
+
return console.log(`No follow-up results from ${results.from} to ${results.to}.`);
|
|
2205
|
+
console.log(`Follow-up emails and texts, most helped sales first, ${results.from} to ${results.to}:`);
|
|
2206
|
+
console.log(renderTable(["message", "channel", "platform", "sent", "clicked", "replied", ...MESSAGE_NUMBERS, "per 1,000 sent"], real.flatMap((m) => salesRows({ message: messageLabel(m), channel: m.channel, platform: m.platform, sent: m.peopleSent ?? (m.toolNumbersInRange === "unknown" ? "unknown" : m.toolSends), clicked: m.peopleClicked, replied: m.peopleReplied, "per 1,000 sent": m.salesPer1000Sent }, m.sales))));
|
|
2207
|
+
if (results.sequences.length > 0) {
|
|
2208
|
+
console.log("\nSequences:");
|
|
2209
|
+
console.log(renderTable(["sequence", "name", "messages", ...MESSAGE_NUMBERS], results.sequences.flatMap((q) => salesRows({ sequence: q.sequenceKey, name: q.name ?? "", messages: q.messages }, q.sales))));
|
|
2210
|
+
}
|
|
2211
|
+
if (claimed.length > 0) {
|
|
2212
|
+
console.log("\nFrom tools that never say who clicked, so no real sales; revenue is the tool's own claim:");
|
|
2213
|
+
console.log(renderTable(["message", "channel", "platform", "sends", "clicks", "tool-claimed revenue"], claimed.map((m) => m.toolNumbersInRange === "unknown"
|
|
2214
|
+
? { message: messageLabel(m), channel: m.channel, platform: m.platform, sends: "unknown", clicks: "unknown", "tool-claimed revenue": "unknown" }
|
|
2215
|
+
: {
|
|
2216
|
+
message: messageLabel(m),
|
|
2217
|
+
channel: m.channel,
|
|
2218
|
+
platform: m.platform,
|
|
2219
|
+
sends: m.toolSends,
|
|
2220
|
+
clicks: m.toolClicks,
|
|
2221
|
+
"tool-claimed revenue": (m.toolClaimedRevenue ?? []).map((r) => `${r.revenue} ${r.currency ?? ""}`.trim()).join(", "),
|
|
2222
|
+
})));
|
|
2223
|
+
}
|
|
2224
|
+
if (results.messages.some((m) => m.toolNumbersInRange === "unknown")) {
|
|
2225
|
+
console.log("\nunknown: the tool keeps only running totals, and none was read before these dates for a message sent earlier, so how much fell in them cannot be told.");
|
|
2226
|
+
}
|
|
2227
|
+
console.log(`\n"helped" counts every message the buyer clicked or replied to in the ${results.creditDays} days before buying, so one sale counts for several messages; "last nudge" gives each sale only to the latest one, so those add up.`);
|
|
2228
|
+
}
|
|
2164
2229
|
/** The rows, then the totals, which are one row per currency. */
|
|
2165
2230
|
/** What confirmed leave-out rules took out of `figures`, one line per rule and currency. */
|
|
2166
2231
|
function printLeftOut(leftOut, figures) {
|
|
@@ -2376,14 +2441,18 @@ Options:
|
|
|
2376
2441
|
--force content unpublish: take the page down even while delivering Meta ads link to it
|
|
2377
2442
|
--plan <json> meta adset publish: the ad set to make, {"kind": "copy", ...} or {"kind": "create", ...}
|
|
2378
2443
|
--yes confirm intake delete or import remove, which delete saved data for good
|
|
2379
|
-
--from <YYYY-MM-DD> first day funnel, campaigns, money, brief and
|
|
2380
|
-
--to <YYYY-MM-DD> last day funnel, campaigns, money, brief and
|
|
2444
|
+
--from <YYYY-MM-DD> first day funnel, campaigns, money, brief, pages results and messages results cover (default: 29 days before --to)
|
|
2445
|
+
--to <YYYY-MM-DD> last day funnel, campaigns, money, brief, pages results and messages results cover (default: today in the workspace reporting time zone)
|
|
2381
2446
|
--grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
|
|
2382
2447
|
--channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
|
|
2383
|
-
for content track-link, where the link goes: facebook, email, sms or web (default)
|
|
2448
|
+
for content track-link, where the link goes: facebook, email, sms or web (default);
|
|
2449
|
+
with messages results, email or text
|
|
2450
|
+
--sequence <id> with messages results, only the messages in this GoHighLevel workflow, and only that sequence
|
|
2384
2451
|
--campaign <id> the campaign content track-link puts in utm_campaign; with brief, the one campaign it covers
|
|
2385
2452
|
--top <n> how many best ads, worst ads, pages and campaigns brief shows, or ads in each winners list (default: 5, at most 20)
|
|
2386
2453
|
--week <YYYY-MM-DD> any day of the week winners covers, Monday to Sunday (default: the latest complete week)
|
|
2454
|
+
--days <n> winners: the last n complete days, ending yesterday, instead of a week (at most 90); or give winners
|
|
2455
|
+
--from and --to for any dates
|
|
2387
2456
|
--body, --html <text | @file> the words, or the full HTML, content create, capture and edit save
|
|
2388
2457
|
--file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several
|
|
2389
2458
|
--transcript <text | @file> what is said in the first video or audio file content capture saves, so none is paid for
|
|
@@ -2489,6 +2558,7 @@ function parseOptions(argv) {
|
|
|
2489
2558
|
campaign: { type: "string" },
|
|
2490
2559
|
top: { type: "string" },
|
|
2491
2560
|
week: { type: "string" },
|
|
2561
|
+
days: { type: "string" },
|
|
2492
2562
|
name: { type: "string" },
|
|
2493
2563
|
subject: { type: "string" },
|
|
2494
2564
|
preview: { type: "string" },
|
|
@@ -2532,6 +2602,7 @@ function parseOptions(argv) {
|
|
|
2532
2602
|
funnel: { type: "string" },
|
|
2533
2603
|
step: { type: "string" },
|
|
2534
2604
|
page: { type: "string" },
|
|
2605
|
+
sequence: { type: "string" },
|
|
2535
2606
|
workspace: { type: "string" },
|
|
2536
2607
|
"max-seconds": { type: "string" },
|
|
2537
2608
|
angle: { type: "string" },
|
|
@@ -2597,6 +2668,7 @@ function parseOptions(argv) {
|
|
|
2597
2668
|
campaign: values.campaign,
|
|
2598
2669
|
top: values.top,
|
|
2599
2670
|
week: values.week,
|
|
2671
|
+
days: values.days,
|
|
2600
2672
|
name: values.name,
|
|
2601
2673
|
subject: values.subject,
|
|
2602
2674
|
preview: values.preview,
|
|
@@ -2640,6 +2712,7 @@ function parseOptions(argv) {
|
|
|
2640
2712
|
funnel: values.funnel,
|
|
2641
2713
|
step: values.step,
|
|
2642
2714
|
page: values.page,
|
|
2715
|
+
sequence: values.sequence,
|
|
2643
2716
|
workspace: values.workspace,
|
|
2644
2717
|
maxSeconds,
|
|
2645
2718
|
entryTags: Object.fromEntries(Object.entries(entryTagFlags).flatMap(([field, flag]) => (values[flag] === undefined ? [] : [[field, values[flag]]]))),
|
|
@@ -3387,15 +3460,17 @@ function parseRetentionDays(text) {
|
|
|
3387
3460
|
throw new CliError("Retention must be a whole number of days, at least 1.", exitCodes.usage);
|
|
3388
3461
|
return days;
|
|
3389
3462
|
}
|
|
3390
|
-
function printIntakeToken({ endpoint, address, token, link }, format) {
|
|
3463
|
+
function printIntakeToken({ endpoint, address, token, link, subscription }, format) {
|
|
3391
3464
|
if (format === "json")
|
|
3392
|
-
printJson({ endpoint, address, token, link });
|
|
3465
|
+
printJson({ endpoint, address, token, link, ...(subscription ? { subscription } : {}) });
|
|
3393
3466
|
else {
|
|
3394
3467
|
if (link)
|
|
3395
3468
|
console.log(`Link: ${link}`);
|
|
3396
3469
|
console.log(`Address: ${address}`);
|
|
3397
3470
|
console.log(`Header: Authorization: Bearer ${token}`);
|
|
3398
3471
|
console.log(`Token: ${token}`);
|
|
3472
|
+
if (subscription)
|
|
3473
|
+
console.log(`Setup: ${subscription.message}`);
|
|
3399
3474
|
}
|
|
3400
3475
|
console.error(`The token for ${endpoint.label} is shown only now. Rotate it if it is lost.`);
|
|
3401
3476
|
}
|
package/dist/playbooks.js
CHANGED
|
@@ -1558,15 +1558,15 @@ const testResultsSql = "SELECT test_id, currency, sum(spend) AS spend, sum(sales
|
|
|
1558
1558
|
const lineageSql = "SELECT facebook_ad_id, hypothesis_id, test_id, creative_id, asset_version FROM adzara_ad_lineage ORDER BY synced_at DESC LIMIT 100";
|
|
1559
1559
|
/** The commands the weekly loop shows, in order; its text is made from them, so a test runs exactly these. */
|
|
1560
1560
|
export const weeklyLoopCommands = {
|
|
1561
|
-
winners: { purpose: "the
|
|
1561
|
+
winners: { purpose: "the Facebook winners and flops of the dates asked for, here a week, and of the 30 days ending on their last day, judged on real first-touch sales", argv: ["winners", "--week", "<week-monday>", "--format", "json"], mcp: "get_winners_report" },
|
|
1562
1562
|
lessons: { purpose: "what earlier weeks taught, the most often seen first", argv: ["lessons", "--format", "json"], mcp: "get_lessons" },
|
|
1563
1563
|
writeLessons: { purpose: "install the lessons playbook, then follow it to add, reinforce or retire this week's lessons", argv: ["skill", "install", "lessons"], mcp: "record_lesson" },
|
|
1564
1564
|
voice: { purpose: "fresh customer words: the most usable single lines said since that Monday", argv: ["voice", "--since", "<week-monday>", "--sort", "useful", "--json"], mcp: "get_customer_voice" },
|
|
1565
1565
|
brief: { purpose: "the campaign brief: offer, voice, lessons, best and worst ads, pages and audience, as Markdown", argv: ["brief"], mcp: "get_campaign_brief" },
|
|
1566
1566
|
funnelMap: { purpose: "the offers and funnels map: what is sold, at what price, and the steps a buyer takes", argv: ["funnels", "show"], mcp: "get_funnel_map" },
|
|
1567
1567
|
cro: { purpose: "install the cro playbook, then follow it to turn the funnel's costliest leaks into test ideas for the owner and read the running tests", argv: ["skill", "install", "cro"], mcp: "draft_funnel_test" },
|
|
1568
|
-
exodusPreview: { purpose: "ask Exodus what it would make from the
|
|
1569
|
-
exodusSend: { purpose: "send
|
|
1568
|
+
exodusPreview: { purpose: "ask Exodus what it would make from the winners of the same dates, writing nothing", argv: ["winners", "send-to-exodus", "--week", "<week-monday>", "--dry-run"], mcp: "send_winners_to_exodus" },
|
|
1569
|
+
exodusSend: { purpose: "send those winners to Exodus so it makes new ads from them; sending again is safe", argv: ["winners", "send-to-exodus", "--week", "<week-monday>"], mcp: "send_winners_to_exodus" },
|
|
1570
1570
|
paidTools: { purpose: "the paid making tools this workspace can run; each run costs money", argv: ["tools", "list"], mcp: "list_tools" },
|
|
1571
1571
|
testResults: { purpose: "spend, first-touch sales and cost per sale for each Adzara test over the last 30 days", argv: ["warehouse", "query", testResultsSql], mcp: "query_warehouse" },
|
|
1572
1572
|
lineage: { purpose: "which Meta ad carries which Adzara labels: idea, test, creative and version", argv: ["warehouse", "query", lineageSql], mcp: "query_warehouse" },
|
|
@@ -1586,7 +1586,7 @@ ${commandLine(c)}
|
|
|
1586
1586
|
Without a shell: MCP \`${c.mcp}\`.`;
|
|
1587
1587
|
export const weeklyLoopPlaybook = `# Weekly loop playbook (sample)
|
|
1588
1588
|
|
|
1589
|
-
A sample for a local orchestrator agent that runs a client's Facebook ads
|
|
1589
|
+
A sample to borrow from, not a schedule, for a local orchestrator agent that runs a client's Facebook ads with its owner. It reads what sold, learns from it, gathers fresh material, gets new ads made, has them published through Adzara, and later reads their results back. It is written as a week, but its steps can run daily, several times a week or weekly, each on its own rhythm: the winners report covers whatever dates you ask for. ${brand.name} supplies the data and keeps no part of the loop's decisions.
|
|
1590
1590
|
|
|
1591
1591
|
Every \`${cli}\` command acts on the one workspace the saved API key belongs to. Each step also names the MCP tool that does the same, for an agent connected over MCP without a shell.
|
|
1592
1592
|
|
|
@@ -1601,7 +1601,7 @@ Every \`${cli}\` command acts on the one workspace the saved API key belongs to.
|
|
|
1601
1601
|
|
|
1602
1602
|
${step(W.winners)}
|
|
1603
1603
|
|
|
1604
|
-
${W.winners.purpose[0].toUpperCase()}${W.winners.purpose.slice(1)}. Each ad has its \`numbers\`, \`pickedBecause\`, \`tags\` and Adzara \`test\`; each section's \`tests\` adds up each Adzara test. Leave out \`--week\` for the latest complete week.
|
|
1604
|
+
${W.winners.purpose[0].toUpperCase()}${W.winners.purpose.slice(1)}. Each ad has its \`numbers\`, \`pickedBecause\`, \`tags\` and Adzara \`test\`; each section's \`tests\` adds up each Adzara test. Leave out \`--week\` for the latest complete week. To check more often, swap \`--week\` for \`--days <n>\`, the last n complete days ending yesterday, or \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` for any dates (the MCP tool takes \`week\`, \`days\` or \`from\` and \`to\`). A short window holds few sales, so fewer ads can be judged on sales and more are judged on sign-ups; the report says so in \`notes\`.
|
|
1605
1605
|
|
|
1606
1606
|
## 2. Lessons
|
|
1607
1607
|
|
|
@@ -1641,7 +1641,7 @@ Follow the installed skill. It drafts at most 3 funnel tests, each with one chan
|
|
|
1641
1641
|
|
|
1642
1642
|
Exodus is one way to make ads among several. Pick what fits the week.
|
|
1643
1643
|
|
|
1644
|
-
**Exodus.** Preview what it would make from the
|
|
1644
|
+
**Exodus.** Preview what it would make from the winners, then send. It takes the same dates as the winners report: \`--week\`, \`--days\` or \`--from\` and \`--to\`; over MCP, \`week\`, \`days\` or \`from\` and \`to\`, and \`dryRun\`. A real send needs the workspace's Exodus brand and a Scrape Creators key on the Exodus account.
|
|
1645
1645
|
|
|
1646
1646
|
${step(W.exodusPreview)}
|
|
1647
1647
|
|