@letterstory/cli 0.87.1 → 0.90.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 +15 -0
- package/lib/cli.mjs +28 -2
- package/lib/commands/admin.mjs +205 -51
- package/lib/commands/insights.mjs +4 -1
- package/lib/commands/lede.mjs +82 -1
- package/lib/commands/outbound.mjs +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -346,6 +346,21 @@ letterstory admin ask "which clients published nothing in the last 30 days?" --s
|
|
|
346
346
|
This is the headless half of `/superadmin/ask` — same function, same schema — so a
|
|
347
347
|
long-running agent can watch the fleet on a schedule without a browser.
|
|
348
348
|
|
|
349
|
+
### Staff: client patterns and Fable
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# What separates clients whose AI visibility moves (docs/architecture/client-patterns.md):
|
|
353
|
+
# every client against itself on share of voice, mention rate and page access, with what we
|
|
354
|
+
# did for it. Needs fleet:read; `run` and `ask` spend Fable tokens and need fleet:query.
|
|
355
|
+
letterstory admin patterns --period 30d
|
|
356
|
+
letterstory admin patterns reports # Fable's weekly reports and runs, newest first
|
|
357
|
+
letterstory admin patterns report <id> # one report's markdown
|
|
358
|
+
letterstory admin patterns run # a report now (takes minutes)
|
|
359
|
+
letterstory admin patterns ask "do answer-gap posts help paying clients too?"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The headless half of Admin → Client performance → Patterns: same functions, same schemas.
|
|
363
|
+
|
|
349
364
|
### Staff: kernel experiments
|
|
350
365
|
|
|
351
366
|
```bash
|
package/lib/cli.mjs
CHANGED
|
@@ -57,13 +57,15 @@ import {
|
|
|
57
57
|
} from "./commands.mjs";
|
|
58
58
|
|
|
59
59
|
// Keep in sync with cli/package.json.
|
|
60
|
-
export const VERSION = "0.
|
|
60
|
+
export const VERSION = "0.90.0";
|
|
61
61
|
|
|
62
62
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
63
63
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
64
64
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
65
65
|
export const BOOLEAN_FLAGS = new Set([
|
|
66
66
|
"json",
|
|
67
|
+
// lede library update / lede proposals accept: clear or drop the line's condition.
|
|
68
|
+
"no-condition",
|
|
67
69
|
"yes",
|
|
68
70
|
"confirm-missing-target",
|
|
69
71
|
"no-wait",
|
|
@@ -639,6 +641,20 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
639
641
|
read-only SQL across every org; --sql prints
|
|
640
642
|
the queries behind the answer. Needs the
|
|
641
643
|
fleet:query capability on the staff key.
|
|
644
|
+
admin brand-detection <org-id> [--term <t>]... [--exact <t>]... [--exclude <phrase>]... [--note "…"] | --clear [--note "…"]
|
|
645
|
+
How a client's brand is detected in AI answers, with
|
|
646
|
+
every earlier setting; with flags, set the exact set
|
|
647
|
+
(fleet:instrument) — e.g. --term Context.dev for a name
|
|
648
|
+
that is also a word. --clear goes back to inferred,
|
|
649
|
+
recorded as your decision.
|
|
650
|
+
admin patterns [--period 14d|30d] [--segment "<segment>"] [--json]
|
|
651
|
+
Client patterns: every client's share of voice,
|
|
652
|
+
mention rate and page access against itself, with
|
|
653
|
+
what we did for it. Needs fleet:read.
|
|
654
|
+
admin patterns reports [--kind reports|weekly|run|ask] | report <id>
|
|
655
|
+
Fable's weekly reports, runs and Ask Fable answers.
|
|
656
|
+
admin patterns run | ask "<question>" Run a Fable report now, or ask Fable a question
|
|
657
|
+
about client patterns (minutes). Needs fleet:query.
|
|
642
658
|
admin replays [--org <uuid>] [--target <uuid>] [--status captured|not_reproduced|queued|running|unsupported|failed] [--limit <n>]
|
|
643
659
|
Live answer replays: breakthroughs re-asked in ChatGPT /
|
|
644
660
|
Gemini / Google / Perplexity / Claude, with screenshots.
|
|
@@ -926,7 +942,7 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
926
942
|
lede balance Ledes left on this org
|
|
927
943
|
lede library list The org's opener library: pre-built + custom lines
|
|
928
944
|
lede library create --text <line> [--alts <v>]... [--peer-alts <v>]... [--requires <trigger>]...
|
|
929
|
-
[--threshold <0..100>] [--tone warm|direct|playful|formal] [--tag <t>]...
|
|
945
|
+
[--threshold <0..100>] [--tone warm|direct|playful|formal] [--tag <t>]... [--condition <use when...>]
|
|
930
946
|
Add a custom line. A {{placeholder}} must appear in
|
|
931
947
|
--requires; proprietary-engine triggers stay
|
|
932
948
|
reserved for the pre-built lines. --threshold
|
|
@@ -935,6 +951,7 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
935
951
|
lower = looser.
|
|
936
952
|
lede library update <lede-id> [--text <line>] [--alts <v>]... [--peer-alts <v>]...
|
|
937
953
|
[--requires <trigger>]... [--threshold <0..100>] [--tone <t>] [--tag <t>]...
|
|
954
|
+
[--condition <use when...> | --no-condition]
|
|
938
955
|
Edit a line; only the flags you pass change
|
|
939
956
|
lede library enable <lede-id> Make a line a run candidate again
|
|
940
957
|
lede library disable <lede-id> Keep a line out of runs (reversible; the way to
|
|
@@ -945,6 +962,13 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
945
962
|
Propose brand-voice rewrites of the wording only.
|
|
946
963
|
Applies nothing: accept a proposal with
|
|
947
964
|
lede library update --text/--alts
|
|
965
|
+
lede proposals [list] [--status proposed|captured|active|rejected] [--limit <n>]
|
|
966
|
+
Lines reviewers wrote while personalizing in
|
|
967
|
+
Dispatch, templated and judged, each with a
|
|
968
|
+
drafted "Use when ..." condition
|
|
969
|
+
lede proposals accept <proposal-id> [--text <line>] [--condition <c> | --no-condition] [--tone <t>]
|
|
970
|
+
Add it to the library (live immediately)
|
|
971
|
+
lede proposals reject <proposal-id> --reason <why> Keep it out; the reason stops it recurring
|
|
948
972
|
|
|
949
973
|
Landings (a link + context; auto-mentioned in articles by collections you enable):
|
|
950
974
|
landings list Your landing pages + mention counts
|
|
@@ -976,6 +1000,7 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
976
1000
|
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
|
977
1001
|
sequences review list Emails waiting for a decision
|
|
978
1002
|
sequences review edit <item-id> [--subject <s>] [--body <text>|--file <path>]
|
|
1003
|
+
[--personalize <line> [--personalize-mode replace|insert]] Mark the edit a personalization (proposed to the opener library)
|
|
979
1004
|
sequences review approve|skip|delete <item-id> Decide one (approve sends now)
|
|
980
1005
|
sequences links list Live Pocket Dispatch links (no tokens)
|
|
981
1006
|
sequences links create --days <1..3> Mint a no-login review link (shown once)
|
|
@@ -1037,6 +1062,7 @@ Insights:
|
|
|
1037
1062
|
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
1038
1063
|
insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
|
|
1039
1064
|
One By-question row: the wordings asked and the AI answers
|
|
1065
|
+
insights detection How your brand is detected in AI answers (terms, who set them)
|
|
1040
1066
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
1041
1067
|
insights post <article-id> [--period 14d|30d|90d]
|
|
1042
1068
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// routes, because they exist precisely to act BEFORE an org exists.
|
|
5
5
|
//
|
|
6
6
|
// admin ask "which kernels are hooked up to the most clients?" [--sql] [--json]
|
|
7
|
+
// admin patterns [--period 14d|30d] [--segment "…"] | reports [--kind reports|weekly|run|ask] | report <id> | run | ask "<question>" (fleet:read / fleet:query)
|
|
7
8
|
// admin experiments list|show|new|update|note|started|spend|conclude|abandon … (tuning:experiment)
|
|
8
9
|
// admin tuning-context [--q "<question>"] (tuning:experiment)
|
|
9
10
|
// admin tuning-import <brief-results.json> [--experiment <id>] (tuning:experiment)
|
|
@@ -15,6 +16,8 @@
|
|
|
15
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)
|
|
16
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
18
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
19
|
+
// admin brand-detection <org-id> (fleet:read)
|
|
20
|
+
// admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
|
|
18
21
|
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
19
22
|
// admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
|
|
20
23
|
// admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
|
|
42
45
|
import { readFileSync } from "node:fs";
|
|
43
46
|
import { CliError, LetterstoryClient } from "../client.mjs";
|
|
44
|
-
import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok } from "./shared.mjs";
|
|
47
|
+
import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok, requirePositional } from "./shared.mjs";
|
|
45
48
|
import { watchDomainJob } from "./onboard.mjs";
|
|
46
49
|
|
|
47
50
|
const SEER_MODES = ["suggest", "skip"];
|
|
@@ -239,6 +242,80 @@ async function cmdAdminAsk(ctx) {
|
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
244
|
|
|
245
|
+
/* ------------------------------------------------------------ client patterns */
|
|
246
|
+
|
|
247
|
+
const PATTERNS = "/api/superadmin/client-patterns";
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `admin patterns …` — the Patterns tab without a browser (docs/architecture/client-patterns.md):
|
|
251
|
+
* the data, Fable's report history, a report on demand, and Ask Fable. `run` and `ask` wait for
|
|
252
|
+
* the finished answer (minutes, not seconds) and print its markdown.
|
|
253
|
+
*/
|
|
254
|
+
async function cmdAdminPatterns(ctx) {
|
|
255
|
+
const [sub = "show", ...rest] = ctx.positionals;
|
|
256
|
+
const json = flagBool(ctx.flags.json);
|
|
257
|
+
switch (sub) {
|
|
258
|
+
case "show": {
|
|
259
|
+
const { payload } = await staffRequest(ctx, "GET", PATTERNS);
|
|
260
|
+
if (!payload) throw new CliError("The patterns snapshot has not been computed yet.");
|
|
261
|
+
const period = flagStr(ctx.flags.period) ?? "30d";
|
|
262
|
+
const segment = flagStr(ctx.flags.segment);
|
|
263
|
+
const clients = payload.clients.filter((c) => !segment || c.segment === segment);
|
|
264
|
+
if (json) return printResult(ctx.io, ctx.flags, { ...payload, clients });
|
|
265
|
+
const pts = (v) => (v == null ? "—" : `${v >= 0 ? "+" : "−"}${Math.abs(v * 100).toFixed(1)} pts`);
|
|
266
|
+
ctx.io.log(`${clients.length} clients, ${payload.postCount} posts · computed ${payload.generated}`);
|
|
267
|
+
for (const c of clients) {
|
|
268
|
+
const s = c.sov?.[period];
|
|
269
|
+
const n = c.named?.[period];
|
|
270
|
+
ctx.io.log(
|
|
271
|
+
`${c.name.padEnd(28)} ${c.segment.padEnd(30)} sov ${pts(s?.change)} mention ${pts(n?.change)} posts30 ${c.posts30}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case "reports": {
|
|
277
|
+
const kind = flagStr(ctx.flags.kind) ?? "reports";
|
|
278
|
+
const limit = flagStr(ctx.flags.limit) ?? "12";
|
|
279
|
+
const { reports } = await staffRequest(
|
|
280
|
+
ctx,
|
|
281
|
+
"GET",
|
|
282
|
+
`${PATTERNS}/reports?kind=${encodeURIComponent(kind)}&limit=${encodeURIComponent(limit)}`
|
|
283
|
+
);
|
|
284
|
+
if (json) return printResult(ctx.io, ctx.flags, { reports });
|
|
285
|
+
for (const r of reports)
|
|
286
|
+
ctx.io.log(
|
|
287
|
+
`${r.id} ${r.created_at} ${r.kind.padEnd(6)} ${r.status.padEnd(7)} ${(r.question ?? (r.summary ?? "").split("\n")[0] ?? "").slice(0, 80)}`
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
case "report": {
|
|
292
|
+
const id = rest[0];
|
|
293
|
+
if (!id) throw new CliError("Usage: admin patterns report <id>");
|
|
294
|
+
const { report } = await staffRequest(ctx, "GET", `${PATTERNS}/reports/${encodeURIComponent(id)}`);
|
|
295
|
+
if (json) return printResult(ctx.io, ctx.flags, { report });
|
|
296
|
+
ctx.io.log(report.report ?? report.error ?? "(no report)");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
case "run": {
|
|
300
|
+
const { report } = await staffPost(ctx, `${PATTERNS}/reports`, {});
|
|
301
|
+
if (json) return printResult(ctx.io, ctx.flags, { report });
|
|
302
|
+
ctx.io.log(report.report ?? report.error ?? "(no report)");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
case "ask": {
|
|
306
|
+
const question = rest.join(" ").trim();
|
|
307
|
+
if (question.length < 3)
|
|
308
|
+
throw new CliError('Usage: admin patterns ask "do answer-gap posts help paying clients too?"');
|
|
309
|
+
const { answer } = await staffPost(ctx, `${PATTERNS}/ask`, { question });
|
|
310
|
+
if (json) return printResult(ctx.io, ctx.flags, { answer });
|
|
311
|
+
ctx.io.log(answer.report ?? answer.error ?? "(no answer)");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
throw new CliError('Usage: admin patterns [show] | reports | report <id> | run | ask "<question>"');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
242
319
|
/* ------------------------------------------------------------ kernel experiments */
|
|
243
320
|
|
|
244
321
|
const EXPERIMENTS = "/api/superadmin/tuning/experiments";
|
|
@@ -453,12 +530,12 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
453
530
|
* admin tuning loop where the loop stands: what waits at each station, and where to go next
|
|
454
531
|
* admin tuning ledger <comparison-id> the defect ledger: per arm, what the diagnosis read named, the floor and the ceiling, the candidate's next arm; then the reader edits per arm by lever, and the schema gaps
|
|
455
532
|
* admin tuning checklist the checklist: every defect category with its status, the picks' agreement (n, agreement, suggestion) and its labels (+/−) (alias: evals)
|
|
456
|
-
* admin tuning checklist-add --id … --label … --how … --layer … --lever … --locality local|structural --axis … [--example "span"] add a category (starts unvetted) (alias: eval-add)
|
|
457
|
-
* admin tuning checklist-set <id> [--status confirmed|rejected|unvetted] [--example "span" [--fix "…"]] confirm / reject (a person's call), or add an example (alias: eval-set)
|
|
533
|
+
* admin tuning checklist-add --id … --label … --looks-like "…" --how "…" --layer … --lever … [--paths voice,…] --locality local|structural --axis … [--example "span"] add a category (starts unvetted; an id already on the list is refused); --looks-like is the defect as the judge reads it, --how the fix, --paths what a bake-off merge copies (alias: eval-add)
|
|
534
|
+
* admin tuning checklist-set <id> [--status confirmed|rejected|unvetted] [--example "span" [--fix "…"]] confirm / reject (a person's call, recorded as the key's owner), or add an example (alias: eval-set)
|
|
458
535
|
* admin tuning checklist-labels <id> the labelled set of one category: positives (examples + feedback filed into it), negatives (rejected feedback suggested for it), grid overrules
|
|
459
536
|
* admin tuning feedback [--status proposed|captured|rejected] the feedback: spans readers marked in a pair's text with their words, each with its disposition (alias: proposals)
|
|
460
537
|
* admin tuning feedback-add --span "…" --comment "…" [--fix "…"] [--comparison <id> --cell <key> --side control|candidate] [--kind narrow|wide] add feedback (no --side from a blind read); --fix is the reader's rewrite, classified and a disposition suggested (alias: propose-eval)
|
|
461
|
-
* admin tuning feedback-file <id> --as checklist --id … --label … --how … --layer … --lever … --locality local|structural --axis … file it on the checklist, the span (and its fix) as the category's example (appends when the id exists) (alias: capture-eval; --as category)
|
|
538
|
+
* admin tuning feedback-file <id> --as checklist --id … --label … --looks-like "…" --how "…" --layer … --lever … [--paths …] --locality local|structural --axis … file it on the checklist, the span (and its fix) as the category's example (appends when the id exists) (alias: capture-eval; --as category)
|
|
462
539
|
* admin tuning feedback-file <id> --as rule file it as a rule; the rule itself is written in the kernels rule lab
|
|
463
540
|
* admin tuning feedback-file <id> --as config [--note "…"] file it as a lever this candidate should pull
|
|
464
541
|
* admin tuning feedback-file <id> --as implementation [--note "…"] file it as a schema gap no setting can express, for the kernels repo (--as lever still works)
|
|
@@ -516,7 +593,9 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
516
593
|
*
|
|
517
594
|
* The bake-off: a screen's versions graded item by item against the checklist's confirmed and
|
|
518
595
|
* unvetted categories and the craft axes commitment / economy / movement, the winner
|
|
519
|
-
* per item tallied across the inputs, and a merge that composes the best of each into one candidate
|
|
596
|
+
* per item tallied across the inputs, and a merge that composes the best of each into one candidate:
|
|
597
|
+
* each kernel-file path takes the value the most trusted wins stand behind (a person-confirmed
|
|
598
|
+
* category or an axis); versions on another writer take no part.
|
|
520
599
|
* Deterministic; a person overrides a grade and confirms the merge, which is a hypothesis until the
|
|
521
600
|
* eye test confirms it.
|
|
522
601
|
* admin tuning bakeoff <screen-id> [--regrade] the winners table: item, trusted, leader, wins per version, graded; then the conflicts a merge would meet
|
|
@@ -614,7 +693,7 @@ function printProgramme(io, r) {
|
|
|
614
693
|
for (const row of sm.table ?? [])
|
|
615
694
|
if (row.leader !== null && row.leader !== sm.base)
|
|
616
695
|
io.log(
|
|
617
|
-
` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : "unvetted "} v${row.leader} ${Object.entries(
|
|
696
|
+
` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : row.item.status === "confirmed" ? "undecided" : "unvetted "} v${row.leader} ${Object.entries(
|
|
618
697
|
row.wins
|
|
619
698
|
)
|
|
620
699
|
.map(([v, n]) => `v${v}:${n}`)
|
|
@@ -806,6 +885,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
806
885
|
if (json) return printResult(io, flags, r);
|
|
807
886
|
const rows = r.categories ?? [];
|
|
808
887
|
if (!rows.length) return io.log("(no categories)");
|
|
888
|
+
io.log('("confirmed?" = confirmed by nobody on record, a seeded status: graded, never merged)');
|
|
809
889
|
const cell = (v, w) => String(v ?? "—").padEnd(w);
|
|
810
890
|
io.log(
|
|
811
891
|
`${cell("id", 24)} ${cell("status", 10)} ${cell("layer", 10)} ${cell("lever", 30)} ${"n".padStart(4)} ${"agree".padStart(6)} ${cell("suggest", 10)} ${"+".padStart(3)} ${"−".padStart(3)}`
|
|
@@ -813,8 +893,10 @@ async function cmdAdminTuning(ctx) {
|
|
|
813
893
|
for (const c of rows) {
|
|
814
894
|
const v = c.vetting ?? {};
|
|
815
895
|
const l = c.labels ?? { positives: (c.examples ?? []).length, negatives: 0 };
|
|
896
|
+
// A confirmed status nobody is on record as deciding is a seeded one: the bake-off does not merge on it.
|
|
897
|
+
const status = c.status === "confirmed" && !c.decided_by ? "confirmed?" : c.status;
|
|
816
898
|
io.log(
|
|
817
|
-
`${cell(c.id, 24)} ${cell(
|
|
899
|
+
`${cell(c.id, 24)} ${cell(status, 10)} ${cell(c.layer, 10)} ${cell(c.lever || "—", 30)} ${String(v.n ?? 0).padStart(4)} ${String(v.agreement ?? "—").padStart(6)} ${cell(v.suggestion, 10)} ${String(l.positives).padStart(3)} ${String(l.negatives).padStart(3)}${(c.paths ?? []).length ? "" : " (no path)"}`
|
|
818
900
|
);
|
|
819
901
|
}
|
|
820
902
|
return;
|
|
@@ -832,30 +914,17 @@ async function cmdAdminTuning(ctx) {
|
|
|
832
914
|
}
|
|
833
915
|
case "checklist-add":
|
|
834
916
|
case "eval-add": {
|
|
835
|
-
const body =
|
|
836
|
-
id: flagStr(flags.id),
|
|
837
|
-
label: flagStr(flags.label),
|
|
838
|
-
how: flagStr(flags.how),
|
|
839
|
-
layer: flagStr(flags.layer),
|
|
840
|
-
lever: flagStr(flags.lever) ?? "",
|
|
841
|
-
locality: flagStr(flags.locality),
|
|
842
|
-
axis: flagStr(flags.axis),
|
|
843
|
-
};
|
|
917
|
+
const body = checklistWords(flags);
|
|
844
918
|
const example = flagStr(flags.example);
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
!body.label ||
|
|
848
|
-
!body.how ||
|
|
849
|
-
!body.layer ||
|
|
850
|
-
!body.axis ||
|
|
851
|
-
!["local", "structural"].includes(body.locality ?? "")
|
|
852
|
-
)
|
|
853
|
-
throw new CliError(
|
|
854
|
-
'Usage: admin tuning checklist-add --id … --label … --how … --layer … --lever … --locality local|structural --axis … [--example "span"]'
|
|
855
|
-
);
|
|
919
|
+
if (!body)
|
|
920
|
+
throw new CliError(`Usage: admin tuning checklist-add ${CHECKLIST_WORDS_USAGE} [--example "span"]`);
|
|
856
921
|
if (example) body.examples = [{ span: example, ...(flags.fix ? { fix: flagStr(flags.fix) } : {}) }];
|
|
857
922
|
const r = await staffRequest(ctx, "POST", `${T}/evals`, body);
|
|
858
|
-
|
|
923
|
+
if (json) return printResult(io, flags, r);
|
|
924
|
+
io.log(`added ${r.category.id} (${r.category.status})`);
|
|
925
|
+
if (!(r.category.paths ?? []).length)
|
|
926
|
+
io.log(" no --paths: a bake-off grades this item but can never merge on it");
|
|
927
|
+
return;
|
|
859
928
|
}
|
|
860
929
|
case "checklist-set":
|
|
861
930
|
case "eval-set": {
|
|
@@ -877,7 +946,9 @@ async function cmdAdminTuning(ctx) {
|
|
|
877
946
|
const r = await staffRequest(ctx, "PATCH", `${T}/evals/${encodeURIComponent(id)}`, patch);
|
|
878
947
|
return json
|
|
879
948
|
? printResult(io, flags, r)
|
|
880
|
-
: io.log(
|
|
949
|
+
: io.log(
|
|
950
|
+
`${r.category.id}: ${r.category.status}${r.category.decided_by ? ` (by ${r.category.decided_by})` : ""}, ${(r.category.examples ?? []).length} examples`
|
|
951
|
+
);
|
|
881
952
|
}
|
|
882
953
|
case "feedback":
|
|
883
954
|
case "proposals": {
|
|
@@ -946,32 +1017,15 @@ async function cmdAdminTuning(ctx) {
|
|
|
946
1017
|
const AS_ALIAS = { category: "checklist", lever: "implementation" };
|
|
947
1018
|
const asGiven = flagStr(flags.as);
|
|
948
1019
|
const as = AS_ALIAS[asGiven] ?? asGiven;
|
|
949
|
-
const usage =
|
|
950
|
-
'Usage: admin tuning feedback-file <id> --as checklist --id … --label … --how … --layer … --lever … --locality local|structural --axis … | --as rule | --as config|implementation|material [--note "…"] | --as note';
|
|
1020
|
+
const usage = `Usage: admin tuning feedback-file <id> --as checklist ${CHECKLIST_WORDS_USAGE} | --as rule | --as config|implementation|material [--note "…"] | --as note`;
|
|
951
1021
|
if (!id || !["checklist", "rule", "config", "implementation", "material", "note"].includes(as ?? ""))
|
|
952
1022
|
throw new CliError(usage);
|
|
953
1023
|
let body = { as };
|
|
954
1024
|
if (as === "config" || as === "implementation" || as === "material")
|
|
955
1025
|
body = compact({ as, note: flagStr(flags.note) });
|
|
956
1026
|
if (as === "checklist") {
|
|
957
|
-
const category =
|
|
958
|
-
|
|
959
|
-
label: flagStr(flags.label),
|
|
960
|
-
how: flagStr(flags.how),
|
|
961
|
-
layer: flagStr(flags.layer),
|
|
962
|
-
lever: flagStr(flags.lever) ?? "",
|
|
963
|
-
locality: flagStr(flags.locality),
|
|
964
|
-
axis: flagStr(flags.axis),
|
|
965
|
-
};
|
|
966
|
-
if (
|
|
967
|
-
!category.id ||
|
|
968
|
-
!category.label ||
|
|
969
|
-
!category.how ||
|
|
970
|
-
!category.layer ||
|
|
971
|
-
!category.axis ||
|
|
972
|
-
!["local", "structural"].includes(category.locality ?? "")
|
|
973
|
-
)
|
|
974
|
-
throw new CliError(usage);
|
|
1027
|
+
const category = checklistWords(flags);
|
|
1028
|
+
if (!category) throw new CliError(usage);
|
|
975
1029
|
body = { as, category };
|
|
976
1030
|
}
|
|
977
1031
|
const r = await staffRequest(ctx, "POST", `${T}/proposals/${id}/capture`, body);
|
|
@@ -1771,7 +1825,11 @@ async function cmdAdminTuning(ctx) {
|
|
|
1771
1825
|
`merged from v${r.base} → candidate v${r.version}${r.comparison_id ? ` · comparison ${r.comparison_id}` : ""}`
|
|
1772
1826
|
);
|
|
1773
1827
|
for (const p of r.provenance)
|
|
1774
|
-
io.log(
|
|
1828
|
+
io.log(
|
|
1829
|
+
` ${p.applied ? (p.from === r.base ? "kept (the base's, back on top of a wider copy)" : "applied") : `skipped (${p.skipped})`} ${p.item} ${p.path} from v${p.from}`
|
|
1830
|
+
);
|
|
1831
|
+
for (const e of r.excluded ?? [])
|
|
1832
|
+
io.log(` left out v${e.version}: runs on ${e.writer}, not the base's writer`);
|
|
1775
1833
|
if (r.conflicts.length)
|
|
1776
1834
|
io.log(
|
|
1777
1835
|
`conflicts left (the base's value kept): ${r.conflicts.map((c) => `${c.path} ← ${c.candidates.map((x) => `${x.item} v${x.version}`).join(" | ")}`).join("; ")}`
|
|
@@ -3347,6 +3405,43 @@ async function cmdAdminSpend(ctx) {
|
|
|
3347
3405
|
}
|
|
3348
3406
|
}
|
|
3349
3407
|
|
|
3408
|
+
/**
|
|
3409
|
+
* A checklist item's words from the flags (checklist-add, feedback-file --as checklist): the
|
|
3410
|
+
* service's add schema — `--looks-like` is the defect as the judge reads it, `--how` the fix,
|
|
3411
|
+
* `--paths` the lever as kernel-file paths (comma-separated) so a bake-off can merge on it.
|
|
3412
|
+
* Null when a required word is missing.
|
|
3413
|
+
*/
|
|
3414
|
+
function checklistWords(flags) {
|
|
3415
|
+
const paths = (flagStr(flags.paths) ?? "")
|
|
3416
|
+
.split(",")
|
|
3417
|
+
.map((p) => p.trim())
|
|
3418
|
+
.filter(Boolean);
|
|
3419
|
+
const words = {
|
|
3420
|
+
id: flagStr(flags.id),
|
|
3421
|
+
label: flagStr(flags.label),
|
|
3422
|
+
looks_like: flagStr(flags["looks-like"]),
|
|
3423
|
+
how: flagStr(flags.how),
|
|
3424
|
+
layer: flagStr(flags.layer),
|
|
3425
|
+
lever: flagStr(flags.lever),
|
|
3426
|
+
locality: flagStr(flags.locality),
|
|
3427
|
+
axis: flagStr(flags.axis),
|
|
3428
|
+
...(paths.length ? { paths } : {}),
|
|
3429
|
+
};
|
|
3430
|
+
const complete =
|
|
3431
|
+
words.id &&
|
|
3432
|
+
words.label &&
|
|
3433
|
+
words.looks_like &&
|
|
3434
|
+
words.how &&
|
|
3435
|
+
words.layer &&
|
|
3436
|
+
words.lever &&
|
|
3437
|
+
words.axis &&
|
|
3438
|
+
["local", "structural"].includes(words.locality ?? "");
|
|
3439
|
+
return complete ? words : null;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
const CHECKLIST_WORDS_USAGE =
|
|
3443
|
+
'--id … --label … --looks-like "what the defect looks like" --how "the fix" --layer writer|frame|voice|economy|tells|length|material --lever … [--paths \'voice,rules["<id>"]\'] --locality local|structural --axis specificity|commitment|economy|movement|none';
|
|
3444
|
+
|
|
3350
3445
|
const FLEET_EXPERIMENTS = "/api/superadmin/experiments";
|
|
3351
3446
|
|
|
3352
3447
|
function pct(rate) {
|
|
@@ -3556,13 +3651,72 @@ export async function cmdAdmin(ctx) {
|
|
|
3556
3651
|
if (sub === "spend") return cmdAdminSpend({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3557
3652
|
if (sub === "create-org") return cmdAdminCreateOrg({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3558
3653
|
if (sub === "ask") return cmdAdminAsk({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3654
|
+
if (sub === "patterns") return cmdAdminPatterns({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3559
3655
|
if (sub === "experiments") return cmdAdminExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3560
3656
|
if (sub === "fleet-experiments") return cmdAdminFleetExperiments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3561
3657
|
if (sub === "tuning-context") return cmdAdminTuningContext({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3562
3658
|
if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3563
3659
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3564
3660
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3661
|
+
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3565
3662
|
throw new CliError(
|
|
3566
|
-
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3663
|
+
'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)'
|
|
3567
3664
|
);
|
|
3568
3665
|
}
|
|
3666
|
+
|
|
3667
|
+
// `admin brand-detection <org-id>` — how the client's brand is detected in AI answers;
|
|
3668
|
+
// with --term / --exact / --exclude it SETS the exact set (replacing the inferred one),
|
|
3669
|
+
// --clear goes back to inferred (recorded as a decision, with --note as the why). Every
|
|
3670
|
+
// earlier setting is kept in `history`. The client's answers are recounted against the new set.
|
|
3671
|
+
async function cmdAdminBrandDetection(ctx) {
|
|
3672
|
+
const { flags, positionals, io } = ctx;
|
|
3673
|
+
const orgId = requirePositional(positionals, 0, "org-id");
|
|
3674
|
+
const path = `/api/superadmin/fleet/mentions/${encodeURIComponent(orgId)}/detection`;
|
|
3675
|
+
const terms = flagList(flags.term);
|
|
3676
|
+
const exact = flagList(flags.exact);
|
|
3677
|
+
const exclude = flagList(flags.exclude);
|
|
3678
|
+
let result;
|
|
3679
|
+
if (flags.clear === true)
|
|
3680
|
+
result = await staffRequest(ctx, "PUT", path, {
|
|
3681
|
+
clear: true,
|
|
3682
|
+
...(flagStr(flags.note) !== undefined ? { note: flagStr(flags.note) } : {}),
|
|
3683
|
+
});
|
|
3684
|
+
else if (terms.length || exact.length || exclude.length || flagStr(flags.note) !== undefined) {
|
|
3685
|
+
if (!terms.length && !exact.length) throw new CliError("Give at least one --term or --exact (or --clear).");
|
|
3686
|
+
result = await staffRequest(ctx, "PUT", path, {
|
|
3687
|
+
terms,
|
|
3688
|
+
exact_case_terms: exact,
|
|
3689
|
+
exclude_phrases: exclude,
|
|
3690
|
+
...(flagStr(flags.note) !== undefined ? { note: flagStr(flags.note) } : {}),
|
|
3691
|
+
});
|
|
3692
|
+
} else result = await staffRequest(ctx, "GET", path);
|
|
3693
|
+
printResult(io, flags, result, (r) => {
|
|
3694
|
+
const e = r.effective;
|
|
3695
|
+
const state = r.override
|
|
3696
|
+
? `exact set, by ${r.override.setBy ?? "?"} at ${r.override.setAt ?? "?"}`
|
|
3697
|
+
: r.cleared
|
|
3698
|
+
? `inferred (exact set cleared by ${r.cleared.by ?? "?"} at ${r.cleared.at ?? "?"})`
|
|
3699
|
+
: "inferred";
|
|
3700
|
+
const lines = [
|
|
3701
|
+
`${r.orgName}: ${state}`,
|
|
3702
|
+
` terms (any case): ${e.terms.join(", ") || "—"}`,
|
|
3703
|
+
` exact case: ${e.exactCaseTerms.join(", ") || "—"}`,
|
|
3704
|
+
` excluded phrases: ${e.excludePhrases.join(", ") || "—"}`,
|
|
3705
|
+
];
|
|
3706
|
+
const why = r.override?.note ?? r.cleared?.note;
|
|
3707
|
+
if (why) lines.push(` why: ${why}`);
|
|
3708
|
+
if (r.override) lines.push(` inferred would be: ${r.inferred.terms.join(", ")}`);
|
|
3709
|
+
for (const h of r.history ?? []) {
|
|
3710
|
+
const set = [
|
|
3711
|
+
h.terms.length ? `terms ${h.terms.join(", ")}` : "",
|
|
3712
|
+
h.exactCaseTerms.length ? `exact ${h.exactCaseTerms.join(", ")}` : "",
|
|
3713
|
+
h.excludePhrases.length ? `excludes ${h.excludePhrases.join(", ")}` : "",
|
|
3714
|
+
].filter(Boolean);
|
|
3715
|
+
lines.push(
|
|
3716
|
+
` earlier: ${h.action} by ${h.by ?? "?"} at ${h.at ?? "?"}${set.length ? ` — ${set.join("; ")}` : ""}${h.note ? ` (${h.note})` : ""}`
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
return lines.join("\n");
|
|
3720
|
+
});
|
|
3721
|
+
return 0;
|
|
3722
|
+
}
|
|
@@ -27,6 +27,7 @@ export const ARGS = {
|
|
|
27
27
|
get_share_of_voice_detail: {
|
|
28
28
|
period: { flag: "period", enum: PERIODS },
|
|
29
29
|
},
|
|
30
|
+
get_brand_detection: {},
|
|
30
31
|
get_share_of_voice_question: {
|
|
31
32
|
question_key: { positional: 0, label: "question-key", required: true },
|
|
32
33
|
period: { flag: "period", enum: PERIODS },
|
|
@@ -55,9 +56,11 @@ export async function cmdInsights(ctx) {
|
|
|
55
56
|
return call(rest, "get_share_of_voice_detail");
|
|
56
57
|
case "question":
|
|
57
58
|
return call(rest, "get_share_of_voice_question");
|
|
59
|
+
case "detection":
|
|
60
|
+
return call(rest, "get_brand_detection");
|
|
58
61
|
default:
|
|
59
62
|
throw new CliError(
|
|
60
|
-
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, competitors, question, site, post, top, bank, access`
|
|
63
|
+
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, competitors, question, detection, site, post, top, bank, access`
|
|
61
64
|
);
|
|
62
65
|
}
|
|
63
66
|
}
|
package/lib/commands/lede.mjs
CHANGED
|
@@ -130,6 +130,8 @@ export const ARGS = {
|
|
|
130
130
|
threshold: { flag: "threshold", type: "num" },
|
|
131
131
|
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
132
132
|
tags: { flag: "tag", type: "list" },
|
|
133
|
+
// "Use when ..." clause, checked per prospect before the line is used.
|
|
134
|
+
condition: "condition",
|
|
133
135
|
},
|
|
134
136
|
update_lede: {
|
|
135
137
|
id: { positional: 0, label: "lede-id", required: true },
|
|
@@ -140,6 +142,8 @@ export const ARGS = {
|
|
|
140
142
|
threshold: { flag: "threshold", type: "num" },
|
|
141
143
|
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
142
144
|
tags: { flag: "tag", type: "list" },
|
|
145
|
+
// --condition "<clause>" sets it; --no-condition clears it.
|
|
146
|
+
condition: { value: (flags) => (flags["no-condition"] ? null : flagStr(flags.condition)) },
|
|
143
147
|
},
|
|
144
148
|
set_lede_enabled: {
|
|
145
149
|
id: { positional: 0, label: "lede-id", required: true },
|
|
@@ -153,6 +157,22 @@ export const ARGS = {
|
|
|
153
157
|
voice: { flag: "voice", required: true },
|
|
154
158
|
ids: { flag: "ids", type: "list" },
|
|
155
159
|
},
|
|
160
|
+
// Opener proposals (`lede proposals ...`): lines captured from Dispatch personalizations,
|
|
161
|
+
// evaluated, waiting for an admin to accept into the library or reject with a reason.
|
|
162
|
+
list_lede_proposals: {
|
|
163
|
+
status: { flag: "status", enum: ["captured", "proposed", "active", "rejected"] },
|
|
164
|
+
limit: { flag: "limit", type: "num" },
|
|
165
|
+
},
|
|
166
|
+
decide_lede_proposal: {
|
|
167
|
+
proposal_id: { positional: 0, label: "proposal-id", required: true },
|
|
168
|
+
// `lede proposals accept|reject <id>` decides this; the subcommand passes it in.
|
|
169
|
+
action: { value: (_flags, _positionals, extra) => extra.action },
|
|
170
|
+
text: "text",
|
|
171
|
+
// --condition "<clause>" edits it on accept; --no-condition accepts with none.
|
|
172
|
+
condition: { value: (flags) => (flags["no-condition"] ? null : flagStr(flags.condition)) },
|
|
173
|
+
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
174
|
+
reason: "reason",
|
|
175
|
+
},
|
|
156
176
|
};
|
|
157
177
|
|
|
158
178
|
export async function cmdLede(ctx) {
|
|
@@ -173,6 +193,9 @@ export async function cmdLede(ctx) {
|
|
|
173
193
|
case "library":
|
|
174
194
|
case "lib":
|
|
175
195
|
return ledeLibrary(rest);
|
|
196
|
+
case "proposals":
|
|
197
|
+
case "proposal":
|
|
198
|
+
return ledeProposals(rest);
|
|
176
199
|
case "run":
|
|
177
200
|
return ledeRun(rest);
|
|
178
201
|
case "runs":
|
|
@@ -193,7 +216,7 @@ export async function cmdLede(ctx) {
|
|
|
193
216
|
return ledeFunnel(rest);
|
|
194
217
|
default:
|
|
195
218
|
throw new CliError(
|
|
196
|
-
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, library, run, runs, history, evals, balance, outcome, funnel`
|
|
219
|
+
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, library, proposals, run, runs, history, evals, balance, outcome, funnel`
|
|
197
220
|
);
|
|
198
221
|
}
|
|
199
222
|
}
|
|
@@ -445,3 +468,61 @@ async function ledeBalance(ctx) {
|
|
|
445
468
|
printResult(io, flags, result);
|
|
446
469
|
return 0;
|
|
447
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* `lede proposals [list] [--status proposed|captured|active|rejected] [--limit n]`,
|
|
474
|
+
* `lede proposals accept <id> [--text ...] [--condition ...|--no-condition] [--tone ...]`,
|
|
475
|
+
* `lede proposals reject <id> --reason ...` — lines reviewers wrote while personalizing on
|
|
476
|
+
* the Dispatch desk, templatized and judged; accept adds one to the library (live now).
|
|
477
|
+
*/
|
|
478
|
+
async function ledeProposals(ctx) {
|
|
479
|
+
const { client, positionals, flags, io } = ctx;
|
|
480
|
+
const action = positionals[0];
|
|
481
|
+
const rest = { ...ctx, positionals: positionals.slice(1) };
|
|
482
|
+
switch (action) {
|
|
483
|
+
case undefined:
|
|
484
|
+
case "list":
|
|
485
|
+
case "ls": {
|
|
486
|
+
const result = await client.callTool("list_lede_proposals", argsFromFlags(ARGS.list_lede_proposals, flags));
|
|
487
|
+
// One block per proposal: id, how often captured, the template, then its condition
|
|
488
|
+
// (or why it was rejected). `--json` for the judge's scores and the raw line.
|
|
489
|
+
printResult(
|
|
490
|
+
io,
|
|
491
|
+
flags,
|
|
492
|
+
result,
|
|
493
|
+
(r) =>
|
|
494
|
+
(r?.proposals ?? [])
|
|
495
|
+
.map((p) =>
|
|
496
|
+
[
|
|
497
|
+
`${p.id} x${p.capture_count} [${p.status}] ${p.templated_text ?? p.raw_line}`,
|
|
498
|
+
p.status_reason
|
|
499
|
+
? ` why: ${p.status_reason}`
|
|
500
|
+
: p.suggested_condition
|
|
501
|
+
? ` ${p.suggested_condition}`
|
|
502
|
+
: null,
|
|
503
|
+
]
|
|
504
|
+
.filter(Boolean)
|
|
505
|
+
.join("\n")
|
|
506
|
+
)
|
|
507
|
+
.join("\n") || "No proposals."
|
|
508
|
+
);
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
case "accept":
|
|
512
|
+
case "reject": {
|
|
513
|
+
const args = argsFromFlags(ARGS.decide_lede_proposal, flags, rest.positionals, { action });
|
|
514
|
+
if (action === "reject" && !args.reason) throw new CliError("A rejection needs --reason <why>.");
|
|
515
|
+
const result = await client.callTool("decide_lede_proposal", args);
|
|
516
|
+
ok(
|
|
517
|
+
ctx,
|
|
518
|
+
action === "accept"
|
|
519
|
+
? `Accepted ${args.proposal_id}: the line is in the library now.`
|
|
520
|
+
: `Rejected ${args.proposal_id}.`
|
|
521
|
+
);
|
|
522
|
+
printResult(io, flags, result);
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
throw new CliError(`Unknown lede proposals action: ${action}. Try: list, accept, reject`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -197,6 +197,23 @@ export const ARGS = {
|
|
|
197
197
|
item_id: { positional: 0, label: "item-id", required: true },
|
|
198
198
|
subject: "subject",
|
|
199
199
|
body: { value: (flags) => readTextInput(flags, "body", "file") },
|
|
200
|
+
// --personalize "<line>" marks the edit as a personalization (captured as an opener
|
|
201
|
+
// proposal for the org's admins); --personalize-mode says whether the line replaced
|
|
202
|
+
// the opener (default) or was inserted after the greeting.
|
|
203
|
+
personalization: {
|
|
204
|
+
value: (flags) => {
|
|
205
|
+
const line = flagStr(flags.personalize);
|
|
206
|
+
const mode = flagStr(flags["personalize-mode"]);
|
|
207
|
+
if (line === undefined) {
|
|
208
|
+
if (mode !== undefined) throw new CliError("--personalize-mode needs --personalize <line>");
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (mode !== undefined && mode !== "replace" && mode !== "insert") {
|
|
212
|
+
throw new CliError(`--personalize-mode must be replace or insert (got ${mode})`);
|
|
213
|
+
}
|
|
214
|
+
return { line, mode: mode ?? "replace" };
|
|
215
|
+
},
|
|
216
|
+
},
|
|
200
217
|
},
|
|
201
218
|
decide_review_item: {
|
|
202
219
|
item_id: { positional: 0, label: "item-id", required: true },
|
|
@@ -366,7 +383,7 @@ const VALUE_FLAGS = {
|
|
|
366
383
|
create_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
367
384
|
update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
368
385
|
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
369
|
-
update_review_item: ["body", "file"],
|
|
386
|
+
update_review_item: ["body", "file", "personalize", "personalize-mode"],
|
|
370
387
|
delete_prospects: ["id"],
|
|
371
388
|
import_prospects_csv: ["csv", "csv-file"],
|
|
372
389
|
update_prospect: ["custom"],
|