@letterstory/cli 0.148.2 → 0.148.4
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 +7 -7
- package/lib/cli.mjs +32 -11
- package/lib/client.mjs +1 -1
- package/lib/commands/admin.mjs +140 -5
- package/lib/commands/auth.mjs +1 -1
- package/lib/commands/collection.mjs +1 -1
- package/lib/commands/feedback.mjs +1 -1
- package/lib/commands/mcp.mjs +2 -2
- package/lib/commands/onboard.mjs +1 -1
- package/lib/commands/outbound.mjs +1 -1
- package/lib/commands/phantom.mjs +1 -1
- package/lib/commands/repo.mjs +3 -3
- package/lib/commands/shredder.mjs +1 -1
- package/lib/commands/sim.mjs +1 -1
- package/lib/commands/site.mjs +1 -0
- package/lib/oauth.mjs +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# `@letterstory/cli`
|
|
2
2
|
|
|
3
|
-
Spin up and manage
|
|
4
|
-
client over the
|
|
3
|
+
Spin up and manage Letter **phantom blogs** from your terminal. The CLI is a thin
|
|
4
|
+
client over the Letter MCP endpoint (`POST /api/mcp`) — it wraps the same tool
|
|
5
5
|
manifest an agent sees, so it never drifts from the API.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
@@ -79,7 +79,7 @@ refresh automatically. Run `letterstory logout` to revoke the session and forget
|
|
|
79
79
|
|
|
80
80
|
**Or use a static API key** (for CI/automation, or when you want scoped-down access):
|
|
81
81
|
|
|
82
|
-
You need a
|
|
82
|
+
You need a Letter API key (starts with `ls_`; legacy `lb_` keys still work) with the `phantom:read` and
|
|
83
83
|
`phantom:write` capabilities — mint one in the app under **Settings → API keys**
|
|
84
84
|
(<https://app.letterstory.com/settings?tab=api-keys>).
|
|
85
85
|
Add `phantom:domain` too if you plan to buy custom domains.
|
|
@@ -96,7 +96,7 @@ Run `letterstory whoami` (alias `status`) any time to confirm which key/url (or
|
|
|
96
96
|
resolved and see your company profile. Run `letterstory config` to see the resolved url and
|
|
97
97
|
credential source without making a network call.
|
|
98
98
|
|
|
99
|
-
## Phantomstory — a collection
|
|
99
|
+
## Phantomstory — a collection Letter hosts as an independent publication
|
|
100
100
|
|
|
101
101
|
A phantom is a site we host that renders one of your collections; it never carries your
|
|
102
102
|
company's name. Phantoms are metered in SITES live at once (`phantom balance`), not posts.
|
|
@@ -129,7 +129,7 @@ letterstory phantom delete <phantom-id> --yes
|
|
|
129
129
|
Pass `--no-wait` to return immediately and poll later with `phantom get`, `--no-configure`
|
|
130
130
|
for a bare site, or `--dry-run` to see the plan without provisioning anything.
|
|
131
131
|
|
|
132
|
-
Publishing to the site is a **collection run** on its collection (below).
|
|
132
|
+
Publishing to the site is a **collection run** on its collection (below). Letter's own
|
|
133
133
|
launch recipe from a client domain — positioning inferred, a masthead proposed, the first
|
|
134
134
|
posts seeded — is `phantom-onboard` (see "Propose, then onboard a phantom").
|
|
135
135
|
|
|
@@ -603,7 +603,7 @@ letterstory arena role set reviewsite.com --role context
|
|
|
603
603
|
letterstory arena role set rival.com --role company --company "Rival" --rival
|
|
604
604
|
letterstory arena role reset rival.com # hand it back to research
|
|
605
605
|
|
|
606
|
-
# Arenas are callable endpoints with one-file defaults (the org's default arena is "
|
|
606
|
+
# Arenas are callable endpoints with one-file defaults (the org's default arena is "Letter"):
|
|
607
607
|
letterstory arena list
|
|
608
608
|
letterstory arena endpoint create --name "Docs research" --defaults '{"depth":"barebones"}'
|
|
609
609
|
letterstory arena endpoint config <arena-id> [--file cfg.json]
|
|
@@ -913,7 +913,7 @@ app; `letterstory mail connections` shows what is connected.
|
|
|
913
913
|
|
|
914
914
|
## Anything else
|
|
915
915
|
|
|
916
|
-
Every
|
|
916
|
+
Every Letter tool is reachable, not just the ones with a dedicated command group:
|
|
917
917
|
|
|
918
918
|
```bash
|
|
919
919
|
letterstory whoami # or: status — verify your key + who it's for
|
package/lib/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Argument parsing, help, and command routing for the
|
|
1
|
+
// Argument parsing, help, and command routing for the Letter CLI. `run(argv)`
|
|
2
2
|
// is the single entry the bin and the tests both call; it returns an exit code and
|
|
3
3
|
// never throws for user-facing problems (those become a printed CliError + code 1).
|
|
4
4
|
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
|
|
67
67
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
68
68
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
69
|
-
export const VERSION = "0.148.
|
|
69
|
+
export const VERSION = "0.148.4";
|
|
70
70
|
|
|
71
71
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
72
72
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -76,6 +76,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
76
76
|
"again",
|
|
77
77
|
"notify-slack",
|
|
78
78
|
"remap",
|
|
79
|
+
// site observations: each kind noticed, not the observations (keys_only).
|
|
80
|
+
"keys",
|
|
79
81
|
// login: one-link device sign-in instead of the loopback browser flow.
|
|
80
82
|
"device",
|
|
81
83
|
"json",
|
|
@@ -132,6 +134,12 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
132
134
|
"verbose",
|
|
133
135
|
"no-color",
|
|
134
136
|
"dry-run",
|
|
137
|
+
// admin demo list --enrollable, show --moves, set --enroll|--unenroll
|
|
138
|
+
"enrollable",
|
|
139
|
+
"moves",
|
|
140
|
+
"enroll",
|
|
141
|
+
"unenroll",
|
|
142
|
+
"clean-slate",
|
|
135
143
|
// admin recipes list --enabled-only, save --enable|--disable, import --retire-missing
|
|
136
144
|
"enabled-only",
|
|
137
145
|
"enable",
|
|
@@ -365,7 +373,7 @@ function buildHelp(bin) {
|
|
|
365
373
|
const heading =
|
|
366
374
|
bin === "phantom"
|
|
367
375
|
? "👻 phantom — deploy and manage Phantomstory blogs from your terminal"
|
|
368
|
-
: "letterstory — spin up and manage
|
|
376
|
+
: "letterstory — spin up and manage Letter phantom blogs from your terminal";
|
|
369
377
|
return `${heading}
|
|
370
378
|
|
|
371
379
|
Usage:
|
|
@@ -410,7 +418,7 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
410
418
|
collection links <id> [--refresh] Internal-link refresh: on/off, posts due (--refresh runs the next batch now)
|
|
411
419
|
collection delete <id> --yes
|
|
412
420
|
|
|
413
|
-
Phantomstory (a collection
|
|
421
|
+
Phantomstory (a collection Letter hosts as an independent publication):
|
|
414
422
|
phantom list Your phantoms (metered in SITES live at once)
|
|
415
423
|
phantom new --name <n> [--description <t>] [--theme <t>] [--collection <id>] [--customer-ref <r>]
|
|
416
424
|
[--no-configure] [--config '<json>'] [--client-domain <d>] [--dry-run] [--no-wait]
|
|
@@ -771,7 +779,7 @@ What does this org still need? (the router — reads only, decides nothing):
|
|
|
771
779
|
Planner, CMS, phantom) with its status:
|
|
772
780
|
done / running / ready / blocked, and what
|
|
773
781
|
a blocked step is waiting on.
|
|
774
|
-
${bin === "phantom" ? "onboard" : "phantom-onboard"} next The first ready step
|
|
782
|
+
${bin === "phantom" ? "onboard" : "phantom-onboard"} next The first ready step Letter recommends,
|
|
775
783
|
as the exact command to run next. Optional
|
|
776
784
|
steps are listed, never chosen.
|
|
777
785
|
|
|
@@ -834,7 +842,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
834
842
|
the queries behind the answer. Needs the
|
|
835
843
|
fleet:query capability on the staff key.
|
|
836
844
|
admin recipes list [--enabled-only] | show <id|name> | vocab [--json]
|
|
837
|
-
Plan recipes:
|
|
845
|
+
Plan recipes: Letter's one global library behind
|
|
838
846
|
every account's Plan (/superadmin/plan-recipes). vocab
|
|
839
847
|
says what a recipe can say (fields, goals, company
|
|
840
848
|
types, primitives). Needs recipes:read.
|
|
@@ -848,6 +856,19 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
848
856
|
[--primitives a,b|none] [--goals a,b|none] [--company-types a,b|none] [--enable|--disable] [--position n]
|
|
849
857
|
Create one (no id) or change one. Needs recipes:write.
|
|
850
858
|
admin recipes delete <id> --yes Delete for good (save --disable retires and keeps it).
|
|
859
|
+
admin demo guide How to run a demo, step by step, and what every knob and
|
|
860
|
+
seed shows on screen. Read it first.
|
|
861
|
+
admin demo list [--enrollable] | show <org-id> [--moves]
|
|
862
|
+
Demo orgs (/superadmin/demo): what each shows beside
|
|
863
|
+
what it really has. Needs demo:read.
|
|
864
|
+
admin demo set <org-id> [--enroll] [--phantom parked] [--search-console hidden] [--inject <key>]... [--save-as n]
|
|
865
|
+
Set what a demo org shows (merged); 'shown' puts a thing
|
|
866
|
+
back. --file takes a scenario as JSON. Needs demo:write.
|
|
867
|
+
admin demo reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none]
|
|
868
|
+
Undo the last take (its PRs closed, builds archived), then
|
|
869
|
+
show a scenario: real, fresh_geo, fresh_seo, after_approve
|
|
870
|
+
or a saved one. --clean-slate closes every open PR and
|
|
871
|
+
archives every build.
|
|
851
872
|
admin suggestion-recipes list|export [--out f.json]|import <f.json> [--dry-run] [--retire-missing]|stats
|
|
852
873
|
Build's suggestion recipes: plain-language ways
|
|
853
874
|
Letterpress finds what a site should build (look at,
|
|
@@ -1152,7 +1173,7 @@ no endpoint, no run. Applied on phantom create --composition '{"key":"..."}'):
|
|
|
1152
1173
|
compositions resolve <composition-key> Preview what create_phantom would
|
|
1153
1174
|
merge in — read-only, costs nothing.
|
|
1154
1175
|
|
|
1155
|
-
Shredder (diversify content produced outside
|
|
1176
|
+
Shredder (diversify content produced outside Letter; structure preserved):
|
|
1156
1177
|
shredder list Your shredder endpoints + run counts
|
|
1157
1178
|
shredder create --name <n> New endpoint (returns a callable public id)
|
|
1158
1179
|
shredder delete <endpoint-id> --yes
|
|
@@ -1357,7 +1378,7 @@ Mail:
|
|
|
1357
1378
|
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
1358
1379
|
mail vercel-key set (--token <t> | --token-file <path|->) [--team <team_id>]
|
|
1359
1380
|
Buy and own mail domains on your Vercel account / team
|
|
1360
|
-
mail vercel-key remove --yes Future purchases go back to
|
|
1381
|
+
mail vercel-key remove --yes Future purchases go back to Letter's account
|
|
1361
1382
|
mail registrant --first-name <f> --last-name <l> --email <e> --phone "(415) 555-1234"
|
|
1362
1383
|
--address <a> [--address2 <a>] --city <c> --state <s> --zip <z> --country "United States" [--company <c>]
|
|
1363
1384
|
WHOIS registrant for purchases on your Vercel key
|
|
@@ -1403,7 +1424,7 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1403
1424
|
site tree <host> [path] [--depth <n>] [--unfold] [--since <iso>]
|
|
1404
1425
|
The tree one folder at a time; --since adds +N/-N per folder
|
|
1405
1426
|
site page <host> <path> One page: facts, bindings, raw URLs, open items
|
|
1406
|
-
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>]
|
|
1427
|
+
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>] [--keys]
|
|
1407
1428
|
site observe <host> [path] [--max-pages <n>] Jev pass: meta accuracy, title, outdated
|
|
1408
1429
|
site dismiss <host> <observation-id> --reason <why> [--kind not_relevant|already_handled|wrong|later] [--decision dismiss|reopen]
|
|
1409
1430
|
site families <host> [path] [--state suggested|tracked|dismissed]
|
|
@@ -1420,8 +1441,8 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1420
1441
|
site watch-competitors [domain] [--limit <n>] Map competitors' sitemaps (the next few unmapped, or one now)
|
|
1421
1442
|
site highlight <domain> [--off] Highlight a competitor; its changes lead the top changes
|
|
1422
1443
|
|
|
1423
|
-
Repo changes (Claude Code edits your repo in a sandbox; the
|
|
1424
|
-
repo repos Repos the
|
|
1444
|
+
Repo changes (Claude Code edits your repo in a sandbox; the Letter GitHub App opens a PR):
|
|
1445
|
+
repo repos Repos the Letter GitHub App can reach
|
|
1425
1446
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1426
1447
|
repo prs [--repo owner/name] Every open PR (whoever opened it), longest-idle first
|
|
1427
1448
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
package/lib/client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The Letter CLI's transport: a thin JSON-RPC client over the MCP endpoint
|
|
2
2
|
// (POST /api/mcp), plus config resolution. The CLI wraps the same tool manifest an
|
|
3
3
|
// agent sees, so it stays in sync with the server by construction — there is no
|
|
4
4
|
// second copy of the surface here, only ergonomic wrappers around tools/call.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
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)
|
|
21
21
|
// admin recipes list|show|vocab|export|import|save|delete … (recipes:read / recipes:write)
|
|
22
|
+
// admin demo guide|list|show|set|reset … (demo:read / demo:write)
|
|
22
23
|
// admin models runs|run|star|unstar|scoreboard|requests|request|sampling … (models:read / models:write)
|
|
23
24
|
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
24
25
|
// admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
|
|
@@ -5097,7 +5098,7 @@ function recipeFromFlags(flags) {
|
|
|
5097
5098
|
}
|
|
5098
5099
|
|
|
5099
5100
|
/**
|
|
5100
|
-
* `admin recipes …` — the Plan recipes page (/superadmin/plan-recipes) for an agent:
|
|
5101
|
+
* `admin recipes …` — the Plan recipes page (/superadmin/plan-recipes) for an agent: Letter's
|
|
5101
5102
|
* one global library of recipes behind every account's Plan. The loop for building it out is
|
|
5102
5103
|
* `vocab` (what a recipe can say) → `export --out recipes.json` → edit → `import recipes.json
|
|
5103
5104
|
* --dry-run` → `import recipes.json`. Reads need recipes:read on the staff key, writes
|
|
@@ -5207,7 +5208,7 @@ const LAGO = "/api/superadmin/billing/lago";
|
|
|
5207
5208
|
const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
5208
5209
|
catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
|
|
5209
5210
|
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]
|
|
5210
|
-
reconcile [--org <org-id>] Lago's tokens this period against what we
|
|
5211
|
+
reconcile [--org <org-id>] Lago's tokens this period and last invoice against what we forwarded
|
|
5211
5212
|
forward forward billable usage to Lago now (the cron's pass)
|
|
5212
5213
|
payments [--org <org-id>] [--open] failed payments and what the retry loop did about each
|
|
5213
5214
|
payments retry <lago-invoice-id> charge one failed invoice again now
|
|
@@ -5266,7 +5267,10 @@ async function cmdAdminBilling(ctx) {
|
|
|
5266
5267
|
case "reconcile": {
|
|
5267
5268
|
const params = new URLSearchParams(compact({ orgId: flagStr(flags.org) }));
|
|
5268
5269
|
const result = await staffRequest(ctx, "GET", `${LAGO}/reconcile${params.size ? `?${params}` : ""}`);
|
|
5269
|
-
ok(
|
|
5270
|
+
ok(
|
|
5271
|
+
ctx,
|
|
5272
|
+
result.summary ?? (result.ok ? "Lago agrees with us." : "Lago and we disagree: see the orgs below.")
|
|
5273
|
+
);
|
|
5270
5274
|
printResult(io, flags, result);
|
|
5271
5275
|
return result.ok ? 0 : 1;
|
|
5272
5276
|
}
|
|
@@ -5274,7 +5278,9 @@ async function cmdAdminBilling(ctx) {
|
|
|
5274
5278
|
const result = await staffPost(ctx, `${LAGO}/forward`, {});
|
|
5275
5279
|
ok(
|
|
5276
5280
|
ctx,
|
|
5277
|
-
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}
|
|
5281
|
+
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}` +
|
|
5282
|
+
`${result.swept ? `, ${result.swept} found behind the cursor` : ""}` +
|
|
5283
|
+
`${result.late ? `, ${result.late} for a closed period (billed only if its invoice was not out yet)` : ""}.`
|
|
5278
5284
|
);
|
|
5279
5285
|
printResult(io, flags, result);
|
|
5280
5286
|
return 0;
|
|
@@ -5380,6 +5386,134 @@ async function cmdAdminBilling(ctx) {
|
|
|
5380
5386
|
}
|
|
5381
5387
|
}
|
|
5382
5388
|
|
|
5389
|
+
// `admin demo` — demo control (docs/architecture/demo-control.md): what each demo org shows,
|
|
5390
|
+
// set, rehearsed and reset. The staff routes behind /superadmin/demo, the same functions and
|
|
5391
|
+
// schema as list_demo_orgs / get_demo_state / set_demo_state / reset_demo_state.
|
|
5392
|
+
const DEMO = "/api/superadmin/demo";
|
|
5393
|
+
const DEMO_ITEM_FLAGS = [
|
|
5394
|
+
"phantom",
|
|
5395
|
+
"main-blog",
|
|
5396
|
+
"visitors",
|
|
5397
|
+
"probes",
|
|
5398
|
+
"cms",
|
|
5399
|
+
"publish-target",
|
|
5400
|
+
"search-console",
|
|
5401
|
+
"github",
|
|
5402
|
+
"domain",
|
|
5403
|
+
"setup",
|
|
5404
|
+
];
|
|
5405
|
+
const DEMO_USAGE =
|
|
5406
|
+
"Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none]";
|
|
5407
|
+
|
|
5408
|
+
async function cmdAdminDemo(ctx) {
|
|
5409
|
+
const { flags, positionals, io } = ctx;
|
|
5410
|
+
const sub = positionals[0];
|
|
5411
|
+
switch (sub) {
|
|
5412
|
+
case "guide": {
|
|
5413
|
+
const guide = await staffRequest(ctx, "GET", `${DEMO}/guide`);
|
|
5414
|
+
printResult(io, flags, guide, (g) =>
|
|
5415
|
+
[
|
|
5416
|
+
"How to run a demo",
|
|
5417
|
+
...g.steps.map(
|
|
5418
|
+
(s) => `\n${s.title}\n ${s.do}\n page: ${s.page}\n cli: ${s.cli}\n -> ${s.result}`
|
|
5419
|
+
),
|
|
5420
|
+
"\nWhat each knob and seed shows",
|
|
5421
|
+
...g.entries.map(
|
|
5422
|
+
(e) =>
|
|
5423
|
+
`\n${e.label} (${e.values})\n ${e.what}\n${e.shows_as.map((x) => ` · ${x}`).join("\n")}${
|
|
5424
|
+
e.on_camera ? `\n on camera: ${e.on_camera}` : ""
|
|
5425
|
+
}${e.mind.length ? `\n${e.mind.map((m) => ` ! ${m}`).join("\n")}` : ""}`
|
|
5426
|
+
),
|
|
5427
|
+
].join("\n")
|
|
5428
|
+
);
|
|
5429
|
+
return 0;
|
|
5430
|
+
}
|
|
5431
|
+
case "list": {
|
|
5432
|
+
const result = await staffRequest(
|
|
5433
|
+
ctx,
|
|
5434
|
+
"GET",
|
|
5435
|
+
`${DEMO}${flagBool(flags.enrollable) ? "?candidates=1" : ""}`
|
|
5436
|
+
);
|
|
5437
|
+
printResult(io, flags, result, (r) =>
|
|
5438
|
+
r.orgs.length
|
|
5439
|
+
? r.orgs
|
|
5440
|
+
.map(
|
|
5441
|
+
(o) =>
|
|
5442
|
+
`${o.enrolled ? "demo" : "test"} ${o.name} ${o.domain ?? "—"} ${o.scenario ?? ""}${
|
|
5443
|
+
o.differs?.length ? ` [${o.differs.join(", ")}]` : ""
|
|
5444
|
+
} ${o.org_id}`
|
|
5445
|
+
)
|
|
5446
|
+
.join("\n")
|
|
5447
|
+
: "No demo orgs yet."
|
|
5448
|
+
);
|
|
5449
|
+
return 0;
|
|
5450
|
+
}
|
|
5451
|
+
case "show": {
|
|
5452
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5453
|
+
const result = await staffRequest(
|
|
5454
|
+
ctx,
|
|
5455
|
+
"GET",
|
|
5456
|
+
`${DEMO}/${encodeURIComponent(orgId)}${flagBool(flags.moves) ? "?moves=1" : ""}`
|
|
5457
|
+
);
|
|
5458
|
+
printResult(io, flags, result, (r) =>
|
|
5459
|
+
[
|
|
5460
|
+
`${r.org.name} ${r.org.domain ?? "no domain"} scenario ${r.state.scenario ?? "—"}`,
|
|
5461
|
+
...r.knobs.map((k) => ` ${k.item.padEnd(16)} real: ${k.real} · shown: ${k.shown} (${k.how})`),
|
|
5462
|
+
` observations open: ${r.observations.open.map((o) => `${o.key}${o.injected ? " (demo)" : ""}`).join(", ") || "none"}`,
|
|
5463
|
+
` answers: ${r.answers.map((q) => `${q.named}/${q.answers} ${q.text}`).join("; ") || "never probed"}`,
|
|
5464
|
+
].join("\n")
|
|
5465
|
+
);
|
|
5466
|
+
return 0;
|
|
5467
|
+
}
|
|
5468
|
+
case "set": {
|
|
5469
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5470
|
+
const file = flagStr(flags.file);
|
|
5471
|
+
const state = file ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
5472
|
+
for (const flag of DEMO_ITEM_FLAGS) {
|
|
5473
|
+
const value = flagStr(flags[flag]);
|
|
5474
|
+
if (value) state[flag.replace(/-/g, "_")] = value;
|
|
5475
|
+
}
|
|
5476
|
+
const cap = flagNum(flags["domain-cap"]);
|
|
5477
|
+
if (cap !== undefined && cap !== null) state.domain_cap = cap;
|
|
5478
|
+
const inject = flagList(flags.inject);
|
|
5479
|
+
const hide = flagList(flags.hide);
|
|
5480
|
+
if (inject.length || hide.length)
|
|
5481
|
+
state.observations = {
|
|
5482
|
+
...state.observations,
|
|
5483
|
+
...(inject.length ? { inject } : {}),
|
|
5484
|
+
...(hide.length ? { hide } : {}),
|
|
5485
|
+
};
|
|
5486
|
+
const body = compact({
|
|
5487
|
+
...(Object.keys(state).length ? { state } : {}),
|
|
5488
|
+
enroll: flagBool(flags.enroll) || undefined,
|
|
5489
|
+
unenroll: flagBool(flags.unenroll) || undefined,
|
|
5490
|
+
save_as: flagStr(flags["save-as"]),
|
|
5491
|
+
});
|
|
5492
|
+
if (!Object.keys(body).length) throw new CliError(DEMO_USAGE);
|
|
5493
|
+
const result = await staffRequest(ctx, "PATCH", `${DEMO}/${encodeURIComponent(orgId)}`, body);
|
|
5494
|
+
printResult(io, flags, result, (r) => r.notes.join("\n") || "No change.");
|
|
5495
|
+
return 0;
|
|
5496
|
+
}
|
|
5497
|
+
case "reset": {
|
|
5498
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5499
|
+
const scenario = requirePositional(positionals, 2, "scenario");
|
|
5500
|
+
const result = await staffPost(
|
|
5501
|
+
ctx,
|
|
5502
|
+
`${DEMO}/${encodeURIComponent(orgId)}/reset`,
|
|
5503
|
+
compact({
|
|
5504
|
+
scenario,
|
|
5505
|
+
close_prs: flagStr(flags["close-prs"]),
|
|
5506
|
+
clean_slate: flagBool(flags["clean-slate"]) || undefined,
|
|
5507
|
+
})
|
|
5508
|
+
);
|
|
5509
|
+
printResult(io, flags, result, (r) => r.notes.join("\n"));
|
|
5510
|
+
return 0;
|
|
5511
|
+
}
|
|
5512
|
+
default:
|
|
5513
|
+
throw new CliError(DEMO_USAGE);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5383
5517
|
export async function cmdAdmin(ctx) {
|
|
5384
5518
|
const sub = ctx.positionals[0];
|
|
5385
5519
|
if (sub === "replays" || sub === "replay")
|
|
@@ -5400,10 +5534,11 @@ export async function cmdAdmin(ctx) {
|
|
|
5400
5534
|
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5401
5535
|
if (sub === "republish") return cmdAdminRepublish({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5402
5536
|
if (sub === "recipes") return cmdAdminRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5537
|
+
if (sub === "demo") return cmdAdminDemo({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5403
5538
|
if (sub === "suggestion-recipes")
|
|
5404
5539
|
return cmdAdminSuggestionRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5405
5540
|
throw new CliError(
|
|
5406
|
-
'Usage: admin recipes … | admin suggestion-recipes … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
5541
|
+
'Usage: admin recipes … | admin demo … | admin suggestion-recipes … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
5407
5542
|
);
|
|
5408
5543
|
}
|
|
5409
5544
|
|
package/lib/commands/auth.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export async function cmdLogin(ctx) {
|
|
|
18
18
|
if (key) {
|
|
19
19
|
if (!key.startsWith("ls_") && !key.startsWith("lb_")) {
|
|
20
20
|
io.error(
|
|
21
|
-
"Warning:
|
|
21
|
+
"Warning: Letter API keys usually start with 'ls_' (legacy keys start with 'lb_'). Saving anyway."
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
const saved = { key, ...(flagStr(flags.url) ? { url } : {}) };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A collection is a publication with a one-file configuration that produces and publishes
|
|
4
4
|
// posts on a cadence; `collection run` produces ONE post on it (sourced, written, gated,
|
|
5
|
-
// published, and — when
|
|
5
|
+
// published, and — when Letter hosts the site — rebuilt). Runs are 20–60 minutes, so
|
|
6
6
|
// `collection run` submits and then follows the run unless you tell it not to
|
|
7
7
|
// (`--no-wait`); `collection run-status` picks it up later. A direct run spends the
|
|
8
8
|
// collection's weekly cadence budget (its posts_per_week); the planner's own slots are the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `feedback` — submit product feedback about
|
|
1
|
+
// `feedback` — submit product feedback about Letter itself: a bug, something
|
|
2
2
|
// missing, something unclear, a feature request, or praise. One subcommand, one tool
|
|
3
3
|
// (submit_feedback) — there is no read/list surface yet, staff triage this table directly.
|
|
4
4
|
|
package/lib/commands/mcp.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `mcp` — print ready-to-paste configuration for wiring the
|
|
1
|
+
// `mcp` — print ready-to-paste configuration for wiring the Letter MCP server
|
|
2
2
|
// into an agent (Claude Code, Claude Desktop, Cursor, …). Same endpoint/auth header
|
|
3
3
|
// the CLI itself uses (see client.mjs's `mcpEndpoint` / `x-integrations-key`), just
|
|
4
4
|
// packaged for copy-paste instead of for this process's own requests.
|
|
@@ -30,7 +30,7 @@ export function cmdMcp(ctx) {
|
|
|
30
30
|
|
|
31
31
|
const masked = config.key ? `${config.key.slice(0, 6)}…${config.key.slice(-4)}` : undefined;
|
|
32
32
|
|
|
33
|
-
io.log(`
|
|
33
|
+
io.log(`Letter MCP`);
|
|
34
34
|
io.log(` endpoint: ${endpoint}`);
|
|
35
35
|
io.log(` transport: streamable-http`);
|
|
36
36
|
io.log(` auth: ${header}: ${masked ?? "(no key found — run `" + ctx.bin + " login` first)"}`);
|
package/lib/commands/onboard.mjs
CHANGED
|
@@ -760,7 +760,7 @@ async function cmdOnboardNext(ctx) {
|
|
|
760
760
|
return 0;
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
-
// `onboard primitives [<domain>]` — what
|
|
763
|
+
// `onboard primitives [<domain>]` — what Letter would stand up for this company: one
|
|
764
764
|
// read of propose_primitive_plan, printed item by item with the org-config record each one
|
|
765
765
|
// stands for. Nothing runs; the plan is applied with `org apply`.
|
|
766
766
|
// A first read crawls the site; `pending` means one is already in flight, so wait and ask.
|
|
@@ -810,7 +810,7 @@ export async function cmdMail(ctx) {
|
|
|
810
810
|
if (action === "remove") {
|
|
811
811
|
if (!inner.flags.yes) {
|
|
812
812
|
ctx.io.error(
|
|
813
|
-
`Removing the key sends future purchases back to
|
|
813
|
+
`Removing the key sends future purchases back to Letter's account and drops the registrant contact. Re-run with --yes to confirm: ${ctx.bin} mail vercel-key remove --yes`
|
|
814
814
|
);
|
|
815
815
|
return 1;
|
|
816
816
|
}
|
package/lib/commands/phantom.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `phantom` — Phantomstory: a collection
|
|
1
|
+
// `phantom` — Phantomstory: a collection Letter hosts as an independent publication.
|
|
2
2
|
//
|
|
3
3
|
// Hosting only: the site, its identity and theme, its domain, its rebuilds, its balance.
|
|
4
4
|
// Publishing a post to it is `collection run --collection <its collection_id>`; its
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `repo` — changes to the org's own repos, made by Claude Code in a sandbox and opened as a
|
|
2
|
-
// pull request by the
|
|
2
|
+
// pull request by the Letter GitHub App (src/lib/repo-changes). One subcommand per tool:
|
|
3
3
|
// repo start → start_repo_change repo status → get_repo_change
|
|
4
4
|
// repo activity → get_repo_change_activity (what the sandbox is doing; --follow polls)
|
|
5
5
|
// repo list → list_repo_changes repo cancel → cancel_repo_change
|
|
@@ -711,7 +711,7 @@ async function repoRepos(ctx) {
|
|
|
711
711
|
return 0;
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
-
/** `repo prs [--repo owner/name]`: every open PR, longest-idle first, and which
|
|
714
|
+
/** `repo prs [--repo owner/name]`: every open PR, longest-idle first, and which Letter opened. */
|
|
715
715
|
async function repoPrs(ctx) {
|
|
716
716
|
const { client, flags, io } = ctx;
|
|
717
717
|
refuseUnknownFlags(ctx, {}, ["repo"]);
|
|
@@ -722,7 +722,7 @@ async function repoPrs(ctx) {
|
|
|
722
722
|
: r.pullRequests
|
|
723
723
|
.map(
|
|
724
724
|
(p) =>
|
|
725
|
-
`${p.repo}#${p.number} ${p.title} (${p.daysSinceUpdate}d idle${p.draft ? ", draft" : ""}${p.letterRun ? `,
|
|
725
|
+
`${p.repo}#${p.number} ${p.title} (${p.daysSinceUpdate}d idle${p.draft ? ", draft" : ""}${p.letterRun ? `, Letter run ${p.letterRun.status}` : ""})\n ${p.url}`
|
|
726
726
|
)
|
|
727
727
|
.join("\n")
|
|
728
728
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `shredder` — create shredder endpoints and run content produced OUTSIDE
|
|
1
|
+
// `shredder` — create shredder endpoints and run content produced OUTSIDE Letter
|
|
2
2
|
// through the cross-provider shredder. A shred rewrites each sentence across multiple
|
|
3
3
|
// LLM providers to diversify, leaving structure (headings, lists, tables) untouched, and
|
|
4
4
|
// logs every call to telemetry against the org's shred balance.
|
package/lib/commands/sim.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// letterstory sim create --name "Attio API" --spec-url https://api.attio.com/openapi/api
|
|
7
7
|
// letterstory sim show <id>
|
|
8
8
|
// letterstory sim rule <id> /v2/webhooks sink --note "never call real endpoints"
|
|
9
|
-
// letterstory sim create --name "
|
|
9
|
+
// letterstory sim create --name "Letter" --tools-file tools.json --tool-route "/api/integrations/tools/{name}"
|
|
10
10
|
// letterstory sim key <id> --agent our-ci --goal "nightly integration test"
|
|
11
11
|
// letterstory sim seed <id> --prompt "a 3-person content team with two blogs" --activate
|
|
12
12
|
// letterstory sim seed <id> --use <seed-id|none>
|
package/lib/commands/site.mjs
CHANGED
|
@@ -133,6 +133,7 @@ const ARGS = {
|
|
|
133
133
|
since: "since",
|
|
134
134
|
min_reach: { flag: "min-reach", type: "num" },
|
|
135
135
|
limit: { flag: "limit", type: "num" },
|
|
136
|
+
keys_only: { flag: "keys", type: "bool" },
|
|
136
137
|
},
|
|
137
138
|
observe_site: {
|
|
138
139
|
host: { positional: 0, required: true, label: "host" },
|
package/lib/oauth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Browser-based OAuth 2.1 login (RFC 8252 loopback redirect + PKCE) for the
|
|
2
|
-
//
|
|
2
|
+
// Letter CLI. This talks to the app's own /api/oauth/* endpoints — see
|
|
3
3
|
// src/lib/oauth/core.ts on the server side for the matching implementation.
|
|
4
4
|
|
|
5
5
|
import { randomBytes, createHash } from "node:crypto";
|
|
@@ -38,12 +38,12 @@ function openBrowser(url) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const CALLBACK_HTML_OK = `<!doctype html><html><head><title>
|
|
41
|
+
const CALLBACK_HTML_OK = `<!doctype html><html><head><title>Letter CLI</title></head>
|
|
42
42
|
<body style="font-family: system-ui, sans-serif; max-width: 28rem; margin: 4rem auto; text-align: center;">
|
|
43
43
|
<h2>You're signed in.</h2><p>You can close this tab and return to your terminal.</p>
|
|
44
44
|
</body></html>`;
|
|
45
45
|
|
|
46
|
-
const CALLBACK_HTML_ERROR = `<!doctype html><html><head><title>
|
|
46
|
+
const CALLBACK_HTML_ERROR = `<!doctype html><html><head><title>Letter CLI</title></head>
|
|
47
47
|
<body style="font-family: system-ui, sans-serif; max-width: 28rem; margin: 4rem auto; text-align: center;">
|
|
48
48
|
<h2>Sign-in failed.</h2><p>Return to your terminal for details.</p>
|
|
49
49
|
</body></html>`;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letterstory/cli",
|
|
3
|
-
"version": "0.148.
|
|
4
|
-
"description": "Spin up and manage
|
|
3
|
+
"version": "0.148.4",
|
|
4
|
+
"description": "Spin up and manage Letter phantom blogs from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "Letter",
|
|
8
8
|
"homepage": "https://github.com/letterstory/letterstory/tree/main/cli#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|