@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 +18 -3
- package/lib/commands/admin.mjs +63 -1
- package/lib/commands/automate.mjs +4 -4
- package/lib/commands/repo.mjs +3 -0
- 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.
|
|
@@ -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|
|
|
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] [--
|
|
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
|
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
|
|
@@ -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
|
|
5
|
-
// Letter runs for the account (source "letter": internal links, the blog's sync, the site's re-audit,
|
|
6
|
-
// autopilot, add-your-name suggestions, Auto-Plan
|
|
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",
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -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",
|
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 ?? ""}]` : "";
|