@letterstory/cli 0.87.1 → 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 +8 -1
- package/lib/commands/admin.mjs +129 -51
- 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
|
|
@@ -639,6 +639,12 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
639
639
|
read-only SQL across every org; --sql prints
|
|
640
640
|
the queries behind the answer. Needs the
|
|
641
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.
|
|
642
648
|
admin replays [--org <uuid>] [--target <uuid>] [--status captured|not_reproduced|queued|running|unsupported|failed] [--limit <n>]
|
|
643
649
|
Live answer replays: breakthroughs re-asked in ChatGPT /
|
|
644
650
|
Gemini / Google / Perplexity / Claude, with screenshots.
|
|
@@ -1037,6 +1043,7 @@ Insights:
|
|
|
1037
1043
|
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
1038
1044
|
insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
|
|
1039
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)
|
|
1040
1047
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
1041
1048
|
insights post <article-id> [--period 14d|30d|90d]
|
|
1042
1049
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
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) {
|
|
@@ -3562,7 +3582,65 @@ export async function cmdAdmin(ctx) {
|
|
|
3562
3582
|
if (sub === "tuning-import") return cmdAdminTuningImport({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3563
3583
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3564
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) });
|
|
3565
3586
|
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)'
|
|
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)'
|
|
3567
3588
|
);
|
|
3568
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
|
+
}
|
|
@@ -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
|
}
|