@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 +6 -1
- package/lib/commands/admin.mjs +120 -7
- package/package.json +1 -1
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.
|
|
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/
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -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 …
|
|
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 "…"] [--
|
|
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
|
-
|
|
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.
|
|
3161
|
-
? `; ${result.
|
|
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
|
}
|