@letterstory/cli 0.57.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 +17 -0
- package/lib/cli.mjs +4 -1
- package/lib/commands/admin.mjs +108 -1
- package/lib/commands/autopen.mjs +2 -0
- package/lib/commands/enhance.mjs +152 -0
- package/lib/commands/evals.mjs +1 -1
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -595,6 +595,23 @@ it to `0` in CI so a test does not wait on a real interval.
|
|
|
595
595
|
`collection run --topic "…" --collection <uuid>` runs topic → draft → publish → rebuild
|
|
596
596
|
end to end, with `collection run-status <job-id>` to follow it.
|
|
597
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
|
+
|
|
598
615
|
## Seers — event-driven signals
|
|
599
616
|
|
|
600
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.
|
|
@@ -936,6 +937,8 @@ const CLIENT_COMMANDS = {
|
|
|
936
937
|
templates: cmdTemplates,
|
|
937
938
|
compositions: cmdCompositions,
|
|
938
939
|
shredder: cmdShredder,
|
|
940
|
+
enhance: cmdEnhance,
|
|
941
|
+
enhancer: cmdEnhance,
|
|
939
942
|
lede: cmdLede,
|
|
940
943
|
ledes: cmdLede,
|
|
941
944
|
landings: cmdLandings,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -463,6 +463,13 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
463
463
|
* admin tuning shredders | shredder <version-id> our shredder's versions / one version's file and diff
|
|
464
464
|
* admin tuning shredder-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
465
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)
|
|
466
473
|
*/
|
|
467
474
|
/** `--flag '<json>'` → the parsed object, or undefined when the flag is absent. */
|
|
468
475
|
function jsonFlag(raw, name) {
|
|
@@ -1011,6 +1018,106 @@ async function cmdAdminTuning(ctx) {
|
|
|
1011
1018
|
)
|
|
1012
1019
|
);
|
|
1013
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
|
+
}
|
|
1014
1121
|
case "promote": {
|
|
1015
1122
|
const list = flagList(flags.versions);
|
|
1016
1123
|
if (!id || !list?.length)
|
|
@@ -1034,7 +1141,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
1034
1141
|
}
|
|
1035
1142
|
default:
|
|
1036
1143
|
throw new CliError(
|
|
1037
|
-
"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 (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)"
|
|
1038
1145
|
);
|
|
1039
1146
|
}
|
|
1040
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", "shred"];
|
|
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. */
|
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";
|