@letterstory/cli 0.66.0 → 0.70.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 CHANGED
@@ -161,6 +161,7 @@ publishing, targets, measurement) that produces and publishes posts on a cadence
161
161
 
162
162
  ```bash
163
163
  letterstory collection list # public_id = the HTTP door's address
164
+ letterstory collection overview # site, state, posts, in Google, AI reads per collection
164
165
  letterstory collection new --name "Launches" --config-file cfg.json
165
166
  letterstory collection config <id> # print the one file
166
167
  letterstory collection config <id> --config '{"targets":{"enabled":true,"tier":"ramping"}}' # patch it
@@ -369,14 +370,17 @@ letterstory admin tuning trial-run <trial> --repeats 2 # runs advance on the se
369
370
  letterstory admin tuning trial-save <trial> --note "…" # a trial as a candidate version (never live); then `admin tuning compare`
370
371
  letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
371
372
  letterstory admin experiments list --q colons # what we've tried
373
+ letterstory admin experiments list --primitive write --stage rarity # what we learned about one stage
374
+ letterstory admin experiments new "A different writer, voice rewritten, rarity rewrite off" --primitive write # an investigation in Write's notebook
372
375
  letterstory admin experiments new "We use too many colons" --as "claude-code"
373
376
  letterstory admin experiments update <id> --plan-file plan.json --budget 120
374
- # ...a person approves the plan in Kernel Tuning -> Experiments...
377
+ # ...a person approves the plan on the investigation's page (approval gates spending a budget; a person's
378
+ # investigation needs none and is concluded straight away)...
375
379
  letterstory admin experiments spend <id> 54
376
380
  letterstory admin tuning-import lab/results/colons/publish/<brief>.json --experiment <id>
377
381
  letterstory admin experiments conclude <id> --decision drop \
378
382
  --conclusion "Colons fell 60%; P(machine) did not move; quality held." \
379
- --evidence lab:"kernels lab/results/colons/report.md"
383
+ --evidence lab:"kernels lab/results/colons/report.md" --stages rules
380
384
  ```
381
385
 
382
386
  The ledger behind Kernel Tuning's Experiments tab
@@ -728,8 +732,10 @@ AI visibility and page access — the Measure tabs' reads, for scripts and agent
728
732
  ```bash
729
733
  # The Measure Overview: how often AI answers name you and cite your pages (rates
730
734
  # with 95% intervals and gated trends), share of voice, search for your site and
731
- # the phantom network kept apart, and each ranking target's standing.
732
- letterstory insights summary [--period 14d|30d|90d]
735
+ # the phantom network kept apart, and each ranking target's standing. --part
736
+ # (repeatable) returns only those blocks: audience, ai, shareOfVoice, pageAccess,
737
+ # search, actions, targets — `ai` is the slow one on a large org.
738
+ letterstory insights summary [--period 14d|30d|90d] [--part <block>]...
733
739
 
734
740
  # The AI Visibility Bank: which questions named you, cited your pages, or both (a
735
741
  # breakthrough). All time unless --period; --source picks the phantom network
package/lib/cli.mjs CHANGED
@@ -56,7 +56,7 @@ import {
56
56
  } from "./commands.mjs";
57
57
 
58
58
  // Keep in sync with cli/package.json.
59
- export const VERSION = "0.66.0";
59
+ export const VERSION = "0.70.0";
60
60
 
61
61
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
62
62
  // can't accidentally swallow the id as --json's value.
@@ -201,6 +201,7 @@ Auth:
201
201
 
202
202
  Collections (the content engine as a primitive — one door, 'collection run'):
203
203
  collection list Your collections (public_id = the HTTP door's address)
204
+ collection overview Each collection's site, state, posts, posts in Google, AI reads
204
205
  collection new --name <n> [--description <t>] [--config '<json>'|--config-file cfg.json]
205
206
  collection config <id> [--config '<json>'|--config-file cfg.json]
206
207
  Print the ONE FILE (identity, content, cadence,
@@ -554,19 +555,24 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
554
555
  Live answer replays: breakthroughs re-asked in ChatGPT /
555
556
  Gemini / Google / Perplexity / Claude, with screenshots.
556
557
  Omit --org for the whole fleet. Needs fleet:read.
557
- admin replay --org <uuid> (<response-id> | --target <uuid>) [--surface openai|anthropic|google-aio|google-gemini|perplexity]
558
+ admin replay --org <uuid> [<response-id> | --target <uuid>] [--surface openai|anthropic|google-aio|google-gemini|perplexity]
558
559
  Replay a breakthrough live now (background; poll
559
- admin replays). Needs fleet:replay.
560
- admin experiments list [--q colons] [--status proposed] | show <id>
561
- | new "<question>" [--hypothesis …] [--plan-file p.json] [--tags a,b] [--as "<agent>"]
562
- | update <id> [--plan-file …] [--result-file …] [--evidence kind:ref,…] [--budget N]
560
+ admin replays). With neither, the client's newest
561
+ breakthrough. Needs fleet:replay.
562
+ admin experiments list [--q colons] [--status proposed] [--primitive write] [--stage rarity] | show <id>
563
+ | new "<what you are investigating>" [--primitive write|research|shred|enhance] [--stages a,b]
564
+ [--hypothesis …] [--plan-file p.json] [--tags a,b] [--as "<agent>"]
565
+ | update <id> [--plan-file …] [--result-file …] [--evidence kind:ref,…] [--stages a,b] [--budget N]
563
566
  | note|started <id> "<text>" | spend <id> <runs>
564
- | conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "…" [--evidence …]
567
+ | conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "…" [--stages a,b] [--evidence …]
565
568
  | abandon <id> --reason "…"
566
- The kernel experiment ledger: what we tried,
567
- how, what it found and why. Approving a plan
568
- stays with a signed-in person. Needs the
569
- tuning:experiment capability.
569
+ Each primitive's notebook of investigations:
570
+ what we tried, how, what it found and why,
571
+ and the pipeline stages it is about. A plan
572
+ is approved by a signed-in person before a
573
+ budget is spent; concluding needs evidence,
574
+ not approval. "ship" hands off: a promoter
575
+ makes it live. Needs tuning:experiment.
570
576
  admin tuning-import <brief-results.json> [--experiment <id>]
571
577
  One brief's kernels lab results as a console
572
578
  session (a trial per arm), linked to its
@@ -630,7 +636,11 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
630
636
  candidate, compare, comparisons, picks, pick,
631
637
  reading-list, pair, side-by-side, promote,
632
638
  sessions, session-new, propose, trial-add,
633
- trial-run, trial-save, and the arena, shredder
639
+ trial-run, trial-save, the Write input bank
640
+ (inputs, input, inputs-harvest, input-save),
641
+ screens (screen-new, screens, screen,
642
+ screen-advance),
643
+ and the arena, shredder
634
644
  and enhancer twins (every flag is listed in
635
645
  cli/lib/commands/admin.mjs).
636
646
  admin create-org --domain <bare-domain> [--name "Acme"] [--test] [--no-key]
@@ -882,7 +892,7 @@ Mail:
882
892
  mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
883
893
 
884
894
  Insights:
885
- insights summary [--period 14d|30d|90d] Measure Overview: AI named/cited rates, share of voice, search, targets
895
+ insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
886
896
  insights site [--period 14d|30d|90d] [--collection <uuid>]
887
897
  insights post <article-id> [--period 14d|30d|90d]
888
898
  insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
@@ -11,7 +11,7 @@
11
11
  // admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
12
12
  // 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)
13
13
  // admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
14
- // admin replay --org <id> (<response-id> | --target <id>) [--surface …] (fleet:replay)
14
+ // admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
15
15
  // admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
16
16
  // [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
17
17
  // [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
@@ -268,7 +268,8 @@ function evidenceFrom(flags) {
268
268
 
269
269
  function experimentLine(e) {
270
270
  const decision = e.decision ? ` → ${e.decision}` : "";
271
- return `${e.id} [${e.status}${decision}] ${e.question}${e.gist ? `\n ${e.gist}` : ""}`;
271
+ const where = e.primitive ? `${e.primitive}${e.stages?.length ? `: ${e.stages.join(", ")}` : ""} ` : "";
272
+ return `${e.id} [${e.status}${decision}] ${where}${e.question}${e.gist ? `\n ${e.gist}` : ""}`;
272
273
  }
273
274
 
274
275
  /**
@@ -293,7 +294,13 @@ async function cmdAdminExperiments(ctx) {
293
294
  switch (sub) {
294
295
  case "list": {
295
296
  const params = new URLSearchParams(
296
- compact({ q: flagStr(flags.q), status: flagStr(flags.status), limit: flagStr(flags.limit) })
297
+ compact({
298
+ q: flagStr(flags.q),
299
+ status: flagStr(flags.status),
300
+ primitive: flagStr(flags.primitive),
301
+ stage: flagStr(flags.stage),
302
+ limit: flagStr(flags.limit),
303
+ })
297
304
  );
298
305
  const result = await staffRequest(ctx, "GET", `${EXPERIMENTS}?${params}`);
299
306
  return out(result, (r) =>
@@ -306,9 +313,13 @@ async function cmdAdminExperiments(ctx) {
306
313
  case "new": {
307
314
  const question = [id, ...rest].filter(Boolean).join(" ").trim();
308
315
  if (question.length < 3)
309
- throw new CliError('Usage: admin experiments new "<question>" [--plan-file plan.json] [--tags a,b]');
316
+ throw new CliError(
317
+ 'Usage: admin experiments new "<what you are investigating>" [--primitive write|research|shred|enhance] [--stages write,rarity] [--plan-file plan.json] [--tags a,b]'
318
+ );
310
319
  const body = compact({
311
320
  question,
321
+ primitive: flagStr(flags.primitive),
322
+ stages: flagList(flags.stages),
312
323
  restated: flagStr(flags.hypothesis),
313
324
  plan: jsonFile(flags, "plan"),
314
325
  method: flagStr(flags.method),
@@ -331,6 +342,7 @@ async function cmdAdminExperiments(ctx) {
331
342
  result: jsonFile(flags, "result"),
332
343
  method: flagStr(flags.method),
333
344
  tags: flagList(flags.tags),
345
+ stages: flagList(flags.stages),
334
346
  budget_runs: flagNum(flags.budget),
335
347
  engine_build: flagStr(flags.build),
336
348
  add_evidence: evidenceFrom(flags),
@@ -357,12 +369,13 @@ async function cmdAdminExperiments(ctx) {
357
369
  const conclusion = flagStr(flags.conclusion);
358
370
  if (!id || !decision || !conclusion)
359
371
  throw new CliError(
360
- 'Usage: admin experiments conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "<what we learned and why>" [--evidence kind:ref,…] [--result-file r.json]'
372
+ 'Usage: admin experiments conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "<what we learned and why>" [--stages write,rarity] [--evidence kind:ref,…] [--result-file r.json]'
361
373
  );
362
374
  const body = compact({
363
375
  action: "conclude",
364
376
  decision,
365
377
  conclusion,
378
+ stages: flagList(flags.stages),
366
379
  result: jsonFile(flags, "result"),
367
380
  add_evidence: evidenceFrom(flags),
368
381
  actor: actorFrom(flags),
@@ -432,6 +445,8 @@ async function cmdAdminTuningContext(ctx) {
432
445
  * admin tuning picks <comparison-id> the blind queue (next pairs)
433
446
  * admin tuning pick <comparison-id> --cell <key> --pick left|right|tie record a blind pick
434
447
  * admin tuning reading-list <comparison-id> [--n 10] your next blind pairs as links, to read off the screen
448
+ * admin tuning archive <comparison-id> [--undo] take a read comparison out of the working list (or back); the record stays
449
+ * admin tuning comparisons [--archived only|all] the list; archived ones only when asked
435
450
  * admin tuning pair <comparison-id> --cell <key> one pair named: live and candidate, both texts, the measures per arm
436
451
  * admin tuning side-by-side <id>,<id>[,…] [--cell <key>] several comparisons as one board: each writer's measures against live, and one input's texts
437
452
  * admin tuning candidates --kernels Hazel,Aspen|all --patch '{…}' [--base N] [--note "…"] the same changes as a candidate of each kernel
@@ -443,13 +458,28 @@ async function cmdAdminTuningContext(ctx) {
443
458
  * admin tuning base-kernels the kernels a session can run on (ids for --kernel)
444
459
  * admin tuning base-versions <kernel-id> the versions a session on it can run against
445
460
  * admin tuning sessions | session <id> [--texts] list / one session: trials, runs and their numbers (--texts adds each trial's latest text)
446
- * admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…") [--kind article|outline|brief]
461
+ * admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…" | --bank <input-id>) [--kind article|outline|brief]
447
462
  * [--version N] [--writers echo/echo,…] [--trials-from <session-id>]
463
+ * (--bank: a bank input's text, and its request's constraints under every trial)
448
464
  * admin tuning propose <session-id> --text "Echo with rarity on; live with the shred after" [--create]
449
465
  * admin tuning trial-add <session-id> [--name "…"] [--patch '{…}'] [--options '{…}'] [--from <trial-id>]
450
466
  * admin tuning trial-run <trial-id> [--repeats 2] queue runs (guardrails apply)
451
467
  * admin tuning trial-save <trial-id> [--note "…"] save a trial as a candidate version (never live)
452
468
  *
469
+ * The Write input bank: real kernel requests (above all the arena outlines Auto-Pen sends), as sent.
470
+ * admin tuning inputs [--filter "origin=arena_outline;material=thin,typical"] [--q "…"] list by tag, with coverage per bucket
471
+ * admin tuning input <input-id> one input in full: text, constraints, tags, run defaults, origin
472
+ * admin tuning inputs-harvest [--n 40] [--pool 400] [--callers auto-pen,endpoint] [--since-days 21] [--min-per-bucket 3]
473
+ * freeze recent production runs, stratified; idempotent by key
474
+ * admin tuning input-save <session-id> [--note "…"] save a session's pasted input or arena outline to the bank
475
+ *
476
+ * Screens: "is this candidate worth a full comparison?", on a slice of the bank (it does not decide).
477
+ * admin tuning screen-new --title "…" (--from <session-id> | --kernel <kernel-id> --versions 23,29) [--version N]
478
+ * [--n 8] [--filter "origin=arena_outline;material=thin"] [--inputs <id>,…] [--repeats 1] [--lift-budget]
479
+ * one session per input with the same trials; runs queue as the console's caps allow
480
+ * admin tuning screens | screen <id> list / one screen: per candidate, each measure against the Base, with its noise
481
+ * admin tuning screen-advance <id> queue what the caps allow now (the cron does it every minute)
482
+ *
453
483
  * Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
454
484
  * admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
455
485
  * admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
@@ -492,6 +522,19 @@ function jsonFlag(raw, name) {
492
522
  }
493
523
  }
494
524
 
525
+ /**
526
+ * `--filter "origin=arena_outline;material=thin,typical"` → the bank's query params (one comma list
527
+ * per tag dimension; lib/tuning/input-tags.ts `parseTagFilter` reads the same form).
528
+ */
529
+ function tagFilterParams(raw) {
530
+ const out = {};
531
+ for (const part of (raw ?? "").split(";")) {
532
+ const [k, v] = part.split("=");
533
+ if (k?.trim() && v?.trim()) out[k.trim()] = v.trim();
534
+ }
535
+ return out;
536
+ }
537
+
495
538
  /**
496
539
  * A session as an agent reads it: each trial's changes and runs with their headline numbers, and
497
540
  * (with `texts`) the latest finished text per trial. The full session (traces, records) is `--json`.
@@ -612,8 +655,18 @@ async function cmdAdminTuning(ctx) {
612
655
  );
613
656
  return json ? printResult(io, flags, r) : io.log(`comparison ${r.comparison?.id ?? r.id ?? ""} started`);
614
657
  }
615
- case "comparisons":
616
- return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons`));
658
+ case "comparisons": {
659
+ const archived = flagStr(flags.archived);
660
+ const qs = archived ? `?${new URLSearchParams({ archived })}` : "";
661
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons${qs}`));
662
+ }
663
+ case "archive": {
664
+ if (!id) throw new CliError("Usage: admin tuning archive <comparison-id> [--undo]");
665
+ const r = await staffRequest(ctx, "POST", `${T}/comparisons/${id}/archive`, { archived: !flags.undo });
666
+ return json
667
+ ? printResult(io, flags, r)
668
+ : io.log(`comparison ${id} ${flags.undo ? "back in the list" : "archived"}`);
669
+ }
617
670
  case "comparison":
618
671
  if (!id) throw new CliError("Usage: admin tuning comparison <id>");
619
672
  return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/comparisons/${id}`));
@@ -770,9 +823,11 @@ async function cmdAdminTuning(ctx) {
770
823
  throw new CliError(`--input-file ${path}: ${err.message}`);
771
824
  }
772
825
  }
773
- if (!title || !kernel || !input)
826
+ // A bank input instead of a paste: its text, and its request's constraints under every trial.
827
+ const bank = flagStr(flags.bank);
828
+ if (!title || !kernel || (!input && !bank))
774
829
  throw new CliError(
775
- 'Usage: admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…") [--kind article|outline|brief] [--version N] [--writers a,b] [--versions 23,29] [--trials-from <session-id>]'
830
+ 'Usage: admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…" | --bank <input-id>) [--kind article|outline|brief] [--version N] [--writers a,b] [--versions 23,29] [--trials-from <session-id>]'
776
831
  );
777
832
  let kernelId = kernel;
778
833
  if (!/^[0-9a-f-]{36}$/i.test(kernel)) {
@@ -788,8 +843,9 @@ async function cmdAdminTuning(ctx) {
788
843
  T,
789
844
  compact({
790
845
  title,
791
- input_text: input,
846
+ input_text: bank ? undefined : input,
792
847
  input_kind: flagStr(flags.kind) ?? "unknown",
848
+ material: bank ? { source: "bank", input_id: bank } : undefined,
793
849
  base_kernel_id: kernelId,
794
850
  base_version: flagNum(flags.version),
795
851
  trials_from_session_id: flagStr(flags["trials-from"]),
@@ -871,6 +927,167 @@ async function cmdAdminTuning(ctx) {
871
927
  `${r.kernel}: saved as candidate v${r.version}${r.baseVersion ? ` (from v${r.baseVersion})` : ""}${r.leftBehind.length ? `; per-run, not saved: ${r.leftBehind.join(", ")}` : ""}`
872
928
  );
873
929
  }
930
+ // --- The Write input bank: real kernel requests a session can start from ---
931
+ case "inputs": {
932
+ const q = new URLSearchParams(
933
+ compact({ ...tagFilterParams(flagStr(flags.filter)), q: flagStr(flags.q), limit: flagStr(flags.limit) })
934
+ );
935
+ const r = await staffRequest(ctx, "GET", `${T}/inputs${q.size ? `?${q}` : ""}`);
936
+ if (json) return printResult(io, flags, r);
937
+ const coverage = Object.entries(r.coverage)
938
+ .map(
939
+ ([dim, buckets]) =>
940
+ ` ${dim.padEnd(11)} ${Object.entries(buckets)
941
+ .map(([b, n]) => `${b} ${n}`)
942
+ .join(" · ")}`
943
+ )
944
+ .join("\n");
945
+ return io.log(
946
+ [
947
+ `bank ${r.set}: ${r.inputs.length} of ${r.total} input(s)`,
948
+ ...r.inputs.map(
949
+ (i) =>
950
+ `${i.id} ${i.source.padEnd(10)} ${String(i.tags.origin ?? "").padEnd(13)} ${String(i.tags.material?.bucket ?? "").padEnd(7)} ${String(i.tags.shape?.bucket ?? "").padEnd(5)} ${i.title}`
951
+ ),
952
+ "coverage (the whole bank):",
953
+ coverage,
954
+ ].join("\n")
955
+ );
956
+ }
957
+ case "input": {
958
+ if (!id) throw new CliError("Usage: admin tuning input <input-id>");
959
+ return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/inputs/${id}`));
960
+ }
961
+ case "inputs-harvest": {
962
+ const r = await staffRequest(
963
+ ctx,
964
+ "POST",
965
+ `${T}/inputs/harvest`,
966
+ compact({
967
+ n: flagNum(flags.n),
968
+ pool: flagNum(flags.pool),
969
+ callers: flagStr(flags.callers)
970
+ ?.split(",")
971
+ .map((c) => c.trim())
972
+ .filter(Boolean),
973
+ since_days: flagNum(flags["since-days"]),
974
+ min_per_bucket: flagNum(flags["min-per-bucket"]),
975
+ })
976
+ );
977
+ if (json) return printResult(io, flags, r);
978
+ return io.log(
979
+ [
980
+ `added ${r.added.length} input(s) to ${r.set} from a pool of ${r.pool} run(s); ${r.alreadyHeld} already held`,
981
+ ...r.added.map((a) => ` ${a.key} (${a.reason})`),
982
+ ...Object.entries(r.skipped).map(([why, n]) => ` skipped ${n}: ${why}`),
983
+ ].join("\n")
984
+ );
985
+ }
986
+ case "input-save": {
987
+ if (!id) throw new CliError('Usage: admin tuning input-save <session-id> [--note "…"]');
988
+ const r = await staffRequest(
989
+ ctx,
990
+ "POST",
991
+ `${T}/sessions/${id}/bank`,
992
+ compact({ note: flagStr(flags.note) })
993
+ );
994
+ return json ? printResult(io, flags, r) : io.log(`bank input ${r.input.id} (${r.input.key})`);
995
+ }
996
+ // --- Screens: candidates across a slice of the bank, read against the Base, beside the noise ---
997
+ case "screen-new": {
998
+ const title = flagStr(flags.title);
999
+ const from = flagStr(flags.from);
1000
+ const versions = (flagStr(flags.versions) ?? "")
1001
+ .split(",")
1002
+ .map((v) => Number(v.trim()))
1003
+ .filter((v) => Number.isInteger(v) && v > 0);
1004
+ if (!title || (!from && !(flagStr(flags.kernel) && versions.length)))
1005
+ throw new CliError(
1006
+ 'Usage: admin tuning screen-new --title "…" (--from <session-id> | --kernel <kernel-id> --versions 23,29) [--version N] [--n 8] [--filter "origin=arena_outline;material=thin"] [--inputs <id>,…] [--repeats 1] [--lift-budget]'
1007
+ );
1008
+ const filter = Object.fromEntries(
1009
+ Object.entries(tagFilterParams(flagStr(flags.filter))).map(([k, v]) => [
1010
+ k,
1011
+ v
1012
+ .split(",")
1013
+ .map((x) => x.trim())
1014
+ .filter(Boolean),
1015
+ ])
1016
+ );
1017
+ const inputIds = (flagStr(flags.inputs) ?? "")
1018
+ .split(",")
1019
+ .map((x) => x.trim())
1020
+ .filter(Boolean);
1021
+ const r = await staffRequest(
1022
+ ctx,
1023
+ "POST",
1024
+ `${T}/screens`,
1025
+ compact({
1026
+ title,
1027
+ from_session_id: from,
1028
+ base_kernel_id: flagStr(flags.kernel),
1029
+ base_version: flagNum(flags.version),
1030
+ versions: versions.length ? versions : undefined,
1031
+ n: flagNum(flags.n),
1032
+ filter: Object.keys(filter).length ? filter : undefined,
1033
+ input_ids: inputIds.length ? inputIds : undefined,
1034
+ repeats: flagNum(flags.repeats),
1035
+ lift_budget: flagBool(flags["lift-budget"]) || undefined,
1036
+ })
1037
+ );
1038
+ if (json) return printResult(io, flags, r);
1039
+ return io.log(
1040
+ `screen ${r.screen.id}: ${r.screen.inputs} input(s), ${r.aggregate.columns.length} column(s); ${r.screen.pending} run(s) waiting${r.screen.waiting ? ` (${r.screen.waiting})` : ""} → ${ctx.client.url}/superadmin/tuning/screens/${r.screen.id}`
1041
+ );
1042
+ }
1043
+ case "screens": {
1044
+ const r = await staffRequest(ctx, "GET", `${T}/screens?limit=${flagNum(flags.limit) ?? 30}`);
1045
+ if (json) return printResult(io, flags, r);
1046
+ return io.log(
1047
+ r.screens
1048
+ .map(
1049
+ (x) =>
1050
+ `${x.id} ${x.status.padEnd(7)} ${x.title} · ${x.inputs} inputs${x.pending ? ` · ${x.pending} waiting` : ""}`
1051
+ )
1052
+ .join("\n") || "(no screens)"
1053
+ );
1054
+ }
1055
+ case "screen": {
1056
+ if (!id) throw new CliError("Usage: admin tuning screen <screen-id>");
1057
+ const r = await staffRequest(ctx, "GET", `${T}/screens/${id}`);
1058
+ if (json) return printResult(io, flags, r);
1059
+ // The reading as an agent reads it: per candidate, each measure's difference from the Base and its noise.
1060
+ return printResult(
1061
+ io,
1062
+ { ...flags, json: true },
1063
+ {
1064
+ screen: r.screen,
1065
+ note: r.aggregate.note,
1066
+ columns: r.aggregate.columns,
1067
+ measures: r.aggregate.measures.map((m) => ({
1068
+ measure: m.label,
1069
+ judge: m.judgeId,
1070
+ standing: m.standing,
1071
+ sigma: m.sigma,
1072
+ per_column: m.perTrial.map((p) =>
1073
+ compact({
1074
+ column: p.column,
1075
+ n: p.n,
1076
+ mean: p.mean,
1077
+ delta: p.delta ?? undefined,
1078
+ })
1079
+ ),
1080
+ })),
1081
+ }
1082
+ );
1083
+ }
1084
+ case "screen-advance": {
1085
+ if (!id) throw new CliError("Usage: admin tuning screen-advance <screen-id>");
1086
+ const r = await staffRequest(ctx, "POST", `${T}/screens/${id}/advance`, {});
1087
+ return json
1088
+ ? printResult(io, flags, r)
1089
+ : io.log(`queued ${r.queued} run(s)${r.waiting ? `; waiting: ${r.waiting}` : ""}`);
1090
+ }
874
1091
  // --- Research: our arena's versions, research comparisons, the deploy split ---
875
1092
  case "arenas": {
876
1093
  const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
@@ -1977,7 +2194,6 @@ async function cmdAdminReplays(ctx, sub) {
1977
2194
  const responseId = positionals[0];
1978
2195
  const target = flagStr(flags.target);
1979
2196
  if (!org) throw new CliError("admin replay needs --org <org-id>");
1980
- if (!responseId && !target) throw new CliError("Pass a breakthrough <response-id>, or --target <target-id>");
1981
2197
  const result = await staffPost(
1982
2198
  ctx,
1983
2199
  REPLAYS,
@@ -54,6 +54,7 @@ const TERMINAL = new Set(["published", "failed", "skipped", "cancelled"]);
54
54
  */
55
55
  export const ARGS = {
56
56
  list_collections: {},
57
+ get_collections_overview: {},
57
58
  create_collection: {
58
59
  name: { flag: "name", required: true },
59
60
  description: "description",
@@ -153,11 +154,13 @@ export async function cmdCollection(ctx) {
153
154
  return collectionCancel(rest);
154
155
  case "inventory":
155
156
  return collectionInventory(rest);
157
+ case "overview":
158
+ return collectionOverview(rest);
156
159
  case "links":
157
160
  return collectionLinks(rest);
158
161
  default:
159
162
  throw new CliError(
160
- `Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, new, config, delete, run, run-status, runs, approve, cancel, inventory, links`
163
+ `Unknown collection subcommand: ${sub ?? "(none)"}. Try: list, overview, new, config, delete, run, run-status, runs, approve, cancel, inventory, links`
161
164
  );
162
165
  }
163
166
  }
@@ -168,6 +171,16 @@ async function collectionList(ctx) {
168
171
  return 0;
169
172
  }
170
173
 
174
+ // `collection overview` — each collection's site, state, posts, posts in Google and AI reads.
175
+ async function collectionOverview(ctx) {
176
+ const result = await ctx.client.callTool(
177
+ "get_collections_overview",
178
+ argsFromFlags(ARGS.get_collections_overview, ctx.flags)
179
+ );
180
+ printResult(ctx.io, ctx.flags, result);
181
+ return 0;
182
+ }
183
+
171
184
  async function collectionCreate(ctx) {
172
185
  const result = await ctx.client.callTool(
173
186
  "create_collection",
@@ -21,6 +21,7 @@ export const ARGS = {
21
21
  },
22
22
  get_measure_summary: {
23
23
  period: { flag: "period", enum: PERIODS },
24
+ parts: { flag: "part", type: "list" },
24
25
  },
25
26
  };
26
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.66.0",
3
+ "version": "0.70.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",