@letterstory/cli 0.25.2 → 0.25.4

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.2";
47
+ export const VERSION = "0.25.4";
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.
@@ -1,44 +1,85 @@
1
1
  // `shredder` — create shredder endpoints and run content produced OUTSIDE Letterstory
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
- // logs every call to telemetry.
4
+ // logs every call to telemetry against the org's shred balance.
5
5
 
6
+ import { readFileSync } from "node:fs";
6
7
  import { CliError } from "../client.mjs";
7
8
  import {
8
9
  flagStr,
9
- flagNum,
10
- requireFlag,
11
10
  requirePositional,
12
11
  readBodyInput,
13
12
  printResult,
14
- compact,
15
13
  ok,
16
14
  toolField,
17
15
  followJob,
18
16
  argsFromFlags,
19
17
  } from "./shared.mjs";
20
18
 
19
+ /** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
20
+ function readJsonFileFlag(flags, flag) {
21
+ const path = flagStr(flags[flag]);
22
+ if (path === undefined) return undefined;
23
+ let text;
24
+ try {
25
+ text = readFileSync(path, "utf8");
26
+ } catch (error) {
27
+ throw new CliError(
28
+ `Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
29
+ );
30
+ }
31
+ try {
32
+ return JSON.parse(text);
33
+ } catch {
34
+ throw new CliError(`--${flag} ${path} is not valid JSON.`);
35
+ }
36
+ }
37
+
21
38
  /**
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.
39
+ * How every command in this file sources its tool's inputs — see `argsFromFlags` in
40
+ * shared.mjs and `scripts/parity-check.ts`, which diffs these keys against each tool's
41
+ * published schema. Every shredder tool is mapped: a field added to one of them and
42
+ * forgotten here fails CI.
25
43
  */
26
44
  export const ARGS = {
27
45
  run_shred: {
28
46
  endpoint_id: { positional: 0, label: "endpoint-id", required: true },
29
47
  text: { value: (flags) => readBodyInput(flags) },
30
- coverage: { flag: "coverage", type: "num" },
31
- attempts_per_unit: { flag: "attempts", type: "num" },
32
48
  // What the text IS. Without this an HTML file could only go through the `auto`
33
49
  // heuristic, which misreads a markdown post carrying an embed.
34
50
  format: { flag: "format", enum: ["auto", "html", "markdown"] },
35
51
  // How hard to rewrite. Validated service-side; sent only when the caller asked.
36
52
  aggression: { flag: "aggression", enum: ["standard", "high", "max"] },
53
+ attempts_per_unit: { flag: "attempts", type: "num" },
37
54
  // Hold every rewrite to the live writing rules. A trade, not a free win.
38
55
  apply_rules: { flag: "apply-rules", type: "bool" },
39
56
  // ASYNCHRONOUS BY DEFAULT — `--sync` is the opt-out (see shredderRun for why).
40
57
  async: { value: (flags) => (flags.sync === true ? undefined : true) },
41
58
  },
59
+ get_shred_job: {
60
+ job_id: { positional: 0, label: "job-id", required: true },
61
+ },
62
+ list_shred_runs: {
63
+ endpoint_id: "endpoint",
64
+ limit: { flag: "limit", type: "num" },
65
+ },
66
+ list_shredder_endpoints: {},
67
+ get_shred_balance: {},
68
+ create_shredder_endpoint: {
69
+ name: { flag: "name", required: true },
70
+ },
71
+ delete_shredder_endpoint: {
72
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
73
+ },
74
+ // The endpoint's configuration as ONE file: `shredder endpoint config <id>` prints it,
75
+ // `shredder endpoint config <id> --file cfg.json` replaces it with the file's document.
76
+ get_shredder_endpoint_config: {
77
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
78
+ },
79
+ set_shredder_endpoint_config: {
80
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
81
+ config: { value: (flags) => readJsonFileFlag(flags, "file") },
82
+ },
42
83
  };
43
84
 
44
85
  export async function cmdShredder(ctx) {
@@ -54,6 +95,8 @@ export async function cmdShredder(ctx) {
54
95
  case "delete":
55
96
  case "rm":
56
97
  return shredderDelete(rest);
98
+ case "endpoint":
99
+ return shredderEndpoint(rest);
57
100
  case "run":
58
101
  case "shred":
59
102
  return shredderRun(rest);
@@ -63,25 +106,27 @@ export async function cmdShredder(ctx) {
63
106
  case "runs":
64
107
  case "telemetry":
65
108
  return shredderRuns(rest);
109
+ case "balance":
110
+ return shredderBalance(rest);
66
111
  default:
67
112
  throw new CliError(
68
- `Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, run, job, runs`
113
+ `Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, job, runs, balance`
69
114
  );
70
115
  }
71
116
  }
72
117
 
73
118
  async function shredderList(ctx) {
74
119
  const { client, flags, io } = ctx;
75
- const result = await client.callTool("list_shredder_endpoints", {});
120
+ const result = await client.callTool("list_shredder_endpoints", argsFromFlags(ARGS.list_shredder_endpoints, flags));
76
121
  printResult(io, flags, result);
77
122
  return 0;
78
123
  }
79
124
 
80
125
  async function shredderCreate(ctx) {
81
- const { client, flags, io } = ctx;
82
- const name = requireFlag(flags, "name");
83
- const result = await client.callTool("create_shredder_endpoint", { name });
84
- ok(ctx, `Created shredder endpoint "${name}".`);
126
+ const { client, flags, positionals, io } = ctx;
127
+ const args = argsFromFlags(ARGS.create_shredder_endpoint, flags, positionals);
128
+ const result = await client.callTool("create_shredder_endpoint", args);
129
+ ok(ctx, `Created shredder endpoint "${args.name}".`);
85
130
  printResult(io, flags, result);
86
131
  return 0;
87
132
  }
@@ -93,12 +138,55 @@ async function shredderDelete(ctx) {
93
138
  io.error(`Re-run with --yes to confirm: ${ctx.bin} shredder delete ${id} --yes`);
94
139
  return 1;
95
140
  }
141
+ // `shredder delete <id>` and `shredder endpoint delete <id>` are the same act; the ARGS
142
+ // map is written for the latter's positional layout.
96
143
  const result = await client.callTool("delete_shredder_endpoint", { endpoint_id: id });
97
144
  ok(ctx, "Shredder endpoint deleted.");
98
145
  printResult(io, flags, result);
99
146
  return 0;
100
147
  }
101
148
 
149
+ /**
150
+ * `shredder endpoint config|delete` — the endpoint's own configuration and lifecycle, the
151
+ * same shape `kernel endpoint …` has, so the two primitives read the same way.
152
+ */
153
+ async function shredderEndpoint(ctx) {
154
+ const { client, positionals, flags, io } = ctx;
155
+ const action = positionals[0];
156
+ if (action === "delete" || action === "rm") {
157
+ const id = requirePositional(positionals, 1, "endpoint-id");
158
+ if (!flags.yes) {
159
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} shredder endpoint delete ${id} --yes`);
160
+ return 1;
161
+ }
162
+ const result = await client.callTool(
163
+ "delete_shredder_endpoint",
164
+ argsFromFlags(ARGS.delete_shredder_endpoint, flags, positionals)
165
+ );
166
+ ok(ctx, "Shredder endpoint deleted.");
167
+ printResult(io, flags, result);
168
+ return 0;
169
+ }
170
+ if (action === "config") {
171
+ const id = requirePositional(positionals, 1, "endpoint-id");
172
+ if (flagStr(flags.file) === undefined) {
173
+ const result = await client.callTool(
174
+ "get_shredder_endpoint_config",
175
+ argsFromFlags(ARGS.get_shredder_endpoint_config, flags, positionals)
176
+ );
177
+ // The document itself, so `> cfg.json` round-trips into `--file cfg.json`.
178
+ printResult(io, flags, result, (r) => JSON.stringify(r?.config ?? r, null, 2));
179
+ return 0;
180
+ }
181
+ const args = argsFromFlags(ARGS.set_shredder_endpoint_config, flags, positionals);
182
+ const result = await client.callTool("set_shredder_endpoint_config", args);
183
+ ok(ctx, `Replaced the configuration of shredder endpoint ${id}.`);
184
+ printResult(io, flags, result);
185
+ return 0;
186
+ }
187
+ throw new CliError(`Unknown shredder endpoint action: ${action ?? "(none)"}. Try: config, delete`);
188
+ }
189
+
102
190
  /**
103
191
  * How long to wait between polls of a queued shred, and for how long in total.
104
192
  *
@@ -175,19 +263,21 @@ const readStatus = (result) => toolField(result, "status");
175
263
 
176
264
  async function shredderJob(ctx) {
177
265
  const { client, positionals, flags, io } = ctx;
178
- const jobId = requirePositional(positionals, 0, "job-id");
179
- const result = await client.callTool("get_shred_job", { job_id: jobId });
266
+ const result = await client.callTool("get_shred_job", argsFromFlags(ARGS.get_shred_job, flags, positionals));
180
267
  printResult(io, flags, result);
181
268
  return 0;
182
269
  }
183
270
 
184
271
  async function shredderRuns(ctx) {
185
272
  const { client, flags, io } = ctx;
186
- const args = compact({
187
- endpoint_id: flagStr(flags.endpoint),
188
- limit: flagNum(flags.limit),
189
- });
190
- const result = await client.callTool("list_shred_runs", args);
273
+ const result = await client.callTool("list_shred_runs", argsFromFlags(ARGS.list_shred_runs, flags));
274
+ printResult(io, flags, result);
275
+ return 0;
276
+ }
277
+
278
+ async function shredderBalance(ctx) {
279
+ const { client, flags, io } = ctx;
280
+ const result = await client.callTool("get_shred_balance", argsFromFlags(ARGS.get_shred_balance, flags));
191
281
  printResult(io, flags, result);
192
282
  return 0;
193
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",