@wrangled/cli 0.3.160 → 0.3.162

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
@@ -154,7 +154,7 @@ For a worked start, run \`${cli} examples\`. It prints plain questions, such as
154
154
  For "how is the funnel doing?", "which campaigns are working?" and "where does the money come from and go?", start with these three reports before writing SQL. All three cover the last 30 days ending today in the workspace reporting time zone unless \`--from\` and \`--to\` (YYYY-MM-DD, both days included) say otherwise, and at most 400 days.
155
155
 
156
156
  - \`${cli} funnel [--grain day|week|month]\` prints ad spend, leads, booked calls, calls held, sales and net revenue per day, week (starting Monday) or month, with cost per lead, per booked call and per sale and ROAS, then totals for the whole range.
157
- - \`${cli} campaigns [--channel <channel>] [--limit <n>]\` prints each campaign's spend, leads, booked calls, sales and revenue, best first (most first-touch revenue, then most spend), then totals over every matching campaign. Each lead and sale is credited to the person's first touch and last touch side by side. \`--limit\` is 1 to 200 (default 50); \`--channel\` is meta, google, affiliate, a utm_source, unknown or recorded_in_crm.
157
+ - \`${cli} campaigns [--channel <channel>] [--limit <n>]\` prints each campaign's spend, leads, booked calls, cost per booked call, sales and revenue, best first, then totals over every matching campaign. Best first means most first-touch revenue, then most spend (most Triple Whale revenue when it stands in for sales). When \`${cli} winners\` judges the client's ads on booked calls, or no sales are connected at all, it means the cheapest booked call first among campaigns that spent, then the most calls; the JSON's \`ranked_by\` names the figure. Each lead and sale is credited to the person's first touch and last touch side by side. \`--limit\` is 1 to 200 (default 50); \`--channel\` is meta, google, affiliate, a utm_source, unknown or recorded_in_crm.
158
158
  - \`${cli} calls unattributed [--attribution first_touch|last_touch] [--limit <n>]\` answers "why do these booked calls have no ad?": it counts the booked calls in the range (by day booked), how many were credited to a Facebook ad and how many were not, as \`ad_results_daily\` credits them (to the first touch by default), splits the calls with no ad by why (\`no_person\`: the booking's contact has no email or phone to match a person; \`meta_without_ad\`: a Facebook visit whose link named no single ad; \`not_meta\`: another channel; \`visit_after_call\`: the visits are dated after the booking; \`other_touch_only\`: only a visit of the other kind, such as a latest visit and no first one when crediting the first touch; \`no_visit\`: no tagged visit at all), then lists those calls newest first with the booking's contact id, the person id and the touch's channel, campaign, ad and landing page. With and without add up to every booked call. \`--limit\` is 1 to 1000 (default 200); the counts always cover every call. The rows are in the \`booked_call_credits\` view; join \`touches\` on \`person_id\` to see each visit's tags.
159
159
  - \`${cli} calls compare [--event <event>]\` answers "do Meta's call numbers match the booked calls?": per Facebook ad over the range, the calls Meta reports (its results, the workspace's chosen result type as Ads Manager shows it, or with \`--event\` that one Meta action type, such as schedule_website), the booked calls \`ad_results_daily\` credits to the ad on the first touch and on the last touch, and each delta, booked minus Meta (negative when Meta reports more). An ad missing from one source counts 0 there. Most Meta calls first, then a total row; \`--format json\` gives the same numbers.
160
160
  - \`${cli} money [--grain day|week|month]\` prints money in (gross, refunds, net, orders), ad spend, affiliate payouts, what is left after ad costs and ROAS per period, then totals for the whole range, money in per payment source (stripe, ghl) and the biggest refund days. Refunds come off on the day they were made, not the day of the sale. \`left_after_ad_costs\` is net less ad spend and affiliate payouts; it is not profit. Its \`roas\` is net over ad spend alone, so it differs from \`${cli} funnel\`'s, which divides by all spend including affiliate payouts.
@@ -324,7 +324,7 @@ Some clients review every batch before it goes out. \`${cli} review settings\` s
324
324
 
325
325
  Each workspace keeps one map of what the client sells (the offer stack) and the steps a person walks from first seeing an ad to buying (its funnels). \`${cli} funnels show\` prints it in plain words: the version and who confirmed it, each offer in stack order with its tier, price and delivery, then each funnel's numbered steps with the offer it sells, its assets (marked \`(not found)\` when the id is not in synced data yet) and what counts as reaching it. It ends with the draft on hold, if any, printed in full. \`--version <n>\` prints an older version; \`--format json\` prints the whole map, which is the shape a draft takes.
326
326
 
327
- To change the map, write the whole map as JSON (\`{"offers": [...], "funnels": [...]}\`, the \`map\` of \`${cli} funnels show --format json\`) and run \`${cli} funnels draft --file map.json --note "<what changed and why>"\`. Add \`--evidence '<json>'\` or \`@file.json\` with what you looked at: counts and ids, never names, emails or phones. It is checked against synced data, and when every step's rule is filled in it goes live at once, confirmed by \`system:auto\`; a person can undo it in the app. A map with a rule not filled in stays on hold as the draft and counts nothing; saving replaces the draft on hold. Never invent an id or a number to fill a rule. Pass \`--based-on <n>\` with the version you read (the draft on hold, else the confirmed map): if someone saved since, the save is refused as \`draft_conflict\`, and you show again and redo your change on top. Leave out \`--based-on\` only for the first map, when there is neither a map nor a draft; once there is one, a save without it is refused as \`draft_conflict\`. A map that does not fit is refused with every problem by path, such as \`funnels[0].steps[1].offer_key: no offer has the key x\`. Saving prints whether the map went live, what is left to fill in (an asset not synced yet, a rule with no calendar chosen), and the parts synced data does not back up, such as an offer with fewer than 3 buyers in a year. Because a save can change the client's numbers at once, run it first with \`--dry-run\`: that saves nothing and prints what saving would do (the version it would make, whether it would go live or wait on hold and why, the problems, warnings and questions), and refuses a stale \`--based-on\` the same way. When you are unsure the map is right, save with \`--hold\`: it is kept as the draft on hold, counting nothing, until a person puts it live in the app. \`--dry-run\` and \`--hold\` cannot be used together.
327
+ To change the map, write the whole map as JSON (\`{"offers": [...], "funnels": [...]}\`, the \`map\` of \`${cli} funnels show --format json\`) and run \`${cli} funnels draft --file map.json --note "<what changed and why>"\`. For a small change, send only the change instead: \`--patch '<json>'\` or \`@patch.json\` with JSON Patch ops on the version you read, named by key, such as \`[{"op": "replace", "path": "/offers/course/price", "value": 997}]\`, and \`--based-on\` that version; the patched map is checked and saved the same way, and an op that no longer applies is refused as \`patch_not_applicable\`. Add \`--evidence '<json>'\` or \`@file.json\` with what you looked at: counts and ids, never names, emails or phones. It is checked against synced data, and when every step's rule is filled in it goes live at once, confirmed by \`system:auto\`; a person can undo it in the app. A map with a rule not filled in stays on hold as the draft and counts nothing; saving replaces the draft on hold. Never invent an id or a number to fill a rule. Pass \`--based-on <n>\` with the version you read (the draft on hold, else the confirmed map): if someone saved since, the save is refused as \`draft_conflict\`, and you show again and redo your change on top. Leave out \`--based-on\` only for the first map, when there is neither a map nor a draft; once there is one, a save without it is refused as \`draft_conflict\`. A map that does not fit is refused with every problem by path, such as \`funnels[0].steps[1].offer_key: no offer has the key x\`. Saving prints whether the map went live, what is left to fill in (an asset not synced yet, a rule with no calendar chosen), and the parts synced data does not back up, such as an offer with fewer than 3 buyers in a year. Because a save can change the client's numbers at once, run it first with \`--dry-run\`: that saves nothing and prints what saving would do (the version it would make, whether it would go live or wait on hold and why, the problems, warnings and questions), and refuses a stale \`--based-on\` the same way. When you are unsure the map is right, save with \`--hold\`: it is kept as the draft on hold, counting nothing, until a person puts it live in the app. \`--dry-run\` and \`--hold\` cannot be used together.
328
328
 
329
329
  \`--counts-from fix\` (the default) makes the map count for every past date the current map counted, as a fix; \`--counts-from <YYYY-MM-DD>\` makes it a change in the business that counts from that day (today or earlier). Save a fix and a change as two drafts, the fix first. \`--questions @questions.json\` asks a person what only they can settle: an array of \`{question_id, text, kind, evidence, options: [{label, patch}], default_option}\`, where the map already holds the default option and each other option's patch is JSON Patch ops on the map, naming offers, funnels and steps by key (\`/offers/course/matches/-\`). A question whose options all count the same sales and people, or that was answered already, is left out; at most 7 stay open, biggest money at stake first, and the checks add a yes/no question for each weak part no question covers. People answer them in the app; an answer saves the map as a fix, and later drafts keep it. \`${cli} funnels show\` lists the open and answered questions and says whether the map counts as a fix or from a date.
330
330
 
@@ -566,7 +566,7 @@ ${cli} funnels show [--version <n>] | funnels versions [--diff <a> <b>] the of
566
566
  ${cli} funnels numbers [--funnel <key>] [--from <date>] [--to <date>] how many reached each funnel step and moved on
567
567
  ${cli} funnels leaks [--funnel <key>] [--to <date>] the steps doing worse than their normal, costliest first
568
568
  ${cli} funnels evidence prices paid, buyer paths, GoHighLevel funnels, calendars and tags, webinars, pages and campaigns, to draft the map from
569
- ${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<date>] [--questions @file.json] [--dry-run | --hold] save the whole map; it goes live when every step can be counted, unless --hold keeps it for a person; --dry-run saves nothing and says what saving would do
569
+ ${cli} funnels draft --file <map.json> | --patch @patch.json [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<date>] [--questions @file.json] [--dry-run | --hold] save the whole map, or a patch on version --based-on; it goes live when every step can be counted, unless --hold keeps it for a person; --dry-run saves nothing and says what saving would do
570
570
  ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
571
571
  ${cli} number-alerts [--since <date>] the key numbers that swung far from normal, day by day
572
572
  ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
package/dist/main.js CHANGED
@@ -574,7 +574,9 @@ const commands = {
574
574
  const { campaignResults } = await api(await resolveConnection(options), `/api/v1/reports/campaigns${query}`);
575
575
  if (options.format === "json")
576
576
  return printJson({ campaignResults });
577
- const title = campaignResults.channel === null ? "Campaigns, best first" : `${campaignResults.channel} campaigns, best first`;
577
+ const byCalls = campaignResults.ranked_by === "cost_per_booked_call_first_touch" && campaignResults.rows.some((row) => Number(row.booked_calls_first_touch) > 0);
578
+ const order = byCalls ? "cheapest booked call first" : "best first";
579
+ const title = campaignResults.channel === null ? `Campaigns, ${order}` : `${campaignResults.channel} campaigns, ${order}`;
578
580
  printReport(title, campaignResults, ["channel", "campaign_id", "campaign_name", "currency"], campaignLayout(campaignResults));
579
581
  printLeftOut(campaignResults.left_out, "the totals (the rows keep it)");
580
582
  },
@@ -2817,22 +2819,24 @@ const commands = {
2817
2819
  },
2818
2820
  },
2819
2821
  "funnels draft": {
2820
- usage: "funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<YYYY-MM-DD>] [--questions '<json>' | @file.json] [--dry-run | --hold] save a whole map; it counts at once when every step can be counted, unless --hold; --dry-run saves nothing and says what saving would do",
2822
+ usage: "funnels draft --file <map.json> | --patch '<json>' | @patch.json [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<YYYY-MM-DD>] [--questions '<json>' | @file.json] [--dry-run | --hold] save a whole map, or a patch on version --based-on; it counts at once when every step can be counted, unless --hold; --dry-run saves nothing and says what saving would do",
2821
2823
  async run(args, options) {
2822
- const usage = "Usage: funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<YYYY-MM-DD>] [--questions '<json>' | @file.json] [--dry-run | --hold]";
2823
- if (args.length > 0 || !options.file)
2824
+ const usage = "Usage: funnels draft --file <map.json> | --patch '<json>' | @patch.json [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<YYYY-MM-DD>] [--questions '<json>' | @file.json] [--dry-run | --hold]";
2825
+ if (args.length > 0 || Boolean(options.file) === (options.patch !== undefined))
2824
2826
  throw new CliError(usage, exitCodes.usage);
2827
+ if (options.patch !== undefined && options.basedOn === undefined)
2828
+ throw new CliError("--patch needs --based-on: the version it changes.", exitCodes.usage);
2825
2829
  if (options.dryRun && options.hold)
2826
2830
  throw new CliError("Give --dry-run or --hold, not both: --dry-run saves nothing, --hold saves the map on hold.", exitCodes.usage);
2827
2831
  if (options.countsFrom !== undefined && options.countsFrom !== "fix" && !/^\d{4}-\d{2}-\d{2}$/.test(options.countsFrom)) {
2828
2832
  throw new CliError("--counts-from is fix or a date, YYYY-MM-DD", exitCodes.usage);
2829
2833
  }
2830
- const map = await parseJson(`@${options.file}`, "--file");
2834
+ const change = options.patch === undefined ? { map: await parseJson(`@${options.file}`, "--file") } : { patch: await parseJson(options.patch, "--patch") };
2831
2835
  const evidence = options.evidence === undefined ? undefined : await parseJson(options.evidence, "--evidence");
2832
2836
  const questions = options.questions === undefined ? undefined : await parseJson(options.questions, "--questions");
2833
2837
  const connection = await resolveConnection(options);
2834
2838
  const basedOn = options.basedOn === undefined ? null : wholeNumber(options.basedOn, "--based-on");
2835
- const saved = await api(connection, `${FUNNELS_PATH}/draft`, { map, note: options.note, evidence, based_on: basedOn, counts_from: options.countsFrom, questions, apply: options.dryRun ? "check" : options.hold ? "hold" : "auto" }, "PUT");
2839
+ const saved = await api(connection, `${FUNNELS_PATH}/draft`, { ...change, note: options.note, evidence, based_on: basedOn, counts_from: options.countsFrom, questions, apply: options.dryRun ? "check" : options.hold ? "hold" : "auto" }, "PUT");
2836
2840
  if (options.format === "json")
2837
2841
  return printJson(saved);
2838
2842
  const based = basedOn === null ? "" : `, based on version ${basedOn}`;
@@ -5205,6 +5209,7 @@ Options:
5205
5209
  --counts-from fix|<YYYY-MM-DD> how funnels draft counts: fix (default) for past dates too, or a change from that day
5206
5210
  --questions <json | @file.json> the questions funnels draft asks a person, as an array; with map, no value: only
5207
5211
  whether each everyday question can be answered
5212
+ --patch <json | @file.json> funnels draft: JSON Patch ops on version --based-on, instead of a whole map in --file
5208
5213
  --person <name> with profile confirm, the name of the person in the chat who gave the answer
5209
5214
  --diff <a> <b> with funnels versions, what changed from version a to version b
5210
5215
  --funnel <key> only this funnel's steps in funnels numbers and funnels leaks (default: every funnel in the confirmed map);
@@ -5457,6 +5462,7 @@ function parseOptions(argv) {
5457
5462
  evidence: { type: "string" },
5458
5463
  "counts-from": { type: "string" },
5459
5464
  questions: { type: "string" },
5465
+ patch: { type: "string" },
5460
5466
  person: { type: "string" },
5461
5467
  about: { type: "string" },
5462
5468
  dimension: { type: "string", multiple: true },
@@ -5687,6 +5693,7 @@ function parseOptions(argv) {
5687
5693
  evidence: values.evidence,
5688
5694
  countsFrom: values["counts-from"],
5689
5695
  questions: values.questions,
5696
+ patch: values.patch,
5690
5697
  person: values.person,
5691
5698
  about: values.about,
5692
5699
  dimension: values.dimension,
package/dist/playbooks.js CHANGED
@@ -701,8 +701,8 @@ Without a shell: MCP \`get_funnel_evidence\`, then MCP \`get_funnel_map\`.
701
701
 
702
702
  \`evidence.json\` is \`{"evidence": {...}, "text": "..."}\`. The parts below are under \`.evidence\`, each either \`{"status": "ok", "rows": [...]}\` or \`{"status": "not_connected", "note": "..."}\`. Amounts are in whole currency units (1500 is 1,500.00), over the last \`.evidence.windowDays\` days unless a part says otherwise.
703
703
 
704
- - **prices**: the prices people pay, on each platform, with how many buyers and payments. A GoHighLevel or Shopify row has the product's \`name\`, a Kajabi row the offer's title, with the Kajabi offer id as \`productId\`; a Stripe charge has none. For GoHighLevel, \`payments\` counts orders, not instalments. A GoHighLevel or Kajabi sale taken through Stripe shows twice, as a row naming the product and a Stripe row with only the amount, and the two amounts can differ a little (1497 and 1500).
705
- - **buyerPaths**: the most common orders in which one person paid, with the median days between each payment. A small payment minutes after a big one is an order bump or upsell; a large payment weeks later is a back-end offer. A path such as \`[1500, 497, 3500]\` with medians \`[0, 60]\` reads: course, then an upsell the same day, then an event about two months later. A GoHighLevel checkout with an order bump is one payment of both prices together: 1994 is 1497 + 497. Before you take an amount for a new offer, check whether it is a sum of prices you already know.
704
+ - **prices**: the prices people pay, on each platform, with how many buyers and payments. A GoHighLevel or Shopify row has the product's \`name\`, a Kajabi row the offer's title, with the Kajabi offer id as \`productId\`; a Stripe charge has none. For GoHighLevel, \`payments\` counts orders, not instalments. A GoHighLevel payment with no order lines, such as an invoice, has only its amount, like a Stripe charge; match it with a \`ghl\` \`amount\` match. A GoHighLevel or Kajabi sale taken through Stripe shows twice, as a row naming the product and a Stripe row with only the amount, and the two amounts can differ a little (1497 and 1500).
705
+ - **buyerPaths**: the most common orders in which one person paid, with the median days between each payment. A path needs at least two payments by one person matched to a known person, so it is empty when no matched buyer paid twice in the window, even when prices has rows. A small payment minutes after a big one is an order bump or upsell; a large payment weeks later is a back-end offer. A path such as \`[1500, 497, 3500]\` with medians \`[0, 60]\` reads: course, then an upsell the same day, then an event about two months later. A GoHighLevel checkout with an order bump is one payment of both prices together: 1994 is 1497 + 497. Before you take an amount for a new offer, check whether it is a sum of prices you already know.
706
706
  - **funnels**, **calendars**, **tags**: what GoHighLevel holds. Tags often name purchases and memberships.
707
707
  - **pipelineStages**: each stage of the sales pipelines in GoHighLevel, HubSpot and Attio (\`source\` is \`ghl\`, \`hubspot\` or \`attio\`; \`accountId\` is the GoHighLevel location id, the HubSpot account id or the Attio workspace id), with the deals in it now and, for GoHighLevel only, how many moved into it in the window; \`entered\` is null for HubSpot and Attio, which keep no stage history, so null means unknown, not none. An Attio \`pipelineId\` is a list id, or \`deals\` for the stage on the deals themselves. Only GoHighLevel stages can be a step's rule (\`ghl_stage\`); HubSpot and Attio stages are evidence of how deals move, not something a rule can count.
708
708
  - **webinars**: sign-ups, viewers and offer clicks per webinar id.
@@ -9,6 +9,7 @@ export const CAMPAIGN_NUMBERS = [
9
9
  "revenue_first_touch",
10
10
  "revenue_last_touch",
11
11
  "cost_per_lead_first_touch",
12
+ "cost_per_booked_call_first_touch",
12
13
  "roas_first_touch",
13
14
  "roas_last_touch",
14
15
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.160",
3
+ "version": "0.3.162",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {