@letterstory/cli 0.25.1 → 0.25.2

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.25.1";
47
+ export const VERSION = "0.25.2";
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.
@@ -78,28 +78,32 @@ export const ARGS = {
78
78
  // it from a path. Omit both to write from the article as it stands.
79
79
  brief: { value: (flags) => flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) }) },
80
80
  },
81
- // The standalone door. Two subcommands share this one tool: `write` sends a `brief`,
82
- // `polish` sends a `document`; the map is one, so a field the tool gains is a field
83
- // BOTH subcommands must learn (or knowingly gap) — that is the ratchet.
81
+ // The standalone door. Two subcommands share this one tool: `write` and `polish` both
82
+ // send `input`, `polish` forcing content_kind to "document" — the map is one, so a field
83
+ // the tool gains is a field BOTH subcommands must learn (or knowingly gap) — that is the
84
+ // ratchet.
84
85
  run_kernel: {
85
86
  endpoint_id: { positional: 0, label: "endpoint-id", required: true },
86
- brief: { value: (flags, _p, extra) => (extra.door === "brief" ? readBodyInput(flags) : undefined) },
87
- document: { value: (flags, _p, extra) => (extra.door === "document" ? readBodyInput(flags) : undefined) },
88
- // Documented dead field on the tool; accepted so an old --format still validates.
89
- format: { flag: "format", enum: ["auto", "html", "markdown"] },
87
+ input: { value: (flags) => readBodyInput(flags) },
90
88
  kernel_id: "kernel",
91
89
  target_word_count: { flag: "words", type: "num" },
92
- content_type: "content-type",
90
+ label: "label",
93
91
  // The flag is --read-as, which is what shipped; an earlier draft called it --brief-mode.
94
- brief_mode: { flag: "read-as", enum: ["auto", "brief", "content", "blog", "outline", "research", "notes"] },
92
+ content_kind: {
93
+ flag: "read-as",
94
+ enum: ["auto", "instruction", "document", "article", "outline", "research", "notes"],
95
+ },
95
96
  // The two products, in one run: every round is shredded before the kernel's jury sees
96
97
  // it, so the verdict lands on the text that actually ships. Costs a shred as well as a
97
98
  // run.
98
99
  shredder_endpoint_id: "shredder",
99
100
  shred_aggression: { flag: "shred-aggression", enum: ["standard", "high", "max"] },
100
- person: { flag: "person", enum: ["third", "first_plural", "first_singular"] },
101
- author_name: "author",
102
- allow_experience_claims: { flag: "experience-claims", type: "bool" },
101
+ voice: {
102
+ object: {
103
+ person: { flag: "person", enum: ["third", "first_plural", "first_singular"] },
104
+ allow_experience_claims: { flag: "experience-claims", type: "bool" },
105
+ },
106
+ },
103
107
  // The mechanism's full contract, declared on the door (2026-09-17, Kernels wave step 1).
104
108
  // A shape and a typed brief are files, not flags — `--skeleton-file shape.json` is the
105
109
  // field a template resolves to, sent by a caller who has their own.
@@ -123,12 +127,13 @@ export const ARGS = {
123
127
  evidence: { value: (flags) => readTextFileFlag(flags, "shred-evidence-file") },
124
128
  },
125
129
  },
126
- // Named-entity treatment (2026-09-17): a list, not a scalar flag, so it's a file like
127
- // skeleton/brief_json — `--entities-file entities.json` of `[{"name":"...","treatment":
128
- // "never"|"name_only"|"subtle"|"woven"|"lead"}, ...]`.
130
+ // Named-entity treatment (2026-09-17, renamed 2026-09-18): a list, not a scalar flag,
131
+ // so it's a file like skeleton/brief_json — `--entities-file entities.json` of
132
+ // `[{"name":"...","treatment":"never"|"mention_once"|"mention_often"|"lead_company",
133
+ // "linkable":false}, ...]` (linkable optional, default true).
129
134
  entities: { value: (flags) => readJsonFileFlag(flags, "entities-file") },
130
- // document-only: hold the document's own shape through the edit. Default true; pass
131
- // --no-preserve-shape to let the kernel restructure freely.
135
+ // content_kind "document" only: hold the document's own shape through the edit. Default
136
+ // true; pass --no-preserve-shape to let the kernel restructure freely.
132
137
  preserve_shape: { flag: "preserve-shape", type: "bool" },
133
138
  },
134
139
  create_kernel_endpoint: {
@@ -228,8 +233,8 @@ async function kernelEndpoints(ctx) {
228
233
 
229
234
  async function kernelWrite(ctx) {
230
235
  const { client, positionals, flags, io } = ctx;
231
- const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "brief" });
232
- if (args.brief === undefined) {
236
+ const args = argsFromFlags(ARGS.run_kernel, flags, positionals);
237
+ if (args.input === undefined) {
233
238
  throw new CliError("Provide the brief with --body <text>, --file <path>, or --file - (stdin).");
234
239
  }
235
240
  const result = await client.callTool("run_kernel", args);
@@ -322,22 +327,24 @@ async function kernelEndpoint(ctx) {
322
327
 
323
328
  /**
324
329
  * The OTHER door: run the same pipeline against a document you already have, instead of
325
- * writing a new one. Skips generation only — the rules, the in-loop shred and the jury all
326
- * still run, which is what keeps your own markup (images, embeds, links, tables, anchors,
327
- * shortcodes) intact through a real pass rather than an unguarded shortcut.
330
+ * writing a new one. Decomposed into a brief and GENERATED FRESH (content_kind: "document",
331
+ * forced below) — not edited in place — with your own headings and embedded media (images,
332
+ * tables) extracted first and spliced back into the generated draft afterward, so they
333
+ * survive a pass that would otherwise be text-only and lose them.
328
334
  *
329
335
  * ASYNCHRONOUS, same as `write` — this used to be a separate, synchronous, unmetered "polish"
330
- * that answered in one request. That path was deleted (2026-09-15, letterstory#1783): it
331
- * worked by skipping the rules and the jury, and that was the actual cost of being instant
332
- * and free. `document` now costs a run and follows the identical submit-and-poll shape
336
+ * that answered in one request, and before that an EARLIER `document` mode that skipped
337
+ * generation to hold shape "in place." Neither exists any more (2026-09-15, letterstory#1783;
338
+ * 2026-09-17). `document` now costs a run and follows the identical submit-and-poll shape
333
339
  * `write` does, so this command does too — `--no-wait` behaves the same way here.
334
340
  */
335
341
  async function kernelPolish(ctx) {
336
342
  const { client, positionals, flags, io } = ctx;
337
- const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "document" });
338
- if (args.document === undefined) {
343
+ const args = argsFromFlags(ARGS.run_kernel, flags, positionals);
344
+ if (args.input === undefined) {
339
345
  throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
340
346
  }
347
+ args.content_kind = args.content_kind ?? "document";
341
348
  const result = await client.callTool("run_kernel", args);
342
349
  const runId = toolField(result, "run_id");
343
350
  if (!runId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",