@letterstory/cli 0.65.0 → 0.69.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 +16 -4
- package/lib/cli.mjs +43 -13
- package/lib/commands/admin.mjs +287 -9
- package/lib/commands/insights.mjs +1 -0
- package/lib/commands/outbound.mjs +99 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -369,14 +369,17 @@ letterstory admin tuning trial-run <trial> --repeats 2 # runs advance on the se
|
|
|
369
369
|
letterstory admin tuning trial-save <trial> --note "…" # a trial as a candidate version (never live); then `admin tuning compare`
|
|
370
370
|
letterstory admin tuning promote <experiment-id> --versions <service-kernel-id>:21 # from a concluded "ship" decision
|
|
371
371
|
letterstory admin experiments list --q colons # what we've tried
|
|
372
|
+
letterstory admin experiments list --primitive write --stage rarity # what we learned about one stage
|
|
373
|
+
letterstory admin experiments new "A different writer, voice rewritten, rarity rewrite off" --primitive write # an investigation in Write's notebook
|
|
372
374
|
letterstory admin experiments new "We use too many colons" --as "claude-code"
|
|
373
375
|
letterstory admin experiments update <id> --plan-file plan.json --budget 120
|
|
374
|
-
# ...a person approves the plan
|
|
376
|
+
# ...a person approves the plan on the investigation's page (approval gates spending a budget; a person's
|
|
377
|
+
# investigation needs none and is concluded straight away)...
|
|
375
378
|
letterstory admin experiments spend <id> 54
|
|
376
379
|
letterstory admin tuning-import lab/results/colons/publish/<brief>.json --experiment <id>
|
|
377
380
|
letterstory admin experiments conclude <id> --decision drop \
|
|
378
381
|
--conclusion "Colons fell 60%; P(machine) did not move; quality held." \
|
|
379
|
-
--evidence lab:"kernels lab/results/colons/report.md"
|
|
382
|
+
--evidence lab:"kernels lab/results/colons/report.md" --stages rules
|
|
380
383
|
```
|
|
381
384
|
|
|
382
385
|
The ledger behind Kernel Tuning's Experiments tab
|
|
@@ -728,8 +731,10 @@ AI visibility and page access — the Measure tabs' reads, for scripts and agent
|
|
|
728
731
|
```bash
|
|
729
732
|
# The Measure Overview: how often AI answers name you and cite your pages (rates
|
|
730
733
|
# 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
|
-
|
|
734
|
+
# the phantom network kept apart, and each ranking target's standing. --part
|
|
735
|
+
# (repeatable) returns only those blocks: audience, ai, shareOfVoice, pageAccess,
|
|
736
|
+
# search, actions, targets — `ai` is the slow one on a large org.
|
|
737
|
+
letterstory insights summary [--period 14d|30d|90d] [--part <block>]...
|
|
733
738
|
|
|
734
739
|
# The AI Visibility Bank: which questions named you, cited your pages, or both (a
|
|
735
740
|
# breakthrough). All time unless --period; --source picks the phantom network
|
|
@@ -777,10 +782,17 @@ letterstory sequences list | get <id> | create | update <id> | pause <id> | resu
|
|
|
777
782
|
letterstory sequences enroll <id> --list <list-id> # or --email a@x.com ...
|
|
778
783
|
letterstory sequences review list | approve <item-id> | skip <item-id> | edit <item-id> --subject "…"
|
|
779
784
|
letterstory sequences links create --days 2 # no-login phone review link, shown once
|
|
785
|
+
letterstory sequences update <id> --test-mode # internal accounts only: walks as live, sends nothing
|
|
786
|
+
|
|
787
|
+
# Dispatches (the review desks a sequence queues to)
|
|
788
|
+
letterstory dispatches list | create --name "Founders" | rename <id> --name "VIPs" | delete <id> --yes
|
|
780
789
|
|
|
781
790
|
# Prospects
|
|
782
791
|
letterstory prospects lists | ls --list <list-id> | get <id> | import --csv-file leads.csv --list-name "Q4"
|
|
783
792
|
letterstory prospects source --description "heads of growth at seed-stage fintech" --list-name "Fintech"
|
|
793
|
+
letterstory prospects openers <list-id> --new-group "Fintech openers" # print an opening line per row
|
|
794
|
+
letterstory prospects columns add "Cake image" | columns delete <column-id> --yes
|
|
795
|
+
letterstory prospects delete <prospect-id>... --yes
|
|
784
796
|
```
|
|
785
797
|
|
|
786
798
|
Connecting a bring-your-own Gmail mailbox needs a browser (Google consent), so it is done in the
|
package/lib/cli.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
cmdLede,
|
|
45
45
|
cmdLandings,
|
|
46
46
|
cmdSequences,
|
|
47
|
+
cmdDispatches,
|
|
47
48
|
cmdProspects,
|
|
48
49
|
cmdMail,
|
|
49
50
|
cmdMailers,
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
} from "./commands.mjs";
|
|
56
57
|
|
|
57
58
|
// Keep in sync with cli/package.json.
|
|
58
|
-
export const VERSION = "0.
|
|
59
|
+
export const VERSION = "0.69.0";
|
|
59
60
|
|
|
60
61
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
61
62
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -549,16 +550,28 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
549
550
|
read-only SQL across every org; --sql prints
|
|
550
551
|
the queries behind the answer. Needs the
|
|
551
552
|
fleet:query capability on the staff key.
|
|
552
|
-
admin
|
|
553
|
-
|
|
554
|
-
|
|
553
|
+
admin replays [--org <uuid>] [--target <uuid>] [--status captured|not_reproduced|queued|running|unsupported|failed] [--limit <n>]
|
|
554
|
+
Live answer replays: breakthroughs re-asked in ChatGPT /
|
|
555
|
+
Gemini / Google / Perplexity / Claude, with screenshots.
|
|
556
|
+
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
|
+
Replay a breakthrough live now (background; poll
|
|
559
|
+
admin replays). With neither, the client's newest
|
|
560
|
+
breakthrough. Needs fleet:replay.
|
|
561
|
+
admin experiments list [--q colons] [--status proposed] [--primitive write] [--stage rarity] | show <id>
|
|
562
|
+
| new "<what you are investigating>" [--primitive write|research|shred|enhance] [--stages a,b]
|
|
563
|
+
[--hypothesis …] [--plan-file p.json] [--tags a,b] [--as "<agent>"]
|
|
564
|
+
| update <id> [--plan-file …] [--result-file …] [--evidence kind:ref,…] [--stages a,b] [--budget N]
|
|
555
565
|
| note|started <id> "<text>" | spend <id> <runs>
|
|
556
|
-
| conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "…" [--evidence …]
|
|
566
|
+
| conclude <id> --decision ship|drop|follow-up|inconclusive --conclusion "…" [--stages a,b] [--evidence …]
|
|
557
567
|
| abandon <id> --reason "…"
|
|
558
|
-
|
|
559
|
-
how, what it found and why
|
|
560
|
-
|
|
561
|
-
|
|
568
|
+
Each primitive's notebook of investigations:
|
|
569
|
+
what we tried, how, what it found and why,
|
|
570
|
+
and the pipeline stages it is about. A plan
|
|
571
|
+
is approved by a signed-in person before a
|
|
572
|
+
budget is spent; concluding needs evidence,
|
|
573
|
+
not approval. "ship" hands off: a promoter
|
|
574
|
+
makes it live. Needs tuning:experiment.
|
|
562
575
|
admin tuning-import <brief-results.json> [--experiment <id>]
|
|
563
576
|
One brief's kernels lab results as a console
|
|
564
577
|
session (a trial per arm), linked to its
|
|
@@ -622,7 +635,11 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
622
635
|
candidate, compare, comparisons, picks, pick,
|
|
623
636
|
reading-list, pair, side-by-side, promote,
|
|
624
637
|
sessions, session-new, propose, trial-add,
|
|
625
|
-
trial-run, trial-save,
|
|
638
|
+
trial-run, trial-save, the Write input bank
|
|
639
|
+
(inputs, input, inputs-harvest, input-save),
|
|
640
|
+
screens (screen-new, screens, screen,
|
|
641
|
+
screen-advance),
|
|
642
|
+
and the arena, shredder
|
|
626
643
|
and enhancer twins (every flag is listed in
|
|
627
644
|
cli/lib/commands/admin.mjs).
|
|
628
645
|
admin create-org --domain <bare-domain> [--name "Acme"] [--test] [--no-key]
|
|
@@ -808,13 +825,14 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
808
825
|
sequences get <sequence-id> Flow, enrollments, who replied at each step
|
|
809
826
|
sequences create --name <n> (--flow '<json>'|--flow-file <path>) [--mailer <mailer-id>]...
|
|
810
827
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
811
|
-
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
828
|
+
[--[no-]test-mode] [--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
812
829
|
New draft sequence (nothing sends yet; rails default on, opt-out line off;
|
|
813
830
|
sends 9:00-17:00 in each recipient's inferred timezone, else the sender's)
|
|
814
831
|
sequences update <sequence-id> [--name <n>] [--flow '<json>'|--flow-file <path>]
|
|
815
832
|
[--status draft|active|paused|archived] [--mailer <mailer-id>]...
|
|
816
833
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
817
|
-
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
834
|
+
[--[no-]test-mode] [--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
835
|
+
--test-mode (internal accounts only): walks as live, sends nothing
|
|
818
836
|
sequences pause|resume <sequence-id> Stop / restart sending
|
|
819
837
|
sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
|
|
820
838
|
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
|
@@ -825,6 +843,12 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
825
843
|
sequences links create --days <1..3> Mint a no-login review link (shown once)
|
|
826
844
|
sequences links revoke <link-id> --yes
|
|
827
845
|
|
|
846
|
+
Dispatches (the named review desks a sequence's review stage queues to):
|
|
847
|
+
dispatches list Desks + how many emails each has queued
|
|
848
|
+
dispatches create --name <n>
|
|
849
|
+
dispatches rename <dispatch-id> --name <n>
|
|
850
|
+
dispatches delete <dispatch-id> --yes Queued emails move to the default desk
|
|
851
|
+
|
|
828
852
|
Prospects (who you write to):
|
|
829
853
|
prospects lists Your lists + counts
|
|
830
854
|
prospects list <list-id> One list
|
|
@@ -837,6 +861,11 @@ Prospects (who you write to):
|
|
|
837
861
|
--enrich starts enrichment as soon as the pull lands
|
|
838
862
|
prospects enrich [--list <list-id>] [--status <s>] [--q <search>]
|
|
839
863
|
Funding + competitors for un-enriched rows
|
|
864
|
+
prospects openers <list-id> [--group <ledes-group-id>|--new-group <name>]
|
|
865
|
+
Print an opening line for every row without one (background)
|
|
866
|
+
prospects delete <prospect-id>... --yes Permanently delete prospects
|
|
867
|
+
prospects columns add <label> Add a custom column (key returned)
|
|
868
|
+
prospects columns delete <column-id> --yes
|
|
840
869
|
prospects update <prospect-id> [--status new|contacted|replied|unsubscribed|archived] [--name <n>]
|
|
841
870
|
[--first-name] [--last-name] [--title] [--company] [--website] [--linkedin] [--school]
|
|
842
871
|
[--city] [--country] [--years] [--notes] [--custom key=value]...
|
|
@@ -862,7 +891,7 @@ Mail:
|
|
|
862
891
|
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
863
892
|
|
|
864
893
|
Insights:
|
|
865
|
-
insights summary [--period
|
|
894
|
+
insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
|
|
866
895
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
867
896
|
insights post <article-id> [--period 14d|30d|90d]
|
|
868
897
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
|
@@ -960,6 +989,7 @@ const CLIENT_COMMANDS = {
|
|
|
960
989
|
ledes: cmdLede,
|
|
961
990
|
landings: cmdLandings,
|
|
962
991
|
sequences: cmdSequences,
|
|
992
|
+
dispatches: cmdDispatches,
|
|
963
993
|
prospects: cmdProspects,
|
|
964
994
|
mail: cmdMail,
|
|
965
995
|
mailers: cmdMailers,
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|promote … (tuning:experiment)
|
|
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
|
+
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
14
|
+
// admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
|
|
13
15
|
// admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
|
|
14
16
|
// [--capabilities a,b] [--onboard] [--about "…"] [--sitemap <url>]
|
|
15
17
|
// [--cms <provider>] [--posts N] [--seers suggest|skip] [--no-visuals]
|
|
@@ -266,7 +268,8 @@ function evidenceFrom(flags) {
|
|
|
266
268
|
|
|
267
269
|
function experimentLine(e) {
|
|
268
270
|
const decision = e.decision ? ` → ${e.decision}` : "";
|
|
269
|
-
|
|
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}` : ""}`;
|
|
270
273
|
}
|
|
271
274
|
|
|
272
275
|
/**
|
|
@@ -291,7 +294,13 @@ async function cmdAdminExperiments(ctx) {
|
|
|
291
294
|
switch (sub) {
|
|
292
295
|
case "list": {
|
|
293
296
|
const params = new URLSearchParams(
|
|
294
|
-
compact({
|
|
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
|
+
})
|
|
295
304
|
);
|
|
296
305
|
const result = await staffRequest(ctx, "GET", `${EXPERIMENTS}?${params}`);
|
|
297
306
|
return out(result, (r) =>
|
|
@@ -304,9 +313,13 @@ async function cmdAdminExperiments(ctx) {
|
|
|
304
313
|
case "new": {
|
|
305
314
|
const question = [id, ...rest].filter(Boolean).join(" ").trim();
|
|
306
315
|
if (question.length < 3)
|
|
307
|
-
throw new CliError(
|
|
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
|
+
);
|
|
308
319
|
const body = compact({
|
|
309
320
|
question,
|
|
321
|
+
primitive: flagStr(flags.primitive),
|
|
322
|
+
stages: flagList(flags.stages),
|
|
310
323
|
restated: flagStr(flags.hypothesis),
|
|
311
324
|
plan: jsonFile(flags, "plan"),
|
|
312
325
|
method: flagStr(flags.method),
|
|
@@ -329,6 +342,7 @@ async function cmdAdminExperiments(ctx) {
|
|
|
329
342
|
result: jsonFile(flags, "result"),
|
|
330
343
|
method: flagStr(flags.method),
|
|
331
344
|
tags: flagList(flags.tags),
|
|
345
|
+
stages: flagList(flags.stages),
|
|
332
346
|
budget_runs: flagNum(flags.budget),
|
|
333
347
|
engine_build: flagStr(flags.build),
|
|
334
348
|
add_evidence: evidenceFrom(flags),
|
|
@@ -355,12 +369,13 @@ async function cmdAdminExperiments(ctx) {
|
|
|
355
369
|
const conclusion = flagStr(flags.conclusion);
|
|
356
370
|
if (!id || !decision || !conclusion)
|
|
357
371
|
throw new CliError(
|
|
358
|
-
'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]'
|
|
359
373
|
);
|
|
360
374
|
const body = compact({
|
|
361
375
|
action: "conclude",
|
|
362
376
|
decision,
|
|
363
377
|
conclusion,
|
|
378
|
+
stages: flagList(flags.stages),
|
|
364
379
|
result: jsonFile(flags, "result"),
|
|
365
380
|
add_evidence: evidenceFrom(flags),
|
|
366
381
|
actor: actorFrom(flags),
|
|
@@ -441,13 +456,28 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
441
456
|
* admin tuning base-kernels the kernels a session can run on (ids for --kernel)
|
|
442
457
|
* admin tuning base-versions <kernel-id> the versions a session on it can run against
|
|
443
458
|
* admin tuning sessions | session <id> [--texts] list / one session: trials, runs and their numbers (--texts adds each trial's latest text)
|
|
444
|
-
* admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…") [--kind article|outline|brief]
|
|
459
|
+
* admin tuning session-new --title "…" --kernel <id|name> (--input-file f.md | --input "…" | --bank <input-id>) [--kind article|outline|brief]
|
|
445
460
|
* [--version N] [--writers echo/echo,…] [--trials-from <session-id>]
|
|
461
|
+
* (--bank: a bank input's text, and its request's constraints under every trial)
|
|
446
462
|
* admin tuning propose <session-id> --text "Echo with rarity on; live with the shred after" [--create]
|
|
447
463
|
* admin tuning trial-add <session-id> [--name "…"] [--patch '{…}'] [--options '{…}'] [--from <trial-id>]
|
|
448
464
|
* admin tuning trial-run <trial-id> [--repeats 2] queue runs (guardrails apply)
|
|
449
465
|
* admin tuning trial-save <trial-id> [--note "…"] save a trial as a candidate version (never live)
|
|
450
466
|
*
|
|
467
|
+
* The Write input bank: real kernel requests (above all the arena outlines Auto-Pen sends), as sent.
|
|
468
|
+
* admin tuning inputs [--filter "origin=arena_outline;material=thin,typical"] [--q "…"] list by tag, with coverage per bucket
|
|
469
|
+
* admin tuning input <input-id> one input in full: text, constraints, tags, run defaults, origin
|
|
470
|
+
* admin tuning inputs-harvest [--n 40] [--pool 400] [--callers auto-pen,endpoint] [--since-days 21] [--min-per-bucket 3]
|
|
471
|
+
* freeze recent production runs, stratified; idempotent by key
|
|
472
|
+
* admin tuning input-save <session-id> [--note "…"] save a session's pasted input or arena outline to the bank
|
|
473
|
+
*
|
|
474
|
+
* Screens: "is this candidate worth a full comparison?", on a slice of the bank (it does not decide).
|
|
475
|
+
* admin tuning screen-new --title "…" (--from <session-id> | --kernel <kernel-id> --versions 23,29) [--version N]
|
|
476
|
+
* [--n 8] [--filter "origin=arena_outline;material=thin"] [--inputs <id>,…] [--repeats 1] [--lift-budget]
|
|
477
|
+
* one session per input with the same trials; runs queue as the console's caps allow
|
|
478
|
+
* admin tuning screens | screen <id> list / one screen: per candidate, each measure against the Base, with its noise
|
|
479
|
+
* admin tuning screen-advance <id> queue what the caps allow now (the cron does it every minute)
|
|
480
|
+
*
|
|
451
481
|
* Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
|
|
452
482
|
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
453
483
|
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
@@ -490,6 +520,19 @@ function jsonFlag(raw, name) {
|
|
|
490
520
|
}
|
|
491
521
|
}
|
|
492
522
|
|
|
523
|
+
/**
|
|
524
|
+
* `--filter "origin=arena_outline;material=thin,typical"` → the bank's query params (one comma list
|
|
525
|
+
* per tag dimension; lib/tuning/input-tags.ts `parseTagFilter` reads the same form).
|
|
526
|
+
*/
|
|
527
|
+
function tagFilterParams(raw) {
|
|
528
|
+
const out = {};
|
|
529
|
+
for (const part of (raw ?? "").split(";")) {
|
|
530
|
+
const [k, v] = part.split("=");
|
|
531
|
+
if (k?.trim() && v?.trim()) out[k.trim()] = v.trim();
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
493
536
|
/**
|
|
494
537
|
* A session as an agent reads it: each trial's changes and runs with their headline numbers, and
|
|
495
538
|
* (with `texts`) the latest finished text per trial. The full session (traces, records) is `--json`.
|
|
@@ -768,9 +811,11 @@ async function cmdAdminTuning(ctx) {
|
|
|
768
811
|
throw new CliError(`--input-file ${path}: ${err.message}`);
|
|
769
812
|
}
|
|
770
813
|
}
|
|
771
|
-
|
|
814
|
+
// A bank input instead of a paste: its text, and its request's constraints under every trial.
|
|
815
|
+
const bank = flagStr(flags.bank);
|
|
816
|
+
if (!title || !kernel || (!input && !bank))
|
|
772
817
|
throw new CliError(
|
|
773
|
-
'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>]'
|
|
818
|
+
'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>]'
|
|
774
819
|
);
|
|
775
820
|
let kernelId = kernel;
|
|
776
821
|
if (!/^[0-9a-f-]{36}$/i.test(kernel)) {
|
|
@@ -786,8 +831,9 @@ async function cmdAdminTuning(ctx) {
|
|
|
786
831
|
T,
|
|
787
832
|
compact({
|
|
788
833
|
title,
|
|
789
|
-
input_text: input,
|
|
834
|
+
input_text: bank ? undefined : input,
|
|
790
835
|
input_kind: flagStr(flags.kind) ?? "unknown",
|
|
836
|
+
material: bank ? { source: "bank", input_id: bank } : undefined,
|
|
791
837
|
base_kernel_id: kernelId,
|
|
792
838
|
base_version: flagNum(flags.version),
|
|
793
839
|
trials_from_session_id: flagStr(flags["trials-from"]),
|
|
@@ -869,6 +915,167 @@ async function cmdAdminTuning(ctx) {
|
|
|
869
915
|
`${r.kernel}: saved as candidate v${r.version}${r.baseVersion ? ` (from v${r.baseVersion})` : ""}${r.leftBehind.length ? `; per-run, not saved: ${r.leftBehind.join(", ")}` : ""}`
|
|
870
916
|
);
|
|
871
917
|
}
|
|
918
|
+
// --- The Write input bank: real kernel requests a session can start from ---
|
|
919
|
+
case "inputs": {
|
|
920
|
+
const q = new URLSearchParams(
|
|
921
|
+
compact({ ...tagFilterParams(flagStr(flags.filter)), q: flagStr(flags.q), limit: flagStr(flags.limit) })
|
|
922
|
+
);
|
|
923
|
+
const r = await staffRequest(ctx, "GET", `${T}/inputs${q.size ? `?${q}` : ""}`);
|
|
924
|
+
if (json) return printResult(io, flags, r);
|
|
925
|
+
const coverage = Object.entries(r.coverage)
|
|
926
|
+
.map(
|
|
927
|
+
([dim, buckets]) =>
|
|
928
|
+
` ${dim.padEnd(11)} ${Object.entries(buckets)
|
|
929
|
+
.map(([b, n]) => `${b} ${n}`)
|
|
930
|
+
.join(" · ")}`
|
|
931
|
+
)
|
|
932
|
+
.join("\n");
|
|
933
|
+
return io.log(
|
|
934
|
+
[
|
|
935
|
+
`bank ${r.set}: ${r.inputs.length} of ${r.total} input(s)`,
|
|
936
|
+
...r.inputs.map(
|
|
937
|
+
(i) =>
|
|
938
|
+
`${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}`
|
|
939
|
+
),
|
|
940
|
+
"coverage (the whole bank):",
|
|
941
|
+
coverage,
|
|
942
|
+
].join("\n")
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
case "input": {
|
|
946
|
+
if (!id) throw new CliError("Usage: admin tuning input <input-id>");
|
|
947
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/inputs/${id}`));
|
|
948
|
+
}
|
|
949
|
+
case "inputs-harvest": {
|
|
950
|
+
const r = await staffRequest(
|
|
951
|
+
ctx,
|
|
952
|
+
"POST",
|
|
953
|
+
`${T}/inputs/harvest`,
|
|
954
|
+
compact({
|
|
955
|
+
n: flagNum(flags.n),
|
|
956
|
+
pool: flagNum(flags.pool),
|
|
957
|
+
callers: flagStr(flags.callers)
|
|
958
|
+
?.split(",")
|
|
959
|
+
.map((c) => c.trim())
|
|
960
|
+
.filter(Boolean),
|
|
961
|
+
since_days: flagNum(flags["since-days"]),
|
|
962
|
+
min_per_bucket: flagNum(flags["min-per-bucket"]),
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
if (json) return printResult(io, flags, r);
|
|
966
|
+
return io.log(
|
|
967
|
+
[
|
|
968
|
+
`added ${r.added.length} input(s) to ${r.set} from a pool of ${r.pool} run(s); ${r.alreadyHeld} already held`,
|
|
969
|
+
...r.added.map((a) => ` ${a.key} (${a.reason})`),
|
|
970
|
+
...Object.entries(r.skipped).map(([why, n]) => ` skipped ${n}: ${why}`),
|
|
971
|
+
].join("\n")
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
case "input-save": {
|
|
975
|
+
if (!id) throw new CliError('Usage: admin tuning input-save <session-id> [--note "…"]');
|
|
976
|
+
const r = await staffRequest(
|
|
977
|
+
ctx,
|
|
978
|
+
"POST",
|
|
979
|
+
`${T}/sessions/${id}/bank`,
|
|
980
|
+
compact({ note: flagStr(flags.note) })
|
|
981
|
+
);
|
|
982
|
+
return json ? printResult(io, flags, r) : io.log(`bank input ${r.input.id} (${r.input.key})`);
|
|
983
|
+
}
|
|
984
|
+
// --- Screens: candidates across a slice of the bank, read against the Base, beside the noise ---
|
|
985
|
+
case "screen-new": {
|
|
986
|
+
const title = flagStr(flags.title);
|
|
987
|
+
const from = flagStr(flags.from);
|
|
988
|
+
const versions = (flagStr(flags.versions) ?? "")
|
|
989
|
+
.split(",")
|
|
990
|
+
.map((v) => Number(v.trim()))
|
|
991
|
+
.filter((v) => Number.isInteger(v) && v > 0);
|
|
992
|
+
if (!title || (!from && !(flagStr(flags.kernel) && versions.length)))
|
|
993
|
+
throw new CliError(
|
|
994
|
+
'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]'
|
|
995
|
+
);
|
|
996
|
+
const filter = Object.fromEntries(
|
|
997
|
+
Object.entries(tagFilterParams(flagStr(flags.filter))).map(([k, v]) => [
|
|
998
|
+
k,
|
|
999
|
+
v
|
|
1000
|
+
.split(",")
|
|
1001
|
+
.map((x) => x.trim())
|
|
1002
|
+
.filter(Boolean),
|
|
1003
|
+
])
|
|
1004
|
+
);
|
|
1005
|
+
const inputIds = (flagStr(flags.inputs) ?? "")
|
|
1006
|
+
.split(",")
|
|
1007
|
+
.map((x) => x.trim())
|
|
1008
|
+
.filter(Boolean);
|
|
1009
|
+
const r = await staffRequest(
|
|
1010
|
+
ctx,
|
|
1011
|
+
"POST",
|
|
1012
|
+
`${T}/screens`,
|
|
1013
|
+
compact({
|
|
1014
|
+
title,
|
|
1015
|
+
from_session_id: from,
|
|
1016
|
+
base_kernel_id: flagStr(flags.kernel),
|
|
1017
|
+
base_version: flagNum(flags.version),
|
|
1018
|
+
versions: versions.length ? versions : undefined,
|
|
1019
|
+
n: flagNum(flags.n),
|
|
1020
|
+
filter: Object.keys(filter).length ? filter : undefined,
|
|
1021
|
+
input_ids: inputIds.length ? inputIds : undefined,
|
|
1022
|
+
repeats: flagNum(flags.repeats),
|
|
1023
|
+
lift_budget: flagBool(flags["lift-budget"]) || undefined,
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
if (json) return printResult(io, flags, r);
|
|
1027
|
+
return io.log(
|
|
1028
|
+
`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}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
case "screens": {
|
|
1032
|
+
const r = await staffRequest(ctx, "GET", `${T}/screens?limit=${flagNum(flags.limit) ?? 30}`);
|
|
1033
|
+
if (json) return printResult(io, flags, r);
|
|
1034
|
+
return io.log(
|
|
1035
|
+
r.screens
|
|
1036
|
+
.map(
|
|
1037
|
+
(x) =>
|
|
1038
|
+
`${x.id} ${x.status.padEnd(7)} ${x.title} · ${x.inputs} inputs${x.pending ? ` · ${x.pending} waiting` : ""}`
|
|
1039
|
+
)
|
|
1040
|
+
.join("\n") || "(no screens)"
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
case "screen": {
|
|
1044
|
+
if (!id) throw new CliError("Usage: admin tuning screen <screen-id>");
|
|
1045
|
+
const r = await staffRequest(ctx, "GET", `${T}/screens/${id}`);
|
|
1046
|
+
if (json) return printResult(io, flags, r);
|
|
1047
|
+
// The reading as an agent reads it: per candidate, each measure's difference from the Base and its noise.
|
|
1048
|
+
return printResult(
|
|
1049
|
+
io,
|
|
1050
|
+
{ ...flags, json: true },
|
|
1051
|
+
{
|
|
1052
|
+
screen: r.screen,
|
|
1053
|
+
note: r.aggregate.note,
|
|
1054
|
+
columns: r.aggregate.columns,
|
|
1055
|
+
measures: r.aggregate.measures.map((m) => ({
|
|
1056
|
+
measure: m.label,
|
|
1057
|
+
judge: m.judgeId,
|
|
1058
|
+
standing: m.standing,
|
|
1059
|
+
sigma: m.sigma,
|
|
1060
|
+
per_column: m.perTrial.map((p) =>
|
|
1061
|
+
compact({
|
|
1062
|
+
column: p.column,
|
|
1063
|
+
n: p.n,
|
|
1064
|
+
mean: p.mean,
|
|
1065
|
+
delta: p.delta ?? undefined,
|
|
1066
|
+
})
|
|
1067
|
+
),
|
|
1068
|
+
})),
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
case "screen-advance": {
|
|
1073
|
+
if (!id) throw new CliError("Usage: admin tuning screen-advance <screen-id>");
|
|
1074
|
+
const r = await staffRequest(ctx, "POST", `${T}/screens/${id}/advance`, {});
|
|
1075
|
+
return json
|
|
1076
|
+
? printResult(io, flags, r)
|
|
1077
|
+
: io.log(`queued ${r.queued} run(s)${r.waiting ? `; waiting: ${r.waiting}` : ""}`);
|
|
1078
|
+
}
|
|
872
1079
|
// --- Research: our arena's versions, research comparisons, the deploy split ---
|
|
873
1080
|
case "arenas": {
|
|
874
1081
|
const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
|
|
@@ -1926,8 +2133,79 @@ async function cmdAdminEvals(ctx) {
|
|
|
1926
2133
|
}
|
|
1927
2134
|
}
|
|
1928
2135
|
|
|
2136
|
+
const REPLAY_SURFACES = ["openai", "anthropic", "google-aio", "google-gemini", "perplexity"];
|
|
2137
|
+
const REPLAY_STATUSES = ["queued", "running", "captured", "not_reproduced", "unsupported", "failed"];
|
|
2138
|
+
const REPLAYS = "/api/superadmin/answer-replays";
|
|
2139
|
+
|
|
2140
|
+
function oneOf(flags, name, values) {
|
|
2141
|
+
const v = flagStr(flags[name]);
|
|
2142
|
+
if (v !== undefined && !values.includes(v)) throw new CliError(`--${name} must be one of: ${values.join(", ")}`);
|
|
2143
|
+
return v;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function formatReplays(r) {
|
|
2147
|
+
const rows = r.replays ?? [];
|
|
2148
|
+
if (!rows.length) return "No answer replays.";
|
|
2149
|
+
return rows
|
|
2150
|
+
.map((x) => {
|
|
2151
|
+
const head = `${x.id} ${x.surfaceLabel} ${x.status} ${x.attemptsMade}/${x.plannedAttempts} asks ${x.createdAt}`;
|
|
2152
|
+
const lines = [head];
|
|
2153
|
+
if (x.capture) {
|
|
2154
|
+
lines.push(
|
|
2155
|
+
` captured on ask ${x.capture.step + 1}: "${x.capture.query}" (web search ${x.capture.webSearch})`
|
|
2156
|
+
);
|
|
2157
|
+
for (const img of x.capture.images ?? []) lines.push(` ${img.kind}: ${img.url}`);
|
|
2158
|
+
}
|
|
2159
|
+
if (x.error) lines.push(` ${x.error}`);
|
|
2160
|
+
return lines.join("\n");
|
|
2161
|
+
})
|
|
2162
|
+
.join("\n");
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/** `admin replays` / `admin replay` — live answer replays (staff only). */
|
|
2166
|
+
async function cmdAdminReplays(ctx, sub) {
|
|
2167
|
+
const { flags, positionals, io } = ctx;
|
|
2168
|
+
if (sub === "replays") {
|
|
2169
|
+
const params = new URLSearchParams(
|
|
2170
|
+
compact({
|
|
2171
|
+
org_id: flagStr(flags.org),
|
|
2172
|
+
target_id: flagStr(flags.target),
|
|
2173
|
+
status: oneOf(flags, "status", REPLAY_STATUSES),
|
|
2174
|
+
limit: flagNum(flags.limit)?.toString(),
|
|
2175
|
+
})
|
|
2176
|
+
);
|
|
2177
|
+
const result = await staffRequest(ctx, "GET", `${REPLAYS}${params.size ? `?${params}` : ""}`);
|
|
2178
|
+
printResult(io, flags, result, formatReplays);
|
|
2179
|
+
return 0;
|
|
2180
|
+
}
|
|
2181
|
+
const org = flagStr(flags.org);
|
|
2182
|
+
const responseId = positionals[0];
|
|
2183
|
+
const target = flagStr(flags.target);
|
|
2184
|
+
if (!org) throw new CliError("admin replay needs --org <org-id>");
|
|
2185
|
+
const result = await staffPost(
|
|
2186
|
+
ctx,
|
|
2187
|
+
REPLAYS,
|
|
2188
|
+
compact({
|
|
2189
|
+
org_id: org,
|
|
2190
|
+
response_id: responseId,
|
|
2191
|
+
target_id: target,
|
|
2192
|
+
surface: oneOf(flags, "surface", REPLAY_SURFACES),
|
|
2193
|
+
})
|
|
2194
|
+
);
|
|
2195
|
+
ok(
|
|
2196
|
+
ctx,
|
|
2197
|
+
result.already_queued
|
|
2198
|
+
? `Already replaying: ${result.replay_id}`
|
|
2199
|
+
: `Replay ${result.replay_id} is ${result.status}. Poll: ${ctx.bin} admin replays --org ${org}`
|
|
2200
|
+
);
|
|
2201
|
+
printResult(io, flags, result);
|
|
2202
|
+
return 0;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1929
2205
|
export async function cmdAdmin(ctx) {
|
|
1930
2206
|
const sub = ctx.positionals[0];
|
|
2207
|
+
if (sub === "replays" || sub === "replay")
|
|
2208
|
+
return cmdAdminReplays({ ...ctx, positionals: ctx.positionals.slice(1) }, sub);
|
|
1931
2209
|
if (sub === "crm") return cmdAdminCrm({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
1932
2210
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
1933
2211
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
@@ -1937,6 +2215,6 @@ export async function cmdAdmin(ctx) {
|
|
|
1937
2215
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
1938
2216
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
1939
2217
|
throw new CliError(
|
|
1940
|
-
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin evals … | admin tuning … | admin tuning-context (see --help)'
|
|
2218
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin crm … | admin evals … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
1941
2219
|
);
|
|
1942
2220
|
}
|
|
@@ -59,6 +59,8 @@ const SEQUENCE_RAIL_ARGS = {
|
|
|
59
59
|
postal_address_footer: { flag: "postal-address-footer", type: "bool", negate: "no-postal-address-footer" },
|
|
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
|
+
// Staff-only to turn on (the server refuses it for anyone else): walks as live, sends nothing.
|
|
63
|
+
test_mode: { flag: "test-mode", type: "bool", negate: "no-test-mode" },
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
// A send-window edge from --<flag> HH:MM (or a raw minute of day) → the
|
|
@@ -158,6 +160,17 @@ export const ARGS = {
|
|
|
158
160
|
item_id: { positional: 0, label: "item-id", required: true },
|
|
159
161
|
action: { flag: "action", required: true, enum: ["approve", "skip", "delete"] },
|
|
160
162
|
},
|
|
163
|
+
list_dispatches: {},
|
|
164
|
+
create_dispatch: {
|
|
165
|
+
name: { flag: "name", required: true },
|
|
166
|
+
},
|
|
167
|
+
update_dispatch: {
|
|
168
|
+
dispatch_id: { positional: 0, label: "dispatch-id", required: true },
|
|
169
|
+
name: { flag: "name", required: true },
|
|
170
|
+
},
|
|
171
|
+
delete_dispatch: {
|
|
172
|
+
dispatch_id: { positional: 0, label: "dispatch-id", required: true },
|
|
173
|
+
},
|
|
161
174
|
list_dispatch_links: {},
|
|
162
175
|
create_dispatch_link: {
|
|
163
176
|
days: { flag: "days", type: "num", required: true },
|
|
@@ -210,6 +223,26 @@ export const ARGS = {
|
|
|
210
223
|
status: { flag: "status", enum: PROSPECT_STATUSES },
|
|
211
224
|
q: "q",
|
|
212
225
|
},
|
|
226
|
+
delete_prospects: {
|
|
227
|
+
prospect_ids: {
|
|
228
|
+
value: (flags, positionals) => {
|
|
229
|
+
const ids = [...positionals, ...flagList(flags.id)];
|
|
230
|
+
if (!ids.length) throw new CliError("Missing <prospect-id>... (or --id <prospect-id>, repeatable)");
|
|
231
|
+
return ids;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
create_prospect_column: {
|
|
236
|
+
label: { positional: 0, label: "label", required: true },
|
|
237
|
+
},
|
|
238
|
+
delete_prospect_column: {
|
|
239
|
+
column_id: { positional: 0, label: "column-id", required: true },
|
|
240
|
+
},
|
|
241
|
+
generate_prospect_openers: {
|
|
242
|
+
list_id: { positional: 0, label: "list-id", required: true },
|
|
243
|
+
group_id: "group",
|
|
244
|
+
new_group_name: "new-group",
|
|
245
|
+
},
|
|
213
246
|
update_prospect: {
|
|
214
247
|
prospect_id: { positional: 0, label: "prospect-id", required: true },
|
|
215
248
|
status: { flag: "status", enum: PROSPECT_STATUSES },
|
|
@@ -292,6 +325,7 @@ const VALUE_FLAGS = {
|
|
|
292
325
|
update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
293
326
|
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
294
327
|
update_review_item: ["body", "file"],
|
|
328
|
+
delete_prospects: ["id"],
|
|
295
329
|
import_prospects_csv: ["csv", "csv-file"],
|
|
296
330
|
update_prospect: ["custom"],
|
|
297
331
|
set_mailer_forwarding: ["to", "clear"],
|
|
@@ -306,6 +340,37 @@ async function run(ctx, tool, message) {
|
|
|
306
340
|
}
|
|
307
341
|
|
|
308
342
|
const SEQUENCES_USAGE = `Usage: sequences <list|get|create|update|pause|resume|enroll|stop|review|links>`;
|
|
343
|
+
const DISPATCHES_USAGE = `Usage: dispatches <list|create|rename|delete>`;
|
|
344
|
+
|
|
345
|
+
// Dispatches: the named review desks a sequence's review stage queues to.
|
|
346
|
+
export async function cmdDispatches(ctx) {
|
|
347
|
+
const sub = ctx.positionals[0];
|
|
348
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
349
|
+
switch (sub) {
|
|
350
|
+
case "list":
|
|
351
|
+
case "ls":
|
|
352
|
+
case undefined:
|
|
353
|
+
return run(rest, "list_dispatches");
|
|
354
|
+
case "create":
|
|
355
|
+
case "new":
|
|
356
|
+
return run(rest, "create_dispatch", "Dispatch created.");
|
|
357
|
+
case "rename":
|
|
358
|
+
case "update":
|
|
359
|
+
return run(rest, "update_dispatch", "Dispatch renamed.");
|
|
360
|
+
case "delete": {
|
|
361
|
+
if (!ctx.flags.yes) {
|
|
362
|
+
const id = flagStr(rest.positionals[0]) ?? "<dispatch-id>";
|
|
363
|
+
ctx.io.error(
|
|
364
|
+
`Its queued emails move to the default desk. Re-run with --yes to confirm: ${ctx.bin} dispatches delete ${id} --yes`
|
|
365
|
+
);
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
return run(rest, "delete_dispatch", "Dispatch deleted; its queued emails moved to the default desk.");
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
throw new CliError(`Unknown dispatches subcommand: ${sub}. ${DISPATCHES_USAGE}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
309
374
|
|
|
310
375
|
export async function cmdSequences(ctx) {
|
|
311
376
|
const sub = ctx.positionals[0];
|
|
@@ -395,7 +460,7 @@ async function sequencesLinks(ctx, sub) {
|
|
|
395
460
|
}
|
|
396
461
|
}
|
|
397
462
|
|
|
398
|
-
const PROSPECTS_USAGE = `Usage: prospects <lists|list|ls|get|create-list|delete-list|import|source|enrich|update>`;
|
|
463
|
+
const PROSPECTS_USAGE = `Usage: prospects <lists|list|ls|get|create-list|delete-list|import|source|enrich|openers|update|delete|columns>`;
|
|
399
464
|
|
|
400
465
|
export async function cmdProspects(ctx) {
|
|
401
466
|
const sub = ctx.positionals[0];
|
|
@@ -424,13 +489,46 @@ export async function cmdProspects(ctx) {
|
|
|
424
489
|
);
|
|
425
490
|
case "enrich":
|
|
426
491
|
return run(rest, "enrich_prospects", "Enrichment queued.");
|
|
492
|
+
case "openers":
|
|
493
|
+
return run(rest, "generate_prospect_openers", "Printing openers; they fill in as each one lands.");
|
|
427
494
|
case "update":
|
|
428
495
|
return run(rest, "update_prospect", "Updated prospect.");
|
|
496
|
+
case "delete":
|
|
497
|
+
case "rm": {
|
|
498
|
+
if (!ctx.flags.yes) {
|
|
499
|
+
ctx.io.error(`Deleting prospects cannot be undone. Re-run with --yes to confirm.`);
|
|
500
|
+
return 1;
|
|
501
|
+
}
|
|
502
|
+
return run(rest, "delete_prospects", "Deleted.");
|
|
503
|
+
}
|
|
504
|
+
case "columns":
|
|
505
|
+
return prospectsColumns(rest, rest.positionals[0]);
|
|
429
506
|
default:
|
|
430
507
|
throw new CliError(`Unknown prospects subcommand: ${sub ?? "(none)"}. ${PROSPECTS_USAGE}`);
|
|
431
508
|
}
|
|
432
509
|
}
|
|
433
510
|
|
|
511
|
+
async function prospectsColumns(ctx, sub) {
|
|
512
|
+
const inner = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
513
|
+
switch (sub) {
|
|
514
|
+
case "add":
|
|
515
|
+
return run(inner, "create_prospect_column", "Column added.");
|
|
516
|
+
case "delete":
|
|
517
|
+
case "rm": {
|
|
518
|
+
if (!ctx.flags.yes) {
|
|
519
|
+
const id = flagStr(inner.positionals[0]) ?? "<column-id>";
|
|
520
|
+
ctx.io.error(`Re-run with --yes to confirm: ${ctx.bin} prospects columns delete ${id} --yes`);
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
return run(inner, "delete_prospect_column", "Column removed.");
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
throw new CliError(
|
|
527
|
+
`Unknown columns subcommand: ${sub ?? "(none)"}. Try: add <label>, delete <column-id> --yes`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
434
532
|
const MAILERS_USAGE = `Usage: mailers <list|domain|messages|message|forward|warmup|limit|retire|suppressions>`;
|
|
435
533
|
|
|
436
534
|
export async function cmdMailers(ctx) {
|