@letterstory/cli 0.176.71 → 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 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.71";
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.
@@ -640,7 +642,7 @@ Reviewers:
640
642
  automate list Your automations: an event, which ones it acts on,
641
643
  and what Letterpress then does
642
644
  automate show <automation-id>
643
- automate add [--template post_new_issues|fix_new_issues|recommended_actions|competitor_pages|competitor_takes|
645
+ automate add [--template post_new_issues|recommended_actions|competitor_pages|competitor_takes|
644
646
  competitor_changes|icp_visitors|vc_visitors|plan_updates|weekly_release_notes|monthly_page_refresh]
645
647
  [--name <n>] [--trigger competitor_page|competitor_change|visitor_identified|issue_found|build_suggestion|plan_update|custom|
646
648
  schedule]
@@ -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.
@@ -1806,7 +1820,8 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letter GitHub App op
1806
1820
  repo suggestions Fixes found and not acted on yet, and how to start each
1807
1821
  repo settings [--action build|build_pr|build_merge|slack|linear] [--branch <name>|none] [--slack-agent <name|U…>|none]
1808
1822
  [--slack-channel <C…>|none] [--autopilot-channel <C…>|none] [--listen <C…,C…>|none] [--autopilot on|off] [--autopilot-weekly <1-20>]
1809
- [--autopilot-severity critical|warning] [--auto-merge 1h|12h|24h|3d|off] [--editor-auto-merge on|off]
1823
+ [--autopilot-severity critical|warning] [--autopilot-scope all|new]
1824
+ [--auto-merge 1h|12h|24h|3d|off] [--editor-auto-merge on|off]
1810
1825
  [--agent letterpress-1|claude-code|default]
1811
1826
  The org's defaults for every fix: build (preview first),
1812
1827
  build_pr, or slack (post it to --slack-channel for your
@@ -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
@@ -1,9 +1,10 @@
1
1
  // `automate` — Letterpress → Automations (docs/architecture/event-automations.md): an event, read
2
2
  // against your filter (--instruction), then done by Letterpress as your prompt (--prompt) says.
3
3
  // options / list / show / add / set / try / runs / approve / reject / send / remove, through the
4
- // same tools the app uses. (A letterbase's writing automations are `automations`.) `list` also shows the jobs
5
- // Letter runs for the account (source "letter": internal links, the blog's sync, the site's re-audit, Fix
6
- // autopilot, add-your-name suggestions, Auto-Plan), each with its scope; `set <id> --on|--off` turns one on or off.
4
+ // same tools the app uses. (A letterbase's writing automations are made and paced with `automations`.) `list` also
5
+ // shows the jobs Letter runs for the account (source "letter": internal links, the blog's sync, the site's re-audit,
6
+ // Fix autopilot, add-your-name suggestions, Auto-Plan, and each letterbase's writing automations), each with its
7
+ // scope; `set <id> --on|--off` turns one on or off.
7
8
  // Same flag/positional/io conventions as posts.mjs — no commander, no color.
8
9
 
9
10
  import { CliError } from "../client.mjs";
@@ -21,7 +22,6 @@ const TRIGGERS = [
21
22
  ];
22
23
  const TEMPLATES = [
23
24
  "post_new_issues",
24
- "fix_new_issues",
25
25
  "recommended_actions",
26
26
  "competitor_pages",
27
27
  "competitor_takes",
@@ -229,6 +229,8 @@ const ARGS = {
229
229
  value: (flags) => (flags["autopilot-weekly"] === undefined ? undefined : Number(flags["autopilot-weekly"])),
230
230
  },
231
231
  autopilot_min_severity: { flag: "autopilot-severity", enum: ["critical", "warning"] },
232
+ // --autopilot-scope all|new: every issue, by priority, or new issues only
233
+ autopilot_scope: { flag: "autopilot-scope", enum: ["all", "new"] },
232
234
  // --auto-merge 1h|12h|24h|3d (or the hours: 1, 12, 24, 72), or --auto-merge off
233
235
  auto_merge_after_hours: { value: (flags) => autoMergeHours(flags["auto-merge"]) },
234
236
  // --agent letterpress-1|claude-code|default: the coding agent every change runs on (default = the deployment's)
@@ -905,6 +907,7 @@ async function repoSettings(ctx) {
905
907
  "autopilot",
906
908
  "autopilot-weekly",
907
909
  "autopilot-severity",
910
+ "autopilot-scope",
908
911
  "auto-merge",
909
912
  "editor-auto-merge",
910
913
  "agent",
@@ -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.71",
3
+ "version": "0.176.73",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",