@letterstory/cli 0.108.0 → 0.108.2
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 +5 -2
- package/lib/commands/admin.mjs +19 -1
- package/lib/commands/site.mjs +51 -2
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./commands.mjs";
|
|
61
61
|
|
|
62
62
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.108.
|
|
63
|
+
export const VERSION = "0.108.2";
|
|
64
64
|
|
|
65
65
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
66
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -1160,7 +1160,8 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1160
1160
|
site refresh <host> [--force-map] [--force-audit] [--skip-observe]
|
|
1161
1161
|
Run the daily loop now: re-map, re-audit, signals, Jev
|
|
1162
1162
|
site unmap <host> Remove a host and everything hanging off it
|
|
1163
|
-
site tree <host> [path] [--depth <n>] [--unfold]
|
|
1163
|
+
site tree <host> [path] [--depth <n>] [--unfold] [--since <iso>]
|
|
1164
|
+
The tree one folder at a time; --since adds +N/-N per folder
|
|
1164
1165
|
site page <host> <path> One page: facts, bindings, raw URLs, open items
|
|
1165
1166
|
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>]
|
|
1166
1167
|
site observe <host> [path] [--max-pages <n>] Jev pass: meta accuracy, title, outdated
|
|
@@ -1172,6 +1173,8 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1172
1173
|
site propose <host> [path] --tool <tool> --input '<json>'|--input-file <p> --label <text> [--why <text>]
|
|
1173
1174
|
[--observation <id>]... Propose a registry tool call; input is validated now
|
|
1174
1175
|
site decide <proposal-id> --decision accept|dismiss [--reason <why>]
|
|
1176
|
+
site competitors [--since <iso>] [--limit <n>] Tracked competitors' sites: new and removed pages, by folder
|
|
1177
|
+
site watch-competitors [domain] Map competitors' sitemaps (every unmapped one, or one now)
|
|
1175
1178
|
|
|
1176
1179
|
Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub App opens a PR):
|
|
1177
1180
|
repo repos Repos the Letterstory GitHub App can reach
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
|
|
15
15
|
// admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
|
|
16
16
|
// admin fleet-experiments list|articles|clients|mark|unmark|new|assign|assign-eligible|assign-orgs|spin-up|apply|start|stop|decide … (fleet:read / fleet:experiment)
|
|
17
|
-
// admin crm import|capture|outreach|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)
|
|
17
|
+
// admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|revenue|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
18
18
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
19
19
|
// admin brand-detection <org-id> (fleet:read)
|
|
20
20
|
// admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
|
|
@@ -3325,6 +3325,24 @@ async function cmdAdminCrm(ctx) {
|
|
|
3325
3325
|
].join("\n")
|
|
3326
3326
|
);
|
|
3327
3327
|
}
|
|
3328
|
+
case "revenue": {
|
|
3329
|
+
// admin crm revenue — monthly NRR/GRR since 2026-08-10, the bridge, spend per client (docs/architecture/retention.md).
|
|
3330
|
+
const pct = (x) => (x == null ? "—" : `${(x * 100).toFixed(1)}%`);
|
|
3331
|
+
const usd = (cents) => `$${Math.round(cents / 100).toLocaleString("en-US")}`;
|
|
3332
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/revenue`), (r) =>
|
|
3333
|
+
[
|
|
3334
|
+
r.headline
|
|
3335
|
+
? `${r.headline.month}: NRR ${pct(r.headline.nrr)} · GRR ${pct(r.headline.grr)} · expansion ${usd(r.headline.expansionCents)} · avg ${r.headline.avgCents == null ? "—" : usd(r.headline.avgCents)}/client`
|
|
3336
|
+
: "No complete month yet.",
|
|
3337
|
+
`Clients counted: ${r.coverage.counted} (${r.coverage.stripe} Stripe, ${r.coverage.deal} deal) · comp ${r.coverage.comp} · ${usd(r.coverage.countedCents)}/mo`,
|
|
3338
|
+
"",
|
|
3339
|
+
...r.periods.map(
|
|
3340
|
+
(p) =>
|
|
3341
|
+
`${p.month}${p.partial ? " (partial)" : ""}${p.estimated ? " ~" : ""} ${usd(p.startCents)} → ${usd(p.endCents)} NRR ${pct(p.nrr)} +new ${usd(p.moves.new)} +back ${usd(p.moves.reactivated)} +exp ${usd(p.moves.expansion)} −con ${usd(p.moves.contraction)} −churn ${usd(p.moves.churn)} ±reclassified ${usd(p.moves.reclassified)}`
|
|
3342
|
+
),
|
|
3343
|
+
].join("\n")
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3328
3346
|
case "record-churn": {
|
|
3329
3347
|
// admin crm record-churn <account-id> <reason> [--note "…"] [--at ISO] [--mrr-cents N]
|
|
3330
3348
|
if (!id || !action) {
|
package/lib/commands/site.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// site families <host> → list_site_families site decide-family <id> → decide_site_family
|
|
8
8
|
// site proposals <host> → list_site_proposals site propose <host> → create_site_proposal
|
|
9
9
|
// site decide <proposal> → decide_site_proposal site fix <host> <id>… → fix_site_observations
|
|
10
|
+
// site competitors → list_competitor_sites site watch-competitors [d] → watch_competitor_sites
|
|
10
11
|
|
|
11
12
|
import { CliError } from "../client.mjs";
|
|
12
13
|
import { argsFromFlags, flagList, flagStr, ok, printResult, readTextInput, refuseUnknownFlags } from "./shared.mjs";
|
|
@@ -56,7 +57,13 @@ const ARGS = {
|
|
|
56
57
|
depth: { flag: "depth", type: "num" },
|
|
57
58
|
unfold: { flag: "unfold", type: "bool" },
|
|
58
59
|
subtree: { flag: "subtree", type: "bool" },
|
|
60
|
+
since: { flag: "since" },
|
|
59
61
|
},
|
|
62
|
+
list_competitor_sites: {
|
|
63
|
+
since: { flag: "since" },
|
|
64
|
+
limit: { flag: "limit", type: "num" },
|
|
65
|
+
},
|
|
66
|
+
watch_competitor_sites: { domain: { positional: 0, label: "domain" } },
|
|
60
67
|
get_site_page: {
|
|
61
68
|
host: { positional: 0, required: true, label: "host" },
|
|
62
69
|
path: { positional: 1, required: true, label: "path" },
|
|
@@ -166,9 +173,13 @@ export async function cmdSite(ctx) {
|
|
|
166
173
|
return siteDecide(rest);
|
|
167
174
|
case "fix":
|
|
168
175
|
return siteFix(rest);
|
|
176
|
+
case "competitors":
|
|
177
|
+
return siteCompetitors(rest);
|
|
178
|
+
case "watch-competitors":
|
|
179
|
+
return siteWatchCompetitors(rest);
|
|
169
180
|
default:
|
|
170
181
|
throw new CliError(
|
|
171
|
-
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix`
|
|
182
|
+
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors`
|
|
172
183
|
);
|
|
173
184
|
}
|
|
174
185
|
}
|
|
@@ -180,7 +191,8 @@ function describeNode(node, indent = "") {
|
|
|
180
191
|
const line = node.item
|
|
181
192
|
? `${node.item.summary}${node.more ? ` and ${node.more} more` : ""}${node.item.action ? ` → ${node.item.action.label}` : ""}`
|
|
182
193
|
: "";
|
|
183
|
-
const
|
|
194
|
+
const changed = `${node.rollup.added ? ` +${node.rollup.added}` : ""}${node.rollup.removed ? ` -${node.rollup.removed}` : ""}`;
|
|
195
|
+
const counts = node.hasChildren ? ` (${node.rollup.pages} pages${changed})` : node.page?.added ? " (new)" : "";
|
|
184
196
|
const out = [`${indent}${glyph.padEnd(2)} ${node.name}${counts}${line ? ` ${line}` : ""}`];
|
|
185
197
|
for (const child of node.children ?? []) out.push(describeNode(child, `${indent} `));
|
|
186
198
|
return out.join("\n");
|
|
@@ -203,6 +215,43 @@ async function siteList(ctx) {
|
|
|
203
215
|
return 0;
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
async function siteCompetitors(ctx) {
|
|
219
|
+
const { client, flags, io } = ctx;
|
|
220
|
+
refuseUnknownFlags(ctx, ARGS.list_competitor_sites);
|
|
221
|
+
const result = await client.callTool(
|
|
222
|
+
"list_competitor_sites",
|
|
223
|
+
argsFromFlags(ARGS.list_competitor_sites, flags, ctx.positionals)
|
|
224
|
+
);
|
|
225
|
+
printResult(io, flags, result, (r) => {
|
|
226
|
+
const lines = [`Since ${r.since.slice(0, 10)}:`];
|
|
227
|
+
for (const c of r.competitors) {
|
|
228
|
+
if (!c.changes) {
|
|
229
|
+
lines.push(` ${c.name} ${c.domain} not mapped (letterstory site watch-competitors ${c.domain})`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const folders = c.changes.folders.map((f) => `${f.path} +${f.added}${f.removed ? ` -${f.removed}` : ""}`);
|
|
233
|
+
lines.push(
|
|
234
|
+
` ${c.name} ${c.host} ${c.changes.pages} pages +${c.changes.added} -${c.changes.removed}${folders.length ? ` ${folders.join(", ")}` : ""}`
|
|
235
|
+
);
|
|
236
|
+
for (const p of c.changes.addedPages.slice(0, 5)) lines.push(` + ${p.path} ${p.at.slice(0, 10)}`);
|
|
237
|
+
}
|
|
238
|
+
return r.competitors.length ? lines.join("\n") : "No competitors tracked yet.";
|
|
239
|
+
});
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function siteWatchCompetitors(ctx) {
|
|
244
|
+
const { client, flags, io } = ctx;
|
|
245
|
+
refuseUnknownFlags(ctx, ARGS.watch_competitor_sites);
|
|
246
|
+
const result = await client.callTool(
|
|
247
|
+
"watch_competitor_sites",
|
|
248
|
+
argsFromFlags(ARGS.watch_competitor_sites, flags, ctx.positionals)
|
|
249
|
+
);
|
|
250
|
+
ok(ctx, result.message);
|
|
251
|
+
printResult(io, flags, result, () => "");
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
206
255
|
async function siteMap(ctx) {
|
|
207
256
|
const { client, flags, io } = ctx;
|
|
208
257
|
refuseUnknownFlags(ctx, ARGS.map_site);
|