@letterstory/cli 0.39.1 → 0.40.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/README.md CHANGED
@@ -172,6 +172,9 @@ letterstory collection run-status <run-id> # status, steps,
172
172
  letterstory collection runs --collection <id> --status ready
173
173
  letterstory collection approve <run-id> --now
174
174
  letterstory collection cancel <run-id>
175
+ letterstory collection inventory <id> # every piece; each finished draft's publish-on-its-own time
176
+ letterstory collection links <id> # internal-link refresh: on/off, posts due
177
+ letterstory collection links <id> --refresh # run the next batch now
175
178
  letterstory collection delete <id> --yes
176
179
 
177
180
  # the legacy verbs still work under `collections`
@@ -245,6 +248,23 @@ letterstory onboarding status
245
248
  letterstory onboarding step --complete connect_domain
246
249
  ```
247
250
 
251
+ ### What needs you — the collection's "needs you" strip, from the terminal
252
+
253
+ Everything in the strip either applies itself after its countdown or waits for your
254
+ decision; the same reads and decisions are here.
255
+
256
+ ```bash
257
+ letterstory suggestions list <collection-id> # strategy cards + when each applies itself
258
+ letterstory suggestions dismiss proposal <id> # reject one inside its window
259
+ letterstory collection inventory <collection-id> # finished drafts + when each publishes on its own
260
+ letterstory collection links <collection-id> # internal links due for a refresh
261
+
262
+ # proposed edits to live posts: a mention an AI citation earned, or fresher internal links
263
+ letterstory revisions list --collection <collection-id>
264
+ letterstory revisions accept <revision-id> # every change; live where we publish
265
+ letterstory revisions reject <revision-id> --change <change-id> --note "not this one"
266
+ ```
267
+
248
268
  ### Offerings — one org, many products
249
269
 
250
270
  Context is layered **org → offering → collection**. An offering is anything the org would
package/lib/cli.mjs CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  cmdDirectives,
28
28
  cmdProbes,
29
29
  cmdSuggestions,
30
+ cmdRevisions,
30
31
  cmdOnboarding,
31
32
  cmdSeries,
32
33
  cmdInsights,
@@ -51,7 +52,7 @@ import {
51
52
  } from "./commands.mjs";
52
53
 
53
54
  // Keep in sync with cli/package.json.
54
- export const VERSION = "0.39.1";
55
+ export const VERSION = "0.40.0";
55
56
 
56
57
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
57
58
  // can't accidentally swallow the id as --json's value.
@@ -208,6 +209,8 @@ Collections (the content engine as a primitive — one door, 'collection run'):
208
209
  collection runs [--collection <id>] [--status <s>] [--limit <n>]
209
210
  collection approve <run-id> [--now] Answer the gate (--now publishes immediately)
210
211
  collection cancel <run-id>
212
+ collection inventory <id> Where every piece is; each finished draft's publish-on-its-own time
213
+ collection links <id> [--refresh] Internal-link refresh: on/off, posts due (--refresh runs the next batch now)
211
214
  collection delete <id> --yes
212
215
 
213
216
  Phantomstory (a collection Letterstory hosts as an independent publication):
@@ -306,6 +309,9 @@ Strategy & onboarding:
306
309
  strategy topics set --collection <uuid> (--topic <topic-id> | --suggestion <suggestion-id>)
307
310
  suggestions list <collection_id> List pending strategy suggestions + their auto-apply countdown
308
311
  suggestions approve | dismiss | snooze <verdict|proposal> <id> [--days <n>]
312
+ revisions list [--collection <uuid>] Proposed edits to live posts waiting for a decision
313
+ revisions accept | reject <revision-id> [--change <change-id>] [--note <text>]
314
+ Decide one change or all; accepting goes live where we publish
309
315
  directives list <collection_id> What is steering this collection right now
310
316
  directives propose <collection_id> <what you want> Read one instruction back as concrete levers (applies nothing)
311
317
  directives apply <directive_id> --effect <id>... Commit the levers you kept (or --reason to decline them all)
@@ -826,6 +832,7 @@ const CLIENT_COMMANDS = {
826
832
  targets: cmdTargets,
827
833
  probes: cmdProbes,
828
834
  suggestions: cmdSuggestions,
835
+ revisions: cmdRevisions,
829
836
  onboarding: cmdOnboarding,
830
837
  series: cmdSeries,
831
838
  insights: cmdInsights,
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { readFileSync } from "node:fs";
12
12
  import { CliError } from "../client.mjs";
13
- import { flagStr, printResult, ok, toolField, followJob, argsFromFlags } from "./shared.mjs";
13
+ import { flagStr, printResult, ok, toolField, followJob, argsFromFlags, refuseUnknownFlags } from "./shared.mjs";
14
14
 
15
15
  function readJsonFileFlag(flags, flag) {
16
16
  const path = flagStr(flags[flag]);
@@ -114,6 +114,15 @@ export const ARGS = {
114
114
  cancel_collection_run: {
115
115
  run_id: { positional: 0, label: "run-id", required: true },
116
116
  },
117
+ get_collection_inventory: {
118
+ collection_id: { positional: 0, label: "collection-id", required: true },
119
+ },
120
+ get_internal_link_status: {
121
+ collection_id: { positional: 0, label: "collection-id", required: true },
122
+ },
123
+ refresh_internal_links: {
124
+ collection_id: { positional: 0, label: "collection-id", required: true },
125
+ },
117
126
  };
118
127
 
119
128
  export async function cmdCollection(ctx) {
@@ -142,9 +151,13 @@ export async function cmdCollection(ctx) {
142
151
  return collectionApprove(rest);
143
152
  case "cancel":
144
153
  return collectionCancel(rest);
154
+ case "inventory":
155
+ return collectionInventory(rest);
156
+ case "links":
157
+ return collectionLinks(rest);
145
158
  default:
146
159
  throw new CliError(
147
- `Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, new, config, delete, run, run-status, runs, approve, cancel`
160
+ `Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, new, config, delete, run, run-status, runs, approve, cancel, inventory, links`
148
161
  );
149
162
  }
150
163
  }
@@ -277,3 +290,72 @@ async function collectionCancel(ctx) {
277
290
  printResult(ctx.io, ctx.flags, result);
278
291
  return 0;
279
292
  }
293
+
294
+ // `collection inventory <id>` — where every piece is, and each finished draft's
295
+ // publish-on-its-own time (the collection room's Work tab + "needs you" strip).
296
+ function formatInventory(r) {
297
+ const lines = [
298
+ `Planner: ${r.planner?.enabled ? "on" : "off"} (${r.planner?.approvalMode ?? "?"})`,
299
+ `Live ${r.live?.count ?? 0} · written, not live ${r.readyNotLive?.count ?? 0} · being written ${r.beingWritten?.count ?? 0} · imported ${r.imported?.count ?? 0}`,
300
+ ];
301
+ const pending = r.pendingReview?.items ?? [];
302
+ if (pending.length) {
303
+ lines.push(`Finished drafts (${r.pendingReview.count}):`);
304
+ for (const d of pending) {
305
+ const when = d.autoPublishAt ? `publishes on its own ${d.autoPublishAt}` : "waits for a person";
306
+ lines.push(` ${d.articleId} ${d.title} [${when}]`);
307
+ }
308
+ }
309
+ return lines.join("\n");
310
+ }
311
+
312
+ async function collectionInventory(ctx) {
313
+ refuseUnknownFlags(ctx, {});
314
+ const result = await ctx.client.callTool(
315
+ "get_collection_inventory",
316
+ argsFromFlags(ARGS.get_collection_inventory, ctx.flags, ctx.positionals)
317
+ );
318
+ printResult(ctx.io, ctx.flags, result, formatInventory);
319
+ return 0;
320
+ }
321
+
322
+ // `collection links <id>` reads the internal-link refresh; `--refresh` runs the next
323
+ // batch now (republished, or proposed for approval on a manual-approval collection).
324
+ async function collectionLinks(ctx) {
325
+ const { client, flags, positionals, io } = ctx;
326
+ refuseUnknownFlags(ctx, {}, ["refresh"]);
327
+ if (flags.refresh === true) {
328
+ const r = await client.callTool(
329
+ "refresh_internal_links",
330
+ argsFromFlags(ARGS.refresh_internal_links, flags, positionals)
331
+ );
332
+ const parts = [];
333
+ if (r.posts_started) parts.push(`${r.posts_started} refreshing (each republishes when it finishes)`);
334
+ if (r.posts_proposed) parts.push(`${r.posts_proposed} proposed for approval (see: revisions list)`);
335
+ if (r.posts_skipped) parts.push(`${r.posts_skipped} busy with another run`);
336
+ if (!parts.length) {
337
+ parts.push(
338
+ r.skipped?.no_sitemap
339
+ ? "no sitemap to link from"
340
+ : r.next_due_in_days === null
341
+ ? "nothing due: every post already links to everything published since"
342
+ : `nothing due yet (next in ${r.next_due_in_days ?? "?"} days)`
343
+ );
344
+ }
345
+ ok(ctx, `Internal links: ${parts.join("; ")}.`);
346
+ if (flags.json) printResult(io, flags, r);
347
+ return 0;
348
+ }
349
+ const s = await client.callTool(
350
+ "get_internal_link_status",
351
+ argsFromFlags(ARGS.get_internal_link_status, flags, positionals)
352
+ );
353
+ printResult(io, flags, s, (r) =>
354
+ [
355
+ `Internal links: ${r.enabled ? "on" : "off"}`,
356
+ `Due now: ${r.due}`,
357
+ `Next due: ${r.next_due_in_days === null ? "only when something new publishes" : `in ${r.next_due_in_days} days`}`,
358
+ ].join("\n")
359
+ );
360
+ return 0;
361
+ }
@@ -0,0 +1,81 @@
1
+ // `revisions` — proposed edits to already-live posts, waiting for a decision: the
2
+ // Approvals queue in a collection's "needs you" strip. A mention an AI citation
3
+ // earned (review-posture collection), or internal links the nightly refresh would
4
+ // add (manual-approval or client-CMS collection). Thin wrappers over the MCP tools
5
+ // list_revisions / decide_revision, which call the same lib/revisions functions the
6
+ // web queue does — accepting on a page we publish puts it live.
7
+
8
+ import { CliError } from "../client.mjs";
9
+ import { printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
10
+
11
+ const DECISIONS = ["accepted", "rejected"];
12
+
13
+ // How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
14
+ // `scripts/parity-check.ts` compares these keys against each tool's published input schema.
15
+ export const ARGS = {
16
+ list_revisions: { collection_id: "collection" },
17
+ decide_revision: {
18
+ revision_id: { positional: 0, label: "revision-id", required: true },
19
+ // `accept` / `reject` set this for you.
20
+ decision: { flag: "decision", enum: DECISIONS },
21
+ change_id: "change",
22
+ note: "note",
23
+ },
24
+ };
25
+
26
+ const USAGE = `Usage: revisions <list|accept|reject>
27
+ revisions list [--collection <uuid>]
28
+ revisions accept <revision-id> [--change <change-id>]
29
+ revisions reject <revision-id> [--change <change-id>] [--note <text>]`;
30
+
31
+ export async function cmdRevisions(ctx) {
32
+ const sub = ctx.positionals[0];
33
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
34
+ switch (sub) {
35
+ case "list":
36
+ case "ls":
37
+ return revisionsList(rest);
38
+ case "accept":
39
+ return revisionsDecide(rest, "accepted");
40
+ case "reject":
41
+ return revisionsDecide(rest, "rejected");
42
+ default:
43
+ throw new CliError(USAGE);
44
+ }
45
+ }
46
+
47
+ function formatRevision(r) {
48
+ const where = r.publish_control === "platform" ? "goes live on accept" : "apply it in your CMS after accepting";
49
+ const lines = [
50
+ `${r.id} ${r.article_title ?? "Untitled"} (${r.source}; ${where})${r.is_stale ? " [STALE — needs recomputing]" : ""}`,
51
+ ];
52
+ if (r.reason) lines.push(` ${r.reason}`);
53
+ for (const c of r.changes ?? []) {
54
+ const state = c.decision ?? "undecided";
55
+ lines.push(` - ${c.id} [${state}] ${c.before ? `${c.before} → ` : ""}${c.after}`);
56
+ }
57
+ return lines.join("\n");
58
+ }
59
+
60
+ async function revisionsList(ctx) {
61
+ const { client, flags, io } = ctx;
62
+ refuseUnknownFlags(ctx, ARGS.list_revisions);
63
+ const result = await client.callTool("list_revisions", argsFromFlags(ARGS.list_revisions, flags));
64
+ printResult(io, flags, result, (r) => {
65
+ const items = r.revisions ?? [];
66
+ return items.length ? items.map(formatRevision).join("\n\n") : "No proposed edits waiting.";
67
+ });
68
+ return 0;
69
+ }
70
+
71
+ async function revisionsDecide(ctx, decision) {
72
+ const { client, flags, positionals, io } = ctx;
73
+ refuseUnknownFlags(ctx, ARGS.decide_revision);
74
+ const result = await client.callTool(
75
+ "decide_revision",
76
+ argsFromFlags(ARGS.decide_revision, { ...flags, decision }, positionals)
77
+ );
78
+ ok(ctx, result.applied ?? `Revision is ${result.status}.`);
79
+ if (flags.json) printResult(io, flags, result);
80
+ return 0;
81
+ }
package/lib/commands.mjs CHANGED
@@ -18,6 +18,7 @@ export * from "./commands/connectors.mjs";
18
18
  export * from "./commands/strategy.mjs";
19
19
  export * from "./commands/directives.mjs";
20
20
  export * from "./commands/probes.mjs";
21
+ export * from "./commands/revisions.mjs";
21
22
  export * from "./commands/series.mjs";
22
23
  export * from "./commands/insights.mjs";
23
24
  export * from "./commands/covers.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",