@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@letterstory/cli`
2
2
 
3
- Spin up and manage Letterstory **phantom blogs** from your terminal. The CLI is a thin
4
- client over the Letterstory MCP endpoint (`POST /api/mcp`) — it wraps the same tool
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 Letterstory API key (starts with `ls_`; legacy `lb_` keys still work) with the `phantom:read` and
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 Letterstory hosts as an independent publication
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). Letterstory's own
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 "Letterstory"):
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 Letterstory tool is reachable, not just the ones with a dedicated command group:
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 Letterstory CLI. `run(argv)`
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.1";
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 Letterstory phantom blogs from your terminal";
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 Letterstory hosts as an independent publication):
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 Letterstory recommends,
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: Letterstory's one global library behind
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 Letterstory; structure preserved):
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 Letterstory's account
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 Letterstory GitHub App opens a PR):
1409
- repo repos Repos the Letterstory GitHub App can reach
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 Letterstory CLI's transport: a thin JSON-RPC client over the MCP endpoint
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.
@@ -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: Letterstory's
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 rated and forwarded
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(ctx, result.ok ? "Lago agrees with us." : "Lago and we disagree: see the orgs below.");
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;
@@ -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: Letterstory API keys usually start with 'ls_' (legacy keys start with 'lb_'). Saving anyway."
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 Letterstory hosts the site — rebuilt). Runs are 20–60 minutes, so
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 Letterstory itself: a bug, something
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
 
@@ -1,4 +1,4 @@
1
- // `mcp` — print ready-to-paste configuration for wiring the Letterstory MCP server
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(`Letterstory MCP`);
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)"}`);
@@ -760,7 +760,7 @@ async function cmdOnboardNext(ctx) {
760
760
  return 0;
761
761
  }
762
762
 
763
- // `onboard primitives [<domain>]` — what Letterstory would stand up for this company: one
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 Letterstory's account and drops the registrant contact. Re-run with --yes to confirm: ${ctx.bin} mail vercel-key remove --yes`
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
  }
@@ -1,4 +1,4 @@
1
- // `phantom` — Phantomstory: a collection Letterstory hosts as an independent publication.
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
@@ -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 Letterstory GitHub App (src/lib/repo-changes). One subcommand per tool:
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 Letterstory opened. */
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 ? `, Letterstory run ${p.letterRun.status}` : ""})\n ${p.url}`
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 Letterstory
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.
@@ -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 "Letterstory" --tools-file tools.json --tool-route "/api/integrations/tools/{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>
@@ -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
- // Letterstory CLI. This talks to the app's own /api/oauth/* endpoints — see
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>Letterstory CLI</title></head>
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>Letterstory CLI</title></head>
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.1",
4
- "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
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": "Letterstory",
7
+ "author": "Letter",
8
8
  "homepage": "https://github.com/letterstory/letterstory/tree/main/cli#readme",
9
9
  "repository": {
10
10
  "type": "git",