@letterstory/cli 0.15.5 → 0.17.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 +24 -2
- package/lib/commands/kernel.mjs +150 -5
- package/lib/commands/shredder.mjs +81 -2
- package/package.json +1 -1
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.
|
|
47
|
+
export const VERSION = "0.17.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,26 @@ 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
|
+
WRITE a new article from a headline,
|
|
304
|
+
outline, notes or a finished post.
|
|
305
|
+
Follows the run unless --no-wait.
|
|
306
|
+
kernel polish <endpoint-id> (--body <html>|--file <path>) [--format auto|html]
|
|
307
|
+
POLISH the document you have: prose
|
|
308
|
+
reworded, your own markup kept
|
|
309
|
+
kernel run-status <run-id> Poll a write started with 'kernel write'
|
|
310
|
+
kernel runs [--endpoint <id>] [--limit <n>] Recent endpoint writes (telemetry)
|
|
311
|
+
kernel balance Kernel runs left on this org
|
|
312
|
+
|
|
297
313
|
Phantom orchestrator:
|
|
298
314
|
phantom-job create --topic <text> --collection <uuid> [--kernel <uuid>]
|
|
299
315
|
Topic -> draft -> publish -> rebuild
|
|
@@ -434,7 +450,13 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
434
450
|
standard (default) keeps the piece's own terminology;
|
|
435
451
|
high rewrites more and may reword recurring terms;
|
|
436
452
|
max rewrites hardest — expect vocabulary drift
|
|
437
|
-
|
|
453
|
+
[--no-wait] queue it and print the job id, don't follow it
|
|
454
|
+
[--sync] wait inside one request (only for a few
|
|
455
|
+
sentences — anything longer is cut off)
|
|
456
|
+
Shred prose through an endpoint (logs telemetry).
|
|
457
|
+
Queues the work and follows it; a shred takes minutes
|
|
458
|
+
and keeps running on the server if you stop waiting.
|
|
459
|
+
shredder job <job-id> Poll a queued shred and print it when it lands
|
|
438
460
|
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
|
|
439
461
|
|
|
440
462
|
Landings (a link + context; auto-mentioned in articles by collections you enable):
|
package/lib/commands/kernel.mjs
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
// `kernel` —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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(
|
|
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,122 @@ 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
|
+
});
|
|
121
|
+
const result = await client.callTool("run_kernel", args);
|
|
122
|
+
const runId = readToolField(result, "run_id");
|
|
123
|
+
if (!runId) {
|
|
124
|
+
printResult(io, flags, result);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (flags["no-wait"] === true) {
|
|
128
|
+
ok(ctx, `Write started as ${runId}. Follow it with: letterstory kernel run-status ${runId}`);
|
|
129
|
+
printResult(io, flags, result);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ok(ctx, `Write started as ${runId}. Waiting — this keeps running on the server if you stop.`);
|
|
134
|
+
const deadline = Date.now() + POLL_CEILING_MS;
|
|
135
|
+
for (;;) {
|
|
136
|
+
await sleep(POLL_INTERVAL_MS);
|
|
137
|
+
const polled = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
|
|
138
|
+
const status = readToolField(polled, "status");
|
|
139
|
+
if (status && status !== "running") {
|
|
140
|
+
ok(ctx, `Write ${status}.`);
|
|
141
|
+
printResult(io, flags, polled);
|
|
142
|
+
return status === "done" ? 0 : 1;
|
|
143
|
+
}
|
|
144
|
+
if (Date.now() > deadline) {
|
|
145
|
+
ok(ctx, `Still running after 45 minutes. Follow it with: letterstory kernel run-status ${runId}`);
|
|
146
|
+
printResult(io, flags, polled);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The OTHER door: reword the document you already have, keeping its own markup. */
|
|
153
|
+
async function kernelPolish(ctx) {
|
|
154
|
+
const { client, positionals, flags, io } = ctx;
|
|
155
|
+
const endpointId = requirePositional(positionals, 0, "endpoint-id");
|
|
156
|
+
const document = readBodyInput(flags);
|
|
157
|
+
if (document === undefined) {
|
|
158
|
+
throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
|
|
159
|
+
}
|
|
160
|
+
const args = compact({
|
|
161
|
+
endpoint_id: endpointId,
|
|
162
|
+
document,
|
|
163
|
+
format: flagStr(flags.format),
|
|
164
|
+
});
|
|
165
|
+
// Synchronous by nature: a polish is one pass over the sentences, not a multi-round write.
|
|
166
|
+
const result = await client.callTool("run_kernel", args);
|
|
167
|
+
printResult(io, flags, result);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function kernelRunStatus(ctx) {
|
|
172
|
+
const { client, positionals, flags, io } = ctx;
|
|
173
|
+
const runId = requirePositional(positionals, 0, "run-id");
|
|
174
|
+
const result = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
|
|
175
|
+
printResult(io, flags, result);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function kernelRuns(ctx) {
|
|
180
|
+
const { client, flags, io } = ctx;
|
|
181
|
+
const args = compact({
|
|
182
|
+
endpoint_id: flagStr(flags.endpoint),
|
|
183
|
+
limit: flags.limit === undefined ? undefined : Number(flags.limit),
|
|
184
|
+
});
|
|
185
|
+
const result = await client.callTool("list_kernel_endpoint_runs", args);
|
|
186
|
+
printResult(io, flags, result);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function kernelBalance(ctx) {
|
|
191
|
+
const { client, flags, io } = ctx;
|
|
192
|
+
const result = await client.callTool("get_kernel_run_balance", {});
|
|
193
|
+
printResult(io, flags, result);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
@@ -31,11 +31,16 @@ export async function cmdShredder(ctx) {
|
|
|
31
31
|
case "run":
|
|
32
32
|
case "shred":
|
|
33
33
|
return shredderRun(rest);
|
|
34
|
+
case "job":
|
|
35
|
+
case "poll":
|
|
36
|
+
return shredderJob(rest);
|
|
34
37
|
case "runs":
|
|
35
38
|
case "telemetry":
|
|
36
39
|
return shredderRuns(rest);
|
|
37
40
|
default:
|
|
38
|
-
throw new CliError(
|
|
41
|
+
throw new CliError(
|
|
42
|
+
`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, run, job, runs`
|
|
43
|
+
);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -68,6 +73,18 @@ async function shredderDelete(ctx) {
|
|
|
68
73
|
return 0;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
/**
|
|
77
|
+
* How long to wait between polls of a queued shred, and for how long in total.
|
|
78
|
+
*
|
|
79
|
+
* A shred runs for minutes and starts on the next advance-shreds tick, so the interval is
|
|
80
|
+
* about feeling alive rather than being quick. The ceiling exists so `--wait` on a document
|
|
81
|
+
* that will never finish returns the job id instead of hanging a terminal forever.
|
|
82
|
+
*/
|
|
83
|
+
const POLL_INTERVAL_MS = 15_000;
|
|
84
|
+
const POLL_CEILING_MS = 30 * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
|
|
71
88
|
async function shredderRun(ctx) {
|
|
72
89
|
const { client, positionals, flags, io } = ctx;
|
|
73
90
|
const id = requirePositional(positionals, 0, "endpoint-id");
|
|
@@ -75,6 +92,14 @@ async function shredderRun(ctx) {
|
|
|
75
92
|
if (text === undefined) {
|
|
76
93
|
throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
|
|
77
94
|
}
|
|
95
|
+
// ASYNCHRONOUS BY DEFAULT, and `--sync` is the opt-out rather than the other way round.
|
|
96
|
+
//
|
|
97
|
+
// A shred has no workable synchronous ceiling — the identical article took 203s, 541s and
|
|
98
|
+
// 599s against the live service — and the MCP endpoint behind this is a 300s function. A
|
|
99
|
+
// synchronous run on anything past a few sentences is killed by the platform before it can
|
|
100
|
+
// answer, so the caller gets no text, no error, and no record. Defaulting the other way
|
|
101
|
+
// made the common case the broken one.
|
|
102
|
+
const sync = flags.sync === true;
|
|
78
103
|
const args = compact({
|
|
79
104
|
endpoint_id: id,
|
|
80
105
|
text,
|
|
@@ -82,9 +107,63 @@ async function shredderRun(ctx) {
|
|
|
82
107
|
attempts_per_unit: flagNum(flags.attempts),
|
|
83
108
|
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
84
109
|
...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
|
|
110
|
+
...(sync ? {} : { async: true }),
|
|
85
111
|
});
|
|
86
112
|
const result = await client.callTool("run_shred", args);
|
|
87
|
-
|
|
113
|
+
if (sync) {
|
|
114
|
+
ok(ctx, "Shred complete.");
|
|
115
|
+
printResult(io, flags, result);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const jobId = readJobId(result);
|
|
120
|
+
if (!jobId) {
|
|
121
|
+
// The service answered something we do not recognise. Show it rather than swallow it.
|
|
122
|
+
printResult(io, flags, result);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (flags.wait === false || flags["no-wait"] === true) {
|
|
126
|
+
ok(ctx, `Shred queued as ${jobId}. Poll it with: letterstory shredder job ${jobId}`);
|
|
127
|
+
printResult(io, flags, result);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ok(ctx, `Shred queued as ${jobId}. Waiting — this keeps running on the server if you stop.`);
|
|
132
|
+
const deadline = Date.now() + POLL_CEILING_MS;
|
|
133
|
+
for (;;) {
|
|
134
|
+
await sleep(POLL_INTERVAL_MS);
|
|
135
|
+
const polled = await client.callTool("get_shred_job", { job_id: jobId });
|
|
136
|
+
const status = readStatus(polled);
|
|
137
|
+
if (status && status !== "queued" && status !== "running") {
|
|
138
|
+
ok(ctx, `Shred ${status}.`);
|
|
139
|
+
printResult(io, flags, polled);
|
|
140
|
+
return status === "done" ? 0 : 1;
|
|
141
|
+
}
|
|
142
|
+
if (Date.now() > deadline) {
|
|
143
|
+
ok(ctx, `Still ${status ?? "running"} after 30 minutes. Poll it with: letterstory shredder job ${jobId}`);
|
|
144
|
+
printResult(io, flags, polled);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The tool result is a text payload holding JSON; pull one field out of it, leniently. */
|
|
151
|
+
function readToolField(result, field) {
|
|
152
|
+
const raw = result?.content?.[0]?.text;
|
|
153
|
+
if (typeof raw !== "string") return undefined;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw)?.[field];
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const readJobId = (result) => readToolField(result, "job_id");
|
|
161
|
+
const readStatus = (result) => readToolField(result, "status");
|
|
162
|
+
|
|
163
|
+
async function shredderJob(ctx) {
|
|
164
|
+
const { client, positionals, flags, io } = ctx;
|
|
165
|
+
const jobId = requirePositional(positionals, 0, "job-id");
|
|
166
|
+
const result = await client.callTool("get_shred_job", { job_id: jobId });
|
|
88
167
|
printResult(io, flags, result);
|
|
89
168
|
return 0;
|
|
90
169
|
}
|