@letterstory/cli 0.39.1 → 0.41.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 +46 -0
- package/lib/cli.mjs +18 -1
- package/lib/commands/admin.mjs +106 -1
- package/lib/commands/collection.mjs +84 -2
- package/lib/commands/revisions.mjs +81 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
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
|
|
@@ -347,6 +367,32 @@ The ledger behind Kernel Tuning's Experiments tab
|
|
|
347
367
|
(docs/architecture/kernel-experiment-agent.md): every question asked of the kernels,
|
|
348
368
|
how it was tested, and what we learned and why.
|
|
349
369
|
|
|
370
|
+
### Staff: the CRM pipeline
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Needs a STAFF key with crm:read (reads) and crm:write (writes). The same routes the
|
|
374
|
+
# crm.letter.dev front end calls with a staff JWT; any /api/superadmin/crm/* route takes
|
|
375
|
+
# the key as x-staff-api-key (GET → crm:read, anything else → crm:write).
|
|
376
|
+
export LETTERSTORY_STAFF_API_KEY=<your staff key with crm:read,crm:write>
|
|
377
|
+
|
|
378
|
+
letterstory admin crm signals --status new # the Inbound feed (unmatched signals)
|
|
379
|
+
letterstory admin crm record --source bookface --kind message \
|
|
380
|
+
--title "Asked about phantoms on Bookface" --name "Jane Doe" --company Acme --domain acme.com
|
|
381
|
+
letterstory admin crm record --source twitter --kind social_engagement \
|
|
382
|
+
--title "Liked our launch tweet" --name "Pat" --linkedin https://linkedin.com/in/pat
|
|
383
|
+
letterstory admin crm signal <signal-id> qualify # link/create the account
|
|
384
|
+
letterstory admin crm signal <signal-id> dismiss # the easy delete (kept, marked)
|
|
385
|
+
letterstory admin crm accounts --stage demo_scheduled # with likelihood
|
|
386
|
+
letterstory admin crm score <account-id> # recompute; prints the factors
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Every trigger — Cal.com bookings, Circleback recordings, Letterhome dinner RSVPs, RB2B
|
|
390
|
+
visits, and what an agent records here — goes through one intake (`ingestSignal`,
|
|
391
|
+
src/lib/crm/signals-intake.ts), so an agent's signal dedupes against a webhook's exactly
|
|
392
|
+
as two webhooks do. Qualified kinds (booking, call recording, dinner, message, form) link
|
|
393
|
+
or create the account; unqualified ones (a visit, a like) only attach to an existing
|
|
394
|
+
account and otherwise wait in the Inbound feed.
|
|
395
|
+
|
|
350
396
|
### Propose, then onboard a phantom
|
|
351
397
|
|
|
352
398
|
The Fleet wizard's Plan and Name screens as two commands: pick the beat, then the name,
|
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.
|
|
55
|
+
export const VERSION = "0.41.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)
|
|
@@ -550,6 +556,16 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
550
556
|
session (a trial per arm), linked to its
|
|
551
557
|
ledger entry. Written by \`npm run lab --
|
|
552
558
|
publish\` in the kernels repo.
|
|
559
|
+
admin crm signals [--status new] [--intent qualified] [--source rb2b] [--account <id>]
|
|
560
|
+
| record --title "…" [--source bookface] [--kind other] [--intent qualified]
|
|
561
|
+
[--name …] [--email …] [--company …] [--domain …] [--linkedin …] [--detail …]
|
|
562
|
+
| signal <id> qualify|dismiss|link [--account <id>]
|
|
563
|
+
| accounts [--stage lead] [--q acme] | account <id> | score <account-id>
|
|
564
|
+
The staff CRM's pipeline: the inbound signal
|
|
565
|
+
feed, recording a signal with no webhook
|
|
566
|
+
(Bookface, a tweet like), qualifying or
|
|
567
|
+
dismissing one, and the explained score.
|
|
568
|
+
Needs crm:read (reads) / crm:write (writes).
|
|
553
569
|
admin tuning-context [--q "<question>"] The experiment agent's context pack (JSON):
|
|
554
570
|
the pipeline as deployed, levers, measures,
|
|
555
571
|
goals, related history.
|
|
@@ -826,6 +842,7 @@ const CLIENT_COMMANDS = {
|
|
|
826
842
|
targets: cmdTargets,
|
|
827
843
|
probes: cmdProbes,
|
|
828
844
|
suggestions: cmdSuggestions,
|
|
845
|
+
revisions: cmdRevisions,
|
|
829
846
|
onboarding: cmdOnboarding,
|
|
830
847
|
series: cmdSeries,
|
|
831
848
|
insights: cmdInsights,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
|
|
8
8
|
// admin tuning-context [--q "<question>"] (tuning:experiment)
|
|
9
9
|
// admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
|
|
10
|
+
// admin crm signals|record|signal|accounts|account|score … (crm:read / crm:write)
|
|
10
11
|
// admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
|
|
11
12
|
// [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
|
|
12
13
|
// [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
|
|
@@ -410,14 +411,118 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
410
411
|
);
|
|
411
412
|
}
|
|
412
413
|
|
|
414
|
+
/* ------------------------------------------------------------ the staff CRM */
|
|
415
|
+
|
|
416
|
+
const CRM = "/api/superadmin/crm";
|
|
417
|
+
|
|
418
|
+
function signalLine(s) {
|
|
419
|
+
const who = [s.personName, s.companyName ?? s.companyDomain].filter(Boolean).join(" · ");
|
|
420
|
+
return `${s.id} [${s.status} · ${s.intent}] ${s.occurredAt.slice(0, 10)} ${s.source}/${s.kind} ${s.title}${who ? `\n ${who}` : ""}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function accountLine(a) {
|
|
424
|
+
const score = a.likelihood == null ? "—" : a.likelihood.toFixed(2);
|
|
425
|
+
return `${a.id} [${a.stage}] ${score} ${a.name}${a.domain ? ` (${a.domain})` : ""}${a.isYc ? " · YC" : ""}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `admin crm …` — the headless half of the staff CRM, so a background agent can watch the
|
|
430
|
+
* pipeline and record the signals that have no webhook (Bookface, tweet likes, DMs).
|
|
431
|
+
* Reads need `crm:read` on the staff key, writes `crm:write`. Every write goes through the
|
|
432
|
+
* same ingestSignal / actOnSignal the webhooks and the app use.
|
|
433
|
+
*/
|
|
434
|
+
async function cmdAdminCrm(ctx) {
|
|
435
|
+
const [sub, id, action] = ctx.positionals;
|
|
436
|
+
const { flags, io } = ctx;
|
|
437
|
+
const json = flagBool(flags.json);
|
|
438
|
+
const out = (result, line) => (json ? printResult(io, flags, result) : io.log(line(result)));
|
|
439
|
+
switch (sub) {
|
|
440
|
+
case "signals": {
|
|
441
|
+
const params = new URLSearchParams(
|
|
442
|
+
compact({
|
|
443
|
+
status: flagStr(flags.status),
|
|
444
|
+
intent: flagStr(flags.intent),
|
|
445
|
+
source: flagStr(flags.source),
|
|
446
|
+
account_id: flagStr(flags.account),
|
|
447
|
+
limit: flagStr(flags.limit),
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/signals?${params}`), (r) =>
|
|
451
|
+
r.signals.length ? r.signals.map(signalLine).join("\n") : "(no signals)"
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
case "record": {
|
|
455
|
+
const body = compact({
|
|
456
|
+
source: flagStr(flags.source) ?? "agent",
|
|
457
|
+
kind: flagStr(flags.kind) ?? "other",
|
|
458
|
+
intent: flagStr(flags.intent),
|
|
459
|
+
title: flagStr(flags.title),
|
|
460
|
+
person_name: flagStr(flags.name),
|
|
461
|
+
person_email: flagStr(flags.email),
|
|
462
|
+
person_title: flagStr(flags["person-title"]),
|
|
463
|
+
linkedin_url: flagStr(flags.linkedin),
|
|
464
|
+
company_name: flagStr(flags.company),
|
|
465
|
+
company_domain: flagStr(flags.domain),
|
|
466
|
+
detail: flagStr(flags.detail),
|
|
467
|
+
url: flagStr(flags.link),
|
|
468
|
+
external_id: flagStr(flags["external-id"]),
|
|
469
|
+
occurred_at: flagStr(flags.at),
|
|
470
|
+
account_id: flagStr(flags.account),
|
|
471
|
+
});
|
|
472
|
+
if (!body.title)
|
|
473
|
+
throw new CliError(
|
|
474
|
+
'Usage: admin crm record --title "Liked our launch tweet" [--source twitter] [--kind social_engagement] …'
|
|
475
|
+
);
|
|
476
|
+
const r = await staffPost(ctx, `${CRM}/signals`, body);
|
|
477
|
+
return out(r, (x) => `${x.duplicate ? "already recorded" : "recorded"}: ${signalLine(x.signal)}`);
|
|
478
|
+
}
|
|
479
|
+
case "signal": {
|
|
480
|
+
if (!id || !["qualify", "dismiss", "link"].includes(action)) {
|
|
481
|
+
throw new CliError("Usage: admin crm signal <id> qualify|dismiss|link [--account <account-id>]");
|
|
482
|
+
}
|
|
483
|
+
const r = await staffRequest(
|
|
484
|
+
ctx,
|
|
485
|
+
"PATCH",
|
|
486
|
+
`${CRM}/signals/${id}`,
|
|
487
|
+
compact({ action, account_id: flagStr(flags.account) })
|
|
488
|
+
);
|
|
489
|
+
return out(r, (x) => signalLine(x.signal));
|
|
490
|
+
}
|
|
491
|
+
case "accounts": {
|
|
492
|
+
const params = new URLSearchParams(compact({ stage: flagStr(flags.stage), q: flagStr(flags.q) }));
|
|
493
|
+
return out(await staffRequest(ctx, "GET", `${CRM}/accounts?${params}`), (r) =>
|
|
494
|
+
r.accounts.length ? r.accounts.map(accountLine).join("\n") : "(no accounts)"
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
case "account":
|
|
498
|
+
if (!id) throw new CliError("Usage: admin crm account <id>");
|
|
499
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${CRM}/accounts/${id}`));
|
|
500
|
+
case "score": {
|
|
501
|
+
if (!id) throw new CliError("Usage: admin crm score <account-id>");
|
|
502
|
+
const r = await staffPost(ctx, `${CRM}/accounts/${id}/score`, {});
|
|
503
|
+
return out(r, (x) =>
|
|
504
|
+
[
|
|
505
|
+
accountLine(x.account),
|
|
506
|
+
...(x.account.scoreFactors ?? []).map(
|
|
507
|
+
(f) => ` ${f.direction === "risk" ? "−" : "+"}${f.weight.toFixed(2)} ${f.label}`
|
|
508
|
+
),
|
|
509
|
+
].join("\n")
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
default:
|
|
513
|
+
throw new CliError("Usage: admin crm signals|record|signal|accounts|account|score … (see --help)");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
413
517
|
export async function cmdAdmin(ctx) {
|
|
414
518
|
const sub = ctx.positionals[0];
|
|
519
|
+
if (sub === "crm") return cmdAdminCrm({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
415
520
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
416
521
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
417
522
|
if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
418
523
|
if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
419
524
|
if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
420
525
|
throw new CliError(
|
|
421
|
-
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin tuning-context (see --help)'
|
|
526
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin tuning-context (see --help)'
|
|
422
527
|
);
|
|
423
528
|
}
|
|
@@ -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";
|