@letterstory/cli 0.55.0 → 0.58.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 +30 -0
- package/lib/cli.mjs +14 -3
- package/lib/commands/admin.mjs +463 -5
- package/lib/commands/autopen.mjs +2 -0
- package/lib/commands/enhance.mjs +152 -0
- package/lib/commands/evals.mjs +1 -1
- package/lib/commands/shredder.mjs +110 -1
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -360,6 +360,11 @@ letterstory admin tuning comparison <id> # per-measure verdicts (b
|
|
|
360
360
|
letterstory admin tuning picks <id> # the blind queue; then: admin tuning pick <id> --cell <key> --pick left|right|tie
|
|
361
361
|
letterstory admin tuning reading-list <id> --n 10 # your next blind pairs as links (Text A / Text B), to read off the screen; pick in the console
|
|
362
362
|
letterstory admin tuning pair <id> --cell <key> # one pair named: live and candidate, both texts, the measures per arm (a key reads every pair)
|
|
363
|
+
letterstory admin tuning candidates --kernels all --patch '{"writer":"echo/echo"}' --note "Echo" # the same change as a candidate of each kernel
|
|
364
|
+
letterstory admin tuning session-new --title "Echo round 1" --kernel Hazel --input-file brief.md --kind outline [--version 22]
|
|
365
|
+
letterstory admin tuning propose <session> --text "Echo with rarity on; live with the shred after" --create # trials from words, over the dials
|
|
366
|
+
letterstory admin tuning trial-run <trial> --repeats 2 # runs advance on the server; poll `admin tuning session <id> --texts`
|
|
367
|
+
letterstory admin tuning trial-save <trial> --note "…" # a trial as a candidate version (never live); then `admin tuning compare`
|
|
363
368
|
letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
|
|
364
369
|
letterstory admin experiments list --q colons # what we've tried
|
|
365
370
|
letterstory admin experiments new "We use too many colons" --as "claude-code"
|
|
@@ -574,6 +579,14 @@ letterstory shredder run <endpoint-id> --file post.md --format markdown
|
|
|
574
579
|
|
|
575
580
|
letterstory shredder run <endpoint-id> --file post.md --no-wait # just queue it
|
|
576
581
|
letterstory shredder job <job-id> # poll it yourself
|
|
582
|
+
|
|
583
|
+
# The shredder as ONE versioned file (like a kernel file; schema at /api/schema/shredder):
|
|
584
|
+
letterstory shredder file <endpoint-id> > shredder.json # the file the endpoint shreds with
|
|
585
|
+
letterstory shredder file <endpoint-id> --patch '{"rewriters":{"excludeWriterFamily":true}}'
|
|
586
|
+
letterstory shredder versions <endpoint-id> # your copy's versions and our releases
|
|
587
|
+
letterstory shredder pin <endpoint-id> <version-id|none> # pin one of our releases, or follow live
|
|
588
|
+
letterstory shredder offer <endpoint-id> [--accept] # our newer version, over your own changes
|
|
589
|
+
letterstory shredder families [--hours 6] # how each rewriter family is doing
|
|
577
590
|
```
|
|
578
591
|
|
|
579
592
|
Both follow loops poll every 15–20 s. `LETTERSTORY_POLL_INTERVAL_MS` overrides that — set
|
|
@@ -582,6 +595,23 @@ it to `0` in CI so a test does not wait on a real interval.
|
|
|
582
595
|
`collection run --topic "…" --collection <uuid>` runs topic → draft → publish → rebuild
|
|
583
596
|
end to end, with `collection run-status <job-id>` to follow it.
|
|
584
597
|
|
|
598
|
+
### Enhancing
|
|
599
|
+
|
|
600
|
+
The passes that run after the writer — visuals, internal links, external links (citations),
|
|
601
|
+
metadata — as one versioned file per collection (models and settings), each run recorded.
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
letterstory enhance run <article-id> --pass internal_links # run one pass on a post now
|
|
605
|
+
letterstory enhance runs --article <article-id> # what each pass did (also --auto-pen, --outcome skipped)
|
|
606
|
+
letterstory enhance get <run-id> # one run: version, models, what it added, cost
|
|
607
|
+
letterstory enhance file <collection-id> > enhancer.json # the file the collection's passes run
|
|
608
|
+
letterstory enhance file <collection-id> --patch '{"internalLinks":{"maxPerPost":3}}'
|
|
609
|
+
letterstory enhance versions <collection-id> # your copy's versions and our releases
|
|
610
|
+
letterstory enhance pin <collection-id> <version-id|none> # pin one of our releases, or follow live
|
|
611
|
+
letterstory enhance offer <collection-id> [--accept] # our newer version, over your own changes
|
|
612
|
+
letterstory autopen run --article <id> --skip finish,metadata # leave passes out of one run
|
|
613
|
+
```
|
|
614
|
+
|
|
585
615
|
## Seers — event-driven signals
|
|
586
616
|
|
|
587
617
|
A **seer** watches an external source (a GitHub repo, a Notion workspace, a spec URL, a
|
package/lib/cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
cmdTemplates,
|
|
41
41
|
cmdCompositions,
|
|
42
42
|
cmdShredder,
|
|
43
|
+
cmdEnhance,
|
|
43
44
|
cmdLede,
|
|
44
45
|
cmdLandings,
|
|
45
46
|
cmdSequences,
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
} from "./commands.mjs";
|
|
55
56
|
|
|
56
57
|
// Keep in sync with cli/package.json.
|
|
57
|
-
export const VERSION = "0.
|
|
58
|
+
export const VERSION = "0.58.0";
|
|
58
59
|
|
|
59
60
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
60
61
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -718,7 +719,15 @@ Shredder (diversify content produced outside Letterstory; structure preserved):
|
|
|
718
719
|
job lands as error or degraded. Pace the polling with
|
|
719
720
|
LETTERSTORY_POLL_INTERVAL_MS.
|
|
720
721
|
shredder job <job-id> Poll a queued shred and print it when it lands
|
|
721
|
-
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first
|
|
722
|
+
shredder runs [--endpoint <id>] [--limit <n>] Recent shred calls (telemetry), newest first:
|
|
723
|
+
the shredder version, level, seats and models each ran
|
|
724
|
+
shredder file <endpoint-id> Print the shredder file the endpoint shreds with
|
|
725
|
+
shredder file <endpoint-id> (--file f.json | --patch '<json>') [--note "…"]
|
|
726
|
+
Save a new version of the endpoint's own copy
|
|
727
|
+
(schema: GET /api/schema/shredder)
|
|
728
|
+
shredder versions <endpoint-id> Your copy's versions and our released ones
|
|
729
|
+
shredder pin <endpoint-id> <version-id|none> Pin one of our versions, or follow our live one
|
|
730
|
+
shredder offer <endpoint-id> [--accept] What our newer version would change; --accept takes it
|
|
722
731
|
|
|
723
732
|
Ledes (opening lines for cold email, chosen per prospect from a hand-approved library):
|
|
724
733
|
lede list Your lede endpoints + run counts
|
|
@@ -852,7 +861,7 @@ Feedback (tell us what's broken, missing, unclear, or wanted):
|
|
|
852
861
|
|
|
853
862
|
Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
|
|
854
863
|
evals scoreboard [--collection <id>] [--days N] Every measure by stage: posts, failing, rate
|
|
855
|
-
evals cases [--status open|guarding|proposed|retired|rejected] [--stage research|brief|links|mentions|accuracy|pipeline]
|
|
864
|
+
evals cases [--status open|guarding|proposed|retired|rejected] [--stage research|brief|links|mentions|accuracy|pipeline|shred]
|
|
856
865
|
evals capture (--article <id> | --research-run <id> | --kernel-job <id>) --title "…"
|
|
857
866
|
(--description "…" | --body … | --file <path>) (--measure <key> | --assertion "…")
|
|
858
867
|
[--stage <s>] [--replay <stage>] [--auto-pen <id>] [--published-version N]
|
|
@@ -928,6 +937,8 @@ const CLIENT_COMMANDS = {
|
|
|
928
937
|
templates: cmdTemplates,
|
|
929
938
|
compositions: cmdCompositions,
|
|
930
939
|
shredder: cmdShredder,
|
|
940
|
+
enhance: cmdEnhance,
|
|
941
|
+
enhancer: cmdEnhance,
|
|
931
942
|
lede: cmdLede,
|
|
932
943
|
ledes: cmdLede,
|
|
933
944
|
landings: cmdLandings,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -424,13 +424,28 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
424
424
|
* admin tuning kernel <service-kernel-id> [--version N|live] one version's file (JSON)
|
|
425
425
|
* admin tuning candidate <service-kernel-id> --file f.json [--note "…"] save a file as a candidate version
|
|
426
426
|
* admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] start a comparison
|
|
427
|
+
* (Hazel:23@22 reads v23 against the test version v22 instead of live; it cannot promote)
|
|
427
428
|
* admin tuning comparisons | comparison <id> list / read one (per-measure verdicts)
|
|
428
429
|
* admin tuning picks <comparison-id> the blind queue (next pairs)
|
|
429
430
|
* admin tuning pick <comparison-id> --cell <key> --pick left|right|tie record a blind pick
|
|
430
431
|
* admin tuning reading-list <comparison-id> [--n 10] your next blind pairs as links, to read off the screen
|
|
431
432
|
* admin tuning pair <comparison-id> --cell <key> one pair named: live and candidate, both texts, the measures per arm
|
|
433
|
+
* admin tuning candidates --kernels Hazel,Aspen|all --patch '{…}' [--base N] [--note "…"] the same changes as a candidate of each kernel
|
|
432
434
|
* admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,… promote from a "ship" decision
|
|
433
435
|
*
|
|
436
|
+
* Sessions: one input written many ways, side by side (the console's Write › Sessions). Runs advance
|
|
437
|
+
* on the server (the advance-tuning-runs cron), so a session finishes with no tab open; the blind
|
|
438
|
+
* read of the results is a person's, in the console.
|
|
439
|
+
* admin tuning base-kernels the kernels a session can run on (ids for --kernel)
|
|
440
|
+
* admin tuning base-versions <kernel-id> the versions a session on it can run against
|
|
441
|
+
* admin tuning sessions | session <id> [--texts] list / one session: trials, runs and their numbers (--texts adds each trial's latest text)
|
|
442
|
+
* admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…") [--kind article|outline|brief]
|
|
443
|
+
* [--version N] [--writers echo/echo,…] [--trials-from <session-id>]
|
|
444
|
+
* admin tuning propose <session-id> --text "Echo with rarity on; live with the shred after" [--create]
|
|
445
|
+
* admin tuning trial-add <session-id> [--name "…"] [--patch '{…}'] [--options '{…}'] [--from <trial-id>]
|
|
446
|
+
* admin tuning trial-run <trial-id> [--repeats 2] queue runs (guardrails apply)
|
|
447
|
+
* admin tuning trial-save <trial-id> [--note "…"] save a trial as a candidate version (never live)
|
|
448
|
+
*
|
|
434
449
|
* Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
|
|
435
450
|
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
436
451
|
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
@@ -438,7 +453,23 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
438
453
|
* admin tuning research-inputs [--set v1] research's fixed inputs
|
|
439
454
|
* admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
|
|
440
455
|
* admin tuning research-standing live's level on the fixed inputs, week by week
|
|
456
|
+
*
|
|
457
|
+
* Shred (Phase 4): the same, for shredder versions on shred's fixed inputs.
|
|
458
|
+
* admin tuning shred-compare --title "…" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys …]
|
|
459
|
+
* admin tuning shred-inputs [--set v1] | shred-split [--since-hours 72] [--at <iso>,…] | shred-standing
|
|
441
460
|
* admin tuning promote-arena <experiment-id> --version <arena-version-id> promote our arena from a "ship" decision
|
|
461
|
+
*
|
|
462
|
+
* Shred (Phase 4): our shredder's versions as files.
|
|
463
|
+
* admin tuning shredders | shredder <version-id> our shredder's versions / one version's file and diff
|
|
464
|
+
* admin tuning shredder-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
465
|
+
* admin tuning promote-shredder <experiment-id> --version <shredder-version-id> promote our shredder from a "ship" decision (blind picks required)
|
|
466
|
+
*
|
|
467
|
+
* Enhance (Phase 5): our enhancer's versions as files, enhance comparisons, the deploy split.
|
|
468
|
+
* admin tuning enhancers | enhancer <version-id> our enhancer's versions / one version's file and diff
|
|
469
|
+
* admin tuning enhancer-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
470
|
+
* admin tuning enhance-compare --title "…" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys …]
|
|
471
|
+
* admin tuning enhance-inputs [--set v1] | enhance-split [--since-hours 72] [--at <iso>,…] | enhance-standing
|
|
472
|
+
* admin tuning promote-enhancer <experiment-id> --version <enhancer-version-id> promote our enhancer from a "ship" decision (picks when it changes visuals)
|
|
442
473
|
*/
|
|
443
474
|
/** `--flag '<json>'` → the parsed object, or undefined when the flag is absent. */
|
|
444
475
|
function jsonFlag(raw, name) {
|
|
@@ -453,6 +484,44 @@ function jsonFlag(raw, name) {
|
|
|
453
484
|
}
|
|
454
485
|
}
|
|
455
486
|
|
|
487
|
+
/**
|
|
488
|
+
* A session as an agent reads it: each trial's changes and runs with their headline numbers, and
|
|
489
|
+
* (with `texts`) the latest finished text per trial. The full session (traces, records) is `--json`.
|
|
490
|
+
*/
|
|
491
|
+
function sessionSummary(session, texts) {
|
|
492
|
+
const runsOf = (trialId) => session.runs.filter((r) => r.trialId === trialId);
|
|
493
|
+
return {
|
|
494
|
+
id: session.id,
|
|
495
|
+
title: session.title,
|
|
496
|
+
kernel: session.baseKernelName,
|
|
497
|
+
base_version: session.baseVersion ?? "live",
|
|
498
|
+
material_ready: session.material?.ready ?? true,
|
|
499
|
+
trials: session.trials.map((t) => {
|
|
500
|
+
const runs = runsOf(t.id);
|
|
501
|
+
const latest = [...runs].reverse().find((r) => r.status === "done" && r.outputText);
|
|
502
|
+
return compact({
|
|
503
|
+
id: t.id,
|
|
504
|
+
name: t.name,
|
|
505
|
+
file_patch: t.filePatch,
|
|
506
|
+
run_options: t.runOptions,
|
|
507
|
+
saved_as_version: t.savedAsVersion ?? undefined,
|
|
508
|
+
runs: runs.map((r) =>
|
|
509
|
+
compact({
|
|
510
|
+
id: r.id,
|
|
511
|
+
status: r.status,
|
|
512
|
+
error: r.error ?? undefined,
|
|
513
|
+
quality: r.metrics?.qualityTotal,
|
|
514
|
+
quality_scale: r.metrics?.qualityScale,
|
|
515
|
+
p_machine: r.metrics?.pMachine,
|
|
516
|
+
words: r.metrics?.words,
|
|
517
|
+
})
|
|
518
|
+
),
|
|
519
|
+
latest_text: texts && latest ? latest.outputText : undefined,
|
|
520
|
+
});
|
|
521
|
+
}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
456
525
|
async function cmdAdminTuning(ctx) {
|
|
457
526
|
const [sub, id] = ctx.positionals;
|
|
458
527
|
const { flags, io } = ctx;
|
|
@@ -509,12 +578,17 @@ async function cmdAdminTuning(ctx) {
|
|
|
509
578
|
throw new CliError(
|
|
510
579
|
'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>]'
|
|
511
580
|
);
|
|
581
|
+
// kernel:version, or kernel:version@control to read it against a test version instead of live.
|
|
512
582
|
const candidates = list.map((item) => {
|
|
513
583
|
const at = item.lastIndexOf(":");
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
584
|
+
const [v, c] = item.slice(at + 1).split("@");
|
|
585
|
+
const version = Number(v);
|
|
586
|
+
const control = c === undefined ? undefined : Number(c);
|
|
587
|
+
if (at < 1 || !Number.isInteger(version) || (control !== undefined && !Number.isInteger(control)))
|
|
588
|
+
throw new CliError(
|
|
589
|
+
`--candidates items are kernel:version or kernel:version@control, got "${item}"`
|
|
590
|
+
);
|
|
591
|
+
return compact({ kernel: item.slice(0, at), candidate_version: version, control_version: control });
|
|
518
592
|
});
|
|
519
593
|
const r = await staffRequest(
|
|
520
594
|
ctx,
|
|
@@ -566,6 +640,190 @@ async function cmdAdminTuning(ctx) {
|
|
|
566
640
|
await staffRequest(ctx, "GET", `${T}/comparisons/${id}/pair?${new URLSearchParams({ cell })}`)
|
|
567
641
|
);
|
|
568
642
|
}
|
|
643
|
+
case "candidates": {
|
|
644
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
645
|
+
const wanted = flagList(flags.kernels);
|
|
646
|
+
if (!patch || !wanted?.length)
|
|
647
|
+
throw new CliError(
|
|
648
|
+
'Usage: admin tuning candidates --kernels Hazel,Aspen|all --patch \'{"writer":"echo/echo"}\' [--base N] [--note "…"]'
|
|
649
|
+
);
|
|
650
|
+
const { kernels } = await staffRequest(ctx, "GET", `${T}/kernel-files`);
|
|
651
|
+
const all = wanted.length === 1 && wanted[0].toLowerCase() === "all";
|
|
652
|
+
const picked = all
|
|
653
|
+
? kernels
|
|
654
|
+
: wanted.map((name) => {
|
|
655
|
+
const k = kernels.find(
|
|
656
|
+
(x) => x.kernel.toLowerCase() === name.toLowerCase() || x.serviceKernelId === name
|
|
657
|
+
);
|
|
658
|
+
if (!k) throw new CliError(`No catalog kernel named ${name}.`);
|
|
659
|
+
return k;
|
|
660
|
+
});
|
|
661
|
+
const base = flagNum(flags.base);
|
|
662
|
+
const r = await staffRequest(
|
|
663
|
+
ctx,
|
|
664
|
+
"POST",
|
|
665
|
+
`${T}/kernel-files/candidates`,
|
|
666
|
+
compact({
|
|
667
|
+
targets: picked.map((k) => compact({ service_kernel_id: k.serviceKernelId, base })),
|
|
668
|
+
file_patch: patch,
|
|
669
|
+
note: flagStr(flags.note),
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
if (json) return printResult(io, flags, r);
|
|
673
|
+
return io.log(
|
|
674
|
+
r.outcomes
|
|
675
|
+
.map((o) =>
|
|
676
|
+
o.version
|
|
677
|
+
? `${(o.kernel ?? o.serviceKernelId).padEnd(12)} candidate v${o.version} (from v${o.base}): ${o.changes.join(", ") || "no change"}`
|
|
678
|
+
: `${(o.kernel ?? o.serviceKernelId).padEnd(12)} refused: ${o.error}`
|
|
679
|
+
)
|
|
680
|
+
.join("\n")
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
// --- Sessions: one input written many ways, side by side ---
|
|
684
|
+
case "base-kernels": {
|
|
685
|
+
const r = await staffRequest(ctx, "GET", `${T}/kernels`);
|
|
686
|
+
if (json) return printResult(io, flags, r);
|
|
687
|
+
return io.log(
|
|
688
|
+
r.kernels.map((k) => `${k.name.padEnd(28)} ${(k.orgName ?? "shared").padEnd(20)} ${k.id}`).join("\n")
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
case "base-versions": {
|
|
692
|
+
if (!id) throw new CliError("Usage: admin tuning base-versions <kernel-id>");
|
|
693
|
+
const r = await staffRequest(ctx, "GET", `${T}/kernels/${id}/versions`);
|
|
694
|
+
if (json) return printResult(io, flags, r);
|
|
695
|
+
return io.log(
|
|
696
|
+
r.versions
|
|
697
|
+
.map((v) => `v${v.version} ${v.status.padEnd(9)} ${v.writer}${v.note ? ` ${v.note}` : ""}`)
|
|
698
|
+
.join("\n") || "(no versions)"
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
case "sessions": {
|
|
702
|
+
const r = await staffRequest(ctx, "GET", `${T}?limit=${flagNum(flags.limit) ?? 30}`);
|
|
703
|
+
if (json) return printResult(io, flags, r);
|
|
704
|
+
return io.log(
|
|
705
|
+
r.sessions
|
|
706
|
+
.map(
|
|
707
|
+
(x) =>
|
|
708
|
+
`${x.id} ${x.title} · ${x.baseKernelName ?? "?"}${x.baseVersion ? ` v${x.baseVersion}` : ""} · ${x.trialCount} trials, ${x.runCount} runs`
|
|
709
|
+
)
|
|
710
|
+
.join("\n") || "(no sessions)"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
case "session": {
|
|
714
|
+
if (!id) throw new CliError("Usage: admin tuning session <session-id> [--texts]");
|
|
715
|
+
const { session } = await staffRequest(ctx, "GET", `${T}/sessions/${id}`);
|
|
716
|
+
if (json) return printResult(io, flags, { session });
|
|
717
|
+
return printResult(io, { ...flags, json: true }, sessionSummary(session, flagBool(flags.texts)));
|
|
718
|
+
}
|
|
719
|
+
case "session-new": {
|
|
720
|
+
const title = flagStr(flags.title);
|
|
721
|
+
const kernel = flagStr(flags.kernel);
|
|
722
|
+
const path = flagStr(flags["input-file"]);
|
|
723
|
+
let input = flagStr(flags.input);
|
|
724
|
+
if (path) {
|
|
725
|
+
try {
|
|
726
|
+
input = readFileSync(path, "utf8");
|
|
727
|
+
} catch (err) {
|
|
728
|
+
throw new CliError(`--input-file ${path}: ${err.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!title || !kernel || !input)
|
|
732
|
+
throw new CliError(
|
|
733
|
+
'Usage: admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…") [--kind article|outline|brief] [--version N] [--writers a,b] [--trials-from <session-id>]'
|
|
734
|
+
);
|
|
735
|
+
let kernelId = kernel;
|
|
736
|
+
if (!/^[0-9a-f-]{36}$/i.test(kernel)) {
|
|
737
|
+
const { kernels } = await staffRequest(ctx, "GET", `${T}/kernels`);
|
|
738
|
+
const matches = kernels.filter((k) => k.name.toLowerCase() === kernel.toLowerCase());
|
|
739
|
+
const shared = matches.find((k) => !k.orgName) ?? matches[0];
|
|
740
|
+
if (!shared) throw new CliError(`No kernel named ${kernel} (admin tuning base-kernels lists them).`);
|
|
741
|
+
kernelId = shared.id;
|
|
742
|
+
}
|
|
743
|
+
const { session } = await staffRequest(
|
|
744
|
+
ctx,
|
|
745
|
+
"POST",
|
|
746
|
+
T,
|
|
747
|
+
compact({
|
|
748
|
+
title,
|
|
749
|
+
input_text: input,
|
|
750
|
+
input_kind: flagStr(flags.kind) ?? "unknown",
|
|
751
|
+
base_kernel_id: kernelId,
|
|
752
|
+
base_version: flagNum(flags.version),
|
|
753
|
+
trials_from_session_id: flagStr(flags["trials-from"]),
|
|
754
|
+
writers: (flagList(flags.writers) ?? []).map((model) => ({ model })),
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
if (json) return printResult(io, flags, { session });
|
|
758
|
+
return io.log(
|
|
759
|
+
`session ${session.id}: ${session.trials.length} trials on ${session.baseKernelName}${session.baseVersion ? ` v${session.baseVersion}` : " (live)"} → ${ctx.client.url}/superadmin/tuning/${session.id}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
case "propose": {
|
|
763
|
+
const text = flagStr(flags.text);
|
|
764
|
+
if (!id || !text)
|
|
765
|
+
throw new CliError(
|
|
766
|
+
'Usage: admin tuning propose <session-id> --text "Echo with rarity on; …" [--create]'
|
|
767
|
+
);
|
|
768
|
+
const r = await staffRequest(ctx, "POST", `${T}/sessions/${id}/propose`, { description: text });
|
|
769
|
+
if (flagBool(flags.create))
|
|
770
|
+
for (const t of r.trials)
|
|
771
|
+
await staffRequest(ctx, "POST", `${T}/sessions/${id}/trials`, {
|
|
772
|
+
name: t.name,
|
|
773
|
+
file_patch: t.file_patch,
|
|
774
|
+
run_options: t.run_options,
|
|
775
|
+
});
|
|
776
|
+
if (json) return printResult(io, flags, r);
|
|
777
|
+
return io.log(
|
|
778
|
+
[
|
|
779
|
+
...r.trials.map(
|
|
780
|
+
(t) => `${flagBool(flags.create) ? "created" : "proposed"} ${t.name}: ${t.changes.join(", ")}`
|
|
781
|
+
),
|
|
782
|
+
...r.unsupported.map((u) => `not expressible with the dials: ${u}`),
|
|
783
|
+
...r.rejected.map((x) => `dropped from ${x.trial}: ${x.dial} (${x.reason})`),
|
|
784
|
+
].join("\n") || "(nothing proposed)"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
case "trial-add": {
|
|
788
|
+
if (!id)
|
|
789
|
+
throw new CliError(
|
|
790
|
+
"Usage: admin tuning trial-add <session-id> [--name] [--patch '{…}'] [--options '{…}'] [--from <trial-id>]"
|
|
791
|
+
);
|
|
792
|
+
const r = await staffRequest(
|
|
793
|
+
ctx,
|
|
794
|
+
"POST",
|
|
795
|
+
`${T}/sessions/${id}/trials`,
|
|
796
|
+
compact({
|
|
797
|
+
name: flagStr(flags.name),
|
|
798
|
+
file_patch: jsonFlag(flags.patch, "--patch"),
|
|
799
|
+
run_options: jsonFlag(flags.options, "--options"),
|
|
800
|
+
from_trial_id: flagStr(flags.from),
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
return json ? printResult(io, flags, r) : io.log(`trial ${r.trial.id} (${r.trial.name})`);
|
|
804
|
+
}
|
|
805
|
+
case "trial-run": {
|
|
806
|
+
if (!id) throw new CliError("Usage: admin tuning trial-run <trial-id> [--repeats N]");
|
|
807
|
+
const r = await staffRequest(ctx, "POST", `${T}/trials/${id}/run`, {
|
|
808
|
+
repeats: flagNum(flags.repeats) ?? 1,
|
|
809
|
+
});
|
|
810
|
+
return json
|
|
811
|
+
? printResult(io, flags, r)
|
|
812
|
+
: io.log(`queued ${r.runs.length} run(s): ${r.runs.map((x) => x.id).join(", ")}`);
|
|
813
|
+
}
|
|
814
|
+
case "trial-save": {
|
|
815
|
+
if (!id) throw new CliError('Usage: admin tuning trial-save <trial-id> [--note "…"]');
|
|
816
|
+
const r = await staffRequest(
|
|
817
|
+
ctx,
|
|
818
|
+
"POST",
|
|
819
|
+
`${T}/trials/${id}/candidate`,
|
|
820
|
+
compact({ note: flagStr(flags.note) })
|
|
821
|
+
);
|
|
822
|
+
if (json) return printResult(io, flags, r);
|
|
823
|
+
return io.log(
|
|
824
|
+
`${r.kernel}: saved as candidate v${r.version}${r.baseVersion ? ` (from v${r.baseVersion})` : ""}${r.leftBehind.length ? `; per-run, not saved: ${r.leftBehind.join(", ")}` : ""}`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
569
827
|
// --- Research: our arena's versions, research comparisons, the deploy split ---
|
|
570
828
|
case "arenas": {
|
|
571
829
|
const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
|
|
@@ -643,6 +901,56 @@ async function cmdAdminTuning(ctx) {
|
|
|
643
901
|
await staffRequest(ctx, "GET", `${T}/research/deploy-split${q.size ? `?${q}` : ""}`)
|
|
644
902
|
);
|
|
645
903
|
}
|
|
904
|
+
case "shred-compare": {
|
|
905
|
+
const title = flagStr(flags.title);
|
|
906
|
+
const versionId = flagStr(flags.candidate);
|
|
907
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
908
|
+
const nullCheck = flagBool(flags["null-check"]);
|
|
909
|
+
const named = [versionId !== undefined, patch !== undefined, nullCheck === true].filter(Boolean).length;
|
|
910
|
+
if (!title || named !== 1)
|
|
911
|
+
throw new CliError(
|
|
912
|
+
"Usage: admin tuning shred-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
913
|
+
);
|
|
914
|
+
const candidate = versionId ? { version_id: versionId } : patch ? { patch } : { null_check: true };
|
|
915
|
+
const r = await staffRequest(
|
|
916
|
+
ctx,
|
|
917
|
+
"POST",
|
|
918
|
+
`${T}/comparisons`,
|
|
919
|
+
compact({
|
|
920
|
+
primitive: "shred",
|
|
921
|
+
title,
|
|
922
|
+
candidate,
|
|
923
|
+
keys: flagList(flags.keys),
|
|
924
|
+
concurrency: flagNum(flags.concurrency),
|
|
925
|
+
experiment_id: flagStr(flags.experiment),
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
return json ? printResult(io, flags, r) : io.log(`shred comparison ${r.comparison?.id ?? ""} started`);
|
|
929
|
+
}
|
|
930
|
+
case "shred-inputs": {
|
|
931
|
+
const set = flagStr(flags.set);
|
|
932
|
+
return printResult(
|
|
933
|
+
io,
|
|
934
|
+
{ ...flags, json: true },
|
|
935
|
+
await staffRequest(ctx, "GET", `${T}/shred/inputs${set ? `?${new URLSearchParams({ set })}` : ""}`)
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
case "shred-split": {
|
|
939
|
+
const q = new URLSearchParams(
|
|
940
|
+
compact({
|
|
941
|
+
since_hours: flagStr(flags["since-hours"]),
|
|
942
|
+
window_hours: flagStr(flags["window-hours"]),
|
|
943
|
+
at: flagStr(flags.at),
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
return printResult(
|
|
947
|
+
io,
|
|
948
|
+
{ ...flags, json: true },
|
|
949
|
+
await staffRequest(ctx, "GET", `${T}/shred/deploy-split${q.size ? `?${q}` : ""}`)
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
case "shred-standing":
|
|
953
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/shred/standing`));
|
|
646
954
|
case "research-standing":
|
|
647
955
|
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/research/standing`));
|
|
648
956
|
case "promote-arena": {
|
|
@@ -660,6 +968,156 @@ async function cmdAdminTuning(ctx) {
|
|
|
660
968
|
)
|
|
661
969
|
);
|
|
662
970
|
}
|
|
971
|
+
// --- Shred: our shredder's versions as files ---
|
|
972
|
+
case "shredders": {
|
|
973
|
+
const r = await staffRequest(ctx, "GET", `${T}/shredder-files`);
|
|
974
|
+
if (json) return printResult(io, flags, r);
|
|
975
|
+
return io.log(
|
|
976
|
+
r.versions
|
|
977
|
+
.map((v) => `${v.label.padEnd(18)} ${v.status.padEnd(9)} ${v.id}${v.note ? ` ${v.note}` : ""}`)
|
|
978
|
+
.join("\n") || "(no versions)"
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
case "shredder": {
|
|
982
|
+
if (!id) throw new CliError("Usage: admin tuning shredder <version-id>");
|
|
983
|
+
return printResult(
|
|
984
|
+
io,
|
|
985
|
+
{ ...flags, json: true },
|
|
986
|
+
await staffRequest(ctx, "GET", `${T}/shredder-files/${id}`)
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
case "shredder-candidate": {
|
|
990
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
991
|
+
const file = jsonFile({ "shredder-file": flags.file }, "shredder");
|
|
992
|
+
if ((patch === undefined) === (file === undefined))
|
|
993
|
+
throw new CliError(
|
|
994
|
+
'Usage: admin tuning shredder-candidate --patch \'{"rewriters":{"excludeWriterFamily":true}}\' | --file shredder.json [--of <version-id>] [--note "…"]'
|
|
995
|
+
);
|
|
996
|
+
const r = await staffRequest(
|
|
997
|
+
ctx,
|
|
998
|
+
"POST",
|
|
999
|
+
`${T}/shredder-files`,
|
|
1000
|
+
compact({ patch, file, patch_of: flagStr(flags.of), note: flagStr(flags.note) })
|
|
1001
|
+
);
|
|
1002
|
+
return json ? printResult(io, flags, r) : io.log(`saved candidate ${r.version.label} (${r.version.id})`);
|
|
1003
|
+
}
|
|
1004
|
+
case "promote-shredder": {
|
|
1005
|
+
const version = flagStr(flags.version);
|
|
1006
|
+
if (!id || !version)
|
|
1007
|
+
throw new CliError(
|
|
1008
|
+
"Usage: admin tuning promote-shredder <experiment-id> --version <shredder-version-id>"
|
|
1009
|
+
);
|
|
1010
|
+
return printResult(
|
|
1011
|
+
io,
|
|
1012
|
+
{ ...flags, json: true },
|
|
1013
|
+
await staffRequest(
|
|
1014
|
+
ctx,
|
|
1015
|
+
"POST",
|
|
1016
|
+
`${EXPERIMENTS}/${id}/promote`,
|
|
1017
|
+
compact({ shredder_version_id: version, actor: actorFrom(flags) })
|
|
1018
|
+
)
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
// --- Enhance: our enhancer's versions as files, comparisons, the deploy split ---
|
|
1022
|
+
case "enhance-compare": {
|
|
1023
|
+
const title = flagStr(flags.title);
|
|
1024
|
+
const versionId = flagStr(flags.candidate);
|
|
1025
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
1026
|
+
const nullCheck = flagBool(flags["null-check"]);
|
|
1027
|
+
const named = [versionId !== undefined, patch !== undefined, nullCheck === true].filter(Boolean).length;
|
|
1028
|
+
if (!title || named !== 1)
|
|
1029
|
+
throw new CliError(
|
|
1030
|
+
"Usage: admin tuning enhance-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
1031
|
+
);
|
|
1032
|
+
const candidate = versionId ? { version_id: versionId } : patch ? { patch } : { null_check: true };
|
|
1033
|
+
const r = await staffRequest(
|
|
1034
|
+
ctx,
|
|
1035
|
+
"POST",
|
|
1036
|
+
`${T}/comparisons`,
|
|
1037
|
+
compact({
|
|
1038
|
+
primitive: "enhance",
|
|
1039
|
+
title,
|
|
1040
|
+
candidate,
|
|
1041
|
+
keys: flagList(flags.keys),
|
|
1042
|
+
concurrency: flagNum(flags.concurrency),
|
|
1043
|
+
experiment_id: flagStr(flags.experiment),
|
|
1044
|
+
})
|
|
1045
|
+
);
|
|
1046
|
+
return json ? printResult(io, flags, r) : io.log(`enhance comparison ${r.comparison?.id ?? ""} started`);
|
|
1047
|
+
}
|
|
1048
|
+
case "enhance-inputs": {
|
|
1049
|
+
const set = flagStr(flags.set);
|
|
1050
|
+
return printResult(
|
|
1051
|
+
io,
|
|
1052
|
+
{ ...flags, json: true },
|
|
1053
|
+
await staffRequest(ctx, "GET", `${T}/enhance/inputs${set ? `?${new URLSearchParams({ set })}` : ""}`)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
case "enhance-split": {
|
|
1057
|
+
const q = new URLSearchParams(
|
|
1058
|
+
compact({
|
|
1059
|
+
since_hours: flagStr(flags["since-hours"]),
|
|
1060
|
+
window_hours: flagStr(flags["window-hours"]),
|
|
1061
|
+
at: flagStr(flags.at),
|
|
1062
|
+
})
|
|
1063
|
+
);
|
|
1064
|
+
return printResult(
|
|
1065
|
+
io,
|
|
1066
|
+
{ ...flags, json: true },
|
|
1067
|
+
await staffRequest(ctx, "GET", `${T}/enhance/deploy-split${q.size ? `?${q}` : ""}`)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
case "enhance-standing":
|
|
1071
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/enhance/standing`));
|
|
1072
|
+
case "enhancers": {
|
|
1073
|
+
const r = await staffRequest(ctx, "GET", `${T}/enhancer-files`);
|
|
1074
|
+
if (json) return printResult(io, flags, r);
|
|
1075
|
+
return io.log(
|
|
1076
|
+
r.versions
|
|
1077
|
+
.map((v) => `${v.label.padEnd(18)} ${v.status.padEnd(9)} ${v.id}${v.note ? ` ${v.note}` : ""}`)
|
|
1078
|
+
.join("\n") || "(no versions)"
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
case "enhancer": {
|
|
1082
|
+
if (!id) throw new CliError("Usage: admin tuning enhancer <version-id>");
|
|
1083
|
+
return printResult(
|
|
1084
|
+
io,
|
|
1085
|
+
{ ...flags, json: true },
|
|
1086
|
+
await staffRequest(ctx, "GET", `${T}/enhancer-files/${id}`)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
case "enhancer-candidate": {
|
|
1090
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
1091
|
+
const file = jsonFile({ "enhancer-file": flags.file }, "enhancer");
|
|
1092
|
+
if ((patch === undefined) === (file === undefined))
|
|
1093
|
+
throw new CliError(
|
|
1094
|
+
'Usage: admin tuning enhancer-candidate --patch \'{"internalLinks":{"maxPerPost":3}}\' | --file enhancer.json [--of <version-id>] [--note "…"]'
|
|
1095
|
+
);
|
|
1096
|
+
const r = await staffRequest(
|
|
1097
|
+
ctx,
|
|
1098
|
+
"POST",
|
|
1099
|
+
`${T}/enhancer-files`,
|
|
1100
|
+
compact({ patch, file, patch_of: flagStr(flags.of), note: flagStr(flags.note) })
|
|
1101
|
+
);
|
|
1102
|
+
return json ? printResult(io, flags, r) : io.log(`saved candidate ${r.version.label} (${r.version.id})`);
|
|
1103
|
+
}
|
|
1104
|
+
case "promote-enhancer": {
|
|
1105
|
+
const version = flagStr(flags.version);
|
|
1106
|
+
if (!id || !version)
|
|
1107
|
+
throw new CliError(
|
|
1108
|
+
"Usage: admin tuning promote-enhancer <experiment-id> --version <enhancer-version-id>"
|
|
1109
|
+
);
|
|
1110
|
+
return printResult(
|
|
1111
|
+
io,
|
|
1112
|
+
{ ...flags, json: true },
|
|
1113
|
+
await staffRequest(
|
|
1114
|
+
ctx,
|
|
1115
|
+
"POST",
|
|
1116
|
+
`${EXPERIMENTS}/${id}/promote`,
|
|
1117
|
+
compact({ enhancer_version_id: version, actor: actorFrom(flags) })
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
663
1121
|
case "promote": {
|
|
664
1122
|
const list = flagList(flags.versions);
|
|
665
1123
|
if (!id || !list?.length)
|
|
@@ -683,7 +1141,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
683
1141
|
}
|
|
684
1142
|
default:
|
|
685
1143
|
throw new CliError(
|
|
686
|
-
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|pair|promote|arenas|arena|arena-candidate|research-compare|research-inputs|research-split|research-standing|promote-arena (see --help)"
|
|
1144
|
+
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|pair|candidates|base-kernels|base-versions|sessions|session|session-new|propose|trial-add|trial-run|trial-save|promote|arenas|arena|arena-candidate|research-compare|research-inputs|research-split|research-standing|promote-arena|shred-compare|shred-inputs|shred-split|shred-standing|shredders|shredder|shredder-candidate|promote-shredder|enhance-compare|enhance-inputs|enhance-split|enhance-standing|enhancers|enhancer|enhancer-candidate|promote-enhancer (see --help)"
|
|
687
1145
|
);
|
|
688
1146
|
}
|
|
689
1147
|
}
|
package/lib/commands/autopen.mjs
CHANGED
|
@@ -18,6 +18,8 @@ export const ARGS = {
|
|
|
18
18
|
run_auto_pen: {
|
|
19
19
|
article_id: { flag: "article", required: true },
|
|
20
20
|
from_scratch: { flag: "from-scratch", type: "bool" },
|
|
21
|
+
// --skip finish,metadata: leave those steps out of this run only.
|
|
22
|
+
skip_steps: { flag: "skip", type: "list" },
|
|
21
23
|
},
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `enhance` — the passes that run after the writer: visuals, internal links, external links
|
|
2
|
+
// (citations) and metadata. Run one on a post now, read what each run did, and read or change the
|
|
3
|
+
// enhance file a collection's passes run (its models and settings), the same versioned-file pattern
|
|
4
|
+
// as a kernel, an arena and a shredder. Every command is a projection of one tool (docs/api).
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { CliError } from "../client.mjs";
|
|
8
|
+
import { flagStr, printResult, ok, toolField, argsFromFlags } from "./shared.mjs";
|
|
9
|
+
|
|
10
|
+
const PASSES = ["visuals", "internal_links", "external_links", "metadata"];
|
|
11
|
+
const OUTCOMES = ["added", "none", "skipped", "failed"];
|
|
12
|
+
|
|
13
|
+
/** `--file <path>` → the file's parsed JSON, or undefined when the flag is not set. */
|
|
14
|
+
function readJsonFile(flags) {
|
|
15
|
+
const path = flagStr(flags.file);
|
|
16
|
+
if (path === undefined) return undefined;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new CliError(`--file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `--patch '<json>'` → a JSON object, or undefined. */
|
|
25
|
+
function readPatch(flags) {
|
|
26
|
+
const inline = flagStr(flags.patch);
|
|
27
|
+
if (inline === undefined) return undefined;
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(inline);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new CliError(`--patch is not valid JSON: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
35
|
+
throw new CliError("--patch must be a JSON object");
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How every command sources its tool's inputs (shared.mjs `argsFromFlags`; scripts/parity-check.ts
|
|
41
|
+
* diffs these keys against each tool's published schema).
|
|
42
|
+
*/
|
|
43
|
+
export const ARGS = {
|
|
44
|
+
run_enhance_pass: {
|
|
45
|
+
article_id: { positional: 0, label: "article-id", required: true },
|
|
46
|
+
pass: { flag: "pass", required: true, enum: PASSES },
|
|
47
|
+
},
|
|
48
|
+
get_enhance_run: {
|
|
49
|
+
run_id: { positional: 0, label: "run-id", required: true },
|
|
50
|
+
},
|
|
51
|
+
list_enhance_runs: {
|
|
52
|
+
article_id: "article",
|
|
53
|
+
auto_pen_id: "auto-pen",
|
|
54
|
+
collection_id: "collection",
|
|
55
|
+
pass: { flag: "pass", enum: PASSES },
|
|
56
|
+
outcome: { flag: "outcome", enum: OUTCOMES },
|
|
57
|
+
since: "since",
|
|
58
|
+
limit: { flag: "limit", type: "num" },
|
|
59
|
+
},
|
|
60
|
+
get_enhancer_file: {
|
|
61
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
62
|
+
},
|
|
63
|
+
save_enhancer_file: {
|
|
64
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
65
|
+
file: { value: (flags) => readJsonFile(flags) },
|
|
66
|
+
patch: { value: (flags) => readPatch(flags) },
|
|
67
|
+
note: "note",
|
|
68
|
+
},
|
|
69
|
+
list_enhancer_versions: {
|
|
70
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
71
|
+
},
|
|
72
|
+
pin_enhancer_version: {
|
|
73
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
74
|
+
version_id: {
|
|
75
|
+
value: (_flags, positionals) => {
|
|
76
|
+
const v = positionals[1];
|
|
77
|
+
if (v === undefined) throw new CliError("Missing required <version-id|none>");
|
|
78
|
+
return v === "none" ? null : v;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
accept_enhancer_offer: {
|
|
83
|
+
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export async function cmdEnhance(ctx) {
|
|
88
|
+
const sub = ctx.positionals[0];
|
|
89
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
90
|
+
switch (sub) {
|
|
91
|
+
case "run":
|
|
92
|
+
return enhanceTool(rest, "run_enhance_pass");
|
|
93
|
+
case "runs":
|
|
94
|
+
return enhanceTool(rest, "list_enhance_runs");
|
|
95
|
+
case "get":
|
|
96
|
+
case "show":
|
|
97
|
+
return enhanceTool(rest, "get_enhance_run");
|
|
98
|
+
case "file":
|
|
99
|
+
return enhanceFile(rest);
|
|
100
|
+
case "versions":
|
|
101
|
+
return enhanceTool(rest, "list_enhancer_versions");
|
|
102
|
+
case "pin":
|
|
103
|
+
return enhanceTool(rest, "pin_enhancer_version");
|
|
104
|
+
case "offer":
|
|
105
|
+
return rest.flags.accept === true ? enhanceTool(rest, "accept_enhancer_offer") : enhanceOffer(rest);
|
|
106
|
+
default:
|
|
107
|
+
throw new CliError(
|
|
108
|
+
`Unknown enhance subcommand: ${sub ?? "(none)"}. Try: run <article-id> --pass <pass>, runs, get <run-id>, file <collection-id>, versions, pin, offer`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function enhanceTool(ctx, tool) {
|
|
114
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
115
|
+
printResult(ctx.io, ctx.flags, result);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function enhanceFile(ctx) {
|
|
120
|
+
const { client, flags, io } = ctx;
|
|
121
|
+
const saving = flagStr(flags.file) !== undefined || flagStr(flags.patch) !== undefined;
|
|
122
|
+
if (!saving) {
|
|
123
|
+
const result = await client.callTool(
|
|
124
|
+
"get_enhancer_file",
|
|
125
|
+
argsFromFlags(ARGS.get_enhancer_file, flags, ctx.positionals)
|
|
126
|
+
);
|
|
127
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.file ?? r, null, 2));
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const result = await client.callTool(
|
|
131
|
+
"save_enhancer_file",
|
|
132
|
+
argsFromFlags(ARGS.save_enhancer_file, flags, ctx.positionals)
|
|
133
|
+
);
|
|
134
|
+
ok(
|
|
135
|
+
ctx,
|
|
136
|
+
`Saved ${toolField(result, "version")?.label ?? "a new version"}; the collection's passes run it from now on.`
|
|
137
|
+
);
|
|
138
|
+
printResult(io, flags, result);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function enhanceOffer(ctx) {
|
|
143
|
+
const { client, flags, io } = ctx;
|
|
144
|
+
const result = await client.callTool(
|
|
145
|
+
"get_enhancer_file",
|
|
146
|
+
argsFromFlags(ARGS.get_enhancer_file, flags, ctx.positionals)
|
|
147
|
+
);
|
|
148
|
+
const offer = toolField(result, "offer");
|
|
149
|
+
if (!offer) ok(ctx, "No offer: the collection runs our latest version, or a copy based on it.");
|
|
150
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.offer ?? null, null, 2));
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
package/lib/commands/evals.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { CliError } from "../client.mjs";
|
|
17
17
|
import { flagStr, readBodyInput, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
18
18
|
|
|
19
|
-
const STAGES = ["research", "brief", "links", "mentions", "accuracy", "pipeline"];
|
|
19
|
+
const STAGES = ["research", "brief", "links", "mentions", "accuracy", "pipeline", "shred", "enhance"];
|
|
20
20
|
const STATUSES = ["proposed", "open", "guarding", "retired", "rejected"];
|
|
21
21
|
|
|
22
22
|
/** `--keys a,b,c` (or repeated `--keys`) → ["a", "b", "c"]; undefined when unset. */
|
|
@@ -35,6 +35,23 @@ function readJsonFileFlag(flags, flag) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** `--<flag> '<json>'` inline, or `--<flag>-file <path>` → a JSON object, or undefined. */
|
|
39
|
+
function readJsonObjectFlag(flags, name) {
|
|
40
|
+
const inline = flagStr(flags[name]);
|
|
41
|
+
if (inline !== undefined) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(inline);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new CliError(`--${name} is not valid JSON: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
49
|
+
throw new CliError(`--${name} must be a JSON object`);
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
return readJsonFileFlag(flags, `${name}-file`);
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
/**
|
|
39
56
|
* How every command in this file sources its tool's inputs — see `argsFromFlags` in
|
|
40
57
|
* shared.mjs and `scripts/parity-check.ts`, which diffs these keys against each tool's
|
|
@@ -68,6 +85,10 @@ export const ARGS = {
|
|
|
68
85
|
},
|
|
69
86
|
list_shredder_endpoints: {},
|
|
70
87
|
get_shred_balance: {},
|
|
88
|
+
// The rewriter families behind the shredder, as the platform's hourly watch reads them.
|
|
89
|
+
get_shred_family_health: {
|
|
90
|
+
hours: { flag: "hours", type: "num" },
|
|
91
|
+
},
|
|
71
92
|
create_shredder_endpoint: {
|
|
72
93
|
name: { flag: "name", required: true },
|
|
73
94
|
},
|
|
@@ -83,6 +104,33 @@ export const ARGS = {
|
|
|
83
104
|
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
84
105
|
config: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
85
106
|
},
|
|
107
|
+
// The shredder as a versioned file (the arena file's twin): `shredder file <id>` prints it,
|
|
108
|
+
// `--file f.json` saves a whole file, `--patch '{…}'` only the fields to change.
|
|
109
|
+
get_shredder_file: {
|
|
110
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
111
|
+
},
|
|
112
|
+
save_shredder_file: {
|
|
113
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
114
|
+
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
115
|
+
patch: { value: (flags) => readJsonObjectFlag(flags, "patch") },
|
|
116
|
+
note: "note",
|
|
117
|
+
},
|
|
118
|
+
list_shredder_versions: {
|
|
119
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
120
|
+
},
|
|
121
|
+
pin_shredder_version: {
|
|
122
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
123
|
+
version_id: {
|
|
124
|
+
value: (_flags, positionals) => {
|
|
125
|
+
const v = positionals[1];
|
|
126
|
+
if (v === undefined) throw new CliError("Missing required <version-id|none>");
|
|
127
|
+
return v === "none" ? null : v;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
accept_shredder_offer: {
|
|
132
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
133
|
+
},
|
|
86
134
|
};
|
|
87
135
|
|
|
88
136
|
export async function cmdShredder(ctx) {
|
|
@@ -111,9 +159,19 @@ export async function cmdShredder(ctx) {
|
|
|
111
159
|
return shredderRuns(rest);
|
|
112
160
|
case "balance":
|
|
113
161
|
return shredderBalance(rest);
|
|
162
|
+
case "file":
|
|
163
|
+
return shredderFile(rest);
|
|
164
|
+
case "versions":
|
|
165
|
+
return shredderTool(rest, "list_shredder_versions");
|
|
166
|
+
case "pin":
|
|
167
|
+
return shredderTool(rest, "pin_shredder_version");
|
|
168
|
+
case "offer":
|
|
169
|
+
return rest.flags.accept === true ? shredderTool(rest, "accept_shredder_offer") : shredderOffer(rest);
|
|
170
|
+
case "families":
|
|
171
|
+
return shredderTool(rest, "get_shred_family_health");
|
|
114
172
|
default:
|
|
115
173
|
throw new CliError(
|
|
116
|
-
`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, job, runs, balance`
|
|
174
|
+
`Unknown shredder subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, job, runs, balance, file, versions, pin, offer, families`
|
|
117
175
|
);
|
|
118
176
|
}
|
|
119
177
|
}
|
|
@@ -284,3 +342,54 @@ async function shredderBalance(ctx) {
|
|
|
284
342
|
printResult(io, flags, result);
|
|
285
343
|
return 0;
|
|
286
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* `shredder file <endpoint-id>` prints the file the endpoint shreds with (the file itself, so
|
|
348
|
+
* `> f.json` round-trips); `shredder file <endpoint-id> --file f.json | --patch '{…}' [--note …]`
|
|
349
|
+
* saves a new live version of the endpoint's own copy.
|
|
350
|
+
*/
|
|
351
|
+
async function shredderFile(ctx) {
|
|
352
|
+
const { client, flags, io } = ctx;
|
|
353
|
+
const saving =
|
|
354
|
+
flagStr(flags.file) !== undefined ||
|
|
355
|
+
flagStr(flags.patch) !== undefined ||
|
|
356
|
+
flagStr(flags["patch-file"]) !== undefined;
|
|
357
|
+
if (!saving) {
|
|
358
|
+
const result = await client.callTool(
|
|
359
|
+
"get_shredder_file",
|
|
360
|
+
argsFromFlags(ARGS.get_shredder_file, flags, ctx.positionals)
|
|
361
|
+
);
|
|
362
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.file ?? r, null, 2));
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
const result = await client.callTool(
|
|
366
|
+
"save_shredder_file",
|
|
367
|
+
argsFromFlags(ARGS.save_shredder_file, flags, ctx.positionals)
|
|
368
|
+
);
|
|
369
|
+
ok(
|
|
370
|
+
ctx,
|
|
371
|
+
`Saved ${toolField(result, "version")?.label ?? "a new version"}; the endpoint shreds with it from now on.`
|
|
372
|
+
);
|
|
373
|
+
printResult(io, flags, result);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** `shredder offer <endpoint-id>`: what our newer version would change; `--accept` takes it. */
|
|
378
|
+
async function shredderOffer(ctx) {
|
|
379
|
+
const { client, flags, io } = ctx;
|
|
380
|
+
const result = await client.callTool(
|
|
381
|
+
"get_shredder_file",
|
|
382
|
+
argsFromFlags(ARGS.get_shredder_file, flags, ctx.positionals)
|
|
383
|
+
);
|
|
384
|
+
const offer = toolField(result, "offer");
|
|
385
|
+
if (!offer) ok(ctx, "No offer: your endpoint shreds with our latest version, or a copy based on it.");
|
|
386
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.offer ?? null, null, 2));
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** One shredder-file tool, its arguments from ARGS. */
|
|
391
|
+
async function shredderTool(ctx, tool) {
|
|
392
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
393
|
+
printResult(ctx.io, ctx.flags, result);
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export * from "./commands/lede.mjs";
|
|
|
32
32
|
export * from "./commands/onboard.mjs";
|
|
33
33
|
export * from "./commands/seers.mjs";
|
|
34
34
|
export * from "./commands/shredder.mjs";
|
|
35
|
+
export * from "./commands/enhance.mjs";
|
|
35
36
|
export * from "./commands/landings.mjs";
|
|
36
37
|
export * from "./commands/outbound.mjs";
|
|
37
38
|
export * from "./commands/offerings.mjs";
|