@wrangled/cli 0.3.28 → 0.3.30

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
@@ -30,6 +30,10 @@ Lessons are short plain sentences about why the winners won and the flops floppe
30
30
 
31
31
  Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<Meta ad id or SKU>", "numbers": "cost per sale $180 vs $410"}], "week": "2026-09-28"}\`, and run \`${cli} lessons add --file lesson.json\`. \`dimension\` (hook, angle, format, offer, persona or other) and \`week\` (any day of the winners report's week; default the latest complete week) are optional. A lesson with the same words that still holds, or the one named in \`"reinforces": "<id>"\`, is reinforced instead of copied: it counts once more for a week it had not counted, so running the same week again changes no count. \`${cli} lessons retire <id> --note "<why>"\` retires one the numbers no longer back; it is kept with why. The \`lessons\` playbook (\`${cli} skill install lessons\` installs it as the \`${LESSONS_SKILL_NAME}\` skill) writes the week's lessons from the winners report.
32
32
 
33
+ ## Sending winners to Exodus
34
+
35
+ \`${cli} winners send-to-exodus\` sends the week's 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 week's freshest customer words. Winners whose page, words or format are unknown are left out and listed with why. \`--week <YYYY-MM-DD>\` picks the week (default: the latest complete week). \`--dry-run\` asks Exodus what it would create or update and writes nothing. Sending the same week 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.
36
+
33
37
  ## Content: search before SQL
34
38
 
35
39
  To find content, such as "the 10 emails with the best click rate", "our 5 best videos by return on ad spend" or "every landing page that mentions the guarantee", use \`${cli} content search\` before writing SQL on the \`content_\` tables.
@@ -167,6 +171,8 @@ To change the map, write the whole map as JSON (\`{"offers": [...], "funnels": [
167
171
 
168
172
  \`${cli} funnels numbers\` counts each step of each funnel in the confirmed map over \`--from\` and \`--to\` (default the last 30 days; \`--funnel <key>\` for one funnel): how many reached it, and how many of those who reached the step before in those days moved on to it within the window, with the drop-off and the median days it took. A step whose rules name a person (webinar, GoHighLevel, payments) counts people and follows the same people from step to step; a step that cannot (ad clicks, page visitors) counts events, and its comparison is a rate of counts. A step whose data is not connected says unknown, never 0. Numbers are worked out after each sync by the rules of the map version confirmed at the time, so a renamed step keeps its numbers. For SQL, read \`saved_funnel_steps\`, \`funnel_step_daily\` and \`funnel_conversion\`.
169
173
 
174
+ \`${cli} funnels leaks\` (\`--funnel <key>\` for one funnel, \`--to <date>\` for the last day) finds where each funnel loses people now: each step's rate from the step before over the last 28 days and the last 7, against its own normal rate over the 56 days before. A step doing worse says about how many fewer people moved on (the recent cohort times the rate it fell by) and what they were worth: an estimate, per currency and never added across them, from the offer prices of this step and the later purchase steps at the recent rates. The worst steps are listed first, by money, then people. A step with fewer than 100 people at the step before in either window says too few to tell and is never ranked; the 7-day rate needs 20. For a step counted by people, the recent cohort is also split by each person's first touch as of the day they reached the step before: channel, then the top 5 Meta campaigns and ads. People who reached the step before near the end of the window may not have moved on yet, so a slow step can look slightly worse. Start here before writing a conversion test idea.
175
+
170
176
  \`${cli} funnels evidence\` gathers what the workspace's own data says it sells, to draft a map from: the prices people paid in the last 365 days on each platform with buyer counts, the most common orders in which one person paid (\`1500 → 497 → 3500\`, with the median days between), GoHighLevel funnels and their steps, calendars with bookings and shows, pipeline stages, the most used tags, webinars with sign-ups, viewers and offer clicks, the landing pages ads link to, and active Facebook campaigns with 30-day spend. A part whose source is not connected says so. Ids, counts and amounts only. The \`map-offers\` playbook (\`${cli} skill install map-offers\` installs it as the \`${MAP_OFFERS_SKILL_NAME}\` skill, with a worked example map beside it; \`${cli} docs map-offers\` prints it) walks an agent from the evidence to a draft.
171
177
 
172
178
  \`${cli} funnels versions\` lists every version with its status (draft, confirmed, replaced or discarded), who wrote and confirmed it and its note; \`--diff <a> <b>\` says what changed from version a to version b.
@@ -257,6 +263,7 @@ ${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library
257
263
  ${cli} guide what this workspace's data holds, how fresh it is, and what is missing: read it first
258
264
  ${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
259
265
  ${cli} winners [--week <date>] [--top <n>] which Facebook ads won and flopped this week and over 30 days, judged on first-touch sales
266
+ ${cli} winners send-to-exodus [--week <date>] [--dry-run] send the week's winners, with their reasons, lessons and customer words, to Exodus; safe to send again
260
267
  ${cli} warehouse schema [--summary] [--table <name> ...] [--search <words>] list tables and columns; --summary for one row per table
261
268
  ${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
262
269
  ${cli} funnel [--from <date>] [--to <date>] [--grain day|week|month] spend, leads, calls and sales per period, with totals
@@ -306,6 +313,7 @@ ${cli} plans status <plan-id> draft|ready|publishing|live|done|cancelled
306
313
  ${cli} trail <plan-id | sku | item-id> how a plan's items were made, where they ran, and their results
307
314
  ${cli} funnels show [--version <n>] | funnels versions [--diff <a> <b>] the offers and funnels map, as text, and its versions
308
315
  ${cli} funnels numbers [--funnel <key>] [--from <date>] [--to <date>] how many reached each funnel step and moved on
316
+ ${cli} funnels leaks [--funnel <key>] [--to <date>] the steps doing worse than their normal, costliest first
309
317
  ${cli} funnels evidence prices paid, buyer paths, GoHighLevel funnels, calendars and tags, webinars, pages and campaigns, to draft the map from
310
318
  ${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<date>] [--questions @file.json] save the whole map; it goes live when every step can be counted
311
319
  ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
package/dist/main.js CHANGED
@@ -657,6 +657,24 @@ const commands = {
657
657
  process.stdout.write(winnersText(report));
658
658
  },
659
659
  },
660
+ "winners send-to-exodus": {
661
+ usage: "winners send-to-exodus [--week <YYYY-MM-DD>] [--dry-run] send the week's winning ads, with their reasons, lessons and customer words, to Exodus; safe to send again",
662
+ async run(args, options) {
663
+ if (args.length > 0)
664
+ throw new CliError("Usage: winners send-to-exodus [--week <date>] [--dry-run]", exitCodes.usage);
665
+ const sent = await api(await resolveConnection(options.apiUrl), "/api/v1/winners/exodus", {
666
+ ...(options.week === undefined ? {} : { week: options.week }),
667
+ dryRun: options.dryRun,
668
+ });
669
+ if (options.format === "json")
670
+ printJson(sent);
671
+ else
672
+ process.stdout.write(winnersSendText(sent));
673
+ const failed = sent.pages.filter((p) => p.run.status !== "succeeded").length;
674
+ if (failed > 0)
675
+ throw new CliError(`${failed} of ${sent.pages.length} ${sent.pages.length === 1 ? "page was" : "pages were"} not sent; see above.`, exitCodes.error);
676
+ },
677
+ },
660
678
  examples: {
661
679
  usage: "examples (plain questions with SQL that answers them, for the tables this workspace has)",
662
680
  async run(args, options) {
@@ -1065,6 +1083,17 @@ const commands = {
1065
1083
  process.stdout.write(view.text);
1066
1084
  },
1067
1085
  },
1086
+ "funnels leaks": {
1087
+ usage: "funnels leaks [--funnel <key>] [--to <YYYY-MM-DD>] the funnel steps doing worse than their normal, costliest first, with what that costs",
1088
+ async run(args, options) {
1089
+ if (args.length > 0 || options.from !== undefined)
1090
+ throw new CliError("Usage: funnels leaks [--funnel <key>] [--to <date>]", exitCodes.usage);
1091
+ const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/leaks${reportQuery(options, { funnel: options.funnel })}`);
1092
+ if (options.format === "json")
1093
+ return printJson(view);
1094
+ process.stdout.write(view.text);
1095
+ },
1096
+ },
1068
1097
  "funnels evidence": {
1069
1098
  usage: "funnels evidence what the data says the client sells: prices paid, buyer paths, GoHighLevel funnels, calendars and tags, webinars, pages, campaigns, past campaigns",
1070
1099
  async run(args, options) {
@@ -1913,6 +1942,34 @@ function winnersWindowText(title, window) {
1913
1942
  .join(""));
1914
1943
  }
1915
1944
  /** The winners report as plain Markdown: what a sale means, then this week and the last 30 days, each currency apart. */
1945
+ function winnersSendText(sent) {
1946
+ const lines = [`Week ${sent.week.from} to ${sent.week.to}${sent.dryRun ? ", dry run: nothing was written to Exodus" : ""}.`];
1947
+ if (sent.pages.length === 0)
1948
+ lines.push("No winners to send.");
1949
+ for (const page of sent.pages) {
1950
+ lines.push(`${page.pageName} (page ${page.pageId}, ${page.adAccountId}): ${page.winners} ${page.winners === 1 ? "winner" : "winners"}`);
1951
+ const answer = page.exodus;
1952
+ if (answer === null)
1953
+ lines.push(` Not sent: ${page.run.error ?? `the run is ${page.run.status}`}`);
1954
+ else if ("dryRun" in answer) {
1955
+ for (const action of ["create", "update"]) {
1956
+ const ads = answer.perWinner.filter((w) => w.action === action).map((w) => w.accountAdId);
1957
+ if (ads.length > 0)
1958
+ lines.push(` Would ${action}: ${ads.join(", ")}`);
1959
+ }
1960
+ if (answer.scrapecreatorsKey === "missing")
1961
+ lines.push(" Exodus has no Scrape Creators key, so a real send would fail. Add it in Exodus under Settings, Keys.");
1962
+ }
1963
+ else {
1964
+ lines.push(` ${page.alreadySent ? "Already sent; not sent again" : "Sent"}: import ${answer.importId}, ${answer.accepted} accepted, ${answer.rejected.length} rejected`);
1965
+ }
1966
+ for (const r of answer?.rejected ?? [])
1967
+ lines.push(` Rejected ${r.accountAdId}: ${r.reason}`);
1968
+ }
1969
+ if (sent.skipped.length > 0)
1970
+ lines.push("Left out:", ...sent.skipped.map((s) => ` ${s.adId}: ${s.reason}`));
1971
+ return `${lines.join("\n")}\n`;
1972
+ }
1916
1973
  function winnersText(report) {
1917
1974
  return (`# Winners and flops for ${report.workspace}\n\n` +
1918
1975
  `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` +
@@ -2123,7 +2180,7 @@ Options:
2123
2180
  --counts-from fix|<YYYY-MM-DD> how funnels draft counts: fix (default) for past dates too, or a change from that day
2124
2181
  --questions <json | @file.json> the questions funnels draft asks a person, as an array
2125
2182
  --diff <a> <b> with funnels versions, what changed from version a to version b
2126
- --funnel <key> only this funnel's steps in funnels numbers (default: every funnel in the confirmed map);
2183
+ --funnel <key> only this funnel's steps in funnels numbers and funnels leaks (default: every funnel in the confirmed map);
2127
2184
  with plans create and --step, the funnel the push is a step of
2128
2185
  --step <key> with plans create, the step of --funnel this push itself is, as on the confirmed map
2129
2186
  --harness <list> which agents skill publish marks the skill for: claude, codex, or claude,codex (default)
@@ -2247,6 +2304,7 @@ function parseOptions(argv) {
2247
2304
  awareness: { type: "string" },
2248
2305
  confidence: { type: "string" },
2249
2306
  remove: { type: "boolean", default: false },
2307
+ "dry-run": { type: "boolean", default: false },
2250
2308
  help: { type: "boolean", short: "h", default: false },
2251
2309
  },
2252
2310
  });
@@ -2347,6 +2405,7 @@ function parseOptions(argv) {
2347
2405
  entryTags: Object.fromEntries(Object.entries(entryTagFlags).flatMap(([field, flag]) => (values[flag] === undefined ? [] : [[field, values[flag]]]))),
2348
2406
  confidence: values.confidence,
2349
2407
  remove: values.remove,
2408
+ dryRun: values["dry-run"],
2350
2409
  },
2351
2410
  };
2352
2411
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {