@letterstory/cli 0.176.72 → 0.176.73
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 +15 -1
- package/lib/commands/admin.mjs +63 -1
- package/lib/commands/targets.mjs +51 -2
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -85,7 +85,7 @@ import {
|
|
|
85
85
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
86
86
|
|
|
87
87
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
88
|
-
export const VERSION = "0.176.
|
|
88
|
+
export const VERSION = "0.176.73";
|
|
89
89
|
|
|
90
90
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
91
91
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -94,6 +94,8 @@ export const EXIT_PAYMENT_REQUIRED = 3;
|
|
|
94
94
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
95
95
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
96
96
|
export const BOOLEAN_FLAGS = new Set([
|
|
97
|
+
// admin targets check: check every target again, checked or not.
|
|
98
|
+
"recheck",
|
|
97
99
|
// build list: also move the builds' open changes along while polling.
|
|
98
100
|
"advance",
|
|
99
101
|
// build list: only what the Plan started.
|
|
@@ -828,6 +830,9 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
|
|
|
828
830
|
targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
|
|
829
831
|
inherit the company's cap / back to the preset)
|
|
830
832
|
targets suggest [--offering <offering-id|company>] Evidence-informed suggestions (nothing saved)
|
|
833
|
+
targets candidates [--limit <n>] New questions the market asks (new Search Console queries,
|
|
834
|
+
questions rivals win, rivals' new pages, answer criteria),
|
|
835
|
+
each checked and deduped against your targets (nothing saved)
|
|
831
836
|
targets subqueries [<target-id>] [--retired] The phrasings asked under each target (who retired one, and why)
|
|
832
837
|
targets subquery add <target-id> <phrasing> Another way your buyers ask a target's question
|
|
833
838
|
targets subquery update <subquery-id> [--text <t>] [--retire | --restore]
|
|
@@ -1059,6 +1064,15 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
1059
1064
|
admin recipes reread --org <id>,… Read these companies from their sites again: what was
|
|
1060
1065
|
only ever inferred is replaced, what the company changed
|
|
1061
1066
|
is kept. Refit them after. Needs recipes:write.
|
|
1067
|
+
admin targets check [--org <id>,…] [--recheck] [--limit 10]
|
|
1068
|
+
Check ranking targets' phrases (buyer intent, self-named,
|
|
1069
|
+
search demand, fit) and store each verdict: --org those
|
|
1070
|
+
accounts; neither, every account with unchecked ones.
|
|
1071
|
+
Needs fleet:instrument.
|
|
1072
|
+
admin targets add-candidates <org-id> [--limit 5] [--apply]
|
|
1073
|
+
Add the account's keep-verdict question candidates as
|
|
1074
|
+
active targets within its cap; a dry run unless --apply.
|
|
1075
|
+
Needs fleet:instrument.
|
|
1062
1076
|
admin letterpress status [--customers] Letterpress as it runs now: per lens its mode, bar, recipes,
|
|
1063
1077
|
runs, model and prompt hash, how often the shadow agreed,
|
|
1064
1078
|
how runs grounded (a recipe / none). Needs recipes:read.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -6981,13 +6981,75 @@ export async function cmdAdmin(ctx) {
|
|
|
6981
6981
|
if (sub === "recipes") return cmdAdminRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
6982
6982
|
if (sub === "letterpress") return cmdAdminLetterpress({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
6983
6983
|
if (sub === "demo") return cmdAdminDemo({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
6984
|
+
if (sub === "targets") return cmdAdminTargets({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
6984
6985
|
if (sub === "deployments" || sub === "deploys")
|
|
6985
6986
|
return cmdAdminDeployments({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
6986
6987
|
throw new CliError(
|
|
6987
|
-
'Usage: admin recipes … | admin letterpress … | admin demo … | admin deployments … | admin republish … | 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)'
|
|
6988
|
+
'Usage: admin recipes … | admin letterpress … | admin demo … | admin targets … | admin deployments … | admin republish … | 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)'
|
|
6988
6989
|
);
|
|
6989
6990
|
}
|
|
6990
6991
|
|
|
6992
|
+
const TARGETS_USAGE = `Usage: admin targets <check|add-candidates>
|
|
6993
|
+
admin targets check [--org <id>,…] [--recheck] [--limit 10]
|
|
6994
|
+
admin targets add-candidates <org-id> [--limit 5] [--apply]`;
|
|
6995
|
+
|
|
6996
|
+
// `admin targets` — ranking-target accuracy (docs/architecture/ranking-target-accuracy.md): the phrase
|
|
6997
|
+
// check over targets already there (score_ranking_targets), and an account's keep-verdict question
|
|
6998
|
+
// candidates added as targets (add_question_candidates, a dry run unless --apply). Needs fleet:instrument.
|
|
6999
|
+
async function cmdAdminTargets(ctx) {
|
|
7000
|
+
const { flags, positionals, io } = ctx;
|
|
7001
|
+
switch (positionals[0]) {
|
|
7002
|
+
case "check": {
|
|
7003
|
+
const orgs = (flagStr(flags.org) ?? "")
|
|
7004
|
+
.split(",")
|
|
7005
|
+
.map((s) => s.trim())
|
|
7006
|
+
.filter(Boolean);
|
|
7007
|
+
const limit = flagNum(flags.limit);
|
|
7008
|
+
const result = await staffPost(ctx, "/api/superadmin/ranking-targets/check", {
|
|
7009
|
+
...(orgs.length ? { org_ids: orgs } : {}),
|
|
7010
|
+
...(flagBool(flags.recheck) ? { recheck: true } : {}),
|
|
7011
|
+
...(limit !== undefined ? { limit } : {}),
|
|
7012
|
+
});
|
|
7013
|
+
printResult(io, flags, result, (r) =>
|
|
7014
|
+
[
|
|
7015
|
+
...r.outcomes.map(
|
|
7016
|
+
(o) =>
|
|
7017
|
+
`${o.status.padEnd(16)} ${o.org_id} keep ${o.counts.keep} · weak ${o.counts.weak} · reject ${o.counts.reject}${
|
|
7018
|
+
o.counts.unchecked ? ` · unchecked ${o.counts.unchecked}` : ""
|
|
7019
|
+
}${o.reason ? ` ${o.reason}` : ""}`
|
|
7020
|
+
),
|
|
7021
|
+
`${r.picked} account(s) taken (${r.mode})${r.left ? `, ${r.left} more past the limit` : ""}.`,
|
|
7022
|
+
].join("\n")
|
|
7023
|
+
);
|
|
7024
|
+
return result.outcomes?.some((o) => o.status === "failed") ? 1 : 0;
|
|
7025
|
+
}
|
|
7026
|
+
case "add-candidates": {
|
|
7027
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
7028
|
+
const limit = flagNum(flags.limit);
|
|
7029
|
+
const result = await staffPost(ctx, "/api/superadmin/ranking-targets/candidates", {
|
|
7030
|
+
org_id: orgId,
|
|
7031
|
+
dry_run: !flagBool(flags.apply),
|
|
7032
|
+
...(limit !== undefined ? { limit } : {}),
|
|
7033
|
+
});
|
|
7034
|
+
printResult(io, flags, result, (r) =>
|
|
7035
|
+
[
|
|
7036
|
+
`${r.dry_run ? "Would add" : "Added"} ${r.added.length} (room under the cap: ${r.room}):`,
|
|
7037
|
+
...r.added.map(
|
|
7038
|
+
(a) => ` ${a.phrase} [${a.source}]${a.target_id ? ` ${a.target_id} ${a.status}` : ""}`
|
|
7039
|
+
),
|
|
7040
|
+
...r.skipped.map((k) => ` skipped ${k.phrase}: ${k.reason}`),
|
|
7041
|
+
r.dry_run ? "Nothing added. Run again with --apply to add them." : "",
|
|
7042
|
+
]
|
|
7043
|
+
.filter(Boolean)
|
|
7044
|
+
.join("\n")
|
|
7045
|
+
);
|
|
7046
|
+
return 0;
|
|
7047
|
+
}
|
|
7048
|
+
default:
|
|
7049
|
+
throw new CliError(TARGETS_USAGE);
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
|
|
6991
7053
|
const MANAGED_HEALTH = ["down", "failing", "blocked", "setting_up", "live", "paused", "archived"];
|
|
6992
7054
|
|
|
6993
7055
|
// `admin deployments` — every site Letter built and hosts, across every organization
|
package/lib/commands/targets.mjs
CHANGED
|
@@ -98,6 +98,7 @@ export const ARGS = {
|
|
|
98
98
|
},
|
|
99
99
|
},
|
|
100
100
|
suggest_ranking_targets: { offering_id: offering },
|
|
101
|
+
list_question_candidates: { limit: { flag: "limit", type: "num" } },
|
|
101
102
|
list_ranking_subqueries: {
|
|
102
103
|
target_id: { positional: 0, label: "target-id" },
|
|
103
104
|
include_retired: { flag: "retired", type: "bool" },
|
|
@@ -126,7 +127,7 @@ export const ARGS = {
|
|
|
126
127
|
},
|
|
127
128
|
};
|
|
128
129
|
|
|
129
|
-
const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest|subqueries|subquery>
|
|
130
|
+
const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest|candidates|subqueries|subquery>
|
|
130
131
|
targets list [--offering <offering-id|company>]
|
|
131
132
|
targets add <phrase> [--framing <f>] [--offering <offering-id|company>] [--priority high|normal|low]
|
|
132
133
|
[--stance <s>]... points of view held on this target's ground only
|
|
@@ -137,6 +138,7 @@ const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest|s
|
|
|
137
138
|
targets deactivate <target-id>
|
|
138
139
|
targets cap <offering-id|company> <n|default>
|
|
139
140
|
targets suggest [--offering <offering-id|company>]
|
|
141
|
+
targets candidates [--limit <n>] new questions the market asks, checked and deduped
|
|
140
142
|
targets subqueries [<target-id>] [--retired] the phrasings asked under each target
|
|
141
143
|
targets subquery add <target-id> <phrasing>
|
|
142
144
|
targets subquery update <subquery-id> [--text <t>] [--retire | --restore]
|
|
@@ -161,6 +163,8 @@ export async function cmdTargets(ctx) {
|
|
|
161
163
|
return targetsCap(rest);
|
|
162
164
|
case "suggest":
|
|
163
165
|
return targetsSuggest(rest);
|
|
166
|
+
case "candidates":
|
|
167
|
+
return targetsCandidates(rest);
|
|
164
168
|
case "subqueries":
|
|
165
169
|
return subqueriesList(rest);
|
|
166
170
|
case "subquery":
|
|
@@ -179,11 +183,24 @@ function formatScope(s) {
|
|
|
179
183
|
return `${scopeName(s)} — ${s.active}/${s.cap} active (${where})`;
|
|
180
184
|
}
|
|
181
185
|
|
|
186
|
+
// The phrase check's verdict and its first reason, then whether an automation may act on it and
|
|
187
|
+
// whether it is losing (docs/architecture/ranking-target-accuracy.md).
|
|
188
|
+
function formatAccuracy(t) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
const check = t.phrase_check;
|
|
191
|
+
if (check) lines.push(`check: ${check.verdict}${check.reasons?.[0] ? ` — ${check.reasons[0]}` : ""}`);
|
|
192
|
+
else if (t.phrase_check === null) lines.push("check: not checked yet");
|
|
193
|
+
if (typeof t.actionable === "boolean")
|
|
194
|
+
lines.push(`${t.actionable ? "actionable" : "not actionable"}: ${t.actionable_reason}`);
|
|
195
|
+
if (t.losing === true) lines.push(t.losing_reason);
|
|
196
|
+
return lines.map((l) => `\n ${l}`).join("");
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
function formatTarget(t) {
|
|
183
200
|
const framing = t.mention_framing ? ` — as ${t.mention_framing}` : "";
|
|
184
201
|
const prio = t.priority ? ` (${t.priority} priority)` : "";
|
|
185
202
|
const stances = (t.stances ?? []).map((s) => `\n · ${s}`).join("");
|
|
186
|
-
return ` ${t.id} [${t.status}] ${t.phrase}${framing}${prio}${stances}`;
|
|
203
|
+
return ` ${t.id} [${t.status}] ${t.phrase}${framing}${prio}${stances}${formatAccuracy(t)}`;
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
// Targets grouped under their scope, each headed by its cap and usage. A target on an
|
|
@@ -323,6 +340,38 @@ async function targetsSuggest(ctx) {
|
|
|
323
340
|
return 0;
|
|
324
341
|
}
|
|
325
342
|
|
|
343
|
+
export function formatCandidates(r) {
|
|
344
|
+
const all = r.candidates ?? [];
|
|
345
|
+
if (!all.length) return "No new questions found yet.";
|
|
346
|
+
const lines = all.map((c) => {
|
|
347
|
+
const verdict = c.check ? c.check.verdict : "unchecked";
|
|
348
|
+
const why = c.check?.reasons?.[0] ? `\n ${c.check.reasons[0]}` : "";
|
|
349
|
+
const evidence = (c.evidence ?? []).map((e) => `\n · ${e.source}: ${e.detail}`).join("");
|
|
350
|
+
return ` [${verdict}] ${c.phrase}${why}${evidence}`;
|
|
351
|
+
});
|
|
352
|
+
const f = r.found ?? {};
|
|
353
|
+
lines.push(
|
|
354
|
+
`Found: ${f.search_console ?? 0} in Search Console, ${f.answer_gap ?? 0} answer gaps, ${f.rival_pages ?? 0} rival pages, ${f.answer_fit ?? 0} answer criteria; ${r.duplicates ?? 0} already tracked.`
|
|
355
|
+
);
|
|
356
|
+
return lines.join("\n");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function targetsCandidates(ctx) {
|
|
360
|
+
const { client, flags, io } = ctx;
|
|
361
|
+
refuseUnknownFlags(ctx, ARGS.list_question_candidates);
|
|
362
|
+
const result = await client.callTool(
|
|
363
|
+
"list_question_candidates",
|
|
364
|
+
argsFromFlags(ARGS.list_question_candidates, flags)
|
|
365
|
+
);
|
|
366
|
+
printResult(
|
|
367
|
+
io,
|
|
368
|
+
flags,
|
|
369
|
+
result,
|
|
370
|
+
(r) => `${formatCandidates(r)}\nNothing saved. Track one with: ${ctx.bin} targets add <phrase>`
|
|
371
|
+
);
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
326
375
|
function formatSubquery(q) {
|
|
327
376
|
const who = q.source === "user" ? " (yours)" : "";
|
|
328
377
|
const retired = q.status === "retired" ? ` [retired by ${q.retired_by ?? "?"}: ${q.retired_reason ?? ""}]` : "";
|