@letterstory/cli 0.15.4 → 0.16.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 +21 -2
- package/lib/commands/reviewers.mjs +62 -0
- package/lib/commands/shredder.mjs +83 -2
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
cmdDomains,
|
|
20
20
|
cmdPosts,
|
|
21
21
|
cmdPublished,
|
|
22
|
+
cmdReviewers,
|
|
22
23
|
cmdCollections,
|
|
23
24
|
cmdAuthors,
|
|
24
25
|
cmdFlows,
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
} from "./commands.mjs";
|
|
44
45
|
|
|
45
46
|
// Keep in sync with cli/package.json.
|
|
46
|
-
export const VERSION = "0.
|
|
47
|
+
export const VERSION = "0.16.0";
|
|
47
48
|
|
|
48
49
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
49
50
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -183,6 +184,13 @@ Content:
|
|
|
183
184
|
published list [--limit <n>] [--collection <uuid>] [--format html|md]
|
|
184
185
|
published show (--id <uuid> | --slug <slug>) [--format html|md]
|
|
185
186
|
|
|
187
|
+
Reviewers:
|
|
188
|
+
reviewers standing list List the org's standing reviewers
|
|
189
|
+
reviewers standing add --email <e> [--name <n>] Auto-invite this contact as a
|
|
190
|
+
guest reviewer on every future
|
|
191
|
+
finished draft (no email sent now)
|
|
192
|
+
reviewers standing remove <reviewer-id> Stop future auto-invites
|
|
193
|
+
|
|
186
194
|
Collections:
|
|
187
195
|
collections list List your collections
|
|
188
196
|
collections new --name <n> [--description <text>]
|
|
@@ -422,7 +430,17 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
422
430
|
shredder create --name <n> New endpoint (returns a callable public id)
|
|
423
431
|
shredder delete <endpoint-id> --yes
|
|
424
432
|
shredder run <endpoint-id> (--body <text>|--file <path>|--file -) [--coverage <0..1>] [--attempts <n>]
|
|
425
|
-
|
|
433
|
+
[--aggression standard|high|max]
|
|
434
|
+
standard (default) keeps the piece's own terminology;
|
|
435
|
+
high rewrites more and may reword recurring terms;
|
|
436
|
+
max rewrites hardest — expect vocabulary drift
|
|
437
|
+
[--no-wait] queue it and print the job id, don't follow it
|
|
438
|
+
[--sync] wait inside one request (only for a few
|
|
439
|
+
sentences — anything longer is cut off)
|
|
440
|
+
Shred prose through an endpoint (logs telemetry).
|
|
441
|
+
Queues the work and follows it; a shred takes minutes
|
|
442
|
+
and keeps running on the server if you stop waiting.
|
|
443
|
+
shredder job <job-id> Poll a queued shred and print it when it lands
|
|
426
444
|
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
|
|
427
445
|
|
|
428
446
|
Landings (a link + context; auto-mentioned in articles by collections you enable):
|
|
@@ -482,6 +500,7 @@ const CLIENT_COMMANDS = {
|
|
|
482
500
|
domains: cmdDomains,
|
|
483
501
|
posts: cmdPosts,
|
|
484
502
|
published: cmdPublished,
|
|
503
|
+
reviewers: cmdReviewers,
|
|
485
504
|
collections: cmdCollections,
|
|
486
505
|
authors: cmdAuthors,
|
|
487
506
|
flows: cmdFlows,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `reviewers standing` — an org's standing reviewers (organization_standing_reviewers):
|
|
2
|
+
// external contacts auto-invited as a guest reviewer on every article that reaches a
|
|
3
|
+
// finished, never-published draft (src/lib/services/standing-reviewers.ts + the
|
|
4
|
+
// notify-standing-reviewers cron), instead of inviting them by hand on each article.
|
|
5
|
+
// Same flag/positional/io conventions as posts.mjs — no commander, no color.
|
|
6
|
+
|
|
7
|
+
import { CliError } from "../client.mjs";
|
|
8
|
+
import { flagStr, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
|
|
9
|
+
|
|
10
|
+
export async function cmdReviewers(ctx) {
|
|
11
|
+
const sub = ctx.positionals[0];
|
|
12
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
13
|
+
switch (sub) {
|
|
14
|
+
case "standing":
|
|
15
|
+
return cmdStandingReviewers(rest);
|
|
16
|
+
default:
|
|
17
|
+
throw new CliError(`Unknown reviewers subcommand: ${sub ?? "(none)"}. Try: standing`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function cmdStandingReviewers(ctx) {
|
|
22
|
+
const sub = ctx.positionals[0];
|
|
23
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
24
|
+
switch (sub) {
|
|
25
|
+
case "list":
|
|
26
|
+
case "ls":
|
|
27
|
+
return standingList(rest);
|
|
28
|
+
case "add":
|
|
29
|
+
return standingAdd(rest);
|
|
30
|
+
case "remove":
|
|
31
|
+
case "rm":
|
|
32
|
+
return standingRemove(rest);
|
|
33
|
+
default:
|
|
34
|
+
throw new CliError(`Unknown reviewers standing subcommand: ${sub ?? "(none)"}. Try: list, add, remove`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function standingList(ctx) {
|
|
39
|
+
const { client, flags, io } = ctx;
|
|
40
|
+
const result = await client.callTool("list_standing_reviewers", {});
|
|
41
|
+
printResult(io, flags, result);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function standingAdd(ctx) {
|
|
46
|
+
const { client, flags, io } = ctx;
|
|
47
|
+
const email = requireFlag(flags, "email");
|
|
48
|
+
const args = compact({ email, name: flagStr(flags.name) });
|
|
49
|
+
const result = await client.callTool("add_standing_reviewer", args);
|
|
50
|
+
ok(ctx, `Added ${email} as a standing reviewer.`);
|
|
51
|
+
printResult(io, flags, result);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function standingRemove(ctx) {
|
|
56
|
+
const { client, positionals, flags, io } = ctx;
|
|
57
|
+
const id = requirePositional(positionals, 0, "reviewer-id");
|
|
58
|
+
const result = await client.callTool("remove_standing_reviewer", { reviewer_id: id });
|
|
59
|
+
ok(ctx, "Removed.");
|
|
60
|
+
printResult(io, flags, result);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
@@ -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,14 +92,78 @@ 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,
|
|
81
106
|
coverage: flagNum(flags.coverage),
|
|
82
107
|
attempts_per_unit: flagNum(flags.attempts),
|
|
108
|
+
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
109
|
+
...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
|
|
110
|
+
...(sync ? {} : { async: true }),
|
|
83
111
|
});
|
|
84
112
|
const result = await client.callTool("run_shred", args);
|
|
85
|
-
|
|
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 });
|
|
86
167
|
printResult(io, flags, result);
|
|
87
168
|
return 0;
|
|
88
169
|
}
|
package/lib/commands.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./commands/discovery.mjs";
|
|
|
9
9
|
export * from "./commands/deploy.mjs";
|
|
10
10
|
export * from "./commands/mcp.mjs";
|
|
11
11
|
export * from "./commands/posts.mjs";
|
|
12
|
+
export * from "./commands/reviewers.mjs";
|
|
12
13
|
export * from "./commands/collections.mjs";
|
|
13
14
|
export * from "./commands/authors.mjs";
|
|
14
15
|
export * from "./commands/flows.mjs";
|