@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 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.60.1";
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
- New draft sequence (nothing sends yet; rails default on, opt-out line off)
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
@@ -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
- const q = query({ owner: flagStr(flags.owner), overdue: flagBool(flags.overdue) ? "true" : undefined });
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.findings ?? []).map(
1808
- (f) =>
1809
- `${f.id} ${(f.owner ?? "unassigned").padEnd(10)} ${f.evalKey.padEnd(36)} ${f.overdue ? "OVERDUE " : ""}${f.title ?? f.summary ?? ""}`
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} open, ${r.overdue ?? 0} overdue`,
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"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.60.1",
3
+ "version": "0.61.1",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",