@letterstory/cli 0.176.72 → 0.176.74

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 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.72";
88
+ export const VERSION = "0.176.74";
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.
@@ -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
@@ -118,6 +118,9 @@ function headline(x) {
118
118
  ` Media ${x.media.files} large file${x.media.files === 1 ? "" : "s"} (${(x.media.bytes / 1e6).toFixed(1)} MB) served from Letter's media CDN, not the repo`,
119
119
  ]
120
120
  : []),
121
+ ...(x.unreplicated?.length
122
+ ? [` Not carried (${x.unreplicated.length}):`, ...x.unreplicated.map((u) => ` - ${u.message}`)]
123
+ : []),
121
124
  ...x.collections.map(
122
125
  (c) =>
123
126
  ` Collection ${c.pattern}: ${c.items} items (${c.markdown_bodies} as Markdown) in ${c.content_path_template}`
@@ -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 ?? ""}]` : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.72",
3
+ "version": "0.176.74",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",