@letterstory/cli 0.25.0 → 0.25.1

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.0";
47
+ export const VERSION = "0.25.1";
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.
@@ -11,7 +11,14 @@
11
11
  // finishes whether or not anyone polls.
12
12
 
13
13
  import { CliError } from "../client.mjs";
14
- import { flagBool, requireFlag, printResult, ok } from "./shared.mjs";
14
+ import { flagBool, requireFlag, printResult, ok, argsFromFlags } from "./shared.mjs";
15
+
16
+ /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
17
+ export const ARGS = {
18
+ run_auto_pen: {
19
+ article_id: { flag: "article", required: true },
20
+ },
21
+ };
15
22
 
16
23
  export async function cmdAutoPen(ctx) {
17
24
  const sub = ctx.positionals[0];
@@ -31,9 +38,9 @@ export async function cmdAutoPen(ctx) {
31
38
 
32
39
  async function autoPenStart(ctx) {
33
40
  const { client, flags, io } = ctx;
34
- const article = requireFlag(flags, "article");
35
- const result = await client.callTool("run_auto_pen", { article_id: article });
36
- ok(ctx, `Auto-Pen started. Check progress with: autopen status --article ${article}`);
41
+ const args = argsFromFlags(ARGS.run_auto_pen, flags, ctx.positionals);
42
+ const result = await client.callTool("run_auto_pen", args);
43
+ ok(ctx, `Auto-Pen started. Check progress with: autopen status --article ${args.article_id}`);
37
44
  printResult(io, flags, result);
38
45
  return 0;
39
46
  }
@@ -2,7 +2,15 @@
2
2
  // flow_run.completed completion webhook. Ported from phantomstory-cli's flows.ts.
3
3
 
4
4
  import { CliError } from "../client.mjs";
5
- import { flagStr, requirePositional, printResult, compact, ok } from "./shared.mjs";
5
+ import { flagStr, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
6
+
7
+ /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
8
+ export const ARGS = {
9
+ run_flow: {
10
+ flow_id: { positional: 0, label: "flow-id", required: true },
11
+ article_id: { positional: 1, label: "article-id", required: true },
12
+ },
13
+ };
6
14
 
7
15
  export async function cmdFlows(ctx) {
8
16
  const sub = ctx.positionals[0];
@@ -31,9 +39,7 @@ async function flowsList(ctx) {
31
39
 
32
40
  async function flowsRun(ctx) {
33
41
  const { client, positionals, flags, io } = ctx;
34
- const flowId = requirePositional(positionals, 0, "flow-id");
35
- const articleId = requirePositional(positionals, 1, "article-id");
36
- const result = await client.callTool("run_flow", { flow_id: flowId, article_id: articleId });
42
+ const result = await client.callTool("run_flow", argsFromFlags(ARGS.run_flow, flags, positionals));
37
43
  ok(ctx, "Flow run started.");
38
44
  printResult(io, flags, result);
39
45
  return 0;
@@ -15,11 +15,10 @@
15
15
  // Neither door blocks: a write runs for minutes, so `write` submits and then follows the run
16
16
  // unless you tell it not to.
17
17
 
18
+ import { readFileSync } from "node:fs";
18
19
  import { CliError } from "../client.mjs";
19
20
  import {
20
21
  flagStr,
21
- flagBool,
22
- requireFlag,
23
22
  requirePositional,
24
23
  printResult,
25
24
  compact,
@@ -30,6 +29,38 @@ import {
30
29
  argsFromFlags,
31
30
  } from "./shared.mjs";
32
31
 
32
+ /** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
33
+ function readJsonFileFlag(flags, flag) {
34
+ const path = flagStr(flags[flag]);
35
+ if (path === undefined) return undefined;
36
+ let text;
37
+ try {
38
+ text = readFileSync(path, "utf8");
39
+ } catch (error) {
40
+ throw new CliError(
41
+ `Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
42
+ );
43
+ }
44
+ try {
45
+ return JSON.parse(text);
46
+ } catch {
47
+ throw new CliError(`--${flag} ${path} is not valid JSON.`);
48
+ }
49
+ }
50
+
51
+ /** `--<flag> <path>` → the file's text, or undefined when the flag is not set. */
52
+ function readTextFileFlag(flags, flag) {
53
+ const path = flagStr(flags[flag]);
54
+ if (path === undefined) return undefined;
55
+ try {
56
+ return readFileSync(path, "utf8");
57
+ } catch (error) {
58
+ throw new CliError(
59
+ `Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
60
+ );
61
+ }
62
+ }
63
+
33
64
  /**
34
65
  * How the commands ADDED to this file source their arguments — see `argsFromFlags`
35
66
  * in shared.mjs, and `scripts/parity-check.ts`, which compares these keys against
@@ -39,6 +70,67 @@ import {
39
70
  * frozen UNMAPPED_TOOLS list. New commands declare; that list only shrinks.
40
71
  */
41
72
  export const ARGS = {
73
+ // The article-bound door: submit a kernel run INTO an article you already have.
74
+ submit_kernel_run: {
75
+ article_id: { flag: "article", required: true },
76
+ kernel_id: { flag: "kernel", required: true },
77
+ // A brief overrides the article's own content as the instruction; --brief-file reads
78
+ // it from a path. Omit both to write from the article as it stands.
79
+ brief: { value: (flags) => flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) }) },
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.
84
+ run_kernel: {
85
+ 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"] },
90
+ kernel_id: "kernel",
91
+ target_word_count: { flag: "words", type: "num" },
92
+ content_type: "content-type",
93
+ // 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"] },
95
+ // The two products, in one run: every round is shredded before the kernel's jury sees
96
+ // it, so the verdict lands on the text that actually ships. Costs a shred as well as a
97
+ // run.
98
+ shredder_endpoint_id: "shredder",
99
+ 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" },
103
+ // The mechanism's full contract, declared on the door (2026-09-17, Kernels wave step 1).
104
+ // A shape and a typed brief are files, not flags — `--skeleton-file shape.json` is the
105
+ // field a template resolves to, sent by a caller who has their own.
106
+ skeleton: { value: (flags) => readJsonFileFlag(flags, "skeleton-file") },
107
+ brief_json: { value: (flags) => readJsonFileFlag(flags, "brief-json-file") },
108
+ max_iterations: { flag: "max-iterations", type: "num" },
109
+ kernel_version: { flag: "kernel-version", type: "num" },
110
+ // In-loop shred tuning. Only sent when a flag is set; ignored when no shred runs.
111
+ shred: {
112
+ object: {
113
+ tier: { flag: "shred-tier", enum: ["cheap", "mid", "strong", "frontier"] },
114
+ families: { flag: "shred-family", type: "list" },
115
+ revise_shredded: { flag: "shred-revise", type: "bool" },
116
+ revision_model: "shred-revision-model",
117
+ skip_voice_bearing: { flag: "shred-skip-voice-bearing", type: "bool" },
118
+ judge: { flag: "shred-judge", type: "bool" },
119
+ attempts_per_unit: { flag: "shred-attempts", type: "num" },
120
+ max_changeable_retention: { flag: "shred-max-retention", type: "num" },
121
+ turnover_retries: { flag: "shred-turnover-retries", type: "num" },
122
+ turnover_avoid_list: { flag: "shred-turnover-avoid-list", type: "bool" },
123
+ evidence: { value: (flags) => readTextFileFlag(flags, "shred-evidence-file") },
124
+ },
125
+ },
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"}, ...]`.
129
+ 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.
132
+ preserve_shape: { flag: "preserve-shape", type: "bool" },
133
+ },
42
134
  create_kernel_endpoint: {
43
135
  name: { flag: "name", required: true },
44
136
  // Both optional bindings the tool has always accepted at create: which kernel
@@ -51,6 +143,15 @@ export const ARGS = {
51
143
  delete_kernel_endpoint: {
52
144
  endpoint_id: { positional: 1, label: "endpoint-id", required: true },
53
145
  },
146
+ // The endpoint's configuration as ONE file: `kernel endpoint config <id>` prints it,
147
+ // `kernel endpoint config <id> --file cfg.json` replaces it with the file's document.
148
+ get_kernel_endpoint_config: {
149
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
150
+ },
151
+ set_kernel_endpoint_config: {
152
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
153
+ config: { value: (flags) => readJsonFileFlag(flags, "file") },
154
+ },
54
155
  };
55
156
 
56
157
  export async function cmdKernel(ctx) {
@@ -94,10 +195,7 @@ async function kernelList(ctx) {
94
195
 
95
196
  async function kernelRun(ctx) {
96
197
  const { client, flags, io } = ctx;
97
- const article = requireFlag(flags, "article");
98
- const kernelId = requireFlag(flags, "kernel");
99
- const brief = flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) });
100
- const args = compact({ article_id: article, kernel_id: kernelId, brief });
198
+ const args = argsFromFlags(ARGS.submit_kernel_run, flags, ctx.positionals);
101
199
  const result = await client.callTool("submit_kernel_run", args);
102
200
  ok(ctx, `Kernel run started. Check progress with: kernel status ${result?.kernel_job_id ?? "<kernel-job-id>"}`);
103
201
  printResult(io, flags, result);
@@ -130,33 +228,10 @@ async function kernelEndpoints(ctx) {
130
228
 
131
229
  async function kernelWrite(ctx) {
132
230
  const { client, positionals, flags, io } = ctx;
133
- const endpointId = requirePositional(positionals, 0, "endpoint-id");
134
- const brief = readBodyInput(flags);
135
- if (brief === undefined) {
231
+ const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "brief" });
232
+ if (args.brief === undefined) {
136
233
  throw new CliError("Provide the brief with --body <text>, --file <path>, or --file - (stdin).");
137
234
  }
138
- const args = compact({
139
- endpoint_id: endpointId,
140
- brief,
141
- kernel_id: flagStr(flags.kernel),
142
- target_word_count: flags.words === undefined ? undefined : Number(flags.words),
143
- content_type: flagStr(flags["content-type"]),
144
- // The two products, in one run: every round is shredded before the kernel's jury sees
145
- // it, so the verdict lands on the text that actually ships. Costs a shred as well as a
146
- // run. Reachable from every other surface and, until now, not from here.
147
- shredder_endpoint_id: flagStr(flags.shredder),
148
- // How hard that in-loop shred rewrites — 'standard' (default, recommended), 'high',
149
- // or 'max'. Ignored when no shred runs.
150
- shred_aggression: flagStr(flags["shred-aggression"]),
151
- // The flag is --read-as, which is what shipped; an earlier draft of this branch
152
- // called it --brief-mode and never reached anyone.
153
- brief_mode: flagStr(flags["read-as"]),
154
- // The other three `run_kernel` has always taken and the CLI still did not expose,
155
- // so the only way to reach them was hand-written JSON through `mcp call`.
156
- person: flagStr(flags.person),
157
- author_name: flagStr(flags.author),
158
- allow_experience_claims: flagBool(flags["experience-claims"]),
159
- });
160
235
  const result = await client.callTool("run_kernel", args);
161
236
  const runId = toolField(result, "run_id");
162
237
  if (!runId) {
@@ -225,7 +300,24 @@ async function kernelEndpoint(ctx) {
225
300
  printResult(io, flags, result);
226
301
  return 0;
227
302
  }
228
- throw new CliError(`Unknown kernel endpoint action: ${action ?? "(none)"}. Try: create, delete`);
303
+ if (action === "config") {
304
+ const id = requirePositional(positionals, 1, "endpoint-id");
305
+ if (flagStr(flags.file) === undefined) {
306
+ const result = await client.callTool(
307
+ "get_kernel_endpoint_config",
308
+ argsFromFlags(ARGS.get_kernel_endpoint_config, flags, positionals)
309
+ );
310
+ // The document itself, so `> cfg.json` round-trips into `--file cfg.json`.
311
+ printResult(io, flags, result, (r) => JSON.stringify(r?.config ?? r, null, 2));
312
+ return 0;
313
+ }
314
+ const args = argsFromFlags(ARGS.set_kernel_endpoint_config, flags, positionals);
315
+ const result = await client.callTool("set_kernel_endpoint_config", args);
316
+ ok(ctx, `Replaced the configuration of kernel endpoint ${id}.`);
317
+ printResult(io, flags, result);
318
+ return 0;
319
+ }
320
+ throw new CliError(`Unknown kernel endpoint action: ${action ?? "(none)"}. Try: create, delete, config`);
229
321
  }
230
322
 
231
323
  /**
@@ -242,16 +334,10 @@ async function kernelEndpoint(ctx) {
242
334
  */
243
335
  async function kernelPolish(ctx) {
244
336
  const { client, positionals, flags, io } = ctx;
245
- const endpointId = requirePositional(positionals, 0, "endpoint-id");
246
- const document = readBodyInput(flags);
247
- if (document === undefined) {
337
+ const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "document" });
338
+ if (args.document === undefined) {
248
339
  throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
249
340
  }
250
- const args = compact({
251
- endpoint_id: endpointId,
252
- document,
253
- format: flagStr(flags.format),
254
- });
255
341
  const result = await client.callTool("run_kernel", args);
256
342
  const runId = toolField(result, "run_id");
257
343
  if (!runId) {
@@ -12,8 +12,16 @@ import {
12
12
  compact,
13
13
  ok,
14
14
  readBodyInput,
15
+ argsFromFlags,
15
16
  } from "./shared.mjs";
16
17
 
18
+ /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
19
+ export const ARGS = {
20
+ publish_article: {
21
+ article_id: { positional: 0, label: "article-id", required: true },
22
+ },
23
+ };
24
+
17
25
  export async function cmdPosts(ctx) {
18
26
  const sub = ctx.positionals[0];
19
27
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
@@ -66,8 +74,7 @@ async function postsNew(ctx) {
66
74
 
67
75
  async function postsPublish(ctx) {
68
76
  const { client, positionals, flags, io } = ctx;
69
- const id = requirePositional(positionals, 0, "article-id");
70
- const result = await client.callTool("publish_article", { article_id: id });
77
+ const result = await client.callTool("publish_article", argsFromFlags(ARGS.publish_article, flags, positionals));
71
78
  ok(ctx, "Published.");
72
79
  printResult(io, flags, result);
73
80
  return 0;
@@ -4,7 +4,19 @@
4
4
  // kernels do NOT research — `research start` then `kernel run` is the full pipeline.
5
5
 
6
6
  import { CliError } from "../client.mjs";
7
- import { flagStr, flagNum, flagBool, flagList, requireFlag, printResult, compact, ok } from "./shared.mjs";
7
+ import { flagStr, flagNum, flagBool, requireFlag, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
8
+
9
+ /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
10
+ export const ARGS = {
11
+ start_research: {
12
+ article_id: { flag: "article", required: true },
13
+ topic: "topic",
14
+ source_urls: { flag: "url", type: "list" },
15
+ must_include: { flag: "must-include", type: "list" },
16
+ depth: { flag: "depth", enum: ["barebones", "fleshed_out"] },
17
+ direction_gate: { flag: "gate", type: "bool" },
18
+ },
19
+ };
8
20
 
9
21
  export async function cmdResearch(ctx) {
10
22
  const sub = ctx.positionals[0];
@@ -28,23 +40,9 @@ export async function cmdResearch(ctx) {
28
40
 
29
41
  async function researchStart(ctx) {
30
42
  const { client, flags, io } = ctx;
31
- const article = requireFlag(flags, "article");
32
- const depth = flagStr(flags.depth);
33
- if (depth !== undefined && depth !== "barebones" && depth !== "fleshed_out") {
34
- throw new CliError(`--depth must be barebones or fleshed_out, got "${depth}"`);
35
- }
36
- const sourceUrls = flagList(flags.url);
37
- const mustInclude = flagList(flags["must-include"]);
38
- const args = compact({
39
- article_id: article,
40
- topic: flagStr(flags.topic),
41
- source_urls: sourceUrls.length ? sourceUrls : undefined,
42
- must_include: mustInclude.length ? mustInclude : undefined,
43
- depth,
44
- direction_gate: flagBool(flags.gate) ? true : undefined,
45
- });
43
+ const args = argsFromFlags(ARGS.start_research, flags, ctx.positionals);
46
44
  const result = await client.callTool("start_research", args);
47
- ok(ctx, `Research started. Check progress with: research status --article ${article}`);
45
+ ok(ctx, `Research started. Check progress with: research status --article ${args.article_id}`);
48
46
  printResult(io, flags, result);
49
47
  return 0;
50
48
  }
@@ -12,7 +12,18 @@
12
12
 
13
13
  import { readFileSync, writeFileSync } from "node:fs";
14
14
  import { CliError } from "../client.mjs";
15
- import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok } from "./shared.mjs";
15
+ import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
16
+
17
+ /**
18
+ * How the ratcheted commands in this file source their tool inputs — see `argsFromFlags` in
19
+ * shared.mjs and `scripts/parity-check.ts`. The rest of the module predates the field-level
20
+ * ratchet and stays in UNMAPPED_TOOLS; that list only shrinks.
21
+ */
22
+ export const ARGS = {
23
+ run_seer: {
24
+ seer_id: { positional: 0, label: "seer-id", required: true },
25
+ },
26
+ };
16
27
 
17
28
  export async function cmdSeers(ctx) {
18
29
  const sub = ctx.positionals[0];
@@ -177,9 +188,9 @@ async function seersDelete(ctx) {
177
188
 
178
189
  async function seersRun(ctx) {
179
190
  const { client, positionals, flags, io } = ctx;
180
- const seerId = requirePositional(positionals, 0, "seer-id");
191
+ const args = argsFromFlags(ARGS.run_seer, flags, positionals);
181
192
  ok(ctx, "Polling now — web-scan providers can take a few minutes…");
182
- const result = await client.callTool("run_seer", { seer_id: seerId });
193
+ const result = await client.callTool("run_seer", args);
183
194
  printResult(
184
195
  io,
185
196
  flags,
@@ -15,8 +15,32 @@ import {
15
15
  ok,
16
16
  toolField,
17
17
  followJob,
18
+ argsFromFlags,
18
19
  } from "./shared.mjs";
19
20
 
21
+ /**
22
+ * How `shredder run` sources `run_shred`'s inputs — see `argsFromFlags` in shared.mjs and
23
+ * `scripts/parity-check.ts`, which diffs these keys against the tool's published schema.
24
+ * The rest of this module predates the ratchet and stays in UNMAPPED_TOOLS.
25
+ */
26
+ export const ARGS = {
27
+ run_shred: {
28
+ endpoint_id: { positional: 0, label: "endpoint-id", required: true },
29
+ text: { value: (flags) => readBodyInput(flags) },
30
+ coverage: { flag: "coverage", type: "num" },
31
+ attempts_per_unit: { flag: "attempts", type: "num" },
32
+ // What the text IS. Without this an HTML file could only go through the `auto`
33
+ // heuristic, which misreads a markdown post carrying an embed.
34
+ format: { flag: "format", enum: ["auto", "html", "markdown"] },
35
+ // How hard to rewrite. Validated service-side; sent only when the caller asked.
36
+ aggression: { flag: "aggression", enum: ["standard", "high", "max"] },
37
+ // Hold every rewrite to the live writing rules. A trade, not a free win.
38
+ apply_rules: { flag: "apply-rules", type: "bool" },
39
+ // ASYNCHRONOUS BY DEFAULT — `--sync` is the opt-out (see shredderRun for why).
40
+ async: { value: (flags) => (flags.sync === true ? undefined : true) },
41
+ },
42
+ };
43
+
20
44
  export async function cmdShredder(ctx) {
21
45
  const sub = ctx.positionals[0];
22
46
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
@@ -87,11 +111,6 @@ const POLL_CEILING_MS = 30 * 60 * 1000;
87
111
 
88
112
  async function shredderRun(ctx) {
89
113
  const { client, positionals, flags, io } = ctx;
90
- const id = requirePositional(positionals, 0, "endpoint-id");
91
- const text = readBodyInput(flags);
92
- if (text === undefined) {
93
- throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
94
- }
95
114
  // ASYNCHRONOUS BY DEFAULT, and `--sync` is the opt-out rather than the other way round.
96
115
  //
97
116
  // A shred has no workable synchronous ceiling — the identical article took 203s, 541s and
@@ -100,21 +119,10 @@ async function shredderRun(ctx) {
100
119
  // answer, so the caller gets no text, no error, and no record. Defaulting the other way
101
120
  // made the common case the broken one.
102
121
  const sync = flags.sync === true;
103
- const args = compact({
104
- endpoint_id: id,
105
- text,
106
- coverage: flagNum(flags.coverage),
107
- attempts_per_unit: flagNum(flags.attempts),
108
- // What the text IS. Without this an HTML file could only go through the
109
- // `auto` heuristic, which misreads a markdown post carrying an embed.
110
- ...(flags.format ? { format: String(flags.format) } : {}),
111
- // How hard to rewrite. Validated service-side; sent only when the caller asked.
112
- ...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
113
- // Hold every rewrite to the live writing rules. A trade, not a free win: refused rewrites
114
- // cost provider attempts and a sentence with no clean rewrite keeps its original.
115
- ...(flags["apply-rules"] === true ? { apply_rules: true } : {}),
116
- ...(sync ? {} : { async: true }),
117
- });
122
+ const args = argsFromFlags(ARGS.run_shred, flags, positionals);
123
+ if (args.text === undefined) {
124
+ throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
125
+ }
118
126
  const result = await client.callTool("run_shred", args);
119
127
  if (sync) {
120
128
  ok(ctx, "Shred complete.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",