@letterstory/cli 0.82.2 → 0.82.4
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 -3
- package/lib/commands/admin.mjs +173 -1
- package/lib/commands/arena.mjs +3 -0
- package/lib/commands/collections.mjs +32 -3
- package/lib/commands/shared.mjs +14 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
} from "./commands.mjs";
|
|
58
58
|
|
|
59
59
|
// Keep in sync with cli/package.json.
|
|
60
|
-
export const VERSION = "0.82.
|
|
60
|
+
export const VERSION = "0.82.4";
|
|
61
61
|
|
|
62
62
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
63
63
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -333,10 +333,16 @@ Collections:
|
|
|
333
333
|
collections autopen <id> [--template <key>|none] [--kernel-family <name>|org]
|
|
334
334
|
[--mention off|subtle|woven] [--outline-depth barebones|fleshed_out]
|
|
335
335
|
[--direction-gate true|false] [--auto-start true|false]
|
|
336
|
+
[--prefer <site> …|none]
|
|
336
337
|
The Auto-Pen settings the app's panel writes. --template binds a content
|
|
337
338
|
template (from 'templates list') as this collection's default, which is how
|
|
338
339
|
a template you author here actually gets used. A patch: unset flags are
|
|
339
|
-
left alone.
|
|
340
|
+
left alone. --prefer REPLACES the preferred sources: SITES research
|
|
341
|
+
searches for every post's topic and reaches for first when a figure fits
|
|
342
|
+
(a URL is saved as its site) — your own site included; 'none' clears them.
|
|
343
|
+
collections cite-preferred <id> [--limit <n>] [--dry-run]
|
|
344
|
+
Bring published posts up to the preferred sources: each eligible post gets
|
|
345
|
+
at most one preferred figure, linked on the figure, then republishes.
|
|
340
346
|
|
|
341
347
|
Authors (the per-collection author bank — recurring bylines):
|
|
342
348
|
authors list <collection-id> List the bank + byline distribution
|
|
@@ -452,7 +458,7 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
|
|
|
452
458
|
Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
|
|
453
459
|
arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
|
|
454
460
|
[--gate] [--no-verify] [--provider native|exa|youcom|context-dev] [--mention off|subtle|woven]
|
|
455
|
-
[--context-file ctx.json] [--block <name> …] [--block-domain <domain> …]
|
|
461
|
+
[--context-file ctx.json] [--block <name> …] [--block-domain <domain> …] [--prefer <site> …]
|
|
456
462
|
[--arena <arena-id>] [--article <uuid>] [--no-wait]
|
|
457
463
|
Start a run and follow it. --article makes
|
|
458
464
|
it HOSTED (outline written into that post,
|
|
@@ -633,6 +639,21 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
633
639
|
Replay a breakthrough live now (background; poll
|
|
634
640
|
admin replays). With neither, the client's newest
|
|
635
641
|
breakthrough. Needs fleet:replay.
|
|
642
|
+
admin fleet-experiments list Fleet experiments (sites, collections, single posts)
|
|
643
|
+
with each arm's rate per AI, before vs after.
|
|
644
|
+
| articles [--experiment <id>] [--org <uuid>] [--filter all|assigned|unassigned] [--arm <arm>] [--q …]
|
|
645
|
+
Posts on independent sites that name the client,
|
|
646
|
+
with the paragraphs that name it and the live URL.
|
|
647
|
+
| mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…>
|
|
648
|
+
| new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1
|
|
649
|
+
| new --spec-file spec.json A custom experiment ({key, hypothesis, unit, arms, response_var}).
|
|
650
|
+
| assign <experiment-id> <unit-id…> | assign-eligible <experiment-id> | apply <experiment-id>
|
|
651
|
+
| assign-orgs <experiment-id> <org-domain-or-id…>
|
|
652
|
+
Site/collection arms (safe mode: our own sites, or one
|
|
653
|
+
site per named client). A running experiment applies
|
|
654
|
+
each new unit's arm at once; start applies them all.
|
|
655
|
+
| start|stop <experiment-id> | decide <experiment-id> --decision "…"
|
|
656
|
+
Reads need fleet:read; writes fleet:experiment.
|
|
636
657
|
admin experiments list [--q colons] [--status proposed] [--primitive write] [--stage rarity] | show <id>
|
|
637
658
|
| new "<what you are investigating>" [--primitive write|research|shred|enhance] [--stages a,b]
|
|
638
659
|
[--hypothesis …] [--plan-file p.json] [--tags a,b] [--as "<agent>"]
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// (feedback-add --fix "…" files the reader's rewrite; the diff is classified and a disposition suggested.
|
|
13
13
|
// The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
|
|
14
14
|
// admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
|
|
15
|
+
// admin fleet-experiments list|articles|mark|unmark|new|assign|assign-eligible|assign-orgs|apply|start|stop|decide … (fleet:read / fleet:experiment)
|
|
15
16
|
// admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
16
17
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
17
18
|
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
@@ -3346,6 +3347,176 @@ async function cmdAdminSpend(ctx) {
|
|
|
3346
3347
|
}
|
|
3347
3348
|
}
|
|
3348
3349
|
|
|
3350
|
+
const FLEET_EXPERIMENTS = "/api/superadmin/experiments";
|
|
3351
|
+
|
|
3352
|
+
function pct(rate) {
|
|
3353
|
+
return rate && rate.asks > 0 ? `${Math.round(rate.point * 100)}% (${rate.hits}/${rate.asks})` : "no answers yet";
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
function signedPoints(value) {
|
|
3357
|
+
if (value === null || value === undefined) return "not enough yet";
|
|
3358
|
+
const r = Number((value * 100).toFixed(1));
|
|
3359
|
+
return `${r > 0 ? "+" : r < 0 ? "-" : ""}${Math.abs(r).toFixed(1)} pts`;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const READOUT_OUTCOMES = [
|
|
3363
|
+
["cited", "cited when probed"],
|
|
3364
|
+
["namedWhenCited", "client named when cited"],
|
|
3365
|
+
["reads", "read by the AI"],
|
|
3366
|
+
];
|
|
3367
|
+
|
|
3368
|
+
function fleetExperimentLines(e) {
|
|
3369
|
+
const lines = [`${e.id} ${e.key} [${e.status}] per ${e.unit} → ${e.responseVar}`, ` ${e.hypothesis}`];
|
|
3370
|
+
for (const arm of e.armStats ?? []) {
|
|
3371
|
+
const live = arm.live !== undefined ? `, ${arm.live} live` : "";
|
|
3372
|
+
lines.push(` ${arm.arm}: ${arm.units} unit${arm.units === 1 ? "" : "s"}${live} · after ${pct(arm.rate)}`);
|
|
3373
|
+
}
|
|
3374
|
+
const r = e.articleReadout;
|
|
3375
|
+
if (r) {
|
|
3376
|
+
for (const s of r.surfaces) {
|
|
3377
|
+
lines.push(` ${s.label}:`);
|
|
3378
|
+
for (const [key, title] of READOUT_OUTCOMES) {
|
|
3379
|
+
const arms = s.arms.map((a) => `${a.arm} ${pct(a[key].before)} → ${pct(a[key].after)}`).join("; ");
|
|
3380
|
+
const effects = s.effects
|
|
3381
|
+
.map((x) => `${x.arm} vs ${r.referenceArm} ${signedPoints(x[key])}`)
|
|
3382
|
+
.join("; ");
|
|
3383
|
+
lines.push(` ${title}: ${arms}${effects ? ` | ${effects}` : ""}`);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (e.decision) lines.push(` decision: ${e.decision}`);
|
|
3388
|
+
return lines.join("\n");
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
function mentionedArticleLines(a) {
|
|
3392
|
+
const state = a.assignment
|
|
3393
|
+
? a.assignment.changedSinceApplied
|
|
3394
|
+
? ", republished mid-test"
|
|
3395
|
+
: a.assignment.appliedAt
|
|
3396
|
+
? `, live since ${a.assignment.appliedAt.slice(0, 10)}`
|
|
3397
|
+
: ", starts with the experiment"
|
|
3398
|
+
: "";
|
|
3399
|
+
const arm = a.assignment
|
|
3400
|
+
? ` [${a.assignment.arm}${state}]`
|
|
3401
|
+
: a.ineligible
|
|
3402
|
+
? ` [can't join: ${a.ineligible}]`
|
|
3403
|
+
: "";
|
|
3404
|
+
const lines = [
|
|
3405
|
+
`${a.articleId} ${a.orgName} · ${a.title}${arm}`,
|
|
3406
|
+
` ${a.url ?? "(no live URL)"} · ${a.mentionCount} mention${a.mentionCount === 1 ? "" : "s"} · cited in ${a.citedAnswers} answer${a.citedAnswers === 1 ? "" : "s"}${a.probed ? " · page-probed" : ""}`,
|
|
3407
|
+
];
|
|
3408
|
+
for (const p of a.passages) lines.push(` > ${p.text}`);
|
|
3409
|
+
if (a.passageTotal > a.passages.length) lines.push(` … ${a.passageTotal - a.passages.length} more`);
|
|
3410
|
+
return lines.join("\n");
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
/**
|
|
3414
|
+
* `admin fleet-experiments …` — the fleet experiment registry and the client-mention
|
|
3415
|
+
* worklist: the headless half of /superadmin/experiments. Reads need `fleet:read` on the
|
|
3416
|
+
* staff key, writes `fleet:experiment`. (`admin experiments` is the kernel tuning ledger.)
|
|
3417
|
+
*/
|
|
3418
|
+
async function cmdAdminFleetExperiments(ctx) {
|
|
3419
|
+
const [sub, id, ...rest] = ctx.positionals;
|
|
3420
|
+
const { flags, io } = ctx;
|
|
3421
|
+
const json = flagBool(flags.json);
|
|
3422
|
+
const out = (result, line) => (json ? printResult(io, flags, result) : io.log(line(result)));
|
|
3423
|
+
const action = (body) => staffRequest(ctx, "POST", FLEET_EXPERIMENTS, body);
|
|
3424
|
+
switch (sub) {
|
|
3425
|
+
case "list":
|
|
3426
|
+
return out(await staffRequest(ctx, "GET", FLEET_EXPERIMENTS), (r) =>
|
|
3427
|
+
r.experiments.length ? r.experiments.map(fleetExperimentLines).join("\n\n") : "(no experiments)"
|
|
3428
|
+
);
|
|
3429
|
+
case "articles": {
|
|
3430
|
+
const params = new URLSearchParams(
|
|
3431
|
+
compact({
|
|
3432
|
+
experiment_id: flagStr(flags.experiment),
|
|
3433
|
+
org_id: flagStr(flags.org),
|
|
3434
|
+
filter: flagStr(flags.filter),
|
|
3435
|
+
arm: flagStr(flags.arm),
|
|
3436
|
+
q: flagStr(flags.q),
|
|
3437
|
+
limit: flagStr(flags.limit),
|
|
3438
|
+
offset: flagStr(flags.offset),
|
|
3439
|
+
})
|
|
3440
|
+
);
|
|
3441
|
+
return out(await staffRequest(ctx, "GET", `${FLEET_EXPERIMENTS}/articles?${params}`), (r) => {
|
|
3442
|
+
const head = `${r.total} post${r.total === 1 ? "" : "s"} name the client (showing ${r.articles.length} from ${r.offset})${r.unaudited ? `; ${r.unaudited} unaudited` : ""}`;
|
|
3443
|
+
return [head, ...r.articles.map(mentionedArticleLines)].join("\n\n");
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
case "mark":
|
|
3447
|
+
case "unmark": {
|
|
3448
|
+
const articleIds = rest.filter(Boolean);
|
|
3449
|
+
const arm = sub === "unmark" ? null : flagStr(flags.arm);
|
|
3450
|
+
if (!id || articleIds.length === 0 || (sub === "mark" && !arm))
|
|
3451
|
+
throw new CliError(
|
|
3452
|
+
`Usage: admin fleet-experiments ${sub} <experiment-id> <article-id> [<article-id> …]${sub === "mark" ? " --arm <arm>" : ""}`
|
|
3453
|
+
);
|
|
3454
|
+
const result = await staffRequest(ctx, "PATCH", `${FLEET_EXPERIMENTS}/articles`, {
|
|
3455
|
+
experiment_id: id,
|
|
3456
|
+
article_ids: articleIds,
|
|
3457
|
+
arm,
|
|
3458
|
+
});
|
|
3459
|
+
return out(
|
|
3460
|
+
result,
|
|
3461
|
+
(r) => `assigned ${r.assigned}, moved ${r.moved}, unchanged ${r.unchanged}, cleared ${r.cleared}`
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
case "new": {
|
|
3465
|
+
const template = flagStr(flags.template);
|
|
3466
|
+
const specFile = flagStr(flags["spec-file"]);
|
|
3467
|
+
if (!template && !specFile)
|
|
3468
|
+
throw new CliError(
|
|
3469
|
+
"Usage: admin fleet-experiments new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1 | --spec-file spec.json"
|
|
3470
|
+
);
|
|
3471
|
+
const body = template
|
|
3472
|
+
? { action: "create_template", template }
|
|
3473
|
+
: { action: "create", experiment: JSON.parse(readFileSync(specFile, "utf8")) };
|
|
3474
|
+
return out(await action(body), (r) => `created ${r.experiment_id}`);
|
|
3475
|
+
}
|
|
3476
|
+
case "assign":
|
|
3477
|
+
case "assign-eligible":
|
|
3478
|
+
case "assign-orgs":
|
|
3479
|
+
case "apply": {
|
|
3480
|
+
const ids = rest.filter(Boolean);
|
|
3481
|
+
if (!id || ((sub === "assign" || sub === "assign-orgs") && ids.length === 0))
|
|
3482
|
+
throw new CliError(
|
|
3483
|
+
`Usage: admin fleet-experiments ${sub} <experiment-id>${sub === "assign" ? " <unit-id…>" : sub === "assign-orgs" ? " <org-domain-or-id…>" : ""}`
|
|
3484
|
+
);
|
|
3485
|
+
const body =
|
|
3486
|
+
sub === "assign"
|
|
3487
|
+
? { action: "assign", experiment_id: id, unit_ids: ids }
|
|
3488
|
+
: sub === "assign-orgs"
|
|
3489
|
+
? { action: "assign_orgs", experiment_id: id, orgs: ids }
|
|
3490
|
+
: { action: sub === "apply" ? "apply" : "assign_eligible", experiment_id: id };
|
|
3491
|
+
return out(await action(body), (r) => {
|
|
3492
|
+
const lines = [];
|
|
3493
|
+
if (r.assigned !== undefined) lines.push(`assigned ${r.assigned}, already in ${r.skipped}`);
|
|
3494
|
+
if (r.applied)
|
|
3495
|
+
lines.push(
|
|
3496
|
+
`applied ${r.applied.applied}, unchanged ${r.applied.unchanged}, failed ${r.applied.failed.length}`
|
|
3497
|
+
);
|
|
3498
|
+
for (const f of r.applied?.failed ?? []) lines.push(` ${f.unitId} (${f.arm}): ${f.message}`);
|
|
3499
|
+
for (const k of r.skippedOrgs ?? []) lines.push(` skipped ${k.orgId}: ${k.reason}`);
|
|
3500
|
+
return lines.join("\n") || "ok";
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
case "start":
|
|
3504
|
+
case "stop":
|
|
3505
|
+
if (!id) throw new CliError(`Usage: admin fleet-experiments ${sub} <experiment-id>`);
|
|
3506
|
+
return out(await action({ action: sub, experiment_id: id }), (r) => `${id} is ${r.status}`);
|
|
3507
|
+
case "decide": {
|
|
3508
|
+
const decision = flagStr(flags.decision);
|
|
3509
|
+
if (!id || !decision)
|
|
3510
|
+
throw new CliError('Usage: admin fleet-experiments decide <experiment-id> --decision "<readout>"');
|
|
3511
|
+
return out(await action({ action: "decide", experiment_id: id, decision }), (r) => `${id} is ${r.status}`);
|
|
3512
|
+
}
|
|
3513
|
+
default:
|
|
3514
|
+
throw new CliError(
|
|
3515
|
+
'Usage: admin fleet-experiments list | articles [--experiment <id>] [--org <id>] [--filter all|assigned|unassigned] [--arm <arm>] [--q …] [--limit n] [--offset n] | mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…> | new --template <key> | start|stop <experiment-id> | decide <experiment-id> --decision "…"'
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3349
3520
|
export async function cmdAdmin(ctx) {
|
|
3350
3521
|
const sub = ctx.positionals[0];
|
|
3351
3522
|
if (sub === "replays" || sub === "replay")
|
|
@@ -3355,11 +3526,12 @@ export async function cmdAdmin(ctx) {
|
|
|
3355
3526
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3356
3527
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3357
3528
|
if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3529
|
+
if (sub === "fleet-experiments") return cmdAdminFleetExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3358
3530
|
if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3359
3531
|
if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3360
3532
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3361
3533
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3362
3534
|
throw new CliError(
|
|
3363
|
-
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin spend … | admin evals … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3535
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3364
3536
|
);
|
|
3365
3537
|
}
|
package/lib/commands/arena.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
toolField,
|
|
23
23
|
followJob,
|
|
24
24
|
argsFromFlags,
|
|
25
|
+
preferredSourcesFlag,
|
|
25
26
|
} from "./shared.mjs";
|
|
26
27
|
|
|
27
28
|
/** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
|
|
@@ -75,6 +76,8 @@ const RUN_OPTIONS = {
|
|
|
75
76
|
context: { value: (flags) => readJsonObjectFlag(flags, "context") },
|
|
76
77
|
blocked_mentions: { flag: "block", type: "list" },
|
|
77
78
|
blocked_domains: { flag: "block-domain", type: "list" },
|
|
79
|
+
// Pages (read every run) and sites research reaches for first; `--prefer none` declares none.
|
|
80
|
+
preferred_sources: { value: (flags) => preferredSourcesFlag(flags.prefer) },
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Ported from phantomstory-cli's collections.ts.
|
|
3
3
|
|
|
4
4
|
import { CliError } from "../client.mjs";
|
|
5
|
-
import { flagStr, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
5
|
+
import { flagStr, printResult, ok, argsFromFlags, preferredSourcesFlag } from "./shared.mjs";
|
|
6
6
|
import { cmdCollection } from "./collection.mjs";
|
|
7
7
|
|
|
8
8
|
// `--flag none|off|""` clears a nullable field; the server takes null for that.
|
|
@@ -47,6 +47,11 @@ export const ARGS = {
|
|
|
47
47
|
regenerate_topic_map: { value: (flags) => (flags["no-topic-map"] === undefined ? undefined : false) },
|
|
48
48
|
stock_headlines: { value: (flags) => (flags["no-headlines"] === undefined ? undefined : false) },
|
|
49
49
|
},
|
|
50
|
+
cite_preferred_sources: {
|
|
51
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
52
|
+
limit: { flag: "limit", type: "num" },
|
|
53
|
+
dry_run: { value: (flags) => (flags["dry-run"] === true ? true : undefined) },
|
|
54
|
+
},
|
|
50
55
|
update_auto_pen_config: {
|
|
51
56
|
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
52
57
|
// `--template none` clears the binding; the server takes null for that.
|
|
@@ -69,6 +74,8 @@ export const ARGS = {
|
|
|
69
74
|
},
|
|
70
75
|
mention: { object: { degree: "mention" } },
|
|
71
76
|
auto_start_on_curate: stringBool("auto-start"),
|
|
77
|
+
// Replaces the collection's preferred sources; `--prefer none` clears them.
|
|
78
|
+
preferred_sources: { value: (flags) => preferredSourcesFlag(flags.prefer) },
|
|
72
79
|
},
|
|
73
80
|
};
|
|
74
81
|
|
|
@@ -99,9 +106,11 @@ export async function cmdCollections(ctx) {
|
|
|
99
106
|
return collectionsAutoPen(rest);
|
|
100
107
|
case "activate":
|
|
101
108
|
return collectionsActivate(rest);
|
|
109
|
+
case "cite-preferred":
|
|
110
|
+
return collectionsCitePreferred(rest);
|
|
102
111
|
default:
|
|
103
112
|
throw new CliError(
|
|
104
|
-
`Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen, activate`
|
|
113
|
+
`Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign, autopen, activate, cite-preferred`
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
@@ -129,6 +138,26 @@ async function collectionsActivate(ctx) {
|
|
|
129
138
|
return 0;
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
/**
|
|
142
|
+
* `collections cite-preferred <id> [--limit <n>] [--dry-run]` — bring the collection's published
|
|
143
|
+
* posts up to its preferred sources: each eligible post places at most one preferred figure and
|
|
144
|
+
* republishes. The same function as the app's "Apply to published posts".
|
|
145
|
+
*/
|
|
146
|
+
async function collectionsCitePreferred(ctx) {
|
|
147
|
+
const { client, positionals, flags, io } = ctx;
|
|
148
|
+
const args = argsFromFlags(ARGS.cite_preferred_sources, flags, positionals);
|
|
149
|
+
const result = await client.callTool("cite_preferred_sources", args);
|
|
150
|
+
const n = result?.started?.length ?? 0;
|
|
151
|
+
ok(
|
|
152
|
+
ctx,
|
|
153
|
+
result?.dry_run
|
|
154
|
+
? `Would run ${n} post${n === 1 ? "" : "s"}; ${result?.skipped?.length ?? 0} skipped.`
|
|
155
|
+
: `Started ${n} post${n === 1 ? "" : "s"}${result?.republishes === false ? " (changes wait on the draft for approval)" : ""}; ${result?.skipped?.length ?? 0} skipped.`
|
|
156
|
+
);
|
|
157
|
+
printResult(io, flags, result);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
async function collectionsUpdate(ctx) {
|
|
133
162
|
const { client, positionals, flags, io } = ctx;
|
|
134
163
|
const patch = argsFromFlags(ARGS.update_collection, flags, positionals);
|
|
@@ -166,7 +195,7 @@ async function collectionsAutoPen(ctx) {
|
|
|
166
195
|
const patch = argsFromFlags(ARGS.update_auto_pen_config, flags, positionals);
|
|
167
196
|
if (Object.keys(patch).length === 1) {
|
|
168
197
|
throw new CliError(
|
|
169
|
-
"Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start."
|
|
198
|
+
"Nothing to change. Pass at least one of --template, --kernel-family, --mention, --outline-depth, --direction-gate, --auto-start, --prefer."
|
|
170
199
|
);
|
|
171
200
|
}
|
|
172
201
|
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -32,6 +32,20 @@ export function flagList(v) {
|
|
|
32
32
|
return typeof v === "string" ? [v] : [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `--prefer <site>` (repeatable or comma-separated) → preferred_sources `[{ url }]` (a site; a URL is read as its site); `--prefer none`
|
|
37
|
+
* → `[]` (clears a standing list); absent → undefined.
|
|
38
|
+
*/
|
|
39
|
+
export function preferredSourcesFlag(v) {
|
|
40
|
+
if (v === undefined) return undefined;
|
|
41
|
+
const list = flagList(v)
|
|
42
|
+
.flatMap((x) => x.split(","))
|
|
43
|
+
.map((x) => x.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
if (list.length === 1 && list[0] === "none") return [];
|
|
46
|
+
return list.map((url) => ({ url }));
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
export function requireFlag(flags, name) {
|
|
36
50
|
const v = flagStr(flags[name]);
|
|
37
51
|
if (v === undefined) throw new CliError(`Missing required --${name}`);
|