@letterstory/cli 0.145.3 → 0.146.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
@@ -64,7 +64,8 @@ import {
64
64
  } from "./commands.mjs";
65
65
 
66
66
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
67
- export const VERSION = "0.145.3";
67
+ // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
68
+ export const VERSION = "0.146.0";
68
69
 
69
70
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
70
71
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -935,6 +936,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
935
936
  | reconcile [--org <org-id>] | forward
936
937
  | payments [--org <org-id>] [--open] | payments retry <lago-invoice-id> | payments run [--dry]
937
938
  | signups [--status open|card_saved|provisioned|paid|abandoned]
939
+ | cap <org-id> <dollars|off> [--why "…"]
938
940
  Usage billing on Lago: the plan catalog, an org's
939
941
  plan and gifted ratio, Lago's tokens against ours,
940
942
  forwarding usage now, failed payments and the
@@ -1092,6 +1094,10 @@ Seers (event-driven signals -> drafts):
1092
1094
  org export --split <dir> …as one file per record (for keeping config in git)
1093
1095
  org join <dir> [--file <path>] Rebuild the bundle from a --split directory
1094
1096
  org integrations Every integration and whether it's connected
1097
+ org billing [cap <dollars|off> [--why "…"] | request [--note "…"]]
1098
+ Usage billing: plan, this period against the
1099
+ allowance and spend cap, failed payments; set
1100
+ the cap (admins) or ask the admins for more
1095
1101
  org linear [teams [--team <id|key>] | set --team <id|key> [--project <id>|none] | disconnect]
1096
1102
  Where Letter files Linear issues (connect in Settings)
1097
1103
  org diff <file|dir> What applying a bundle would change (reads only)
@@ -5089,7 +5089,8 @@ const LAGO_USAGE = `Usage: admin billing lago <command>
5089
5089
  payments [--org <org-id>] [--open] failed payments and what the retry loop did about each
5090
5090
  payments retry <lago-invoice-id> charge one failed invoice again now
5091
5091
  payments run [--dry] one pass of the failed-payment loop now (--dry: report only)
5092
- signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago`;
5092
+ signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago
5093
+ cap <org-id> <dollars|off> [--why "…"] set an account's monthly spend cap on overage (off: no cap)`;
5093
5094
 
5094
5095
  /** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
5095
5096
  async function cmdAdminBilling(ctx) {
@@ -5212,6 +5213,27 @@ async function cmdAdminBilling(ctx) {
5212
5213
  );
5213
5214
  return 0;
5214
5215
  }
5216
+ case "cap": {
5217
+ const orgId = positionals[2];
5218
+ const value = positionals[3];
5219
+ if (!orgId || value === undefined) throw new CliError(LAGO_USAGE);
5220
+ const off = ["off", "none", "null"].includes(value.toLowerCase());
5221
+ const capUsd = off ? null : Number(value.replace(/^\$/, ""));
5222
+ if (capUsd !== null && (!Number.isFinite(capUsd) || capUsd < 0))
5223
+ throw new CliError(`Not a cap: ${value}. Give dollars (0 stops at the allowance) or off.`);
5224
+ const result = await staffRequest(
5225
+ ctx,
5226
+ "POST",
5227
+ `${LAGO}/cap`,
5228
+ compact({ orgId, cap_usd: capUsd, reason: flagStr(flags.why) })
5229
+ );
5230
+ printResult(io, flags, result, (r) =>
5231
+ r.spendCap
5232
+ ? `Spend cap: $${r.spendCap.usd} of overage a month (alerts: ${r.alert}).`
5233
+ : "No spend cap: overage is on."
5234
+ );
5235
+ return 0;
5236
+ }
5215
5237
  default:
5216
5238
  throw new CliError(LAGO_USAGE);
5217
5239
  }
@@ -28,6 +28,11 @@ export const ARGS = {
28
28
  describe_org_config_fields: {},
29
29
  get_org_config_source: {},
30
30
  get_billing_payments: {},
31
+ set_billing_spend_cap: {
32
+ cap_usd: { value: (_flags, positionals) => capFrom(requirePositional(positionals, 0, "dollars|off")) },
33
+ reason: { flag: "why" },
34
+ },
35
+ request_billing_tokens: { note: { flag: "note" } },
31
36
  list_integrations: {},
32
37
  get_linear_connection: {},
33
38
  list_linear_targets: { team_id: { flag: "team" } },
@@ -90,9 +95,45 @@ async function orgIntegrations(ctx) {
90
95
  return 0;
91
96
  }
92
97
 
93
- /** `org billing`: the org's plan on Lago, whether it is past due, and its failed payments (get_billing_payments). */
98
+ /** A cap from the command line: dollars, or `off` (no cap). */
99
+ function capFrom(value) {
100
+ if (["off", "none", "null"].includes(String(value).toLowerCase())) return null;
101
+ const usd = Number(String(value).replace(/^\$/, ""));
102
+ if (!Number.isFinite(usd) || usd < 0)
103
+ throw new CliError(`Not a cap: ${value}. Give dollars (0 stops at the allowance) or off.`);
104
+ return usd;
105
+ }
106
+
107
+ const usd = (n) => `$${Number(n).toFixed(2)}`;
108
+
109
+ /**
110
+ * `org billing`: the org's plan on Lago, this period against its allowance and spend cap, whether
111
+ * it is past due, and its failed payments (get_billing_payments). `org billing cap <dollars|off>`
112
+ * sets the cap (set_billing_spend_cap, admins); `org billing request` asks the admins for more
113
+ * (request_billing_tokens).
114
+ */
94
115
  async function orgBilling(ctx) {
95
- const { client, flags, io } = ctx;
116
+ const { client, flags, io, positionals } = ctx;
117
+ if (positionals[0] === "cap") {
118
+ refuseUnknownFlags(ctx, ARGS.set_billing_spend_cap, []);
119
+ const args = argsFromFlags(ARGS.set_billing_spend_cap, flags, positionals.slice(1));
120
+ const result = await client.callTool("set_billing_spend_cap", args);
121
+ printResult(io, flags, result, (r) =>
122
+ r.spendCap
123
+ ? `Spend cap: ${usd(r.spendCap.usd)} of overage a month${r.spendCap.usd === 0 ? " (work stops at the allowance)" : ""}.${r.alert === "failed" ? " The alerts couldn't be moved; the cap holds." : ""}`
124
+ : "No spend cap: overage is on."
125
+ );
126
+ return 0;
127
+ }
128
+ if (positionals[0] === "request") {
129
+ refuseUnknownFlags(ctx, ARGS.request_billing_tokens, []);
130
+ const result = await client.callTool(
131
+ "request_billing_tokens",
132
+ argsFromFlags(ARGS.request_billing_tokens, flags, [])
133
+ );
134
+ printResult(io, flags, result, (r) => `Asked ${r.emailedTo} admin${r.emailedTo === 1 ? "" : "s"} for more.`);
135
+ return 0;
136
+ }
96
137
  refuseUnknownFlags(ctx, {}, []);
97
138
  const result = await client.callTool("get_billing_payments", {});
98
139
  printResult(io, flags, result, (r) =>
@@ -100,6 +141,16 @@ async function orgBilling(ctx) {
100
141
  r.plan
101
142
  ? `Plan: ${r.plan.name}${r.plan.ratio !== 1 ? ` (${r.plan.ratio}x)` : ""}`
102
143
  : "Not billed by usage yet.",
144
+ ...(r.usage
145
+ ? [
146
+ `This period (${r.usage.periodFrom.slice(0, 10)} to ${r.usage.periodTo.slice(0, 10)}): ${r.usage.usedTokens} of ${r.usage.allowanceTokens} tokens included${r.usage.overageUsd > 0 ? `, ${usd(r.usage.overageUsd)} of overage` : ""}.`,
147
+ ]
148
+ : []),
149
+ r.spendCap
150
+ ? `Spend cap: ${usd(r.spendCap.usd)} of overage a month${r.usage?.availableTokens != null ? `, ${Math.max(0, r.usage.availableTokens)} tokens left under it` : ""}.`
151
+ : r.plan
152
+ ? "No spend cap: overage is on."
153
+ : null,
103
154
  ...(r.pastDueAt ? [`Past due since ${r.pastDueAt}.`] : []),
104
155
  ...(r.payments.length
105
156
  ? r.payments.map(
@@ -107,7 +158,9 @@ async function orgBilling(ctx) {
107
158
  ` ${p.invoiceNumber ?? p.invoiceId} $${(p.amountCents / 100).toFixed(2)} ${p.status}${p.nextAttemptAt ? `, next try ${p.nextAttemptAt}` : ""}${p.payUrl ? ` pay: ${p.payUrl}` : ""}`
108
159
  )
109
160
  : ["No failed payments."]),
110
- ].join("\n")
161
+ ]
162
+ .filter((line) => line !== null)
163
+ .join("\n")
111
164
  );
112
165
  return 0;
113
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.145.3",
3
+ "version": "0.146.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",