@letterstory/cli 0.142.0 → 0.143.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/README.md CHANGED
@@ -481,6 +481,7 @@ letterstory admin crm onboarding <account-id> # step-by-step progress (
481
481
  letterstory admin crm retry-onboarding <onboarding-id> # re-run only the failed steps
482
482
  letterstory admin crm onboard-plan <account-id> # what the calls say: sites, Slack/app people, paid?
483
483
  letterstory admin crm payment-link <account-id> # Stripe Payment Link at the deal amount (billing:write)
484
+ letterstory admin crm payment-link <account-id> --plan growth # with Lago signups on: a card-only start link, Lago bills the plan
484
485
  ```
485
486
 
486
487
  `onboard` runs org → link → phantom sites → Slack channel → checklist → app invites →
@@ -490,7 +491,10 @@ disclosure ("safe mode") for that site, and `--disclose-note "…"` records the
490
491
  as the reason; `onboard-plan` flags the sites whose calls asked for it. `onboard-plan` reads the account's recorded calls
491
492
  (newest first, cached per set of calls; `--refresh` re-reads) and says how many sites, what each
492
493
  is about, who to invite, and whether they've paid. `payment-link` stamps the account's org on
493
- the Stripe subscription, so paying through it binds and marks the account paid by itself.
494
+ the Stripe subscription, so paying through it binds and marks the account paid by itself. With
495
+ Lago signups on (`LETTERBRACE_LAGO_SIGNUP`), it is a start link of ours instead: the client saves a
496
+ card, Lago charges the plan (`--plan`, default the one priced at the deal amount; `--ratio`, `--why`
497
+ as for `admin billing lago provision`), and no Stripe subscription is made. The account needs an org.
494
498
 
495
499
  Every trigger — Cal.com bookings, Circleback recordings, Letterhome dinner RSVPs, RB2B
496
500
  visits, and what an agent records here — goes through one intake (`ingestSignal`,
package/lib/cli.mjs CHANGED
@@ -64,7 +64,7 @@ import {
64
64
  } from "./commands.mjs";
65
65
 
66
66
  // Keep in sync with cli/package.json.
67
- export const VERSION = "0.142.0";
67
+ export const VERSION = "0.143.0";
68
68
 
69
69
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
70
70
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -297,6 +297,9 @@ export const BOOLEAN_FLAGS = new Set([
297
297
  "nested",
298
298
  // admin billing lago catalog --apply --include-drafts
299
299
  "include-drafts",
300
+ // admin billing lago payments --open / payments run --dry
301
+ "open",
302
+ "dry",
300
303
  ]);
301
304
 
302
305
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -926,10 +929,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
926
929
  admin billing lago catalog [--apply] [--include-drafts]
927
930
  | provision <org-id> --plan starter|growth|scale [--ratio 1|2|4|8 (8 needs --why "…")] [--complimentary N] [--stripe-customer cus_…] [--start ISO] [--review D]
928
931
  | reconcile [--org <org-id>] | forward
932
+ | payments [--org <org-id>] [--open] | payments retry <lago-invoice-id> | payments run [--dry]
933
+ | signups [--status open|card_saved|provisioned|paid|abandoned]
929
934
  Usage billing on Lago: the plan catalog, an org's
930
935
  plan and gifted ratio, Lago's tokens against ours,
931
- and forwarding usage now. Needs spend:read (reads)
932
- / billing:write (writes).
936
+ forwarding usage now, failed payments and the
937
+ retry loop, and new signups on their way onto
938
+ Lago. Needs spend:read (reads) / billing:write
939
+ (writes).
933
940
  admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
934
941
  | capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
935
942
  [--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
@@ -960,7 +967,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
960
967
  [--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
961
968
  | onboarding <account-id> | retry-onboarding <onboarding-id>
962
969
  | onboard-plan <account-id> [--refresh]
963
- | payment-link <account-id> [--amount-cents N] [--description "…"]
970
+ | payment-link <account-id> [--amount-cents N] [--description "…"] [--plan starter] [--ratio N] [--why "…"]
964
971
  The staff CRM's pipeline: bulk-importing a
965
972
  CSV of leads, the inbound signal
966
973
  feed, recording a signal with no webhook
@@ -3719,14 +3719,24 @@ async function cmdAdminCrm(ctx) {
3719
3719
  }
3720
3720
  case "payment-link": {
3721
3721
  // admin crm payment-link <account-id> [--amount-cents N] [--description "…"] — a Stripe
3722
- // Payment Link for the monthly subscription (default: the deal amount). The key needs
3723
- // crm:write + billing:write.
3722
+ // Payment Link for the monthly subscription (default: the deal amount). With Lago signups on,
3723
+ // a start link that saves a card and puts the org on a Lago plan instead: [--plan starter]
3724
+ // (default: the plan priced at the deal) [--ratio 2] [--why "…" under the price floor].
3725
+ // The key needs crm:write + billing:write.
3724
3726
  if (!id)
3725
- throw new CliError('Usage: admin crm payment-link <account-id> [--amount-cents N] [--description "…"]');
3727
+ throw new CliError(
3728
+ 'Usage: admin crm payment-link <account-id> [--amount-cents N] [--description "…"] [--plan starter] [--ratio 1|2|4|8] [--why "…"]'
3729
+ );
3726
3730
  const r = await staffPost(
3727
3731
  ctx,
3728
3732
  `${CRM}/accounts/${id}/payment-link`,
3729
- compact({ amountCents: flagNum(flags["amount-cents"]), description: flagStr(flags.description) })
3733
+ compact({
3734
+ amountCents: flagNum(flags["amount-cents"]),
3735
+ description: flagStr(flags.description),
3736
+ planCode: flagStr(flags.plan),
3737
+ ratio: flagNum(flags.ratio),
3738
+ exceptionReason: flagStr(flags.why),
3739
+ })
3730
3740
  );
3731
3741
  return out(
3732
3742
  r,
@@ -5001,7 +5011,11 @@ const LAGO_USAGE = `Usage: admin billing lago <command>
5001
5011
  catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
5002
5012
  provision <org-id> --plan starter [--ratio 4] [--why "…" above 4x] [--complimentary 20000] [--stripe-customer cus_…] [--start 2026-11-02T00:00:00Z] [--review 2027-05-01]
5003
5013
  reconcile [--org <org-id>] Lago's tokens this period against what we rated and forwarded
5004
- forward forward billable usage to Lago now (the cron's pass)`;
5014
+ forward forward billable usage to Lago now (the cron's pass)
5015
+ payments [--org <org-id>] [--open] failed payments and what the retry loop did about each
5016
+ payments retry <lago-invoice-id> charge one failed invoice again now
5017
+ payments run [--dry] one pass of the failed-payment loop now (--dry: report only)
5018
+ signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago`;
5005
5019
 
5006
5020
  /** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
5007
5021
  async function cmdAdminBilling(ctx) {
@@ -5066,6 +5080,64 @@ async function cmdAdminBilling(ctx) {
5066
5080
  printResult(io, flags, result);
5067
5081
  return 0;
5068
5082
  }
5083
+ case "payments": {
5084
+ const action = positionals[2];
5085
+ if (action === "retry") {
5086
+ const invoiceId = requirePositional(positionals, 3, "lago-invoice-id");
5087
+ const result = await staffPost(ctx, `${LAGO}/payments/retry`, { invoiceId });
5088
+ ok(ctx, `Charging ${invoiceId} again; the loop reads the result within the hour.`);
5089
+ printResult(io, flags, result);
5090
+ return 0;
5091
+ }
5092
+ if (action === "run") {
5093
+ const result = await staffPost(
5094
+ ctx,
5095
+ `${LAGO}/payments/run`,
5096
+ compact({ dry: flagBool(flags.dry) || undefined })
5097
+ );
5098
+ ok(
5099
+ ctx,
5100
+ result.mode === "dry"
5101
+ ? `Would: ${result.actions.length ? result.actions.join("; ") : "nothing"}.`
5102
+ : `Opened ${result.opened}, resolved ${result.resolved}, emailed ${result.emails}, retried ${result.retries}, gave up ${result.gaveUp}.`
5103
+ );
5104
+ printResult(io, flags, result);
5105
+ return result.errors?.length ? 1 : 0;
5106
+ }
5107
+ if (action !== undefined) throw new CliError(LAGO_USAGE);
5108
+ const params = new URLSearchParams(
5109
+ compact({ orgId: flagStr(flags.org), open: flagBool(flags.open) ? "true" : undefined })
5110
+ );
5111
+ const result = await staffRequest(ctx, "GET", `${LAGO}/payments${params.size ? `?${params}` : ""}`);
5112
+ printResult(io, flags, result, (r) =>
5113
+ [
5114
+ `Failed-payment loop: ${r.mode}`,
5115
+ ...(r.payments.length
5116
+ ? r.payments.map(
5117
+ (p) =>
5118
+ ` ${p.invoiceNumber ?? p.invoiceId} ${p.orgName ?? p.orgId} $${(p.amountCents / 100).toFixed(2)} ${p.status} tries ${p.attempts}, emails ${p.emailsSent}${p.nextAttemptAt ? `, next ${p.nextAttemptAt}` : ""}${p.lastError ? ` (${p.lastError})` : ""}`
5119
+ )
5120
+ : [" No failed payments."]),
5121
+ ].join("\n")
5122
+ );
5123
+ return 0;
5124
+ }
5125
+ case "signups": {
5126
+ const params = new URLSearchParams(compact({ status: flagStr(flags.status) }));
5127
+ const result = await staffRequest(ctx, "GET", `${LAGO}/signups${params.size ? `?${params}` : ""}`);
5128
+ printResult(io, flags, result, (r) =>
5129
+ [
5130
+ `Lago signups: ${r.active ? "on" : "off"}${r.from ? ` (from ${r.from})` : ""}`,
5131
+ ...(r.signups.length
5132
+ ? r.signups.map(
5133
+ (x) =>
5134
+ ` ${x.orgName ?? x.orgId} ${x.planCode}${x.ratio !== 1 ? ` ${x.ratio}x` : ""} ${x.status} ${x.source}${x.lastError ? ` (${x.lastError})` : ""}`
5135
+ )
5136
+ : [" None."]),
5137
+ ].join("\n")
5138
+ );
5139
+ return 0;
5140
+ }
5069
5141
  default:
5070
5142
  throw new CliError(LAGO_USAGE);
5071
5143
  }
@@ -27,6 +27,7 @@ export const ARGS = {
27
27
  },
28
28
  describe_org_config_fields: {},
29
29
  get_org_config_source: {},
30
+ get_billing_payments: {},
30
31
  list_integrations: {},
31
32
  get_linear_connection: {},
32
33
  list_linear_targets: { team_id: { flag: "team" } },
@@ -68,9 +69,11 @@ export async function cmdOrg(ctx) {
68
69
  return orgFields(rest);
69
70
  case "linear":
70
71
  return orgLinear(rest);
72
+ case "billing":
73
+ return orgBilling(rest);
71
74
  default:
72
75
  throw new CliError(
73
- `Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, fields`
76
+ `Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, fields, billing`
74
77
  );
75
78
  }
76
79
  }
@@ -87,6 +90,28 @@ async function orgIntegrations(ctx) {
87
90
  return 0;
88
91
  }
89
92
 
93
+ /** `org billing`: the org's plan on Lago, whether it is past due, and its failed payments (get_billing_payments). */
94
+ async function orgBilling(ctx) {
95
+ const { client, flags, io } = ctx;
96
+ refuseUnknownFlags(ctx, {}, []);
97
+ const result = await client.callTool("get_billing_payments", {});
98
+ printResult(io, flags, result, (r) =>
99
+ [
100
+ r.plan
101
+ ? `Plan: ${r.plan.name}${r.plan.ratio !== 1 ? ` (${r.plan.ratio}x)` : ""}`
102
+ : "Not billed by usage yet.",
103
+ ...(r.pastDueAt ? [`Past due since ${r.pastDueAt}.`] : []),
104
+ ...(r.payments.length
105
+ ? r.payments.map(
106
+ (p) =>
107
+ ` ${p.invoiceNumber ?? p.invoiceId} $${(p.amountCents / 100).toFixed(2)} ${p.status}${p.nextAttemptAt ? `, next try ${p.nextAttemptAt}` : ""}${p.payUrl ? ` pay: ${p.payUrl}` : ""}`
108
+ )
109
+ : ["No failed payments."]),
110
+ ].join("\n")
111
+ );
112
+ return 0;
113
+ }
114
+
90
115
  /** `org fields [section]`: what each setting in the bundle means (describe_org_config_fields). */
91
116
  async function orgFields(ctx) {
92
117
  const { client, flags, io, positionals } = ctx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.142.0",
3
+ "version": "0.143.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",