@letterstory/cli 0.75.0 → 0.76.1
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 +8 -0
- package/lib/cli.mjs +6 -1
- package/lib/commands/admin.mjs +88 -7
- package/lib/commands/insights.mjs +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -749,6 +749,14 @@ AI visibility and page access — the Measure tabs' reads, for scripts and agent
|
|
|
749
749
|
# search, actions, targets — `ai` is the slow one on a large org.
|
|
750
750
|
letterstory insights summary [--period 14d|30d|90d] [--part <block>]...
|
|
751
751
|
|
|
752
|
+
# GEO → Competitors: every tracked brand's share of mentions (one per brand per
|
|
753
|
+
# answer) on the window, whether each is moving (the same question on the same
|
|
754
|
+
# engine in both windows — the only up/down read), the question × engine cells
|
|
755
|
+
# behind it, the line per engine, each question's top rival, and every dated
|
|
756
|
+
# change under the number. --period sets the windows: 14d → last 2 complete
|
|
757
|
+
# weeks vs the 2 before, 30d → 4 vs 4, 90d → 13 vs 13.
|
|
758
|
+
letterstory insights competitors [--period 14d|30d|90d]
|
|
759
|
+
|
|
752
760
|
# The AI Visibility Bank: which questions named you, cited your pages, or both (a
|
|
753
761
|
# breakthrough). All time unless --period; --source picks the phantom network
|
|
754
762
|
# (default), your marketing site, or the whole portfolio; --phantom narrows to
|
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.1";
|
|
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,8 +582,12 @@ 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]
|
|
588
592
|
| usage <events.json> per-call costs as kernels/lettertrace push them
|
|
589
593
|
| remove <entry-id> | services | service <slug> [--owner …] [--monthly N|none] [--renewal D]
|
|
@@ -918,6 +922,7 @@ Mail:
|
|
|
918
922
|
|
|
919
923
|
Insights:
|
|
920
924
|
insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
|
|
925
|
+
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
921
926
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
922
927
|
insights post <article-id> [--period 14d|30d|90d]
|
|
923
928
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
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|usage|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
|
);
|
|
@@ -3211,6 +3213,85 @@ async function cmdAdminSpend(ctx) {
|
|
|
3211
3213
|
printResult(io, flags, result);
|
|
3212
3214
|
return 0;
|
|
3213
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
|
+
}
|
|
3214
3295
|
case "services": {
|
|
3215
3296
|
const result = await staffRequest(ctx, "GET", `${SPEND}/services`);
|
|
3216
3297
|
printResult(io, flags, result, formatSpendServices);
|
|
@@ -3240,7 +3321,7 @@ async function cmdAdminSpend(ctx) {
|
|
|
3240
3321
|
}
|
|
3241
3322
|
default:
|
|
3242
3323
|
throw new CliError(
|
|
3243
|
-
"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> … | usage <events.json> | 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 …"
|
|
3244
3325
|
);
|
|
3245
3326
|
}
|
|
3246
3327
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// `insights` — Search Console impressions/clicks across the phantom network, at the
|
|
2
2
|
// network, single-post, and top-posts levels (ported from phantomstory-cli's
|
|
3
|
-
// insights.ts); plus Measure's Overview (`summary`),
|
|
4
|
-
// and phantom Page Access (`access`)
|
|
3
|
+
// insights.ts); plus Measure's Overview (`summary`), GEO → Competitors
|
|
4
|
+
// (`competitors`), AI Visibility Bank (`bank`) and phantom Page Access (`access`)
|
|
5
|
+
// — the same reads the web app's Measure tabs render.
|
|
5
6
|
|
|
6
7
|
import { CliError } from "../client.mjs";
|
|
7
8
|
import { flagStr, flagNum, requirePositional, printResult, compact, argsFromFlags } from "./shared.mjs";
|
|
@@ -23,6 +24,9 @@ export const ARGS = {
|
|
|
23
24
|
period: { flag: "period", enum: PERIODS },
|
|
24
25
|
parts: { flag: "part", type: "list" },
|
|
25
26
|
},
|
|
27
|
+
get_share_of_voice_detail: {
|
|
28
|
+
period: { flag: "period", enum: PERIODS },
|
|
29
|
+
},
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
export async function cmdInsights(ctx) {
|
|
@@ -41,9 +45,11 @@ export async function cmdInsights(ctx) {
|
|
|
41
45
|
return call(rest, "get_page_access");
|
|
42
46
|
case "summary":
|
|
43
47
|
return call(rest, "get_measure_summary");
|
|
48
|
+
case "competitors":
|
|
49
|
+
return call(rest, "get_share_of_voice_detail");
|
|
44
50
|
default:
|
|
45
51
|
throw new CliError(
|
|
46
|
-
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, site, post, top, bank, access`
|
|
52
|
+
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, competitors, site, post, top, bank, access`
|
|
47
53
|
);
|
|
48
54
|
}
|
|
49
55
|
}
|