@letterstory/cli 0.102.1 → 0.102.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
@@ -815,6 +815,12 @@ here can SEND email or spend money, so they refuse unknown flags instead of igno
815
815
 
816
816
  ```bash
817
817
  # Mailers
818
+ letterstory mail vercel-key set --token-file - --team team_AbC123 # optional: buy domains on YOUR Vercel team (token on stdin)
819
+ letterstory mail registrant --first-name Dana --last-name Lee --email d@acme.com --phone +1.4155550100 \
820
+ --address "1 Main St" --city Oakland --state CA --zip 94607 --country US # WHOIS owner for purchases on that key
821
+ letterstory mailers quote acme-mail.com # price it first: nothing is bought
822
+ letterstory mailers launch acme-mail.com --mailer "hello:Hello Team" --forward-to me@acme.com --yes
823
+ letterstory mailers domains # every domain: setup job, mailers, warmup
818
824
  letterstory mailers list [--domain acme.com] # status + daily send limit
819
825
  letterstory mailers domain <owned-domain-id> # provisioning stage, DNS, mailers on it
820
826
  letterstory mailers messages <mailer-id> [--folder inbox|sent|all] [--limit 25] # envelopes
@@ -828,9 +834,6 @@ letterstory mailers suppressions [list] [--reason bounce] [--limit 100]
828
834
  letterstory mailers suppressions add person@x.com --reason manual --note "asked to stop"
829
835
  letterstory mailers suppressions remove person@x.com # only 'manual' entries; opt-outs stay
830
836
 
831
- # Launching buys a domain (up to $30) and the mailboxes, so it is a tool call, not a command:
832
- letterstory call launch_mailers --args '{"domain":"acme-mail.com","mailers":[{"local_part":"hello"}],"consent_attested":true,"mail_from_postal_address":"…"}'
833
-
834
837
  # Org-level mail settings
835
838
  letterstory mail connections # connected mailboxes, key + postal address status
836
839
  letterstory mail postal-address "123 Main St, Portland, OR 97201" # CAN-SPAM footer
package/lib/cli.mjs CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  } from "./commands.mjs";
61
61
 
62
62
  // Keep in sync with cli/package.json.
63
- export const VERSION = "0.102.1";
63
+ export const VERSION = "0.102.3";
64
64
 
65
65
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
66
66
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -71,6 +71,11 @@ export const BOOLEAN_FLAGS = new Set([
71
71
  "unfold",
72
72
  // site map: do not start an audit for a host that has none.
73
73
  "no-audit",
74
+ // site refresh / observe: the loop's switches.
75
+ "force-map",
76
+ "force-audit",
77
+ "skip-observe",
78
+ "ignore-budget",
74
79
  // lede library update / lede proposals accept: clear or drop the line's condition.
75
80
  "no-condition",
76
81
  // prospects update: clear the HeyReach LinkedIn-reply marker.
@@ -178,6 +183,10 @@ export const BOOLEAN_FLAGS = new Set([
178
183
  "no-notify-staff",
179
184
  "no-opt-out-footer",
180
185
  "no-postal-address-footer",
186
+ // `mailers launch … --no-warmup` / `--warmup` / `--consent`: value-less switches.
187
+ "no-warmup",
188
+ "warmup",
189
+ "consent",
181
190
  "no-redesign",
182
191
  "no-test-mode",
183
192
  "no-unsubscribe-links",
@@ -1091,7 +1100,13 @@ Prospects (who you write to):
1091
1100
  [--first-name] [--last-name] [--title] [--company] [--website] [--linkedin] [--school]
1092
1101
  [--city] [--country] [--years] [--notes] [--custom key=value]... [--clear-linkedin-reply]
1093
1102
 
1094
- Mailers (the sending mailboxes; launching one buys a domain, so use: call launch_mailers):
1103
+ Mailers (the sending mailboxes):
1104
+ mailers quote <domain> Price a launch domain (read-only, nothing bought)
1105
+ mailers launch <domain> --mailer <name>[:<Display Name>]... --yes
1106
+ [--forward-to <email>] [--no-warmup] [--postal-address "<addr>"] [--consent] [--customer-ref <r>]
1107
+ Buy or reuse the domain, create mailers, warm them up
1108
+ (a net-new domain is a real, auto-renewing purchase)
1109
+ mailers domains Every domain you own: mail status, setup job, mailers
1095
1110
  mailers list [--domain <apex>] Your mailers + status + daily send limit
1096
1111
  mailers domain <owned-domain-id> Provisioning stage, DNS state, mailers on a domain
1097
1112
  mailers messages <mailer-id> [--folder all|inbox|sent] [--limit <n>]
@@ -1110,6 +1125,12 @@ Mailers (the sending mailboxes; launching one buys a domain, so use: call launch
1110
1125
  Mail:
1111
1126
  mail connections Connected mailboxes, Google connect, Vercel key + mailing address status
1112
1127
  mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
1128
+ mail vercel-key set (--token <t> | --token-file <path|->) [--team <team_id>]
1129
+ Buy and own mail domains on your Vercel account / team
1130
+ mail vercel-key remove --yes Future purchases go back to Letterstory's account
1131
+ mail registrant --first-name <f> --last-name <l> --email <e> --phone +1.4155551234
1132
+ --address <a> [--address2 <a>] --city <c> --state <s> --zip <z> --country US [--company <c>]
1133
+ WHOIS registrant for purchases on your Vercel key
1113
1134
 
1114
1135
  Insights:
1115
1136
  insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
@@ -1127,13 +1148,16 @@ Insights:
1127
1148
 
1128
1149
  The Site (one model of your whole marketing site, keyed by host and path):
1129
1150
  site list Mapped hosts, and hosts you already have elsewhere
1130
- site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit]
1151
+ site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit] [--relation owned|watched]
1131
1152
  Map a host from its sitemap; starts an audit if it has none
1153
+ site refresh <host> [--force-map] [--force-audit] [--skip-observe]
1154
+ Run the daily loop now: re-map, re-audit, signals, Jev
1155
+ site unmap <host> Remove a host and everything hanging off it
1132
1156
  site tree <host> [path] [--depth <n>] [--unfold] The tree one folder at a time, one item per node
1133
1157
  site page <host> <path> One page: facts, bindings, raw URLs, open items
1134
- site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info]
1158
+ site observations <host> [path] [--state open|dismissed|resolved] [--tone risk|warn|info] [--key <k>] [--since <iso>] [--min-reach <0..1>]
1135
1159
  site observe <host> [path] [--max-pages <n>] Jev pass: meta accuracy, title, outdated
1136
- site dismiss <host> <observation-id> --reason <why> [--decision dismiss|reopen]
1160
+ site dismiss <host> <observation-id> --reason <why> [--kind not_relevant|already_handled|wrong|later] [--decision dismiss|reopen]
1137
1161
  site families <host> [path] [--state suggested|tracked|dismissed]
1138
1162
  site decide-family <family-id> [--decision tracked|dismissed] [--reason <why>] [--name <n>]
1139
1163
  [--include <path>]... [--exclude <path>]...
@@ -2810,6 +2810,43 @@ async function cmdAdminCrmCapture(ctx, paths) {
2810
2810
  })
2811
2811
  );
2812
2812
  if (flagBool(flags.json)) return printResult(io, flags, r);
2813
+ for (const line of captureSummary(r)) io.log(line);
2814
+ }
2815
+
2816
+ /** What a batch entry's status says, in the CLI's words. */
2817
+ const BATCH_STATUS = {
2818
+ would_match: "would add to",
2819
+ would_create: "would create",
2820
+ matched: "added to",
2821
+ created: "created",
2822
+ duplicate: "already in",
2823
+ unidentified: "not found",
2824
+ not_found: "no account",
2825
+ failed: "didn't save",
2826
+ };
2827
+
2828
+ /**
2829
+ * A capture result as lines (pure). A single capture is one line and what it
2830
+ * read; one that became several entries — people on a list, or notes on
2831
+ * several accounts from one roundup — is a line per entry, so the person sees
2832
+ * where each went (the CRM's paste box and the Slack reply list the same).
2833
+ */
2834
+ export function captureSummary(r) {
2835
+ if (r.batch) {
2836
+ const width = Math.max(...r.batch.map((b) => (BATCH_STATUS[b.status] ?? b.status).length), 0);
2837
+ return [
2838
+ `${r.dryRun ? "would save" : "saved"} ${r.accountName ?? `${r.batch.length} entries`}`,
2839
+ ...r.batch.map((b) => {
2840
+ const status = (BATCH_STATUS[b.status] ?? b.status).padEnd(width);
2841
+ const where = b.accountName ?? b.companyName ?? "?";
2842
+ const extra =
2843
+ b.status === "failed" || b.status === "not_found" || b.status === "unidentified"
2844
+ ? b.why
2845
+ : b.headline;
2846
+ return ` ${status} ${b.name} → ${where}${b.accountUrl ? ` ${b.accountUrl}` : ""}${extra ? ` — ${extra}` : ""}`;
2847
+ }),
2848
+ ];
2849
+ }
2813
2850
  const verb = r.dryRun
2814
2851
  ? r.accountId
2815
2852
  ? "would add to"
@@ -2819,15 +2856,16 @@ async function cmdAdminCrmCapture(ctx, paths) {
2819
2856
  : r.accountCreated
2820
2857
  ? "created"
2821
2858
  : "added to";
2822
- io.log(
2823
- `${verb} ${r.accountName ?? "a new account"}${r.accountUrl ? ` ${r.accountUrl}` : ""}${r.matchedVia ? ` (via ${r.matchedVia})` : ""}`
2824
- );
2859
+ const lines = [
2860
+ `${verb} ${r.accountName ?? "a new account"}${r.accountUrl ? ` ${r.accountUrl}` : ""}${r.matchedVia ? ` (via ${r.matchedVia})` : ""}`,
2861
+ ];
2825
2862
  if (r.read) {
2826
- io.log(` ${r.read.title}`);
2863
+ lines.push(` ${r.read.title}`);
2827
2864
  if (r.contacts.length)
2828
- io.log(` contacts: ${r.contacts.map((p) => p.name + (p.email ? ` <${p.email}>` : "")).join(", ")}`);
2829
- if (r.read.notes) io.log(` notes: ${r.read.notes}`);
2865
+ lines.push(` contacts: ${r.contacts.map((p) => p.name + (p.email ? ` <${p.email}>` : "")).join(", ")}`);
2866
+ if (r.read.notes) lines.push(` notes: ${r.read.notes}`);
2830
2867
  }
2868
+ return lines;
2831
2869
  }
2832
2870
 
2833
2871
  /**
@@ -2845,6 +2883,7 @@ async function cmdAdminCrmCapture(ctx, paths) {
2845
2883
  * outreach catch-up [--dry-run] handle #launch-outreach posts the bot missed
2846
2884
  * outreach set <id> [--reached|--not-reached] [--response "…" --tone positive]
2847
2885
  * [--owner-id <uuid>] [--channel …] [--ask …] [--draft "…"] [--archive|--restore]
2886
+ * [--timezone Europe/London|PT|""] "" goes back to the guess from their location
2848
2887
  */
2849
2888
  async function cmdAdminCrmOutreach(ctx, args) {
2850
2889
  const { flags, io } = ctx;
@@ -2952,6 +2991,8 @@ async function cmdAdminCrmOutreach(ctx, args) {
2952
2991
  relationship: flagStr(flags.relationship),
2953
2992
  draftMessage: flagStr(flags.draft),
2954
2993
  notes: flagStr(flags.notes),
2994
+ // "Europe/London", or "PT" / "eastern"; "" clears it back to the guess from their location.
2995
+ timezone: flagStr(flags.timezone),
2955
2996
  archived: flagBool(flags.archive) ? true : flagBool(flags.restore) ? false : undefined,
2956
2997
  });
2957
2998
  const r = await staffRequest(ctx, "PATCH", `${CRM}/outreach/${id}`, body);
@@ -393,8 +393,62 @@ export const ARGS = {
393
393
  set_mail_postal_address: {
394
394
  mail_postal_address: { positional: 0, label: "address", required: true },
395
395
  },
396
+ list_mail_domains: {},
397
+ quote_mail_domain: {
398
+ domain: { positional: 0, label: "domain", required: true },
399
+ },
400
+ launch_mailers: {
401
+ domain: { positional: 0, label: "domain", required: true },
402
+ mailers: { value: launchMailerRows },
403
+ customer_ref: "customer-ref",
404
+ forward_to: "forward-to",
405
+ warmup: { flag: "warmup", type: "bool", negate: "no-warmup" },
406
+ consent_attested: { flag: "consent", type: "bool" },
407
+ mail_from_postal_address: "postal-address",
408
+ },
409
+ set_vercel_key: {
410
+ // --token-file <path|-> keeps the token out of shell history.
411
+ token: {
412
+ value: (flags) => {
413
+ const token = readTextInput(flags, "token", "token-file")?.trim();
414
+ if (!token) throw new CliError("Pass --token <vercel-token> (or --token-file <path|->)");
415
+ return token;
416
+ },
417
+ },
418
+ team_id: "team",
419
+ },
420
+ remove_vercel_key: {},
421
+ set_registrant_contact: {
422
+ registrant_contact: {
423
+ object: {
424
+ firstName: "first-name",
425
+ lastName: "last-name",
426
+ email: "email",
427
+ phone: "phone",
428
+ address1: "address",
429
+ address2: "address2",
430
+ city: "city",
431
+ state: "state",
432
+ zip: "zip",
433
+ country: "country",
434
+ companyName: "company",
435
+ },
436
+ },
437
+ },
396
438
  };
397
439
 
440
+ // `--mailer <name>[:<Display Name>]`, repeatable: the addresses a launch creates.
441
+ function launchMailerRows(flags) {
442
+ const rows = flagList(flags.mailer).map((raw) => {
443
+ const at = raw.indexOf(":");
444
+ const localPart = (at < 0 ? raw : raw.slice(0, at)).trim();
445
+ const displayName = at < 0 ? "" : raw.slice(at + 1).trim();
446
+ return displayName ? { local_part: localPart, display_name: displayName } : { local_part: localPart };
447
+ });
448
+ if (rows.length === 0) throw new CliError("Pass at least one --mailer <name>[:<Display Name>]");
449
+ return rows;
450
+ }
451
+
398
452
  // `message` may be a function of the tool's result, for a line that reports what actually happened.
399
453
  async function run(ctx, tool, message) {
400
454
  refuseUnknownFlags(ctx, ARGS[tool]);
@@ -639,7 +693,7 @@ async function prospectsColumns(ctx, sub) {
639
693
  }
640
694
  }
641
695
 
642
- const MAILERS_USAGE = `Usage: mailers <list|domain|messages|message|forward|warmup|limit|retire|suppressions>`;
696
+ const MAILERS_USAGE = `Usage: mailers <list|domains|domain|quote|launch|messages|message|forward|warmup|limit|retire|suppressions>`;
643
697
 
644
698
  export async function cmdMailers(ctx) {
645
699
  const sub = ctx.positionals[0];
@@ -648,8 +702,23 @@ export async function cmdMailers(ctx) {
648
702
  case "list":
649
703
  case "ls":
650
704
  return run(rest, "list_mailers");
705
+ case "domains":
706
+ return run(rest, "list_mail_domains");
651
707
  case "domain":
652
708
  return run(rest, "get_mailer_domain_status");
709
+ case "quote":
710
+ return run(rest, "quote_mail_domain");
711
+ case "launch": {
712
+ // A net-new domain is a real purchase, so a launch needs --yes (quote it first).
713
+ if (!rest.flags.yes) {
714
+ const domain = flagStr(rest.positionals[0]) ?? "<domain>";
715
+ ctx.io.error(
716
+ `Launching may BUY ${domain} (auto-renewing). Check the price with: ${ctx.bin} mailers quote ${domain}\nThen re-run with --yes to confirm.`
717
+ );
718
+ return 1;
719
+ }
720
+ return run(rest, "launch_mailers", (r) => `Mail setup started on ${r?.domain ?? "the domain"}.`);
721
+ }
653
722
  case "messages":
654
723
  return run(rest, "list_mailer_messages");
655
724
  case "message":
@@ -708,7 +777,32 @@ export async function cmdMail(ctx) {
708
777
  return run(rest, "get_mail_connections");
709
778
  case "postal-address":
710
779
  return run(rest, "set_mail_postal_address", "Mailing address saved.");
780
+ case "vercel-key": {
781
+ const action = rest.positionals[0];
782
+ const inner = { ...rest, positionals: rest.positionals.slice(1) };
783
+ if (action === "set") {
784
+ return run(inner, "set_vercel_key", (r) =>
785
+ r?.team_id
786
+ ? `Vercel key saved: new domains are bought in team ${r.team_id}.`
787
+ : "Vercel key saved: new domains are bought in the token's personal account."
788
+ );
789
+ }
790
+ if (action === "remove") {
791
+ if (!inner.flags.yes) {
792
+ ctx.io.error(
793
+ `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`
794
+ );
795
+ return 1;
796
+ }
797
+ return run(inner, "remove_vercel_key", "Vercel key removed.");
798
+ }
799
+ throw new CliError(`Unknown vercel-key action: ${action ?? "(none)"}. Try: set, remove`);
800
+ }
801
+ case "registrant":
802
+ return run(rest, "set_registrant_contact", "Registrant contact saved.");
711
803
  default:
712
- throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections, postal-address`);
804
+ throw new CliError(
805
+ `Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections, postal-address, vercel-key, registrant`
806
+ );
713
807
  }
714
808
  }
@@ -128,16 +128,22 @@ export function valueSourceFlags(value) {
128
128
  */
129
129
  export function refuseUnknownFlags(ctx, argsMap, own = []) {
130
130
  const allowed = new Set([...GLOBAL_FLAGS, ...own]);
131
- for (const source of Object.values(argsMap ?? {})) {
132
- if (typeof source === "string") allowed.add(source);
133
- else if (Array.isArray(source)) source.forEach((f) => allowed.add(f));
134
- else if (source && typeof source === "object" && source.flag) {
135
- allowed.add(source.flag);
136
- if (source.negate) allowed.add(source.negate);
137
- } else if (source && typeof source === "object" && typeof source.value === "function") {
138
- valueSourceFlags(source.value).forEach((f) => allowed.add(f));
131
+ const collect = (map) => {
132
+ for (const source of Object.values(map ?? {})) {
133
+ if (typeof source === "string") allowed.add(source);
134
+ else if (Array.isArray(source)) source.forEach((f) => allowed.add(f));
135
+ else if (source && typeof source === "object" && source.flag) {
136
+ allowed.add(source.flag);
137
+ if (source.negate) allowed.add(source.negate);
138
+ } else if (source && typeof source === "object" && typeof source.value === "function") {
139
+ valueSourceFlags(source.value).forEach((f) => allowed.add(f));
140
+ } else if (source && typeof source === "object" && source.object) {
141
+ // A nested `{ object }` source reads its own flags (e.g. a contact's fields).
142
+ collect(source.object);
143
+ }
139
144
  }
140
- }
145
+ };
146
+ collect(argsMap);
141
147
  const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
142
148
  if (unknown.length) {
143
149
  throw new CliError(
@@ -41,7 +41,15 @@ const ARGS = {
41
41
  sitemap_url: "sitemap",
42
42
  max_pages: { flag: "max-pages", type: "num" },
43
43
  audit: { flag: "no-audit", type: "bool", invert: true },
44
+ relation: { flag: "relation", enum: ["owned", "watched"] },
44
45
  },
46
+ refresh_site: {
47
+ host: { positional: 0, required: true, label: "host" },
48
+ force_map: { flag: "force-map", type: "bool" },
49
+ force_audit: { flag: "force-audit", type: "bool" },
50
+ skip_observe: { flag: "skip-observe", type: "bool" },
51
+ },
52
+ unmap_site: { host: { positional: 0, required: true, label: "host" } },
45
53
  get_site_tree: {
46
54
  host: { positional: 0, required: true, label: "host" },
47
55
  path: { positional: 1, label: "path" },
@@ -58,18 +66,23 @@ const ARGS = {
58
66
  status: { flag: "state", enum: ["open", "dismissed", "resolved"] },
59
67
  tone: { flag: "tone", enum: ["ok", "warn", "risk", "info"] },
60
68
  source: "source",
69
+ key: "key",
70
+ since: "since",
71
+ min_reach: { flag: "min-reach", type: "num" },
61
72
  limit: { flag: "limit", type: "num" },
62
73
  },
63
74
  observe_site: {
64
75
  host: { positional: 0, required: true, label: "host" },
65
76
  path: { positional: 1, label: "path" },
66
77
  max_pages: { flag: "max-pages", type: "num" },
78
+ ignore_budget: { flag: "ignore-budget", type: "bool" },
67
79
  },
68
80
  dismiss_site_observation: {
69
81
  host: { positional: 0, required: true, label: "host" },
70
82
  observation_id: { positional: 1, required: true, label: "observation-id" },
71
83
  decision: { flag: "decision", enum: ["dismiss", "reopen"] },
72
84
  reason: "reason",
85
+ reason_kind: { flag: "kind", enum: ["not_relevant", "already_handled", "wrong", "later"] },
73
86
  },
74
87
  list_site_families: {
75
88
  host: { positional: 0, required: true, label: "host" },
@@ -113,6 +126,10 @@ export async function cmdSite(ctx) {
113
126
  return siteList(rest);
114
127
  case "map":
115
128
  return siteMap(rest);
129
+ case "refresh":
130
+ return siteRefresh(rest);
131
+ case "unmap":
132
+ return siteUnmap(rest);
116
133
  case "tree":
117
134
  return siteTree(rest);
118
135
  case "page":
@@ -135,7 +152,7 @@ export async function cmdSite(ctx) {
135
152
  return siteDecide(rest);
136
153
  default:
137
154
  throw new CliError(
138
- `Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide`
155
+ `Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide`
139
156
  );
140
157
  }
141
158
  }
@@ -187,6 +204,24 @@ async function siteMap(ctx) {
187
204
  return 0;
188
205
  }
189
206
 
207
+ async function siteRefresh(ctx) {
208
+ const { client, flags, io } = ctx;
209
+ refuseUnknownFlags(ctx, ARGS.refresh_site);
210
+ const result = await client.callTool("refresh_site", argsFromFlags(ARGS.refresh_site, flags, ctx.positionals));
211
+ ok(ctx, result.message);
212
+ printResult(io, flags, result.report, (r) => r.skipped.map((s) => ` skipped: ${s}`).join("\n"));
213
+ return 0;
214
+ }
215
+
216
+ async function siteUnmap(ctx) {
217
+ const { client, flags, io } = ctx;
218
+ refuseUnknownFlags(ctx, ARGS.unmap_site);
219
+ const result = await client.callTool("unmap_site", argsFromFlags(ARGS.unmap_site, flags, ctx.positionals));
220
+ ok(ctx, result.message);
221
+ printResult(io, flags, result, () => "");
222
+ return 0;
223
+ }
224
+
190
225
  async function siteTree(ctx) {
191
226
  const { client, flags, io } = ctx;
192
227
  refuseUnknownFlags(ctx, ARGS.get_site_tree);
package/lib/documents.mjs CHANGED
@@ -83,6 +83,8 @@ export const JSON_ARG_FLAG_PAIRS = [
83
83
  ["csv", "csv-file"],
84
84
  // A site proposal's tool input, parsed as JSON (site.mjs proposalInput).
85
85
  ["input", "input-file"],
86
+ // A Vercel API token read verbatim, so it stays out of shell history (outbound.mjs mail vercel-key set).
87
+ ["token", "token-file"],
86
88
  ];
87
89
 
88
90
  /** Lower-case extension without the dot. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.102.1",
3
+ "version": "0.102.3",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",