@letterstory/cli 0.135.0 → 0.136.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/README.md +1 -0
- package/lib/cli.mjs +10 -1
- package/lib/commands/admin.mjs +76 -0
- package/lib/commands/site.mjs +47 -2
- package/lib/commands/strategy.mjs +30 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -254,6 +254,7 @@ letterstory strategy company set --name "Acme" --domain acme.com --manifesto "
|
|
|
254
254
|
|
|
255
255
|
letterstory strategy positioning get
|
|
256
256
|
letterstory strategy positioning set --topic seo --topic ai --stance "…" --clear-avoid
|
|
257
|
+
letterstory strategy positioning set --company-kind devtools --stage seed --motion product-led --category "AI visibility" # the company model; `none` clears a field
|
|
257
258
|
|
|
258
259
|
letterstory strategy competitors list
|
|
259
260
|
letterstory strategy competitors add "Rival Co" rival.com
|
package/lib/cli.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
} from "./commands.mjs";
|
|
63
63
|
|
|
64
64
|
// Keep in sync with cli/package.json.
|
|
65
|
-
export const VERSION = "0.
|
|
65
|
+
export const VERSION = "0.136.0";
|
|
66
66
|
|
|
67
67
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
68
68
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -288,6 +288,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
288
288
|
"todo",
|
|
289
289
|
// admin spend margin: list nested work (never billed twice) beside its parent.
|
|
290
290
|
"nested",
|
|
291
|
+
// admin billing lago catalog --apply --include-drafts
|
|
292
|
+
"include-drafts",
|
|
291
293
|
]);
|
|
292
294
|
|
|
293
295
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -908,6 +910,13 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
908
910
|
cover what the app ledger cannot see; a CSV
|
|
909
911
|
import sums Concentrate's log export.
|
|
910
912
|
Needs spend:read (reads) / spend:write (writes).
|
|
913
|
+
admin billing lago catalog [--apply] [--include-drafts]
|
|
914
|
+
| provision <org-id> --plan starter|growth|scale [--ratio 1|2|4|8 (8 needs --why "…")] [--complimentary N] [--stripe-customer cus_…] [--start ISO] [--review D]
|
|
915
|
+
| reconcile [--org <org-id>] | forward
|
|
916
|
+
Usage billing on Lago: the plan catalog, an org's
|
|
917
|
+
plan and gifted ratio, Lago's tokens against ours,
|
|
918
|
+
and forwarding usage now. Needs spend:read (reads)
|
|
919
|
+
/ billing:write (writes).
|
|
911
920
|
admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
|
|
912
921
|
| capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
|
|
913
922
|
[--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -4996,12 +4996,88 @@ async function cmdAdminRecipes(ctx) {
|
|
|
4996
4996
|
}
|
|
4997
4997
|
}
|
|
4998
4998
|
|
|
4999
|
+
const LAGO = "/api/superadmin/billing/lago";
|
|
5000
|
+
const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
5001
|
+
catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
|
|
5002
|
+
provision <org-id> --plan starter [--ratio 4] [--why "…" above 4x] [--complimentary 20000] [--stripe-customer cus_…] [--start 2026-11-02T00:00:00Z] [--review 2027-05-01]
|
|
5003
|
+
reconcile [--org <org-id>] Lago's tokens this period against what we rated and forwarded
|
|
5004
|
+
forward forward billable usage to Lago now (the cron's pass)`;
|
|
5005
|
+
|
|
5006
|
+
/** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
|
|
5007
|
+
async function cmdAdminBilling(ctx) {
|
|
5008
|
+
const { flags, positionals, io } = ctx;
|
|
5009
|
+
if (positionals[0] !== "lago") throw new CliError(LAGO_USAGE);
|
|
5010
|
+
const sub = positionals[1];
|
|
5011
|
+
switch (sub) {
|
|
5012
|
+
case "catalog": {
|
|
5013
|
+
const result = flagBool(flags.apply)
|
|
5014
|
+
? await staffPost(
|
|
5015
|
+
ctx,
|
|
5016
|
+
`${LAGO}/catalog`,
|
|
5017
|
+
compact({ includeDrafts: flagBool(flags["include-drafts"]) || undefined })
|
|
5018
|
+
)
|
|
5019
|
+
: await staffRequest(ctx, "GET", `${LAGO}/catalog`);
|
|
5020
|
+
const changed = (result.changes ?? []).filter((c) => c.action === "create" || c.action === "update");
|
|
5021
|
+
ok(
|
|
5022
|
+
ctx,
|
|
5023
|
+
`${result.dryRun ? "Would change" : "Changed"} ${changed.length} of ${(result.changes ?? []).length}.`
|
|
5024
|
+
);
|
|
5025
|
+
printResult(io, flags, result);
|
|
5026
|
+
return 0;
|
|
5027
|
+
}
|
|
5028
|
+
case "provision": {
|
|
5029
|
+
const orgId = requirePositional(positionals, 2, "org-id");
|
|
5030
|
+
const plan = flagStr(flags.plan);
|
|
5031
|
+
if (!plan) throw new CliError(LAGO_USAGE);
|
|
5032
|
+
const result = await staffPost(
|
|
5033
|
+
ctx,
|
|
5034
|
+
`${LAGO}/provision`,
|
|
5035
|
+
compact({
|
|
5036
|
+
orgId,
|
|
5037
|
+
planCode: plan,
|
|
5038
|
+
ratio: flagNum(flags.ratio),
|
|
5039
|
+
exceptionReason: flagStr(flags.why),
|
|
5040
|
+
complimentaryTokens: flagNum(flags.complimentary),
|
|
5041
|
+
stripeCustomerId: flagStr(flags["stripe-customer"]),
|
|
5042
|
+
startAt: flagStr(flags.start),
|
|
5043
|
+
reviewAt: flagStr(flags.review),
|
|
5044
|
+
})
|
|
5045
|
+
);
|
|
5046
|
+
ok(
|
|
5047
|
+
ctx,
|
|
5048
|
+
`${result.action === "created" ? "On Lago" : "Already on Lago"}: ${result.subscriptionId}, ${result.includedTokens} tokens included, overage $${result.overageUsdPerToken} a token, from ${result.startsAt}.`
|
|
5049
|
+
);
|
|
5050
|
+
printResult(io, flags, result);
|
|
5051
|
+
return 0;
|
|
5052
|
+
}
|
|
5053
|
+
case "reconcile": {
|
|
5054
|
+
const params = new URLSearchParams(compact({ orgId: flagStr(flags.org) }));
|
|
5055
|
+
const result = await staffRequest(ctx, "GET", `${LAGO}/reconcile${params.size ? `?${params}` : ""}`);
|
|
5056
|
+
ok(ctx, result.ok ? "Lago agrees with us." : "Lago and we disagree: see the orgs below.");
|
|
5057
|
+
printResult(io, flags, result);
|
|
5058
|
+
return result.ok ? 0 : 1;
|
|
5059
|
+
}
|
|
5060
|
+
case "forward": {
|
|
5061
|
+
const result = await staffPost(ctx, `${LAGO}/forward`, {});
|
|
5062
|
+
ok(
|
|
5063
|
+
ctx,
|
|
5064
|
+
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}.`
|
|
5065
|
+
);
|
|
5066
|
+
printResult(io, flags, result);
|
|
5067
|
+
return 0;
|
|
5068
|
+
}
|
|
5069
|
+
default:
|
|
5070
|
+
throw new CliError(LAGO_USAGE);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
4999
5074
|
export async function cmdAdmin(ctx) {
|
|
5000
5075
|
const sub = ctx.positionals[0];
|
|
5001
5076
|
if (sub === "replays" || sub === "replay")
|
|
5002
5077
|
return cmdAdminReplays({ ...ctx, positionals: ctx.positionals.slice(1) }, sub);
|
|
5003
5078
|
if (sub === "crm") return cmdAdminCrm({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5004
5079
|
if (sub === "spend") return cmdAdminSpend({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5080
|
+
if (sub === "billing") return cmdAdminBilling({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5005
5081
|
if (sub === "models") return cmdAdminModels({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5006
5082
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5007
5083
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
package/lib/commands/site.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (src/lib/site). One subcommand per tool:
|
|
3
3
|
// site list → list_sites site map <host> → map_site
|
|
4
4
|
// site tree <host> [path] → get_site_tree site page <host> <path> → get_site_page
|
|
5
|
+
// site builds <host> → list_build_suggestions (--limit n)
|
|
5
6
|
// site metrics <host> [path] → get_site_metrics (--window 7d|28d|90d, --page <path>…, --no-compare)
|
|
6
7
|
// site observations <host> → list_site_observations site observe <host> → observe_site
|
|
7
8
|
// site dismiss <host> <id> → dismiss_site_observation
|
|
@@ -78,6 +79,10 @@ const ARGS = {
|
|
|
78
79
|
},
|
|
79
80
|
},
|
|
80
81
|
},
|
|
82
|
+
list_build_suggestions: {
|
|
83
|
+
host: { positional: 0, required: true, label: "host" },
|
|
84
|
+
limit: { flag: "limit", type: "num" },
|
|
85
|
+
},
|
|
81
86
|
get_site_metrics: {
|
|
82
87
|
host: { positional: 0, required: true, label: "host" },
|
|
83
88
|
path: { positional: 1, label: "path" },
|
|
@@ -199,6 +204,8 @@ export async function cmdSite(ctx) {
|
|
|
199
204
|
return siteUnmap(rest);
|
|
200
205
|
case "tree":
|
|
201
206
|
return siteTree(rest);
|
|
207
|
+
case "builds":
|
|
208
|
+
return siteBuilds(rest);
|
|
202
209
|
case "metrics":
|
|
203
210
|
return siteMetrics(rest);
|
|
204
211
|
case "page":
|
|
@@ -229,7 +236,7 @@ export async function cmdSite(ctx) {
|
|
|
229
236
|
return siteHighlight(rest);
|
|
230
237
|
default:
|
|
231
238
|
throw new CliError(
|
|
232
|
-
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, metrics, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors, highlight`
|
|
239
|
+
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, builds, metrics, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors, highlight`
|
|
233
240
|
);
|
|
234
241
|
}
|
|
235
242
|
}
|
|
@@ -243,7 +250,26 @@ function describeNode(node, indent = "") {
|
|
|
243
250
|
: "";
|
|
244
251
|
const changed = `${node.rollup.added ? ` +${node.rollup.added}` : ""}${node.rollup.removed ? ` -${node.rollup.removed}` : ""}`;
|
|
245
252
|
const counts = node.hasChildren ? ` (${node.rollup.pages} pages${changed})` : node.page?.added ? " (new)" : "";
|
|
246
|
-
|
|
253
|
+
// A folder's measured pages, only where the source is measured (null draws nothing: never zero).
|
|
254
|
+
const m = node.rollup.measured ?? {};
|
|
255
|
+
const measured = [
|
|
256
|
+
m.indexed != null ? `${m.indexed} indexed` : null,
|
|
257
|
+
m.cited != null ? `${m.cited} cited` : null,
|
|
258
|
+
m.read != null ? `${m.read} read` : null,
|
|
259
|
+
].filter(Boolean);
|
|
260
|
+
const coverage = node.rollup.coverage
|
|
261
|
+
? `${node.rollup.coverage.covered} of ${node.rollup.coverage.total} covered`
|
|
262
|
+
: null;
|
|
263
|
+
const rollup =
|
|
264
|
+
node.hasChildren && (measured.length || coverage)
|
|
265
|
+
? ` [${[...measured, coverage].filter(Boolean).join(" · ")}]`
|
|
266
|
+
: "";
|
|
267
|
+
const out = [`${indent}${glyph.padEnd(2)} ${node.name}${counts}${rollup}${line ? ` ${line}` : ""}`];
|
|
268
|
+
// What is next in the pattern the folder keeps: violet on the Site, its state the letterbase's.
|
|
269
|
+
if (node.next) {
|
|
270
|
+
const owner = node.next.owner ? `${node.next.owner.name} · ${node.next.state}` : "next in the pattern";
|
|
271
|
+
out.push(`${indent} ◇ ${node.next.path ?? node.next.title} ${node.next.why} (${owner})`);
|
|
272
|
+
}
|
|
247
273
|
for (const child of node.children ?? []) out.push(describeNode(child, `${indent} `));
|
|
248
274
|
return out.join("\n");
|
|
249
275
|
}
|
|
@@ -366,6 +392,25 @@ async function siteTree(ctx) {
|
|
|
366
392
|
return 0;
|
|
367
393
|
}
|
|
368
394
|
|
|
395
|
+
async function siteBuilds(ctx) {
|
|
396
|
+
const { client, flags, io } = ctx;
|
|
397
|
+
refuseUnknownFlags(ctx, ARGS.list_build_suggestions);
|
|
398
|
+
const result = await client.callTool(
|
|
399
|
+
"list_build_suggestions",
|
|
400
|
+
argsFromFlags(ARGS.list_build_suggestions, flags, ctx.positionals)
|
|
401
|
+
);
|
|
402
|
+
printResult(io, flags, result, (r) => {
|
|
403
|
+
const lines = r.suggestions.map(
|
|
404
|
+
(s) =>
|
|
405
|
+
`${s.summary} [${s.sources.join(", ")}]${s.action === "article" ? " (an article: the letterbase's)" : ""}\n ${s.relevance}\n ${s.id}`
|
|
406
|
+
);
|
|
407
|
+
if (!lines.length) lines.push("Nothing to build: no source makes a surface this site's right now.");
|
|
408
|
+
if (r.skipped.length) lines.push(`Not read: ${r.skipped.join(", ")}`);
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
});
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
369
414
|
async function siteMetrics(ctx) {
|
|
370
415
|
const { client, flags, io } = ctx;
|
|
371
416
|
refuseUnknownFlags(ctx, ARGS.get_site_metrics);
|
|
@@ -16,6 +16,18 @@ const clearableList = (flag, clearFlag) => ({
|
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
// A scalar the API clears with an explicit null: `--stage none` sends null, a value sends it
|
|
20
|
+
// (one of `values` when given), nothing sends nothing.
|
|
21
|
+
const clearable = (flag, values) => ({
|
|
22
|
+
value: (flags) => {
|
|
23
|
+
const v = flagStr(flags[flag]);
|
|
24
|
+
if (v === undefined || v === null || v === "") return undefined;
|
|
25
|
+
if (v === "none") return null;
|
|
26
|
+
if (values && !values.includes(v)) throw new CliError(`--${flag} must be one of: ${values.join(", ")}, none`);
|
|
27
|
+
return v;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
19
31
|
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
20
32
|
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
21
33
|
export const ARGS = {
|
|
@@ -40,6 +52,21 @@ export const ARGS = {
|
|
|
40
52
|
priority_topics: clearableList("topic", "clear-topics"),
|
|
41
53
|
stances: clearableList("stance", "clear-stances"),
|
|
42
54
|
avoid_topics: clearableList("avoid", "clear-avoid"),
|
|
55
|
+
// The company model (lib/positioning/company-model.ts): a vocabulary word, or "none" to clear.
|
|
56
|
+
company_kind: clearable("company-kind", [
|
|
57
|
+
"b2b_saas",
|
|
58
|
+
"devtools",
|
|
59
|
+
"ai_product",
|
|
60
|
+
"ecommerce",
|
|
61
|
+
"marketplace",
|
|
62
|
+
"agency",
|
|
63
|
+
"consumer",
|
|
64
|
+
"media",
|
|
65
|
+
"local_business",
|
|
66
|
+
]),
|
|
67
|
+
stage: clearable("stage", ["pre-seed", "seed", "series-a-plus", "established"]),
|
|
68
|
+
motion: clearable("motion", ["self-serve", "sales-led", "product-led", "open-source"]),
|
|
69
|
+
category: clearable("category"),
|
|
43
70
|
},
|
|
44
71
|
list_competitors: {},
|
|
45
72
|
add_competitor: {
|
|
@@ -155,7 +182,9 @@ async function positioningSet(ctx) {
|
|
|
155
182
|
const { client, flags, io } = ctx;
|
|
156
183
|
const args = argsFromFlags(ARGS.set_positioning, flags);
|
|
157
184
|
if (Object.keys(args).length === 0) {
|
|
158
|
-
throw new CliError(
|
|
185
|
+
throw new CliError(
|
|
186
|
+
"Pass at least one --topic/--stance/--avoid (or a --clear-* flag), or --company-kind/--stage/--motion/--category (none clears)."
|
|
187
|
+
);
|
|
159
188
|
}
|
|
160
189
|
const result = await client.callTool("set_positioning", args);
|
|
161
190
|
ok(ctx, "Positioning updated.");
|