@letterstory/cli 0.74.0 → 0.76.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
@@ -56,7 +56,7 @@ import {
56
56
  } from "./commands.mjs";
57
57
 
58
58
  // Keep in sync with cli/package.json.
59
- export const VERSION = "0.74.0";
59
+ export const VERSION = "0.76.0";
60
60
 
61
61
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
62
62
  // can't accidentally swallow the id as --json's value.
@@ -582,9 +582,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
582
582
  ledger entry. Written by \`npm run lab --
583
583
  publish\` in the kernels repo.
584
584
  admin spend summary [--range day|week|month|year | --from D --to D] [--grain day|week|month]
585
+ | reconcile [--from D --to D] | pnl [--range day|week|month|year | --from D --to D]
586
+ | reference add --vendor V [--key K] --metric billed_usd --value N --from D [--to D] --ref "…"
587
+ | stripe-sync --from D --to D
585
588
  | entries [--from D] [--to D] [--vendor V] [--area A] [--source invoice|csv_import|api|manual]
586
589
  | add --vendor V --amount N --from D [--to D] --area A --category C [--basis billed|estimated] …
590
+ | import <file.csv> [--vendor Concentrate] [--area A] [--ledger-key K] [--dry-run]
587
591
  | import <file.csv> [--vendor Concentrate] [--area A] [--keep-ledger-key] [--dry-run]
592
+ | usage <events.json> per-call costs as kernels/lettertrace push them
588
593
  | remove <entry-id> | services | service <slug> [--owner …] [--monthly N|none] [--renewal D]
589
594
  Where Letter's money goes (spending.letter.dev):
590
595
  totals, run rate, area/vendor/provider/model/
@@ -13,7 +13,7 @@
13
13
  // The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
14
14
  // admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
15
15
  // admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
16
- // admin spend summary|entries|add|import|remove|services|service … (spend:read / spend:write)
16
+ // admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
17
17
  // admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
18
18
  // admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
19
19
  // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
@@ -3019,7 +3019,7 @@ const SPEND_CATEGORIES = [
3019
3019
  "devtools",
3020
3020
  "other",
3021
3021
  ];
3022
- const SPEND_BASES = ["billed", "estimated"];
3022
+ const SPEND_BASES = ["billed", "reported", "estimated"];
3023
3023
  const SPEND_SOURCES = ["invoice", "csv_import", "api", "manual"];
3024
3024
 
3025
3025
  function usd(n) {
@@ -3088,6 +3088,8 @@ function spendEntryFromFlags(flags) {
3088
3088
  source_ref: flagStr(flags.ref),
3089
3089
  note: flagStr(flags.note),
3090
3090
  external_id: flagStr(flags["external-id"]),
3091
+ byok: flags.byok === undefined ? undefined : flagBool(flags.byok),
3092
+ ledger_scope: flags["ledger-scope"] === undefined ? undefined : flagBool(flags["ledger-scope"]),
3091
3093
  });
3092
3094
  }
3093
3095
 
@@ -3140,7 +3142,7 @@ async function cmdAdminSpend(ctx) {
3140
3142
  const path = positionals[1];
3141
3143
  if (!path)
3142
3144
  throw new CliError(
3143
- 'Usage: admin spend import <file.csv> [--vendor Concentrate] [--area kernels] [--category ai_model] [--basis billed|estimated] [--ref "…"] [--keep-ledger-key] [--dry-run]'
3145
+ 'Usage: admin spend import <file.csv> [--vendor Concentrate] [--area kernels] [--category ai_model] [--basis billed|estimated] [--ref "…"] [--ledger-key \"Main Concentrate Key\"] [--dry-run]'
3144
3146
  );
3145
3147
  const body = compact({
3146
3148
  csv: readFileSync(path, "utf8"),
@@ -3149,7 +3151,7 @@ async function cmdAdminSpend(ctx) {
3149
3151
  category: oneOf(flags, "category", SPEND_CATEGORIES),
3150
3152
  basis: oneOf(flags, "basis", SPEND_BASES),
3151
3153
  source_ref: flagStr(flags.ref) ?? `CSV import: ${path.split("/").pop()}`,
3152
- exclude_ledger_key: flagBool(flags["keep-ledger-key"]) ? null : undefined,
3154
+ ledger_key: flagStr(flags["ledger-key"]),
3153
3155
  dry_run: flagBool(flags["dry-run"]) || undefined,
3154
3156
  });
3155
3157
  const result = await staffPost(ctx, `${SPEND}/import`, body);
@@ -3157,8 +3159,8 @@ async function cmdAdminSpend(ctx) {
3157
3159
  ctx,
3158
3160
  `${result.used} of ${result.rows} rows → ${result.entries.length} entries, ${usd(result.total)}` +
3159
3161
  (result.days ? ` (${result.days.from} → ${result.days.to})` : "") +
3160
- (result.excludedLedgerRows
3161
- ? `; ${result.excludedLedgerRows} BYOK rows on the ledger's key dropped`
3162
+ (result.ledgerScopeRows
3163
+ ? `; ${result.ledgerScopeRows} rows on the ledger's own key kept as ledger-scope (reconciled, not added)`
3162
3164
  : "") +
3163
3165
  (flagBool(flags["dry-run"]) ? " (dry run, nothing written)" : "")
3164
3166
  );
@@ -3171,6 +3173,38 @@ async function cmdAdminSpend(ctx) {
3171
3173
  printResult(io, flags, result, () => "");
3172
3174
  return 0;
3173
3175
  }
3176
+ case "usage": {
3177
+ // Per-call model costs, as kernels / lettertrace push them (POST /api/superadmin/spend/usage):
3178
+ // a JSON file holding { events: [...] } or a bare array. Idempotent on each event_id.
3179
+ const path = positionals[1];
3180
+ if (!path) throw new CliError("Usage: admin spend usage <events.json> ({ events: [...] } or an array)");
3181
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
3182
+ const events = Array.isArray(parsed) ? parsed : parsed?.events;
3183
+ if (!Array.isArray(events) || events.length === 0) throw new CliError(`${path} holds no events.`);
3184
+ let inserted = 0;
3185
+ let duplicates = 0;
3186
+ let cost = 0;
3187
+ const rejected = [];
3188
+ for (let i = 0; i < events.length; i += 1000) {
3189
+ const r = await staffPost(ctx, `${SPEND}/usage`, { events: events.slice(i, i + 1000) });
3190
+ inserted += r.inserted;
3191
+ duplicates += r.duplicates;
3192
+ cost += r.cost_usd;
3193
+ rejected.push(...(r.rejected ?? []));
3194
+ }
3195
+ ok(
3196
+ ctx,
3197
+ `${events.length} events: ${inserted} new (${usd(cost)}), ${duplicates} already on file` +
3198
+ (rejected.length ? `, ${rejected.length} refused` : "")
3199
+ );
3200
+ printResult(
3201
+ io,
3202
+ flags,
3203
+ { received: events.length, inserted, duplicates, cost_usd: cost, rejected },
3204
+ () => ""
3205
+ );
3206
+ return 0;
3207
+ }
3174
3208
  case "remove": {
3175
3209
  const id = positionals[1];
3176
3210
  if (!id) throw new CliError("Usage: admin spend remove <entry-id>");
@@ -3179,6 +3213,85 @@ async function cmdAdminSpend(ctx) {
3179
3213
  printResult(io, flags, result);
3180
3214
  return 0;
3181
3215
  }
3216
+ case "reconcile": {
3217
+ const params = new URLSearchParams(compact({ from: flagStr(flags.from), to: flagStr(flags.to) }));
3218
+ const result = await staffRequest(ctx, "GET", `${SPEND}/reconcile${params.size ? `?${params}` : ""}`);
3219
+ printResult(io, flags, result, (r) =>
3220
+ [
3221
+ `${r.window.from} → ${r.window.to}: ${r.rows.filter((x) => x.flag).length} of ${r.rows.length} checks flagged`,
3222
+ ...r.rows.map(
3223
+ (x) =>
3224
+ `${x.flag ? "⚑" : " "} ${x.label.slice(0, 44).padEnd(44)} ours ${usd(x.ours.value).padStart(10)} theirs ${usd(x.theirs.value).padStart(10)} ${x.diffPct === null ? "" : `${x.diffPct > 0 ? "+" : ""}${x.diffPct}%`}`
3225
+ ),
3226
+ ...r.notes.map((n) => ` · ${n}`),
3227
+ ].join("\n")
3228
+ );
3229
+ return 0;
3230
+ }
3231
+ case "pnl": {
3232
+ const params = new URLSearchParams(
3233
+ compact({
3234
+ range: oneOf(flags, "range", SPEND_RANGES),
3235
+ from: flagStr(flags.from),
3236
+ to: flagStr(flags.to),
3237
+ })
3238
+ );
3239
+ const result = await staffRequest(ctx, "GET", `${SPEND}/pnl${params.size ? `?${params}` : ""}`);
3240
+ printResult(io, flags, result, (p) =>
3241
+ [
3242
+ `${p.range.from} → ${p.range.to}: revenue ${usd(p.totals.revenue)} (${p.revenueSource}) · cost ${usd(p.totals.cost.total)} · margin ${usd(p.totals.margin)}${p.totals.marginPct === null ? "" : ` (${p.totals.marginPct}%)`}`,
3243
+ ` MRR ${usd(p.totals.mrr)} · invoiced ${usd(p.totals.invoiced)} · owed ${usd(p.totals.owed)} · Stripe fees ${usd(p.totals.stripeFees)}`,
3244
+ "",
3245
+ ...p.lines.map(
3246
+ (l) =>
3247
+ ` ${l.label.slice(0, 46).padEnd(46)} rev ${usd(l.revenue).padStart(9)} cost ${usd(l.cost).padStart(9)} margin ${usd(l.margin).padStart(10)}`
3248
+ ),
3249
+ ].join("\n")
3250
+ );
3251
+ return 0;
3252
+ }
3253
+ case "reference": {
3254
+ const vendor = flagStr(flags.vendor);
3255
+ const metric = flagStr(flags.metric);
3256
+ const value = flagNum(flags.value);
3257
+ const from = flagStr(flags.from);
3258
+ const to = flagStr(flags.to) ?? from;
3259
+ const ref = flagStr(flags.ref);
3260
+ if (positionals[1] !== "add" || !vendor || !metric || value === undefined || !from || !ref) {
3261
+ throw new CliError(
3262
+ 'Usage: admin spend reference add --vendor Concentrate [--key "Writing Kernels v2"] --metric billed_usd --value 17324.24 --from 2026-09-10 --to 2026-10-09 --ref "Concentrate dashboard 2026-10-09"'
3263
+ );
3264
+ }
3265
+ const result = await staffPost(
3266
+ ctx,
3267
+ `${SPEND}/references`,
3268
+ compact({
3269
+ vendor,
3270
+ key_label: flagStr(flags.key),
3271
+ metric,
3272
+ value,
3273
+ period_start: from,
3274
+ period_end: to,
3275
+ source_ref: ref,
3276
+ note: flagStr(flags.note),
3277
+ })
3278
+ );
3279
+ ok(ctx, `Recorded ${metric} ${value} for ${vendor}${flags.key ? ` (${flagStr(flags.key)})` : ""}.`);
3280
+ printResult(io, flags, result);
3281
+ return 0;
3282
+ }
3283
+ case "stripe-sync": {
3284
+ const from = flagStr(flags.from);
3285
+ const to = flagStr(flags.to);
3286
+ if (!from || !to) throw new CliError("Usage: admin spend stripe-sync --from 2026-01-01 --to 2026-10-09");
3287
+ const result = await staffPost(ctx, `${SPEND}/stripe-sync`, { from, to });
3288
+ ok(
3289
+ ctx,
3290
+ `Stripe ${result.status}: ${result.transactions ?? 0} balance transactions, fees filed for ${result.feeDays ?? 0} days.`
3291
+ );
3292
+ printResult(io, flags, result);
3293
+ return 0;
3294
+ }
3182
3295
  case "services": {
3183
3296
  const result = await staffRequest(ctx, "GET", `${SPEND}/services`);
3184
3297
  printResult(io, flags, result, formatSpendServices);
@@ -3208,7 +3321,7 @@ async function cmdAdminSpend(ctx) {
3208
3321
  }
3209
3322
  default:
3210
3323
  throw new CliError(
3211
- "Usage: admin spend summary [--range day|week|month|year | --from D --to D] [--grain day|week|month] | entries [--from D] [--to D] [--vendor V] [--area A] [--source S] | add --vendor V --amount N --from D … | import <file.csv> … | remove <entry-id> | services | service <slug> --owner … --monthly …"
3324
+ "Usage: admin spend summary [--range day|week|month|year | --from D --to D] [--grain day|week|month] | reconcile [--from D --to D] | pnl [--range …] | reference add … | stripe-sync --from D --to D | entries [--from D] [--to D] [--vendor V] [--area A] [--source S] | add --vendor V --amount N --from D … | import <file.csv> … | usage <events.json> | remove <entry-id> | services | service <slug> --owner … --monthly …"
3212
3325
  );
3213
3326
  }
3214
3327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",