@letterstory/cli 0.141.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.141.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
@@ -75,6 +75,8 @@ export const BOOLEAN_FLAGS = new Set([
75
75
  // sim seed: make the new seed active; collections it doesn't cover start empty.
76
76
  "activate",
77
77
  "empty",
78
+ // sim create: agents can use it at once, instead of starting paused for review.
79
+ "live",
78
80
  // admin models runs: only the starred ones.
79
81
  "starred",
80
82
  // shredder file / arena file: save the version as a candidate beside live.
@@ -295,6 +297,9 @@ export const BOOLEAN_FLAGS = new Set([
295
297
  "nested",
296
298
  // admin billing lago catalog --apply --include-drafts
297
299
  "include-drafts",
300
+ // admin billing lago payments --open / payments run --dry
301
+ "open",
302
+ "dry",
298
303
  ]);
299
304
 
300
305
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -924,10 +929,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
924
929
  admin billing lago catalog [--apply] [--include-drafts]
925
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]
926
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]
927
934
  Usage billing on Lago: the plan catalog, an org's
928
935
  plan and gifted ratio, Lago's tokens against ours,
929
- and forwarding usage now. Needs spend:read (reads)
930
- / 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).
931
940
  admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
932
941
  | capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
933
942
  [--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
@@ -958,7 +967,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
958
967
  [--slack a@acme.com,b@acme.com] [--invite a@acme.com[:admin]]… [--no-checklist]
959
968
  | onboarding <account-id> | retry-onboarding <onboarding-id>
960
969
  | onboard-plan <account-id> [--refresh]
961
- | payment-link <account-id> [--amount-cents N] [--description "…"]
970
+ | payment-link <account-id> [--amount-cents N] [--description "…"] [--plan starter] [--ratio N] [--why "…"]
962
971
  The staff CRM's pipeline: bulk-importing a
963
972
  CSV of leads, the inbound signal
964
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;
@@ -10,6 +10,7 @@
10
10
  // letterstory sim key <id> --agent our-ci --goal "nightly integration test"
11
11
  // letterstory sim seed <id> --prompt "a 3-person content team with two blogs" --activate
12
12
  // letterstory sim seed <id> --use <seed-id|none>
13
+ // letterstory sim limits <id> --daily-calls 5000 --daily-sessions 100
13
14
  // letterstory sim report <id> --since 7d
14
15
  // letterstory sim sessions <id>
15
16
  // letterstory sim session <session-id>
@@ -57,12 +58,15 @@ export const ARGS = {
57
58
  },
58
59
  },
59
60
  tool_route: "tool-route",
61
+ live: { flag: "live", type: "bool" },
60
62
  auth_header: "auth-header",
61
63
  },
62
64
  list_simulations: {},
63
65
  get_simulation: { simulation_id: simulationId },
64
66
  update_simulation: {
65
67
  simulation_id: simulationId,
68
+ daily_call_cap: { flag: "daily-calls", type: "num" },
69
+ daily_session_cap: { flag: "daily-sessions", type: "num" },
66
70
  status: { flag: "status", enum: ["live", "paused"] },
67
71
  active_seed_id: {
68
72
  value: (flags) => {
@@ -119,6 +123,9 @@ export async function cmdSim(ctx) {
119
123
  if (sub !== "rule") rest.flags = { ...rest.flags, status: sub === "pause" ? "paused" : "live" };
120
124
  return simShow(rest, await call(rest, "update_simulation"));
121
125
  }
126
+ case "limits":
127
+ refuseUnknownFlags(rest, ARGS.update_simulation, []);
128
+ return simShow(rest, await call(rest, "update_simulation"));
122
129
  case "seed":
123
130
  return simSeed(rest);
124
131
  case "key":
@@ -130,7 +137,9 @@ export async function cmdSim(ctx) {
130
137
  case "report":
131
138
  return simReport(rest);
132
139
  default:
133
- throw new CliError("Usage: sim [list|create|show|rule|pause|resume|seed|key|sessions|session|report]");
140
+ throw new CliError(
141
+ "Usage: sim [list|create|show|rule|pause|resume|limits|seed|key|sessions|session|report]"
142
+ );
134
143
  }
135
144
  }
136
145
 
@@ -157,6 +166,10 @@ function simShow(ctx, result) {
157
166
  ` REST ${s.urls.base}`,
158
167
  ` MCP ${s.urls.mcp}`,
159
168
  ` Docs ${s.urls.llms}`,
169
+ ` Limits ${s.daily_call_cap} calls and ${s.daily_session_cap} new sessions per 24 h; ${s.session_call_cap} calls per session`,
170
+ ...(s.status === "paused"
171
+ ? [" Paused: review the rules below, then `sim resume <id>` to let agents in."]
172
+ : []),
160
173
  "",
161
174
  "Resources (rule · side effect · who set it):",
162
175
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.141.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",