@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 +5 -1
- package/lib/cli.mjs +11 -4
- package/lib/commands/admin.mjs +77 -5
- package/lib/commands/org.mjs +26 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
932
|
-
|
|
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
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -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).
|
|
3723
|
-
//
|
|
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(
|
|
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({
|
|
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
|
}
|
package/lib/commands/org.mjs
CHANGED
|
@@ -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;
|