@letterstory/cli 0.98.4 → 0.99.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/lib/cli.mjs CHANGED
@@ -59,7 +59,7 @@ import {
59
59
  } from "./commands.mjs";
60
60
 
61
61
  // Keep in sync with cli/package.json.
62
- export const VERSION = "0.98.4";
62
+ export const VERSION = "0.99.0";
63
63
 
64
64
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
65
65
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -93,6 +93,7 @@ export const BOOLEAN_FLAGS = new Set([
93
93
  "stdin",
94
94
  // `authors generate <id> --with-bios`.
95
95
  "with-bios",
96
+ "all-live",
96
97
  // `admin tuning climb-new … --no-run`; `rung-propose --accept --with-replay` / `rung-accept --with-replay`
97
98
  // (not `--replay`: `evals` takes `--replay <stage>`).
98
99
  "no-run",
@@ -213,6 +214,12 @@ export const BOOLEAN_FLAGS = new Set([
213
214
  "suggested",
214
215
  "sync",
215
216
  "texts",
217
+ // admin crm outreach (launch-day list)
218
+ "archive",
219
+ "lookup",
220
+ "not-reached",
221
+ "reached",
222
+ "todo",
216
223
  ]);
217
224
 
218
225
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -653,6 +660,11 @@ Domain onboarding (ONE SHOT: a bare domain in, a fully-configured organization o
653
660
 
654
661
  Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org exists):
655
662
  admin ask "<question>" [--sql] [--json] Ask the whole fleet a question in English.
663
+ admin republish [<deployment-id>…] [--domains a.com,b.com] [--all-live] [--dry-run] [--json]
664
+ Rebuild live phantom sites (no content regenerated), 12 per
665
+ request; prints each failure's reason and exits 1 if any
666
+ failed. Needs a staff key with fleet:rebuild (+ fleet:read
667
+ to resolve --domains / --all-live).
656
668
  Answered from the live database with
657
669
  read-only SQL across every org; --sql prints
658
670
  the queries behind the answer. Needs the
@@ -691,13 +703,17 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
691
703
  Posts on independent sites that name the client,
692
704
  with the paragraphs that name it and the live URL.
693
705
  | mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…>
694
- | new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1
706
+ | new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|sponsor_disclosure_fresh_v1|article_disclosure_v1|article_disclosure_fresh_v1|mention_degree_v1|citation_shape_v1
695
707
  | new --spec-file spec.json A custom experiment ({key, hypothesis, unit, arms, response_var}).
696
708
  | assign <experiment-id> <unit-id…> | assign-eligible <experiment-id> | apply <experiment-id>
697
709
  | assign-orgs <experiment-id> <org-domain-or-id…>
698
710
  Site/collection arms (safe mode: our own sites, or one
699
711
  site per named client). A running experiment applies
700
712
  each new unit's arm at once; start applies them all.
713
+ | post-sites | spin-up-posts <experiment-id> <collection-id> [--pairs 1-5]
714
+ Fresh-post experiment: pairs of new posts on sibling
715
+ topics of one site, both naming the client once, one
716
+ born disclosed. Written and published on autopilot.
701
717
  | start|stop <experiment-id> | decide <experiment-id> --decision "…"
702
718
  Reads need fleet:read; writes fleet:experiment.
703
719
  admin experiments list [--q colons] [--status proposed] [--primitive write] [--stage rarity] | show <id>
@@ -14,7 +14,7 @@
14
14
  // The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
15
15
  // admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
16
16
  // admin fleet-experiments list|articles|clients|mark|unmark|new|assign|assign-eligible|assign-orgs|spin-up|apply|start|stop|decide … (fleet:read / fleet:experiment)
17
- // admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
17
+ // admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
18
18
  // admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
19
19
  // admin brand-detection <org-id> (fleet:read)
20
20
  // admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
@@ -543,7 +543,7 @@ async function cmdAdminTuningContext(ctx) {
543
543
  * admin tuning kernels every catalog kernel, live version, writer, candidates
544
544
  * admin tuning kernel <service-kernel-id> [--version N|live] one version's file (JSON)
545
545
  * admin tuning candidate <service-kernel-id> --file f.json [--note "…"] save a file as a candidate version
546
- * admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] start a comparison (--pairs caps it: breadth at 10–20, depth at the full 66)
546
+ * admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] start a comparison (default 10 pairs; --pairs caps it: 20 for breadth, 66 for depth; comparison-extend adds more)
547
547
  * (Hazel:23@22 reads v23 against the test version v22 instead of live; it cannot promote)
548
548
  * admin tuning comparisons | comparison <id> list / read one (per-measure verdicts)
549
549
  * admin tuning picks <comparison-id> [--criterion opening] the blind queue (next pairs); with a criterion, the pairs not yet read for it (Eye Test mode)
@@ -656,7 +656,7 @@ async function cmdAdminTuningContext(ctx) {
656
656
  * admin tuning research-compare --title "…" --candidates <version-id>,… | --patches '[{…},{…}]' several
657
657
  * candidates against ONE control arm (k + 1 runs per input, not 2k)
658
658
  * admin tuning arena-dials every arena setting: help, default, evidence, live's value
659
- * admin tuning comparison-extend <comparison-id> --keys k1,k2 | --all add inputs to a comparison on fixed inputs
659
+ * admin tuning comparison-extend <comparison-id> --pairs N | --keys k1 --keys k2 | --all add the next N pairs to a Write comparison, or inputs to one on fixed inputs
660
660
  * admin tuning research-inputs [--set v1] research's fixed inputs
661
661
  * admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
662
662
  * admin tuning research-standing live's level on the fixed inputs, week by week
@@ -845,7 +845,7 @@ async function cmdAdminTuning(ctx) {
845
845
  const list = flagList(flags.candidates);
846
846
  if (!title || !list?.length)
847
847
  throw new CliError(
848
- 'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20]'
848
+ 'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] (default 10 pairs)'
849
849
  );
850
850
  // kernel:version, or kernel:version@control to read it against a test version instead of live.
851
851
  const candidates = list.map((item) => {
@@ -2238,14 +2238,25 @@ async function cmdAdminTuning(ctx) {
2238
2238
  return io.log(lines.join("\n"));
2239
2239
  }
2240
2240
  case "comparison-extend": {
2241
+ // A Write comparison grows by the next N pairs; one on fixed inputs by named keys or all.
2242
+ const pairs = flagNum(flags.pairs);
2241
2243
  const keys = flagBool(flags.all) ? "all" : flagList(flags.keys);
2242
- if (!id || !keys?.length)
2243
- throw new CliError("Usage: admin tuning comparison-extend <comparison-id> --keys k1,k2 | --all");
2244
- const r = await staffRequest(ctx, "POST", `${T}/comparisons/${id}/extend`, { keys });
2244
+ const usage =
2245
+ "Usage: admin tuning comparison-extend <comparison-id> --pairs N | --keys k1 --keys k2 | --all";
2246
+ if (!id || (pairs === undefined && !keys?.length) || (pairs !== undefined && keys?.length))
2247
+ throw new CliError(usage);
2248
+ if (pairs !== undefined && (!Number.isInteger(pairs) || pairs < 1))
2249
+ throw new CliError("--pairs must be a whole number, 1 or more");
2250
+ const r = await staffRequest(
2251
+ ctx,
2252
+ "POST",
2253
+ `${T}/comparisons/${id}/extend`,
2254
+ pairs !== undefined ? { pairs } : { keys }
2255
+ );
2245
2256
  return json
2246
2257
  ? printResult(io, flags, r)
2247
2258
  : io.log(
2248
- `comparison ${r.comparison.id}: ${r.comparison.added.length} input(s) added, ${r.comparison.pairs} in all`
2259
+ `comparison ${r.comparison.id}: ${r.comparison.added.length} ${pairs !== undefined ? "pair" : "input"}(s) added, ${r.comparison.pairs} in all`
2249
2260
  );
2250
2261
  }
2251
2262
  case "research-inputs": {
@@ -2819,6 +2830,133 @@ async function cmdAdminCrmCapture(ctx, paths) {
2819
2830
  }
2820
2831
  }
2821
2832
 
2833
+ /**
2834
+ * `admin crm outreach …` — the launch-day list (or `--list <key>`), the same operations the
2835
+ * CRM's Launch day tab calls (src/lib/crm/outreach.ts, /api/superadmin/crm/outreach):
2836
+ *
2837
+ * outreach [list] [--owner Neha] [--todo] who is on it; --todo = not reached yet
2838
+ * outreach add "Jane Doe" [--company …] [--owner …] [--channel x] [--relationship …]
2839
+ * [--ask …] [--linkedin …] [--email …] [--lookup]
2840
+ * outreach lookup "Jane Doe from Acme" [shot.png …] prefilled from the web; writes nothing
2841
+ * outreach set <id> [--reached|--not-reached] [--response "…" --tone positive]
2842
+ * [--owner-id <uuid>] [--channel …] [--ask …] [--draft "…"] [--archive|--restore]
2843
+ */
2844
+ async function cmdAdminCrmOutreach(ctx, args) {
2845
+ const { flags, io } = ctx;
2846
+ const [sub = "list", ...rest] = args;
2847
+ const list = flagStr(flags.list);
2848
+ const json = flagBool(flags.json);
2849
+ const line = (p) =>
2850
+ [
2851
+ p.reachedAt ? "✓" : "·",
2852
+ p.name,
2853
+ p.company ? `(${p.company})` : null,
2854
+ p.ownerName ? `— ${p.ownerName}` : "— unowned",
2855
+ p.channel ? `via ${p.channel}` : null,
2856
+ p.response ? `“${p.response}”` : null,
2857
+ ` ${p.id}`,
2858
+ ]
2859
+ .filter(Boolean)
2860
+ .join(" ");
2861
+ switch (sub) {
2862
+ case "list": {
2863
+ const r = await staffRequest(
2864
+ ctx,
2865
+ "GET",
2866
+ `${CRM}/outreach${list ? `?list=${encodeURIComponent(list)}` : ""}`
2867
+ );
2868
+ const owner = flagStr(flags.owner)?.toLowerCase();
2869
+ const people = r.people
2870
+ .filter((p) => !owner || (p.ownerName ?? "").toLowerCase().includes(owner))
2871
+ .filter((p) => !flagBool(flags.todo) || !p.reachedAt);
2872
+ if (json) return printResult(io, flags, { ...r, people });
2873
+ const reached = r.people.filter((p) => p.reachedAt).length;
2874
+ io.log(`${r.list}: ${r.people.length} people, ${reached} reached`);
2875
+ return io.log(people.length ? people.map(line).join("\n") : "(nobody)");
2876
+ }
2877
+ case "add": {
2878
+ const name = rest.join(" ").trim();
2879
+ if (!name)
2880
+ throw new CliError('Usage: admin crm outreach add "Jane Doe" [--company …] [--owner …] [--lookup]');
2881
+ const r = await staffPost(
2882
+ ctx,
2883
+ `${CRM}/outreach`,
2884
+ compact({
2885
+ list,
2886
+ source: "api",
2887
+ lookup: flagBool(flags.lookup) || undefined,
2888
+ people: [
2889
+ compact({
2890
+ name,
2891
+ company: flagStr(flags.company),
2892
+ title: flagStr(flags.title),
2893
+ owner: flagStr(flags.owner),
2894
+ channel: flagStr(flags.channel),
2895
+ relationship: flagStr(flags.relationship),
2896
+ ask: flagStr(flags.ask),
2897
+ linkedinUrl: flagStr(flags.linkedin),
2898
+ email: flagStr(flags.email),
2899
+ notes: flagStr(flags.notes),
2900
+ }),
2901
+ ],
2902
+ })
2903
+ );
2904
+ if (json) return printResult(io, flags, r);
2905
+ for (const p of r.created) io.log(`added ${line(p)}`);
2906
+ for (const d of r.duplicates)
2907
+ io.log(`already on the list: ${d.existingName} (matched on ${d.matchedOn}) ${d.existingId}`);
2908
+ return;
2909
+ }
2910
+ case "lookup": {
2911
+ const images = rest
2912
+ .filter((a) => CRM_CAPTURE_TYPES[a.split(".").pop()?.toLowerCase() ?? ""])
2913
+ .map((path) => ({
2914
+ media_type: CRM_CAPTURE_TYPES[path.split(".").pop().toLowerCase()],
2915
+ data: readFileSync(path).toString("base64"),
2916
+ }));
2917
+ const text = rest
2918
+ .filter((a) => !CRM_CAPTURE_TYPES[a.split(".").pop()?.toLowerCase() ?? ""])
2919
+ .join(" ")
2920
+ .trim();
2921
+ if (!text && !images.length)
2922
+ throw new CliError('Usage: admin crm outreach lookup "Jane Doe from Acme" [shot.png …]');
2923
+ const r = await staffPost(
2924
+ ctx,
2925
+ `${CRM}/outreach/lookup`,
2926
+ compact({ text: text || undefined, images: images.length ? images : undefined })
2927
+ );
2928
+ if (json) return printResult(io, flags, r);
2929
+ for (const p of r.people)
2930
+ io.log(
2931
+ `${p.name}${p.title ? `, ${p.title}` : ""}${p.company ? ` @ ${p.company}` : ""}${p.fundingStage ? ` · ${p.fundingStage}` : ""}${p.ycBatch ? ` · YC ${p.ycBatch}` : ""}${p.lookup ? ` [${p.lookup.confidence}: ${p.lookup.why}]` : ""}`
2932
+ );
2933
+ if (r.notes) io.log(`notes: ${r.notes}`);
2934
+ return;
2935
+ }
2936
+ case "set": {
2937
+ const [id] = rest;
2938
+ if (!id)
2939
+ throw new CliError('Usage: admin crm outreach set <id> [--reached|--not-reached] [--response "…"] …');
2940
+ const body = compact({
2941
+ reached: flagBool(flags.reached) ? true : flagBool(flags["not-reached"]) ? false : undefined,
2942
+ response: flagStr(flags.response),
2943
+ responseTone: flagStr(flags.tone),
2944
+ ownerId: flagStr(flags["owner-id"]),
2945
+ channel: flagStr(flags.channel),
2946
+ ask: flagStr(flags.ask),
2947
+ relationship: flagStr(flags.relationship),
2948
+ draftMessage: flagStr(flags.draft),
2949
+ notes: flagStr(flags.notes),
2950
+ archived: flagBool(flags.archive) ? true : flagBool(flags.restore) ? false : undefined,
2951
+ });
2952
+ const r = await staffRequest(ctx, "PATCH", `${CRM}/outreach/${id}`, body);
2953
+ return json ? printResult(io, flags, r) : io.log(line(r));
2954
+ }
2955
+ default:
2956
+ throw new CliError("Usage: admin crm outreach [list|add|lookup|set] … (see --help)");
2957
+ }
2958
+ }
2959
+
2822
2960
  /**
2823
2961
  * `admin crm …` — the headless half of the staff CRM, so a background agent can watch the
2824
2962
  * pipeline and record the signals that have no webhook (Bookface, tweet likes, DMs).
@@ -2835,6 +2973,8 @@ async function cmdAdminCrm(ctx) {
2835
2973
  return cmdAdminCrmImport(ctx, id);
2836
2974
  case "capture":
2837
2975
  return cmdAdminCrmCapture(ctx, ctx.positionals.slice(1));
2976
+ case "outreach":
2977
+ return cmdAdminCrmOutreach(ctx, ctx.positionals.slice(1));
2838
2978
  case "signals": {
2839
2979
  const params = new URLSearchParams(
2840
2980
  compact({
@@ -3076,7 +3216,7 @@ async function cmdAdminCrm(ctx) {
3076
3216
  }
3077
3217
  default:
3078
3218
  throw new CliError(
3079
- "Usage: admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
3219
+ "Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
3080
3220
  );
3081
3221
  }
3082
3222
  }
@@ -3847,6 +3987,42 @@ async function cmdAdminFleetExperiments(ctx) {
3847
3987
  ].join("\n")
3848
3988
  );
3849
3989
  }
3990
+ case "post-sites": {
3991
+ const result = await staffRequest(ctx, "GET", `${FLEET_EXPERIMENTS}/post-sites`);
3992
+ return out(result, (r) =>
3993
+ r.sites.length
3994
+ ? r.sites
3995
+ .map(
3996
+ (s) =>
3997
+ `${s.collectionId} ${s.orgName} · ${s.siteName} ${s.notEligible ? `(can't: ${s.notEligible})` : `up to ${s.pairsAvailable} pair${s.pairsAvailable === 1 ? "" : "s"}`}`
3998
+ )
3999
+ .join("\n")
4000
+ : "(no sites)"
4001
+ );
4002
+ }
4003
+ case "spin-up-posts": {
4004
+ const collectionId = rest.find(Boolean);
4005
+ if (!id || !collectionId)
4006
+ throw new CliError(
4007
+ "Usage: admin fleet-experiments spin-up-posts <experiment-id> <collection-id> [--pairs n] (see post-sites)"
4008
+ );
4009
+ const body = {
4010
+ action: "spin_up_posts",
4011
+ experiment_id: id,
4012
+ collection_id: collectionId,
4013
+ ...(flagStr(flags.pairs) ? { pairs: Number(flagStr(flags.pairs)) } : {}),
4014
+ };
4015
+ return out(await action(body), (r) =>
4016
+ [
4017
+ `launching ${r.launched.length} pair${r.launched.length === 1 ? "" : "s"} on ${r.site}:`,
4018
+ ...r.launched.map(
4019
+ (p) =>
4020
+ ` ${p.pair}: ${p.posts.map((x) => `${x.arm} "${x.topic ?? x.topicId}" (run ${x.runId})`).join(" vs ")}`
4021
+ ),
4022
+ ...r.failed.map((f) => ` not launched (${f.pair}): ${f.message}`),
4023
+ ].join("\n")
4024
+ );
4025
+ }
3850
4026
  case "start":
3851
4027
  case "stop":
3852
4028
  if (!id) throw new CliError(`Usage: admin fleet-experiments ${sub} <experiment-id>`);
@@ -3864,6 +4040,69 @@ async function cmdAdminFleetExperiments(ctx) {
3864
4040
  }
3865
4041
  }
3866
4042
 
4043
+ const REPUBLISH = "/api/superadmin/deployments/republish";
4044
+ /** Sites per POST — the same chunk the Fleet dialog sends. */
4045
+ const REPUBLISH_BATCH = 12;
4046
+
4047
+ /**
4048
+ * `admin republish`: the Fleet "Republish sites" action for an agent. Pick sites by
4049
+ * id, by --domains, or --all-live (every live, unpaused site); rebuild them in
4050
+ * batches; print every failure with its reason. Exit 1 when any site failed, so a
4051
+ * scheduled agent can tell a partial run from a clean one.
4052
+ */
4053
+ async function cmdAdminRepublish(ctx) {
4054
+ const { flags, io } = ctx;
4055
+ const domains = flagList(flags.domains)
4056
+ .flatMap((d) => d.split(","))
4057
+ .map((d) =>
4058
+ d
4059
+ .trim()
4060
+ .toLowerCase()
4061
+ .replace(/^www\./, "")
4062
+ )
4063
+ .filter(Boolean);
4064
+ const allLive = flagBool(flags["all-live"]);
4065
+ let ids = ctx.positionals.filter(Boolean);
4066
+ let names = new Map();
4067
+ if (allLive || domains.length) {
4068
+ const { sites = [] } = await staffRequest(ctx, "GET", REPUBLISH);
4069
+ names = new Map(sites.map((s) => [s.id, s.domain ?? s.subdomain ?? s.name]));
4070
+ const picked = allLive
4071
+ ? sites
4072
+ : sites.filter((s) => domains.includes((s.domain ?? "").toLowerCase().replace(/^www\./, "")));
4073
+ if (domains.length) {
4074
+ const found = new Set(picked.map((s) => (s.domain ?? "").toLowerCase().replace(/^www\./, "")));
4075
+ const missing = domains.filter((d) => !found.has(d));
4076
+ if (missing.length) io.error(`! Not a live, unpaused site: ${missing.join(", ")}`);
4077
+ }
4078
+ ids = [...new Set([...ids, ...picked.map((s) => s.id)])];
4079
+ }
4080
+ if (ids.length === 0) {
4081
+ throw new CliError(
4082
+ "Usage: admin republish [<deployment-id>…] [--domains a.com,b.com] [--all-live] [--dry-run]"
4083
+ );
4084
+ }
4085
+ if (flagBool(flags["dry-run"])) {
4086
+ io.log(`Would republish ${ids.length} site(s):`);
4087
+ for (const id of ids) io.log(` ${names.get(id) ?? id} ${id}`);
4088
+ return 0;
4089
+ }
4090
+ const results = [];
4091
+ for (let i = 0; i < ids.length; i += REPUBLISH_BATCH) {
4092
+ const res = await staffPost(ctx, REPUBLISH, { deployment_ids: ids.slice(i, i + REPUBLISH_BATCH) });
4093
+ results.push(...(res.results ?? []));
4094
+ if (!flags.json) io.error(`… ${Math.min(i + REPUBLISH_BATCH, ids.length)}/${ids.length}`);
4095
+ }
4096
+ const failed = results.filter((r) => !r.ok);
4097
+ printResult(io, flags, { rebuilt: results.length - failed.length, failed: failed.length, results }, () =>
4098
+ [
4099
+ `Rebuilt ${results.length - failed.length} site(s), ${failed.length} failed. Live pages refresh once each build finishes (~1–2 min).`,
4100
+ ...failed.map((r) => ` ✗ ${r.name ?? names.get(r.id) ?? r.id} → ${r.error ?? "unknown error"}`),
4101
+ ].join("\n")
4102
+ );
4103
+ return failed.length ? 1 : 0;
4104
+ }
4105
+
3867
4106
  export async function cmdAdmin(ctx) {
3868
4107
  const sub = ctx.positionals[0];
3869
4108
  if (sub === "replays" || sub === "replay")
@@ -3880,8 +4119,9 @@ export async function cmdAdmin(ctx) {
3880
4119
  if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
3881
4120
  if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
3882
4121
  if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
4122
+ if (sub === "republish") return cmdAdminRepublish({ ...ctx, positionals: ctx.positionals.slice(1) });
3883
4123
  throw new CliError(
3884
- 'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
4124
+ 'Usage: admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
3885
4125
  );
3886
4126
  }
3887
4127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.98.4",
3
+ "version": "0.99.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",