@letterstory/cli 0.16.0 → 0.18.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.16.0";
47
+ export const VERSION = "0.18.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.
@@ -290,10 +290,29 @@ Cover images (which image model draws the post's cover):
290
290
 
291
291
  Writing kernels (kernels write the draft; they do not research — run research first):
292
292
  kernel list List available writing kernels
293
+
294
+ Into an article you already have (metered on the AI-usage gate):
293
295
  kernel run --article <uuid> --kernel <uuid> [--brief <text>|--brief-file <path>]
294
296
  Submit a run; takes 15-20 min
295
297
  kernel status <kernel-job-id> Check a run's status
296
298
 
299
+ Standalone — prose back, nothing saved into an article (metered in kernel-run credits):
300
+ kernel endpoints Your kernel endpoints
301
+ kernel write <endpoint-id> (--body <text>|--file <path>|--file -)
302
+ [--kernel <uuid>] [--words <n>] [--no-wait]
303
+ [--shredder <uuid>] shred every round in the loop, so the
304
+ kernel judges the text that ships
305
+ [--read-as brief|blog|outline|research|notes]
306
+ WRITE a new article from a headline,
307
+ outline, notes or a finished post.
308
+ Follows the run unless --no-wait.
309
+ kernel polish <endpoint-id> (--body <html>|--file <path>) [--format auto|html]
310
+ POLISH the document you have: prose
311
+ reworded, your own markup kept
312
+ kernel run-status <run-id> Poll a write started with 'kernel write'
313
+ kernel runs [--endpoint <id>] [--limit <n>] Recent endpoint writes (telemetry)
314
+ kernel balance Kernel runs left on this org
315
+
297
316
  Phantom orchestrator:
298
317
  phantom-job create --topic <text> --collection <uuid> [--kernel <uuid>]
299
318
  Topic -> draft -> publish -> rebuild
@@ -1,7 +1,19 @@
1
- // `kernel` — list writing kernels, submit a run against an article, and check a
2
- // run's status. A run can take 15-20 minutes, so unlike `deploy create` this does
3
- // not block waiting for it: `run` returns as soon as the job is queued and `status`
4
- // checks on demand, same shape as `flows run` / `flows status`.
1
+ // `kernel` — the writing kernels, on both of their doors.
2
+ //
3
+ // TWO PRODUCTS SHARE THIS NOUN, and the subcommands are split along that line because
4
+ // confusing them wastes fifteen minutes and a credit:
5
+ //
6
+ // kernel run/status writes INTO an article you already have (submit_kernel_run).
7
+ // Metered on the AI-usage gate; needs --article.
8
+ // kernel write/… the STANDALONE endpoint (run_kernel) — you get prose back and
9
+ // nothing is saved into an article. Metered in kernel-run
10
+ // credits, the balance the Kernels page shows.
11
+ //
12
+ // The standalone door had MCP tools and no CLI at all, so the only way to reach the product
13
+ // the Kernels page bills for was `letterstory mcp call run_kernel` with hand-written JSON.
14
+ //
15
+ // Neither door blocks: a write runs for minutes, so `write` submits and then follows the run
16
+ // unless you tell it not to.
5
17
 
6
18
  import { CliError } from "../client.mjs";
7
19
  import { flagStr, requireFlag, requirePositional, printResult, compact, ok, readBodyInput } from "./shared.mjs";
@@ -17,8 +29,22 @@ export async function cmdKernel(ctx) {
17
29
  return kernelRun(rest);
18
30
  case "status":
19
31
  return kernelStatus(rest);
32
+ case "endpoints":
33
+ return kernelEndpoints(rest);
34
+ case "write":
35
+ return kernelWrite(rest);
36
+ case "polish":
37
+ return kernelPolish(rest);
38
+ case "run-status":
39
+ return kernelRunStatus(rest);
40
+ case "runs":
41
+ return kernelRuns(rest);
42
+ case "balance":
43
+ return kernelBalance(rest);
20
44
  default:
21
- throw new CliError(`Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, run, status`);
45
+ throw new CliError(
46
+ `Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, endpoints, write, polish, run-status, runs, balance, run, status`
47
+ );
22
48
  }
23
49
  }
24
50
 
@@ -48,3 +74,127 @@ async function kernelStatus(ctx) {
48
74
  printResult(io, flags, result);
49
75
  return 0;
50
76
  }
77
+
78
+ /* ------------------------------------------------------------------ */
79
+ /* The standalone endpoint: prose back, nothing written into an article */
80
+ /* ------------------------------------------------------------------ */
81
+
82
+ /** A kernel write runs for minutes (median ~8), so this is about feeling alive. */
83
+ const POLL_INTERVAL_MS = 20_000;
84
+ /** Stop following after this; the run keeps going and `run-status` picks it up. */
85
+ const POLL_CEILING_MS = 45 * 60 * 1000;
86
+
87
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
88
+
89
+ /** The tool result is a text payload holding JSON; pull one field out, leniently. */
90
+ function readToolField(result, field) {
91
+ const raw = result?.content?.[0]?.text;
92
+ if (typeof raw !== "string") return undefined;
93
+ try {
94
+ return JSON.parse(raw)?.[field];
95
+ } catch {
96
+ return undefined;
97
+ }
98
+ }
99
+
100
+ async function kernelEndpoints(ctx) {
101
+ const { client, flags, io } = ctx;
102
+ const result = await client.callTool("list_kernel_endpoints", {});
103
+ printResult(io, flags, result);
104
+ return 0;
105
+ }
106
+
107
+ async function kernelWrite(ctx) {
108
+ const { client, positionals, flags, io } = ctx;
109
+ const endpointId = requirePositional(positionals, 0, "endpoint-id");
110
+ const brief = readBodyInput(flags);
111
+ if (brief === undefined) {
112
+ throw new CliError("Provide the brief with --body <text>, --file <path>, or --file - (stdin).");
113
+ }
114
+ const args = compact({
115
+ endpoint_id: endpointId,
116
+ brief,
117
+ kernel_id: flagStr(flags.kernel),
118
+ target_word_count: flags.words === undefined ? undefined : Number(flags.words),
119
+ content_type: flagStr(flags["content-type"]),
120
+ // The two products, in one run: every round is shredded before the kernel's jury sees
121
+ // it, so the verdict lands on the text that actually ships. Costs a shred as well as a
122
+ // run. Reachable from every other surface and, until now, not from here.
123
+ shredder_endpoint_id: flagStr(flags.shredder),
124
+ brief_mode: flagStr(flags["read-as"]),
125
+ });
126
+ const result = await client.callTool("run_kernel", args);
127
+ const runId = readToolField(result, "run_id");
128
+ if (!runId) {
129
+ printResult(io, flags, result);
130
+ return 0;
131
+ }
132
+ if (flags["no-wait"] === true) {
133
+ ok(ctx, `Write started as ${runId}. Follow it with: letterstory kernel run-status ${runId}`);
134
+ printResult(io, flags, result);
135
+ return 0;
136
+ }
137
+
138
+ ok(ctx, `Write started as ${runId}. Waiting — this keeps running on the server if you stop.`);
139
+ const deadline = Date.now() + POLL_CEILING_MS;
140
+ for (;;) {
141
+ await sleep(POLL_INTERVAL_MS);
142
+ const polled = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
143
+ const status = readToolField(polled, "status");
144
+ if (status && status !== "running") {
145
+ ok(ctx, `Write ${status}.`);
146
+ printResult(io, flags, polled);
147
+ return status === "done" ? 0 : 1;
148
+ }
149
+ if (Date.now() > deadline) {
150
+ ok(ctx, `Still running after 45 minutes. Follow it with: letterstory kernel run-status ${runId}`);
151
+ printResult(io, flags, polled);
152
+ return 0;
153
+ }
154
+ }
155
+ }
156
+
157
+ /** The OTHER door: reword the document you already have, keeping its own markup. */
158
+ async function kernelPolish(ctx) {
159
+ const { client, positionals, flags, io } = ctx;
160
+ const endpointId = requirePositional(positionals, 0, "endpoint-id");
161
+ const document = readBodyInput(flags);
162
+ if (document === undefined) {
163
+ throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
164
+ }
165
+ const args = compact({
166
+ endpoint_id: endpointId,
167
+ document,
168
+ format: flagStr(flags.format),
169
+ });
170
+ // Synchronous by nature: a polish is one pass over the sentences, not a multi-round write.
171
+ const result = await client.callTool("run_kernel", args);
172
+ printResult(io, flags, result);
173
+ return 0;
174
+ }
175
+
176
+ async function kernelRunStatus(ctx) {
177
+ const { client, positionals, flags, io } = ctx;
178
+ const runId = requirePositional(positionals, 0, "run-id");
179
+ const result = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
180
+ printResult(io, flags, result);
181
+ return 0;
182
+ }
183
+
184
+ async function kernelRuns(ctx) {
185
+ const { client, flags, io } = ctx;
186
+ const args = compact({
187
+ endpoint_id: flagStr(flags.endpoint),
188
+ limit: flags.limit === undefined ? undefined : Number(flags.limit),
189
+ });
190
+ const result = await client.callTool("list_kernel_endpoint_runs", args);
191
+ printResult(io, flags, result);
192
+ return 0;
193
+ }
194
+
195
+ async function kernelBalance(ctx) {
196
+ const { client, flags, io } = ctx;
197
+ const result = await client.callTool("get_kernel_run_balance", {});
198
+ printResult(io, flags, result);
199
+ return 0;
200
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",