@letterstory/cli 0.29.3 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.mjs CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  } from "./commands.mjs";
45
45
 
46
46
  // Keep in sync with cli/package.json.
47
- export const VERSION = "0.29.3";
47
+ export const VERSION = "0.30.0";
48
48
 
49
49
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
50
50
  // can't accidentally swallow the id as --json's value.
@@ -567,6 +567,7 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
567
567
  no_match is free. --no-enrich skips the company
568
568
  lookup when latency matters more than fit.
569
569
  lede runs [--endpoint <id>] [--limit <n>] Recent lede calls (telemetry), newest first
570
+ lede history [<domain>] [--days <n>] [--limit <n>] Openers already sent to an account
570
571
  lede balance Ledes left on this org
571
572
 
572
573
  Landings (a link + context; auto-mentioned in articles by collections you enable):
@@ -93,14 +93,15 @@ export const ARGS = {
93
93
  // it from a path. Omit both to write from the article as it stands.
94
94
  brief: { value: (flags) => flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) }) },
95
95
  // How to read --brief: auto (default) treats a short topic as instruction and a pasted
96
- // draft/article as source material to decompose first. Same flag name as run_kernel's.
96
+ // draft/article as source material to decompose first — your own finished post is
97
+ // `article`. Same flag name as run_kernel's.
97
98
  content_kind: {
98
99
  flag: "read-as",
99
100
  enum: ["auto", "instruction", "document", "article", "outline", "research", "notes"],
100
101
  },
101
102
  },
102
103
  // The standalone door. Two subcommands share this one tool: `write` and `polish` both
103
- // send `input`, `polish` forcing content_kind to "document" — the map is one, so a field
104
+ // send `input`, `polish` defaulting content_kind to "article" — the map is one, so a field
104
105
  // the tool gains is a field BOTH subcommands must learn (or knowingly gap) — that is the
105
106
  // ratchet.
106
107
  run_kernel: {
@@ -169,8 +170,9 @@ export const ARGS = {
169
170
  // `[{"name":"...","treatment":"never"|"mention_once"|"mention_often"|"lead_company",
170
171
  // "linkable":false}, ...]` (linkable optional, default true).
171
172
  entities: { value: (flags) => readJsonFileFlag(flags, "entities-file") },
172
- // content_kind "document" only: hold the document's own shape through the edit. Default
173
- // true; pass --no-preserve-shape to let the kernel restructure freely.
173
+ // Hold the input's own shape and embedded media through the rewrite, on any kind that
174
+ // is decomposed. Default true; pass --no-preserve-shape to let the kernel restructure
175
+ // freely.
174
176
  preserve_shape: { flag: "preserve-shape", type: "bool" },
175
177
  },
176
178
  create_kernel_endpoint: {
@@ -364,16 +366,22 @@ async function kernelEndpoint(ctx) {
364
366
 
365
367
  /**
366
368
  * The OTHER door: run the same pipeline against a document you already have, instead of
367
- * writing a new one. Decomposed into a brief and GENERATED FRESH (content_kind: "document",
368
- * forced below) — not edited in place — with your own headings and embedded media (images,
369
+ * writing a new one. Decomposed into a brief and GENERATED FRESH (content_kind: "article",
370
+ * defaulted below) — not edited in place — with your own headings and embedded media (images,
369
371
  * tables) extracted first and spliced back into the generated draft afterward, so they
370
372
  * survive a pass that would otherwise be text-only and lose them.
371
373
  *
374
+ * IT IS NOT ITS OWN CONTENT KIND any more (2026-09-22). This used to send `document`, the one
375
+ * kind that held media — which meant `letterstory kernel write --read-as article` on the same
376
+ * file quietly dropped it. Holding what a text-only decompose cannot carry is now the default
377
+ * on every decomposed kind, so `polish` is `write` with the kind named for you, and nothing
378
+ * else. `document` still parses if you pass it; it resolves to `article`.
379
+ *
372
380
  * ASYNCHRONOUS, same as `write` — this used to be a separate, synchronous, unmetered "polish"
373
381
  * that answered in one request, and before that an EARLIER `document` mode that skipped
374
382
  * generation to hold shape "in place." Neither exists any more (2026-09-15, letterstory#1783;
375
- * 2026-09-17). `document` now costs a run and follows the identical submit-and-poll shape
376
- * `write` does, so this command does too — `--no-wait` behaves the same way here.
383
+ * 2026-09-17). It now costs a run and follows the identical submit-and-poll shape `write`
384
+ * does, so this command does too — `--no-wait` behaves the same way here.
377
385
  */
378
386
  async function kernelPolish(ctx) {
379
387
  const { client, positionals, flags, io } = ctx;
@@ -381,7 +389,7 @@ async function kernelPolish(ctx) {
381
389
  if (args.input === undefined) {
382
390
  throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
383
391
  }
384
- args.content_kind = args.content_kind ?? "document";
392
+ args.content_kind = args.content_kind ?? "article";
385
393
  const result = await client.callTool("run_kernel", args);
386
394
  const runId = toolField(result, "run_id");
387
395
  if (!runId) {
@@ -51,6 +51,8 @@ export const ARGS = {
51
51
  notes: "notes",
52
52
  max_candidates: { flag: "candidates", type: "num" },
53
53
  tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
54
+ // How picky to be, in bits of distinctiveness. Below the floor is a free no_match.
55
+ distinctiveness_floor: { flag: "floor", type: "num" },
54
56
  // Your own fallback line, returned (unbilled, still no_match) when nothing qualifies.
55
57
  default_lede: { flag: "default-lede" },
56
58
  // Enrichment is ON by default server-side; `--no-enrich` skips it for latency.
@@ -60,6 +62,13 @@ export const ARGS = {
60
62
  endpoint_id: "endpoint",
61
63
  limit: { flag: "limit", type: "num" },
62
64
  },
65
+ // What has already been said to an account, so the next send does not repeat it.
66
+ lede_account_history: {
67
+ domain: { positional: 0, label: "domain" },
68
+ endpoint_id: "endpoint",
69
+ days: { flag: "days", type: "num" },
70
+ limit: { flag: "limit", type: "num" },
71
+ },
63
72
  list_lede_endpoints: {},
64
73
  get_lede_balance: {},
65
74
  create_lede_endpoint: {
@@ -99,11 +108,14 @@ export async function cmdLede(ctx) {
99
108
  case "runs":
100
109
  case "telemetry":
101
110
  return ledeRuns(rest);
111
+ case "history":
112
+ case "sent":
113
+ return ledeHistory(rest);
102
114
  case "balance":
103
115
  return ledeBalance(rest);
104
116
  default:
105
117
  throw new CliError(
106
- `Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, runs, balance`
118
+ `Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, runs, history, balance`
107
119
  );
108
120
  }
109
121
  }
@@ -217,6 +229,16 @@ async function ledeRuns(ctx) {
217
229
  return 0;
218
230
  }
219
231
 
232
+ // `lede history acme.com` — what has already been said to that account, so the next
233
+ // send does not repeat it. Omit the domain for a picture across every account.
234
+ async function ledeHistory(ctx) {
235
+ const { client, flags, io, positionals } = ctx;
236
+ const args = argsFromFlags(ARGS.lede_account_history, flags, positionals);
237
+ const result = await client.callTool("lede_account_history", args);
238
+ printResult(io, flags, result);
239
+ return 0;
240
+ }
241
+
220
242
  async function ledeBalance(ctx) {
221
243
  const { client, flags, io } = ctx;
222
244
  const result = await client.callTool("get_lede_balance", argsFromFlags(ARGS.get_lede_balance, flags));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.29.3",
3
+ "version": "0.30.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",