@letterstory/cli 0.87.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.mjs +11 -2
- package/lib/commands/admin.mjs +163 -54
- package/lib/commands/collections.mjs +2 -0
- package/lib/commands/insights.mjs +4 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ 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.89.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
|
|
@@ -68,6 +68,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
68
68
|
"confirm-missing-target",
|
|
69
69
|
"no-wait",
|
|
70
70
|
"no-checklist",
|
|
71
|
+
"no-control",
|
|
72
|
+
"own-beat",
|
|
71
73
|
"refresh",
|
|
72
74
|
"accept",
|
|
73
75
|
"help",
|
|
@@ -347,7 +349,7 @@ Collections:
|
|
|
347
349
|
preferred sites — dry, nothing written. Proposals are judged by fit.
|
|
348
350
|
collections preferred-preview <id>
|
|
349
351
|
Step 2: the proposals ranked, what the ceiling chooses, and the 1-in-N.
|
|
350
|
-
collections cite-preferred <id>
|
|
352
|
+
collections cite-preferred <id> [--exclude <article-id> …]
|
|
351
353
|
Step 3: apply exactly the chosen sentences, then republish.
|
|
352
354
|
|
|
353
355
|
Authors (the per-collection author bank — recurring bylines):
|
|
@@ -637,6 +639,12 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
637
639
|
read-only SQL across every org; --sql prints
|
|
638
640
|
the queries behind the answer. Needs the
|
|
639
641
|
fleet:query capability on the staff key.
|
|
642
|
+
admin brand-detection <org-id> [--term <t>]... [--exact <t>]... [--exclude <phrase>]... [--note "…"] | --clear [--note "…"]
|
|
643
|
+
How a client's brand is detected in AI answers, with
|
|
644
|
+
every earlier setting; with flags, set the exact set
|
|
645
|
+
(fleet:instrument) — e.g. --term Context.dev for a name
|
|
646
|
+
that is also a word. --clear goes back to inferred,
|
|
647
|
+
recorded as your decision.
|
|
640
648
|
admin replays [--org <uuid>] [--target <uuid>] [--status captured|not_reproduced|queued|running|unsupported|failed] [--limit <n>]
|
|
641
649
|
Live answer replays: breakthroughs re-asked in ChatGPT /
|
|
642
650
|
Gemini / Google / Perplexity / Claude, with screenshots.
|
|
@@ -1035,6 +1043,7 @@ Insights:
|
|
|
1035
1043
|
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
1036
1044
|
insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
|
|
1037
1045
|
One By-question row: the wordings asked and the AI answers
|
|
1046
|
+
insights detection How your brand is detected in AI answers (terms, who set them)
|
|
1038
1047
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
1039
1048
|
insights post <article-id> [--period 14d|30d|90d]
|
|
1040
1049
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
// (feedback-add --fix "…" files the reader's rewrite; the diff is classified and a disposition suggested.
|
|
13
13
|
// The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
|
|
14
14
|
// admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
|
|
15
|
-
// admin fleet-experiments list|articles|mark|unmark|new|assign|assign-eligible|assign-orgs|apply|start|stop|decide … (fleet:read / fleet:experiment)
|
|
15
|
+
// 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
16
|
// 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
17
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
18
|
+
// admin brand-detection <org-id> (fleet:read)
|
|
19
|
+
// admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
|
|
18
20
|
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
19
21
|
// admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
|
|
20
22
|
// admin create-org --domain acme.com [--name "Acme"] [--test] [--no-key] [--key-name N]
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
|
|
42
44
|
import { readFileSync } from "node:fs";
|
|
43
45
|
import { CliError, LetterstoryClient } from "../client.mjs";
|
|
44
|
-
import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok } from "./shared.mjs";
|
|
46
|
+
import { flagStr, flagNum, flagBool, flagList, printResult, compact, ok, requirePositional } from "./shared.mjs";
|
|
45
47
|
import { watchDomainJob } from "./onboard.mjs";
|
|
46
48
|
|
|
47
49
|
const SEER_MODES = ["suggest", "skip"];
|
|
@@ -453,12 +455,12 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
453
455
|
* admin tuning loop where the loop stands: what waits at each station, and where to go next
|
|
454
456
|
* 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
457
|
* 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)
|
|
458
|
+
* 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)
|
|
459
|
+
* 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
460
|
* 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
461
|
* 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
462
|
* 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)
|
|
463
|
+
* 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
464
|
* admin tuning feedback-file <id> --as rule file it as a rule; the rule itself is written in the kernels rule lab
|
|
463
465
|
* admin tuning feedback-file <id> --as config [--note "…"] file it as a lever this candidate should pull
|
|
464
466
|
* 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 +518,9 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
516
518
|
*
|
|
517
519
|
* The bake-off: a screen's versions graded item by item against the checklist's confirmed and
|
|
518
520
|
* 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
|
|
521
|
+
* per item tallied across the inputs, and a merge that composes the best of each into one candidate:
|
|
522
|
+
* each kernel-file path takes the value the most trusted wins stand behind (a person-confirmed
|
|
523
|
+
* category or an axis); versions on another writer take no part.
|
|
520
524
|
* Deterministic; a person overrides a grade and confirms the merge, which is a hypothesis until the
|
|
521
525
|
* eye test confirms it.
|
|
522
526
|
* 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 +618,7 @@ function printProgramme(io, r) {
|
|
|
614
618
|
for (const row of sm.table ?? [])
|
|
615
619
|
if (row.leader !== null && row.leader !== sm.base)
|
|
616
620
|
io.log(
|
|
617
|
-
` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : "unvetted "} v${row.leader} ${Object.entries(
|
|
621
|
+
` ${row.item.id.padEnd(32)} ${row.trusted ? "trusted " : row.item.status === "confirmed" ? "undecided" : "unvetted "} v${row.leader} ${Object.entries(
|
|
618
622
|
row.wins
|
|
619
623
|
)
|
|
620
624
|
.map(([v, n]) => `v${v}:${n}`)
|
|
@@ -806,6 +810,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
806
810
|
if (json) return printResult(io, flags, r);
|
|
807
811
|
const rows = r.categories ?? [];
|
|
808
812
|
if (!rows.length) return io.log("(no categories)");
|
|
813
|
+
io.log('("confirmed?" = confirmed by nobody on record, a seeded status: graded, never merged)');
|
|
809
814
|
const cell = (v, w) => String(v ?? "—").padEnd(w);
|
|
810
815
|
io.log(
|
|
811
816
|
`${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 +818,10 @@ async function cmdAdminTuning(ctx) {
|
|
|
813
818
|
for (const c of rows) {
|
|
814
819
|
const v = c.vetting ?? {};
|
|
815
820
|
const l = c.labels ?? { positives: (c.examples ?? []).length, negatives: 0 };
|
|
821
|
+
// A confirmed status nobody is on record as deciding is a seeded one: the bake-off does not merge on it.
|
|
822
|
+
const status = c.status === "confirmed" && !c.decided_by ? "confirmed?" : c.status;
|
|
816
823
|
io.log(
|
|
817
|
-
`${cell(c.id, 24)} ${cell(
|
|
824
|
+
`${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
825
|
);
|
|
819
826
|
}
|
|
820
827
|
return;
|
|
@@ -832,30 +839,17 @@ async function cmdAdminTuning(ctx) {
|
|
|
832
839
|
}
|
|
833
840
|
case "checklist-add":
|
|
834
841
|
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
|
-
};
|
|
842
|
+
const body = checklistWords(flags);
|
|
844
843
|
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
|
-
);
|
|
844
|
+
if (!body)
|
|
845
|
+
throw new CliError(`Usage: admin tuning checklist-add ${CHECKLIST_WORDS_USAGE} [--example "span"]`);
|
|
856
846
|
if (example) body.examples = [{ span: example, ...(flags.fix ? { fix: flagStr(flags.fix) } : {}) }];
|
|
857
847
|
const r = await staffRequest(ctx, "POST", `${T}/evals`, body);
|
|
858
|
-
|
|
848
|
+
if (json) return printResult(io, flags, r);
|
|
849
|
+
io.log(`added ${r.category.id} (${r.category.status})`);
|
|
850
|
+
if (!(r.category.paths ?? []).length)
|
|
851
|
+
io.log(" no --paths: a bake-off grades this item but can never merge on it");
|
|
852
|
+
return;
|
|
859
853
|
}
|
|
860
854
|
case "checklist-set":
|
|
861
855
|
case "eval-set": {
|
|
@@ -877,7 +871,9 @@ async function cmdAdminTuning(ctx) {
|
|
|
877
871
|
const r = await staffRequest(ctx, "PATCH", `${T}/evals/${encodeURIComponent(id)}`, patch);
|
|
878
872
|
return json
|
|
879
873
|
? printResult(io, flags, r)
|
|
880
|
-
: io.log(
|
|
874
|
+
: io.log(
|
|
875
|
+
`${r.category.id}: ${r.category.status}${r.category.decided_by ? ` (by ${r.category.decided_by})` : ""}, ${(r.category.examples ?? []).length} examples`
|
|
876
|
+
);
|
|
881
877
|
}
|
|
882
878
|
case "feedback":
|
|
883
879
|
case "proposals": {
|
|
@@ -946,32 +942,15 @@ async function cmdAdminTuning(ctx) {
|
|
|
946
942
|
const AS_ALIAS = { category: "checklist", lever: "implementation" };
|
|
947
943
|
const asGiven = flagStr(flags.as);
|
|
948
944
|
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';
|
|
945
|
+
const usage = `Usage: admin tuning feedback-file <id> --as checklist ${CHECKLIST_WORDS_USAGE} | --as rule | --as config|implementation|material [--note "…"] | --as note`;
|
|
951
946
|
if (!id || !["checklist", "rule", "config", "implementation", "material", "note"].includes(as ?? ""))
|
|
952
947
|
throw new CliError(usage);
|
|
953
948
|
let body = { as };
|
|
954
949
|
if (as === "config" || as === "implementation" || as === "material")
|
|
955
950
|
body = compact({ as, note: flagStr(flags.note) });
|
|
956
951
|
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);
|
|
952
|
+
const category = checklistWords(flags);
|
|
953
|
+
if (!category) throw new CliError(usage);
|
|
975
954
|
body = { as, category };
|
|
976
955
|
}
|
|
977
956
|
const r = await staffRequest(ctx, "POST", `${T}/proposals/${id}/capture`, body);
|
|
@@ -1771,7 +1750,11 @@ async function cmdAdminTuning(ctx) {
|
|
|
1771
1750
|
`merged from v${r.base} → candidate v${r.version}${r.comparison_id ? ` · comparison ${r.comparison_id}` : ""}`
|
|
1772
1751
|
);
|
|
1773
1752
|
for (const p of r.provenance)
|
|
1774
|
-
io.log(
|
|
1753
|
+
io.log(
|
|
1754
|
+
` ${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}`
|
|
1755
|
+
);
|
|
1756
|
+
for (const e of r.excluded ?? [])
|
|
1757
|
+
io.log(` left out v${e.version}: runs on ${e.writer}, not the base's writer`);
|
|
1775
1758
|
if (r.conflicts.length)
|
|
1776
1759
|
io.log(
|
|
1777
1760
|
`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 +3330,43 @@ async function cmdAdminSpend(ctx) {
|
|
|
3347
3330
|
}
|
|
3348
3331
|
}
|
|
3349
3332
|
|
|
3333
|
+
/**
|
|
3334
|
+
* A checklist item's words from the flags (checklist-add, feedback-file --as checklist): the
|
|
3335
|
+
* service's add schema — `--looks-like` is the defect as the judge reads it, `--how` the fix,
|
|
3336
|
+
* `--paths` the lever as kernel-file paths (comma-separated) so a bake-off can merge on it.
|
|
3337
|
+
* Null when a required word is missing.
|
|
3338
|
+
*/
|
|
3339
|
+
function checklistWords(flags) {
|
|
3340
|
+
const paths = (flagStr(flags.paths) ?? "")
|
|
3341
|
+
.split(",")
|
|
3342
|
+
.map((p) => p.trim())
|
|
3343
|
+
.filter(Boolean);
|
|
3344
|
+
const words = {
|
|
3345
|
+
id: flagStr(flags.id),
|
|
3346
|
+
label: flagStr(flags.label),
|
|
3347
|
+
looks_like: flagStr(flags["looks-like"]),
|
|
3348
|
+
how: flagStr(flags.how),
|
|
3349
|
+
layer: flagStr(flags.layer),
|
|
3350
|
+
lever: flagStr(flags.lever),
|
|
3351
|
+
locality: flagStr(flags.locality),
|
|
3352
|
+
axis: flagStr(flags.axis),
|
|
3353
|
+
...(paths.length ? { paths } : {}),
|
|
3354
|
+
};
|
|
3355
|
+
const complete =
|
|
3356
|
+
words.id &&
|
|
3357
|
+
words.label &&
|
|
3358
|
+
words.looks_like &&
|
|
3359
|
+
words.how &&
|
|
3360
|
+
words.layer &&
|
|
3361
|
+
words.lever &&
|
|
3362
|
+
words.axis &&
|
|
3363
|
+
["local", "structural"].includes(words.locality ?? "");
|
|
3364
|
+
return complete ? words : null;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
const CHECKLIST_WORDS_USAGE =
|
|
3368
|
+
'--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';
|
|
3369
|
+
|
|
3350
3370
|
const FLEET_EXPERIMENTS = "/api/superadmin/experiments";
|
|
3351
3371
|
|
|
3352
3372
|
function pct(rate) {
|
|
@@ -3466,7 +3486,7 @@ async function cmdAdminFleetExperiments(ctx) {
|
|
|
3466
3486
|
const specFile = flagStr(flags["spec-file"]);
|
|
3467
3487
|
if (!template && !specFile)
|
|
3468
3488
|
throw new CliError(
|
|
3469
|
-
"Usage: admin fleet-experiments new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1 | --spec-file spec.json"
|
|
3489
|
+
"Usage: admin fleet-experiments new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|sponsor_disclosure_fresh_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1 | --spec-file spec.json"
|
|
3470
3490
|
);
|
|
3471
3491
|
const body = template
|
|
3472
3492
|
? { action: "create_template", template }
|
|
@@ -3500,6 +3520,37 @@ async function cmdAdminFleetExperiments(ctx) {
|
|
|
3500
3520
|
return lines.join("\n") || "ok";
|
|
3501
3521
|
});
|
|
3502
3522
|
}
|
|
3523
|
+
case "clients":
|
|
3524
|
+
return out(await staffRequest(ctx, "GET", `${FLEET_EXPERIMENTS}/clients`), (r) =>
|
|
3525
|
+
r.clients.length
|
|
3526
|
+
? r.clients
|
|
3527
|
+
.map((c) => `${c.name} ${c.domain} ${c.sites} site${c.sites === 1 ? "" : "s"}`)
|
|
3528
|
+
.join("\n")
|
|
3529
|
+
: "(no clients)"
|
|
3530
|
+
);
|
|
3531
|
+
case "spin-up": {
|
|
3532
|
+
const org = rest.find(Boolean);
|
|
3533
|
+
if (!id || !org)
|
|
3534
|
+
throw new CliError(
|
|
3535
|
+
"Usage: admin fleet-experiments spin-up <experiment-id> <org-domain-or-id> [--no-control] [--own-beat] [--posts n]"
|
|
3536
|
+
);
|
|
3537
|
+
const body = {
|
|
3538
|
+
action: "spin_up",
|
|
3539
|
+
experiment_id: id,
|
|
3540
|
+
org,
|
|
3541
|
+
control: !flagBool(flags["no-control"]),
|
|
3542
|
+
match: flagBool(flags["own-beat"]) ? "own_beat" : "twin",
|
|
3543
|
+
...(flagStr(flags.posts) ? { posts_to_seed: Number(flagStr(flags.posts)) } : {}),
|
|
3544
|
+
};
|
|
3545
|
+
return out(await action(body), (r) =>
|
|
3546
|
+
[
|
|
3547
|
+
r.twins
|
|
3548
|
+
? `launching twins for ${r.org.name} on "${r.sites[0]?.beat}":`
|
|
3549
|
+
: `launching for ${r.org.name}:`,
|
|
3550
|
+
...r.sites.map((s) => ` ${s.arm}: ${s.name ?? s.beat} (onboarding ${s.jobId})`),
|
|
3551
|
+
].join("\n")
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3503
3554
|
case "start":
|
|
3504
3555
|
case "stop":
|
|
3505
3556
|
if (!id) throw new CliError(`Usage: admin fleet-experiments ${sub} <experiment-id>`);
|
|
@@ -3512,7 +3563,7 @@ async function cmdAdminFleetExperiments(ctx) {
|
|
|
3512
3563
|
}
|
|
3513
3564
|
default:
|
|
3514
3565
|
throw new CliError(
|
|
3515
|
-
'Usage: admin fleet-experiments list | articles [--experiment <id>] [--org <id>] [--filter all|assigned|unassigned] [--arm <arm>] [--q …] [--limit n] [--offset n] | mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…> | new --template <key> | start|stop <experiment-id> | decide <experiment-id> --decision "…"'
|
|
3566
|
+
'Usage: admin fleet-experiments list | articles [--experiment <id>] [--org <id>] [--filter all|assigned|unassigned] [--arm <arm>] [--q …] [--limit n] [--offset n] | mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…> | new --template <key> | clients | spin-up <experiment-id> <org> [--no-control] [--own-beat] | start|stop <experiment-id> | decide <experiment-id> --decision "…"'
|
|
3516
3567
|
);
|
|
3517
3568
|
}
|
|
3518
3569
|
}
|
|
@@ -3531,7 +3582,65 @@ export async function cmdAdmin(ctx) {
|
|
|
3531
3582
|
if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3532
3583
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3533
3584
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3585
|
+
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3534
3586
|
throw new CliError(
|
|
3535
|
-
'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)'
|
|
3587
|
+
'Usage: admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
3536
3588
|
);
|
|
3537
3589
|
}
|
|
3590
|
+
|
|
3591
|
+
// `admin brand-detection <org-id>` — how the client's brand is detected in AI answers;
|
|
3592
|
+
// with --term / --exact / --exclude it SETS the exact set (replacing the inferred one),
|
|
3593
|
+
// --clear goes back to inferred (recorded as a decision, with --note as the why). Every
|
|
3594
|
+
// earlier setting is kept in `history`. The client's answers are recounted against the new set.
|
|
3595
|
+
async function cmdAdminBrandDetection(ctx) {
|
|
3596
|
+
const { flags, positionals, io } = ctx;
|
|
3597
|
+
const orgId = requirePositional(positionals, 0, "org-id");
|
|
3598
|
+
const path = `/api/superadmin/fleet/mentions/${encodeURIComponent(orgId)}/detection`;
|
|
3599
|
+
const terms = flagList(flags.term);
|
|
3600
|
+
const exact = flagList(flags.exact);
|
|
3601
|
+
const exclude = flagList(flags.exclude);
|
|
3602
|
+
let result;
|
|
3603
|
+
if (flags.clear === true)
|
|
3604
|
+
result = await staffRequest(ctx, "PUT", path, {
|
|
3605
|
+
clear: true,
|
|
3606
|
+
...(flagStr(flags.note) !== undefined ? { note: flagStr(flags.note) } : {}),
|
|
3607
|
+
});
|
|
3608
|
+
else if (terms.length || exact.length || exclude.length || flagStr(flags.note) !== undefined) {
|
|
3609
|
+
if (!terms.length && !exact.length) throw new CliError("Give at least one --term or --exact (or --clear).");
|
|
3610
|
+
result = await staffRequest(ctx, "PUT", path, {
|
|
3611
|
+
terms,
|
|
3612
|
+
exact_case_terms: exact,
|
|
3613
|
+
exclude_phrases: exclude,
|
|
3614
|
+
...(flagStr(flags.note) !== undefined ? { note: flagStr(flags.note) } : {}),
|
|
3615
|
+
});
|
|
3616
|
+
} else result = await staffRequest(ctx, "GET", path);
|
|
3617
|
+
printResult(io, flags, result, (r) => {
|
|
3618
|
+
const e = r.effective;
|
|
3619
|
+
const state = r.override
|
|
3620
|
+
? `exact set, by ${r.override.setBy ?? "?"} at ${r.override.setAt ?? "?"}`
|
|
3621
|
+
: r.cleared
|
|
3622
|
+
? `inferred (exact set cleared by ${r.cleared.by ?? "?"} at ${r.cleared.at ?? "?"})`
|
|
3623
|
+
: "inferred";
|
|
3624
|
+
const lines = [
|
|
3625
|
+
`${r.orgName}: ${state}`,
|
|
3626
|
+
` terms (any case): ${e.terms.join(", ") || "—"}`,
|
|
3627
|
+
` exact case: ${e.exactCaseTerms.join(", ") || "—"}`,
|
|
3628
|
+
` excluded phrases: ${e.excludePhrases.join(", ") || "—"}`,
|
|
3629
|
+
];
|
|
3630
|
+
const why = r.override?.note ?? r.cleared?.note;
|
|
3631
|
+
if (why) lines.push(` why: ${why}`);
|
|
3632
|
+
if (r.override) lines.push(` inferred would be: ${r.inferred.terms.join(", ")}`);
|
|
3633
|
+
for (const h of r.history ?? []) {
|
|
3634
|
+
const set = [
|
|
3635
|
+
h.terms.length ? `terms ${h.terms.join(", ")}` : "",
|
|
3636
|
+
h.exactCaseTerms.length ? `exact ${h.exactCaseTerms.join(", ")}` : "",
|
|
3637
|
+
h.excludePhrases.length ? `excludes ${h.excludePhrases.join(", ")}` : "",
|
|
3638
|
+
].filter(Boolean);
|
|
3639
|
+
lines.push(
|
|
3640
|
+
` earlier: ${h.action} by ${h.by ?? "?"} at ${h.at ?? "?"}${set.length ? ` — ${set.join("; ")}` : ""}${h.note ? ` (${h.note})` : ""}`
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
return lines.join("\n");
|
|
3644
|
+
});
|
|
3645
|
+
return 0;
|
|
3646
|
+
}
|
|
@@ -55,6 +55,8 @@ export const ARGS = {
|
|
|
55
55
|
},
|
|
56
56
|
cite_preferred_sources: {
|
|
57
57
|
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
58
|
+
// Chosen posts a reviewer decided against.
|
|
59
|
+
exclude_article_ids: { flag: "exclude", type: "list" },
|
|
58
60
|
},
|
|
59
61
|
update_auto_pen_config: {
|
|
60
62
|
collection_id: { positional: 0, label: "collection-id", required: true },
|
|
@@ -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
|
}
|