@letterstory/cli 0.148.1 → 0.148.3
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 +29 -11
- package/lib/client.mjs +1 -1
- package/lib/commands/admin.mjs +9 -4
- package/lib/commands/auth.mjs +1 -1
- package/lib/commands/build.mjs +99 -0
- 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 +5 -5
- package/lib/commands/shredder.mjs +1 -1
- package/lib/commands/sim.mjs +1 -1
- package/lib/commands/site.mjs +1 -0
- package/lib/commands.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
|
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
cmdAdmin,
|
|
54
54
|
cmdFeedback,
|
|
55
55
|
cmdRepo,
|
|
56
|
+
cmdBuild,
|
|
56
57
|
cmdSite,
|
|
57
58
|
cmdEvals,
|
|
58
59
|
cmdOfferings,
|
|
@@ -65,13 +66,18 @@ import {
|
|
|
65
66
|
|
|
66
67
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
67
68
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
68
|
-
export const VERSION = "0.148.
|
|
69
|
+
export const VERSION = "0.148.3";
|
|
69
70
|
|
|
70
71
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
71
72
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
72
73
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
73
74
|
export const BOOLEAN_FLAGS = new Set([
|
|
75
|
+
// build: repeat an open build's ask; post its progress in the org's Slack channel.
|
|
76
|
+
"again",
|
|
77
|
+
"notify-slack",
|
|
74
78
|
"remap",
|
|
79
|
+
// site observations: each kind noticed, not the observations (keys_only).
|
|
80
|
+
"keys",
|
|
75
81
|
// login: one-link device sign-in instead of the loopback browser flow.
|
|
76
82
|
"device",
|
|
77
83
|
"json",
|
|
@@ -361,7 +367,7 @@ function buildHelp(bin) {
|
|
|
361
367
|
const heading =
|
|
362
368
|
bin === "phantom"
|
|
363
369
|
? "👻 phantom — deploy and manage Phantomstory blogs from your terminal"
|
|
364
|
-
: "letterstory — spin up and manage
|
|
370
|
+
: "letterstory — spin up and manage Letter phantom blogs from your terminal";
|
|
365
371
|
return `${heading}
|
|
366
372
|
|
|
367
373
|
Usage:
|
|
@@ -406,7 +412,7 @@ Collections (the content engine as a primitive — one door, 'collection run'):
|
|
|
406
412
|
collection links <id> [--refresh] Internal-link refresh: on/off, posts due (--refresh runs the next batch now)
|
|
407
413
|
collection delete <id> --yes
|
|
408
414
|
|
|
409
|
-
Phantomstory (a collection
|
|
415
|
+
Phantomstory (a collection Letter hosts as an independent publication):
|
|
410
416
|
phantom list Your phantoms (metered in SITES live at once)
|
|
411
417
|
phantom new --name <n> [--description <t>] [--theme <t>] [--collection <id>] [--customer-ref <r>]
|
|
412
418
|
[--no-configure] [--config '<json>'] [--client-domain <d>] [--dry-run] [--no-wait]
|
|
@@ -767,7 +773,7 @@ What does this org still need? (the router — reads only, decides nothing):
|
|
|
767
773
|
Planner, CMS, phantom) with its status:
|
|
768
774
|
done / running / ready / blocked, and what
|
|
769
775
|
a blocked step is waiting on.
|
|
770
|
-
${bin === "phantom" ? "onboard" : "phantom-onboard"} next The first ready step
|
|
776
|
+
${bin === "phantom" ? "onboard" : "phantom-onboard"} next The first ready step Letter recommends,
|
|
771
777
|
as the exact command to run next. Optional
|
|
772
778
|
steps are listed, never chosen.
|
|
773
779
|
|
|
@@ -830,7 +836,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
830
836
|
the queries behind the answer. Needs the
|
|
831
837
|
fleet:query capability on the staff key.
|
|
832
838
|
admin recipes list [--enabled-only] | show <id|name> | vocab [--json]
|
|
833
|
-
Plan recipes:
|
|
839
|
+
Plan recipes: Letter's one global library behind
|
|
834
840
|
every account's Plan (/superadmin/plan-recipes). vocab
|
|
835
841
|
says what a recipe can say (fields, goals, company
|
|
836
842
|
types, primitives). Needs recipes:read.
|
|
@@ -1148,7 +1154,7 @@ no endpoint, no run. Applied on phantom create --composition '{"key":"..."}'):
|
|
|
1148
1154
|
compositions resolve <composition-key> Preview what create_phantom would
|
|
1149
1155
|
merge in — read-only, costs nothing.
|
|
1150
1156
|
|
|
1151
|
-
Shredder (diversify content produced outside
|
|
1157
|
+
Shredder (diversify content produced outside Letter; structure preserved):
|
|
1152
1158
|
shredder list Your shredder endpoints + run counts
|
|
1153
1159
|
shredder create --name <n> New endpoint (returns a callable public id)
|
|
1154
1160
|
shredder delete <endpoint-id> --yes
|
|
@@ -1353,7 +1359,7 @@ Mail:
|
|
|
1353
1359
|
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
1354
1360
|
mail vercel-key set (--token <t> | --token-file <path|->) [--team <team_id>]
|
|
1355
1361
|
Buy and own mail domains on your Vercel account / team
|
|
1356
|
-
mail vercel-key remove --yes Future purchases go back to
|
|
1362
|
+
mail vercel-key remove --yes Future purchases go back to Letter's account
|
|
1357
1363
|
mail registrant --first-name <f> --last-name <l> --email <e> --phone "(415) 555-1234"
|
|
1358
1364
|
--address <a> [--address2 <a>] --city <c> --state <s> --zip <z> --country "United States" [--company <c>]
|
|
1359
1365
|
WHOIS registrant for purchases on your Vercel key
|
|
@@ -1378,6 +1384,17 @@ Insights:
|
|
|
1378
1384
|
Page access: who's reading your phantom sites
|
|
1379
1385
|
insights activity [--phantom <uuid>] [--limit <n>] What hit your phantom sites in the last 7 days, newest first
|
|
1380
1386
|
|
|
1387
|
+
Build (a change to your site, or launching what isn't code; the Build box's own door):
|
|
1388
|
+
build <words…> [--prompt <text>] [--site <host>] [--subject <host/path>]... [--action build|build_pr|build_merge|slack|linear]
|
|
1389
|
+
[--attach <path>]... [--attach-url <name>=<https url>]... [--attach-ref <path>]... [--attach-url-ref <name>=<https url>]...
|
|
1390
|
+
[--harness claude-code|letterpress-1] [--notify-slack] [--again]
|
|
1391
|
+
Build from your words: "add a pricing page" opens a preview or PR;
|
|
1392
|
+
"launch a phantom for us", "set up mailers" or "onboard my blog" is
|
|
1393
|
+
done by Build itself (a phantom site buys its own domain), and the
|
|
1394
|
+
build reads Done with no PR to merge. --again repeats an open build's ask
|
|
1395
|
+
build talk <words…> [--conversation <id>|--build <id>] [--site <host>] [--harness claude-code|letterpress-1]
|
|
1396
|
+
One turn of a conversation with Build (questions, follow-ups on a build)
|
|
1397
|
+
|
|
1381
1398
|
The Site (one model of your whole marketing site, keyed by host and path):
|
|
1382
1399
|
site list Mapped hosts, and hosts you already have elsewhere
|
|
1383
1400
|
site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit] [--relation owned|watched]
|
|
@@ -1388,7 +1405,7 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1388
1405
|
site tree <host> [path] [--depth <n>] [--unfold] [--since <iso>]
|
|
1389
1406
|
The tree one folder at a time; --since adds +N/-N per folder
|
|
1390
1407
|
site page <host> <path> One page: facts, bindings, raw URLs, open items
|
|
1391
|
-
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>]
|
|
1408
|
+
site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>] [--keys]
|
|
1392
1409
|
site observe <host> [path] [--max-pages <n>] Jev pass: meta accuracy, title, outdated
|
|
1393
1410
|
site dismiss <host> <observation-id> --reason <why> [--kind not_relevant|already_handled|wrong|later] [--decision dismiss|reopen]
|
|
1394
1411
|
site families <host> [path] [--state suggested|tracked|dismissed]
|
|
@@ -1405,8 +1422,8 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1405
1422
|
site watch-competitors [domain] [--limit <n>] Map competitors' sitemaps (the next few unmapped, or one now)
|
|
1406
1423
|
site highlight <domain> [--off] Highlight a competitor; its changes lead the top changes
|
|
1407
1424
|
|
|
1408
|
-
Repo changes (Claude Code edits your repo in a sandbox; the
|
|
1409
|
-
repo repos Repos the
|
|
1425
|
+
Repo changes (Claude Code edits your repo in a sandbox; the Letter GitHub App opens a PR):
|
|
1426
|
+
repo repos Repos the Letter GitHub App can reach
|
|
1410
1427
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1411
1428
|
repo prs [--repo owner/name] Every open PR (whoever opened it), longest-idle first
|
|
1412
1429
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
|
@@ -1587,6 +1604,7 @@ const CLIENT_COMMANDS = {
|
|
|
1587
1604
|
mailers: cmdMailers,
|
|
1588
1605
|
feedback: cmdFeedback,
|
|
1589
1606
|
repo: cmdRepo,
|
|
1607
|
+
build: cmdBuild,
|
|
1590
1608
|
site: cmdSite,
|
|
1591
1609
|
evals: cmdEvals,
|
|
1592
1610
|
visits: cmdVisits,
|
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
|
@@ -5097,7 +5097,7 @@ function recipeFromFlags(flags) {
|
|
|
5097
5097
|
}
|
|
5098
5098
|
|
|
5099
5099
|
/**
|
|
5100
|
-
* `admin recipes …` — the Plan recipes page (/superadmin/plan-recipes) for an agent:
|
|
5100
|
+
* `admin recipes …` — the Plan recipes page (/superadmin/plan-recipes) for an agent: Letter's
|
|
5101
5101
|
* one global library of recipes behind every account's Plan. The loop for building it out is
|
|
5102
5102
|
* `vocab` (what a recipe can say) → `export --out recipes.json` → edit → `import recipes.json
|
|
5103
5103
|
* --dry-run` → `import recipes.json`. Reads need recipes:read on the staff key, writes
|
|
@@ -5207,7 +5207,7 @@ const LAGO = "/api/superadmin/billing/lago";
|
|
|
5207
5207
|
const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
5208
5208
|
catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
|
|
5209
5209
|
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
|
|
5210
|
+
reconcile [--org <org-id>] Lago's tokens this period and last invoice against what we forwarded
|
|
5211
5211
|
forward forward billable usage to Lago now (the cron's pass)
|
|
5212
5212
|
payments [--org <org-id>] [--open] failed payments and what the retry loop did about each
|
|
5213
5213
|
payments retry <lago-invoice-id> charge one failed invoice again now
|
|
@@ -5266,7 +5266,10 @@ async function cmdAdminBilling(ctx) {
|
|
|
5266
5266
|
case "reconcile": {
|
|
5267
5267
|
const params = new URLSearchParams(compact({ orgId: flagStr(flags.org) }));
|
|
5268
5268
|
const result = await staffRequest(ctx, "GET", `${LAGO}/reconcile${params.size ? `?${params}` : ""}`);
|
|
5269
|
-
ok(
|
|
5269
|
+
ok(
|
|
5270
|
+
ctx,
|
|
5271
|
+
result.summary ?? (result.ok ? "Lago agrees with us." : "Lago and we disagree: see the orgs below.")
|
|
5272
|
+
);
|
|
5270
5273
|
printResult(io, flags, result);
|
|
5271
5274
|
return result.ok ? 0 : 1;
|
|
5272
5275
|
}
|
|
@@ -5274,7 +5277,9 @@ async function cmdAdminBilling(ctx) {
|
|
|
5274
5277
|
const result = await staffPost(ctx, `${LAGO}/forward`, {});
|
|
5275
5278
|
ok(
|
|
5276
5279
|
ctx,
|
|
5277
|
-
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}
|
|
5280
|
+
`Sent ${result.sent}, already there ${result.duplicates}, held ${result.held}, refused ${result.refused}` +
|
|
5281
|
+
`${result.swept ? `, ${result.swept} found behind the cursor` : ""}` +
|
|
5282
|
+
`${result.late ? `, ${result.late} for a closed period (billed only if its invoice was not out yet)` : ""}.`
|
|
5278
5283
|
);
|
|
5279
5284
|
printResult(io, flags, result);
|
|
5280
5285
|
return 0;
|
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 } : {}) };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// `build` — Build from words, the way the Build box and the letterpress terminal do it
|
|
2
|
+
// (src/lib/growth-builds/start-build.ts). What the words name is done: a change to the site
|
|
3
|
+
// becomes a preview or a pull request, and launching phantom sites or mailers, or onboarding the
|
|
4
|
+
// main blog, is carried out by Build itself with nothing to merge.
|
|
5
|
+
// build <words…> → start_build (--prompt for a long ask; --again to repeat one)
|
|
6
|
+
// build talk <words…> → send_build_message (--conversation <id> | --build <id> to continue)
|
|
7
|
+
|
|
8
|
+
import { CliError } from "../client.mjs";
|
|
9
|
+
import { argsFromFlags, flagList, flagStr, ok, printResult, refuseUnknownFlags } from "./shared.mjs";
|
|
10
|
+
import { attachmentUrls, uploadAttachments } from "./repo.mjs";
|
|
11
|
+
|
|
12
|
+
const ARGS = {
|
|
13
|
+
start_build: {
|
|
14
|
+
// The words after `build`, or --prompt for a longer ask.
|
|
15
|
+
prompt: { value: (flags, positionals) => flagStr(flags.prompt) ?? (positionals.join(" ").trim() || undefined) },
|
|
16
|
+
domain: "site",
|
|
17
|
+
// --subject host/path (repeatable): the pages it is about.
|
|
18
|
+
subjects: { flag: "subject", type: "list" },
|
|
19
|
+
// --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable;
|
|
20
|
+
// -ref ones are context the agent reads, never put in the repo.
|
|
21
|
+
files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
|
|
22
|
+
delivery: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
23
|
+
notify_slack: { flag: "notify-slack", type: "bool" },
|
|
24
|
+
harness: { flag: "harness", enum: ["claude-code", "letterpress-1"] },
|
|
25
|
+
again: { flag: "again", type: "bool" },
|
|
26
|
+
},
|
|
27
|
+
send_build_message: {
|
|
28
|
+
message: {
|
|
29
|
+
value: (flags, positionals) => flagStr(flags.message) ?? (positionals.join(" ").trim() || undefined),
|
|
30
|
+
},
|
|
31
|
+
conversation_id: "conversation",
|
|
32
|
+
build_id: "build",
|
|
33
|
+
domain: "site",
|
|
34
|
+
subjects: { flag: "subject", type: "list" },
|
|
35
|
+
files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
|
|
36
|
+
harness: { flag: "harness", enum: ["claude-code", "letterpress-1"] },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const FILE_FLAGS = ["attach", "attach-url", "attach-ref", "attach-url-ref"];
|
|
41
|
+
|
|
42
|
+
/** The files named by --attach / --attach-url (and their -ref forms), uploaded first. */
|
|
43
|
+
async function attachedFiles(ctx) {
|
|
44
|
+
const { flags } = ctx;
|
|
45
|
+
const place = flagList(flags.attach);
|
|
46
|
+
const uploaded = await uploadAttachments(ctx, [...place, ...flagList(flags["attach-ref"])]);
|
|
47
|
+
return [
|
|
48
|
+
...uploaded.map((f, i) => (i < place.length ? f : { ...f, reference: true })),
|
|
49
|
+
...attachmentUrls(flagList(flags["attach-url"])),
|
|
50
|
+
...attachmentUrls(flagList(flags["attach-url-ref"])).map((f) => ({ ...f, reference: true })),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdBuild(ctx) {
|
|
55
|
+
if (ctx.positionals[0] === "talk") return buildTalk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
56
|
+
return buildStart(ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function buildStart(ctx) {
|
|
60
|
+
const { client, flags, io } = ctx;
|
|
61
|
+
refuseUnknownFlags(ctx, {}, [
|
|
62
|
+
"prompt",
|
|
63
|
+
"site",
|
|
64
|
+
"subject",
|
|
65
|
+
"action",
|
|
66
|
+
"notify-slack",
|
|
67
|
+
"harness",
|
|
68
|
+
"again",
|
|
69
|
+
...FILE_FLAGS,
|
|
70
|
+
]);
|
|
71
|
+
const files = await attachedFiles(ctx);
|
|
72
|
+
const args = argsFromFlags(ARGS.start_build, flags, ctx.positionals, { files });
|
|
73
|
+
if (!args.prompt) throw new CliError('Say what to build: letterstory build "launch a phantom for us"');
|
|
74
|
+
const result = await client.callTool("start_build", args);
|
|
75
|
+
printResult(io, flags, result, (r) => {
|
|
76
|
+
if (r.status !== "started") return r.message ?? r.status;
|
|
77
|
+
const steps = (r.build?.steps ?? []).map((s) => ` ${s.status} ${s.title}${s.note ? `: ${s.note}` : ""}`);
|
|
78
|
+
return [`Started: ${r.build.title} (${r.build.id})`, ...steps].join("\n");
|
|
79
|
+
});
|
|
80
|
+
if (result.status !== "started" && !flags.json) ok(ctx, "Nothing was started.");
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function buildTalk(ctx) {
|
|
85
|
+
const { client, flags, io } = ctx;
|
|
86
|
+
refuseUnknownFlags(ctx, {}, ["message", "conversation", "build", "site", "subject", "harness", ...FILE_FLAGS]);
|
|
87
|
+
const files = await attachedFiles(ctx);
|
|
88
|
+
const args = argsFromFlags(ARGS.send_build_message, flags, ctx.positionals, { files });
|
|
89
|
+
if (!args.message) throw new CliError('Say something: letterstory build talk "make the hero darker"');
|
|
90
|
+
const result = await client.callTool("send_build_message", args);
|
|
91
|
+
printResult(io, flags, result, (r) =>
|
|
92
|
+
[r.reply, ...(r.questions ?? []).map((q) => `? ${q.question}`), r.build ? `Build: ${r.build.id}` : null]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join("\n")
|
|
95
|
+
);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { ARGS };
|
|
@@ -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
|
|
@@ -347,7 +347,7 @@ export async function cmdRepo(ctx) {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
/** `--attach-url name=https://…` → `{ name, url }`. */
|
|
350
|
-
function attachmentUrls(specs) {
|
|
350
|
+
export function attachmentUrls(specs) {
|
|
351
351
|
return specs.map((spec) => {
|
|
352
352
|
const at = spec.indexOf("=");
|
|
353
353
|
if (at < 1) throw new CliError(`--attach-url must be name=https://… (got ${spec})`);
|
|
@@ -356,7 +356,7 @@ function attachmentUrls(specs) {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/** Reserve an upload per --attach file, PUT the bytes, and return `{ name, upload_path }` for each. */
|
|
359
|
-
async function uploadAttachments(ctx, paths) {
|
|
359
|
+
export async function uploadAttachments(ctx, paths) {
|
|
360
360
|
if (!paths.length) return [];
|
|
361
361
|
const names = paths.map((p) =>
|
|
362
362
|
basename(p)
|
|
@@ -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/commands.mjs
CHANGED
|
@@ -44,6 +44,7 @@ export * from "./commands/targets.mjs";
|
|
|
44
44
|
export * from "./commands/admin.mjs";
|
|
45
45
|
export * from "./commands/feedback.mjs";
|
|
46
46
|
export * from "./commands/repo.mjs";
|
|
47
|
+
export * from "./commands/build.mjs";
|
|
47
48
|
export * from "./commands/site.mjs";
|
|
48
49
|
export * from "./commands/evals.mjs";
|
|
49
50
|
export * from "./commands/visits.mjs";
|
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.3",
|
|
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",
|