@letterstory/cli 0.60.1 → 0.61.1
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 +16 -2
- package/lib/commands/admin.mjs +46 -5
- package/lib/commands/outbound.mjs +29 -2
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from "./commands.mjs";
|
|
56
56
|
|
|
57
57
|
// Keep in sync with cli/package.json.
|
|
58
|
-
export const VERSION = "0.
|
|
58
|
+
export const VERSION = "0.61.1";
|
|
59
59
|
|
|
60
60
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
61
61
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -604,6 +604,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
604
604
|
| cases [--org <uuid>] [--status s] [--stage s] | runs [--limit N] [--mode record|full]
|
|
605
605
|
| run [--mode record|full] [--keys a,b] [--org <uuid>] [--baseline latest|<id>]
|
|
606
606
|
| triage [--owner research|kernel|links|mentions|accuracy|pipeline] [--overdue]
|
|
607
|
+
[--eval <key> [--org <id>|fleet] --assign <owner> | --dismiss "…"] patterns (measure × org)
|
|
607
608
|
| guard <case-key> [--sha <commit>] [--fix-pr #N] | retire <case-key> --reason "…"
|
|
608
609
|
The fleet-wide eval surface: scoreboard,
|
|
609
610
|
every org's cases and suite runs, the
|
|
@@ -613,6 +614,16 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
613
614
|
admin tuning-context [--q "<question>"] The experiment agent's context pack (JSON):
|
|
614
615
|
the pipeline as deployed, levers, measures,
|
|
615
616
|
goals, related history.
|
|
617
|
+
admin tuning desk What waits on a person: comparisons to read
|
|
618
|
+
blind, questions to decide, versions to
|
|
619
|
+
promote, open corpus flags, overdue patterns.
|
|
620
|
+
admin tuning <sub> … The console's loop over a staff key: kernels,
|
|
621
|
+
candidate, compare, comparisons, picks, pick,
|
|
622
|
+
reading-list, pair, side-by-side, promote,
|
|
623
|
+
sessions, session-new, propose, trial-add,
|
|
624
|
+
trial-run, trial-save, and the arena, shredder
|
|
625
|
+
and enhancer twins (every flag is listed in
|
|
626
|
+
cli/lib/commands/admin.mjs).
|
|
616
627
|
admin create-org --domain <bare-domain> [--name "Acme"] [--test] [--no-key]
|
|
617
628
|
[--key-name N] [--capabilities a,b] [--onboard] [--watch] [+ the onboard domain flags]
|
|
618
629
|
The key exchange: create the client org from
|
|
@@ -796,10 +807,13 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
796
807
|
sequences get <sequence-id> Flow, enrollments, who replied at each step
|
|
797
808
|
sequences create --name <n> (--flow '<json>'|--flow-file <path>) [--mailer <mailer-id>]...
|
|
798
809
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
799
|
-
|
|
810
|
+
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
811
|
+
New draft sequence (nothing sends yet; rails default on, opt-out line off;
|
|
812
|
+
sends 9:00-17:00 in each recipient's inferred timezone, else the sender's)
|
|
800
813
|
sequences update <sequence-id> [--name <n>] [--flow '<json>'|--flow-file <path>]
|
|
801
814
|
[--status draft|active|paused|archived] [--mailer <mailer-id>]...
|
|
802
815
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
816
|
+
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
803
817
|
sequences pause|resume <sequence-id> Stop / restart sending
|
|
804
818
|
sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
|
|
805
819
|
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -420,6 +420,7 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
420
420
|
* and promotion from the ledger (docs/architecture/tuning-foundation.md): the headless half of the
|
|
421
421
|
* tuning console's Write › Kernels and Compare tabs. Needs `tuning:experiment` on the key.
|
|
422
422
|
*
|
|
423
|
+
* admin tuning desk what waits on a person: comparisons to read blind, questions to decide, versions to promote, open flags, overdue findings
|
|
423
424
|
* admin tuning kernels every catalog kernel, live version, writer, candidates
|
|
424
425
|
* admin tuning kernel <service-kernel-id> [--version N|live] one version's file (JSON)
|
|
425
426
|
* admin tuning candidate <service-kernel-id> --file f.json [--note "…"] save a file as a candidate version
|
|
@@ -529,6 +530,20 @@ async function cmdAdminTuning(ctx) {
|
|
|
529
530
|
const json = flagBool(flags.json);
|
|
530
531
|
const T = "/api/superadmin/tuning";
|
|
531
532
|
switch (sub) {
|
|
533
|
+
case "desk": {
|
|
534
|
+
const d = await staffRequest(ctx, "GET", `${T}/desk`);
|
|
535
|
+
if (json) return printResult(io, flags, d);
|
|
536
|
+
const lines = [];
|
|
537
|
+
for (const c of d.to_read)
|
|
538
|
+
lines.push(`read blind ${c.read}/${c.target} ${c.primitive.padEnd(8)} ${c.title} ${c.id}`);
|
|
539
|
+
for (const e of d.to_decide) lines.push(`decide ${e.status.padEnd(9)} ${e.question} ${e.id}`);
|
|
540
|
+
for (const e of d.to_promote) lines.push(`promote ship ${e.question} ${e.id}`);
|
|
541
|
+
if (d.flags_open) lines.push(`flags ${d.flags_open} corpus flags open`);
|
|
542
|
+
if (d.findings_overdue)
|
|
543
|
+
lines.push(`triage ${d.findings_overdue} shipped patterns past their due date`);
|
|
544
|
+
for (const err of d.errors ?? []) lines.push(`(could not read: ${err})`);
|
|
545
|
+
return io.log(lines.join("\n") || "Nothing waits on a person.");
|
|
546
|
+
}
|
|
532
547
|
case "kernels": {
|
|
533
548
|
const r = await staffRequest(ctx, "GET", `${T}/kernel-files`);
|
|
534
549
|
if (json) return printResult(io, flags, r);
|
|
@@ -1801,14 +1816,40 @@ async function cmdAdminEvals(ctx) {
|
|
|
1801
1816
|
return r.run?.verdict === "fail" ? 1 : 0;
|
|
1802
1817
|
}
|
|
1803
1818
|
case "triage": {
|
|
1804
|
-
|
|
1819
|
+
// A decision names its pattern: --eval <key> and --org <id> (omitted = the fleet-wide one).
|
|
1820
|
+
const assign = flagStr(flags.assign);
|
|
1821
|
+
const dismiss = flagStr(flags.dismiss);
|
|
1822
|
+
if (assign || dismiss) {
|
|
1823
|
+
const evalKey = flagStr(flags.eval);
|
|
1824
|
+
if (!evalKey)
|
|
1825
|
+
throw new CliError(
|
|
1826
|
+
'Usage: admin evals triage --eval <key> [--org <id>] --assign <owner> | --dismiss "…"'
|
|
1827
|
+
);
|
|
1828
|
+
if (assign && dismiss) throw new CliError("Send --assign or --dismiss, not both.");
|
|
1829
|
+
const orgFlag = flagStr(flags.org);
|
|
1830
|
+
const body = {
|
|
1831
|
+
eval_key: evalKey,
|
|
1832
|
+
org_id: orgFlag && orgFlag !== "fleet" ? orgFlag : null,
|
|
1833
|
+
...(assign ? { owner: assign } : { reason: dismiss }),
|
|
1834
|
+
};
|
|
1835
|
+
return out(
|
|
1836
|
+
await staffRequest(ctx, "PATCH", `${EVALS}/triage`, body),
|
|
1837
|
+
(r) =>
|
|
1838
|
+
`${r.affected ?? 0} finding(s) ${r.action === "assign" ? `handed to ${assign}` : "dismissed"} in ${evalKey} × ${body.org_id ?? "fleet"}`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const q = query({
|
|
1842
|
+
owner: flagStr(flags.owner),
|
|
1843
|
+
overdue: flagBool(flags.overdue) ? "true" : undefined,
|
|
1844
|
+
limit: flagStr(flags.limit),
|
|
1845
|
+
});
|
|
1805
1846
|
return out(await staffRequest(ctx, "GET", `${EVALS}/triage${q}`), (r) =>
|
|
1806
1847
|
[
|
|
1807
|
-
...(r.
|
|
1808
|
-
(
|
|
1809
|
-
`${
|
|
1848
|
+
...(r.patterns ?? []).map(
|
|
1849
|
+
(p) =>
|
|
1850
|
+
`${p.eval_key.padEnd(36)} ${(p.org_name ?? p.org_id ?? "fleet").padEnd(24)} ${(p.owner ?? "unassigned").padEnd(10)} ${String(p.count).padStart(5)} ${p.overdue ? "OVERDUE " : "due "}${p.sla_due_at ? p.sla_due_at.slice(0, 10) : "—"}`
|
|
1810
1851
|
),
|
|
1811
|
-
`${r.total ?? 0}
|
|
1852
|
+
`${r.total ?? 0} patterns (${r.findings ?? 0} findings), ${r.overdue ?? 0} overdue`,
|
|
1812
1853
|
].join("\n")
|
|
1813
1854
|
);
|
|
1814
1855
|
}
|
|
@@ -60,6 +60,31 @@ const SEQUENCE_RAIL_ARGS = {
|
|
|
60
60
|
unsubscribe_links: { flag: "unsubscribe-links", type: "bool", negate: "no-unsubscribe-links" },
|
|
61
61
|
detect_reply_opt_outs: { flag: "detect-reply-opt-outs", type: "bool", negate: "no-detect-reply-opt-outs" },
|
|
62
62
|
};
|
|
63
|
+
|
|
64
|
+
// A send-window edge from --<flag> HH:MM (or a raw minute of day) → the
|
|
65
|
+
// schema's minute-of-day number.
|
|
66
|
+
function windowMinute(flag) {
|
|
67
|
+
return (flags) => {
|
|
68
|
+
const raw = flagStr(flags[flag]);
|
|
69
|
+
if (raw === undefined) return undefined;
|
|
70
|
+
if (/^\d+$/.test(raw)) return Number(raw);
|
|
71
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(raw);
|
|
72
|
+
if (!match || Number(match[1]) > 23 || Number(match[2]) > 59) {
|
|
73
|
+
throw new CliError(`--${flag} takes HH:MM or a minute of day 0–1439 (got ${raw})`);
|
|
74
|
+
}
|
|
75
|
+
return Number(match[1]) * 60 + Number(match[2]);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The send window, shared by create and update: when emails may go out
|
|
80
|
+
// (default 9:00–17:00), in each recipient's inferred timezone, falling back to
|
|
81
|
+
// the sender's --send-window-timezone when the location is unknown. Equal
|
|
82
|
+
// start and end turns the window off.
|
|
83
|
+
const SEQUENCE_WINDOW_ARGS = {
|
|
84
|
+
send_window_start_minute: { value: windowMinute("send-window-start") },
|
|
85
|
+
send_window_end_minute: { value: windowMinute("send-window-end") },
|
|
86
|
+
send_window_timezone: "send-window-timezone",
|
|
87
|
+
};
|
|
63
88
|
const PROSPECT_STATUSES = ["new", "contacted", "replied", "unsubscribed", "archived"];
|
|
64
89
|
const mailerIds = {
|
|
65
90
|
value: (flags) => {
|
|
@@ -101,6 +126,7 @@ export const ARGS = {
|
|
|
101
126
|
},
|
|
102
127
|
mailer_ids: mailerIds,
|
|
103
128
|
...SEQUENCE_RAIL_ARGS,
|
|
129
|
+
...SEQUENCE_WINDOW_ARGS,
|
|
104
130
|
},
|
|
105
131
|
update_sequence: {
|
|
106
132
|
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
@@ -109,6 +135,7 @@ export const ARGS = {
|
|
|
109
135
|
status: { flag: "status", enum: SEQUENCE_STATUSES },
|
|
110
136
|
mailer_ids: mailerIds,
|
|
111
137
|
...SEQUENCE_RAIL_ARGS,
|
|
138
|
+
...SEQUENCE_WINDOW_ARGS,
|
|
112
139
|
},
|
|
113
140
|
enroll_in_sequence: {
|
|
114
141
|
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
@@ -260,8 +287,8 @@ export const ARGS = {
|
|
|
260
287
|
|
|
261
288
|
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
262
289
|
const VALUE_FLAGS = {
|
|
263
|
-
create_sequence: ["flow", "flow-file", "mailer"],
|
|
264
|
-
update_sequence: ["flow", "flow-file", "mailer"],
|
|
290
|
+
create_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
291
|
+
update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
265
292
|
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
266
293
|
update_review_item: ["body", "file"],
|
|
267
294
|
import_prospects_csv: ["csv", "csv-file"],
|