@letterstory/cli 0.43.0 → 0.44.0

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/lib/cli.mjs CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  } from "./commands.mjs";
53
53
 
54
54
  // Keep in sync with cli/package.json.
55
- export const VERSION = "0.43.0";
55
+ export const VERSION = "0.44.0";
56
56
 
57
57
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
58
58
  // can't accidentally swallow the id as --json's value.
@@ -563,12 +563,17 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
563
563
  | accounts [--stage lead] [--q acme] | account <id> | score <account-id>
564
564
  | slack-channels [--all] | link-slack <account-id> <channel-id|none>
565
565
  | white-glove [--suggested] | set-white-glove <account-id> on|off [--note "…"]
566
+ | churn [--months 12] | record-churn <account-id> <reason> [--note "…"] [--at ISO]
567
+ [--mrr-cents N] | undo-churn <account-id>
566
568
  The staff CRM's pipeline: the inbound signal
567
569
  feed, recording a signal with no webhook
568
570
  (Bookface, a tweet like), qualifying or
569
571
  dismissing one, and the explained score.
570
572
  White glove is a person's decision: the
571
573
  desk only suggests ($1K+/mo post-sales).
574
+ Churn: 30/90-day logo + revenue rates, who
575
+ churned and why; Stripe cancellations are
576
+ only suggestions until a person records one.
572
577
  Needs crm:read (reads) / crm:write (writes).
573
578
  admin tuning-context [--q "<question>"] The experiment agent's context pack (JSON):
574
579
  the pipeline as deployed, levers, measures,
@@ -7,7 +7,7 @@
7
7
  // admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
8
8
  // admin tuning-context [--q "<question>"] (tuning:experiment)
9
9
  // admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
10
- // admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove … (crm:read / crm:write)
10
+ // admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn … (crm:read / crm:write)
11
11
  // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
12
12
  // [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
13
13
  // [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
@@ -567,9 +567,65 @@ async function cmdAdminCrm(ctx) {
567
567
  );
568
568
  return out(r, () => (action === "on" ? "moved to white glove" : "left white glove"));
569
569
  }
570
+ case "churn": {
571
+ // admin crm churn [--months 12] — the 30/90-day rates, who churned and why, likely churns.
572
+ const params = new URLSearchParams(compact({ months: flagStr(flags.months) }));
573
+ const pct = (x) => (x == null ? "—" : `${(x * 100).toFixed(1)}%`);
574
+ const usd = (cents) => `$${Math.round(cents / 100).toLocaleString("en-US")}`;
575
+ const win = (label, w) =>
576
+ `${label}: ${w.churned}/${w.customersAtStart} customers (${pct(w.rate)}) · ${usd(w.mrrLostCents)}/mo lost (${pct(w.revenueRate)} of MRR)`;
577
+ return out(await staffRequest(ctx, "GET", `${CRM}/churn?${params}`), (r) =>
578
+ [
579
+ win("30d", r.window.days30),
580
+ win("90d", r.window.days90),
581
+ "",
582
+ "Churned:",
583
+ ...(r.churned.length
584
+ ? r.churned.map(
585
+ (c) =>
586
+ ` ${c.churnedAt.slice(0, 10)}${c.churnedAtSource === "undated" ? "?" : ""} ${c.account.name} ${c.reason ?? "unrecorded"} ${usd(c.mrrLostCents)}/mo${c.note ? ` — ${c.note}` : ""}`
587
+ )
588
+ : [" (none)"]),
589
+ "",
590
+ "Likely churned (not recorded):",
591
+ ...(r.likely.length
592
+ ? r.likely.map((l) => ` ${accountLine(l.account)}\n ${l.signal}`)
593
+ : [" (none)"]),
594
+ ].join("\n")
595
+ );
596
+ }
597
+ case "record-churn": {
598
+ // admin crm record-churn <account-id> <reason> [--note "…"] [--at ISO] [--mrr-cents N]
599
+ if (!id || !action) {
600
+ throw new CliError(
601
+ 'Usage: admin crm record-churn <account-id> <price|no_results|competitor|in_house|shut_down|budget|bad_fit|service|other> [--note "…"] [--at 2026-09-22T00:00:00Z] [--mrr-cents 50000]'
602
+ );
603
+ }
604
+ const r = await staffPost(
605
+ ctx,
606
+ `${CRM}/accounts/${id}/churn`,
607
+ compact({
608
+ reason: action,
609
+ note: flagStr(flags.note),
610
+ churned_at: flagStr(flags.at),
611
+ mrr_cents: flagNum(flags["mrr-cents"]),
612
+ })
613
+ );
614
+ return out(
615
+ r,
616
+ (x) =>
617
+ `${x.change === "updated" ? "churn updated" : "churn recorded"}: ${accountLine(x.account)} · ${x.account.churnMrrCents == null ? "no MRR on file" : `$${Math.round(x.account.churnMrrCents / 100)}/mo lost`}`
618
+ );
619
+ }
620
+ case "undo-churn": {
621
+ // admin crm undo-churn <account-id> — clear the churn and move back to active.
622
+ if (!id) throw new CliError("Usage: admin crm undo-churn <account-id>");
623
+ const r = await staffRequest(ctx, "DELETE", `${CRM}/accounts/${id}/churn`);
624
+ return out(r, (x) => `churn undone: ${accountLine(x.account)}`);
625
+ }
570
626
  default:
571
627
  throw new CliError(
572
- "Usage: admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove … (see --help)"
628
+ "Usage: admin crm signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn … (see --help)"
573
629
  );
574
630
  }
575
631
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",