@letterstory/cli 0.81.1 → 0.82.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 CHANGED
@@ -57,11 +57,12 @@ import {
57
57
  } from "./commands.mjs";
58
58
 
59
59
  // Keep in sync with cli/package.json.
60
- export const VERSION = "0.81.1";
60
+ export const VERSION = "0.82.0";
61
61
 
62
62
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
63
- // can't accidentally swallow the id as --json's value.
64
- const BOOLEAN_FLAGS = new Set([
63
+ // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
64
+ // `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
65
+ export const BOOLEAN_FLAGS = new Set([
65
66
  "json",
66
67
  "yes",
67
68
  "confirm-missing-target",
@@ -75,6 +76,7 @@ const BOOLEAN_FLAGS = new Set([
75
76
  "verbose",
76
77
  "no-color",
77
78
  "dry-run",
79
+ "prune",
78
80
  "buy",
79
81
  "rebuild",
80
82
  "print-key",
@@ -128,8 +130,68 @@ const BOOLEAN_FLAGS = new Set([
128
130
  // `prospects import|source --one-per-company`, `prospects source --enrich`.
129
131
  "one-per-company",
130
132
  "enrich",
131
- // `targets update <id> --clear-framing`.
133
+ // `targets update <id> --clear-framing`; `targets subqueries --retired`,
134
+ // `targets subquery update <id> --retire` / `--restore`.
132
135
  "clear-framing",
136
+ "retired",
137
+ "retire",
138
+ "restore",
139
+ // Bool flags from command ARGS maps that were missing here.
140
+ "apply-rules",
141
+ "async",
142
+ "configure",
143
+ "detect-reply-opt-outs",
144
+ "experience-claims",
145
+ "maverick",
146
+ "no-detect-reply-opt-outs",
147
+ "no-maverick",
148
+ "no-notify-staff",
149
+ "no-opt-out-footer",
150
+ "no-postal-address-footer",
151
+ "no-redesign",
152
+ "no-test-mode",
153
+ "no-unsubscribe-links",
154
+ "now",
155
+ "opt-out-footer",
156
+ "postal-address-footer",
157
+ "redesign",
158
+ "release-mailbox",
159
+ "rival",
160
+ "shred",
161
+ "shred-judge",
162
+ "shred-skip-voice-bearing",
163
+ "shred-turnover-avoid-list",
164
+ "test-mode",
165
+ "unsubscribe-links",
166
+ // Switches read directly (`flags["x"] === true` / `flagBool(flags["x"])`) or once read
167
+ // through `value:` functions.
168
+ "active-only",
169
+ "clear-seer",
170
+ "flag-wrong",
171
+ "ledger-scope",
172
+ "lift-budget",
173
+ "no-configure",
174
+ "no-produce",
175
+ "no-publish",
176
+ "no-rebuild",
177
+ "null-check",
178
+ "replied",
179
+ "sent",
180
+ "sweep",
181
+ // Switches read in dot form (`flags.x === true` / `flagBool(flags.x)`).
182
+ "all",
183
+ "byok",
184
+ "clear",
185
+ "closed",
186
+ "create",
187
+ "live",
188
+ "regrade",
189
+ "rule",
190
+ "sql",
191
+ "steps",
192
+ "suggested",
193
+ "sync",
194
+ "texts",
133
195
  ]);
134
196
 
135
197
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -380,6 +442,9 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
380
442
  targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
381
443
  inherit the company's cap / back to the preset)
382
444
  targets suggest [--offering <offering-id|company>] Evidence-informed suggestions (nothing saved)
445
+ targets subqueries [<target-id>] [--retired] The phrasings asked under each target (who retired one, and why)
446
+ targets subquery add <target-id> <phrasing> Another way your buyers ask a target's question
447
+ targets subquery update <subquery-id> [--text <t>] [--retire | --restore]
383
448
 
384
449
  Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
385
450
  arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
@@ -929,6 +994,9 @@ Mail:
929
994
 
930
995
  Insights:
931
996
  insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
997
+ insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
998
+ insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
999
+ One By-question row: the wordings asked and the AI answers
932
1000
  insights site [--period 14d|30d|90d] [--collection <uuid>]
933
1001
  insights post <article-id> [--period 14d|30d|90d]
934
1002
  insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
@@ -98,7 +98,7 @@ async function authorsEdit(ctx) {
98
98
  role: flagStr(flags.role),
99
99
  expertise: flagList(flags.expertise),
100
100
  started_at: flagStr(flags["started-at"]),
101
- active: flagBool(flags.active),
101
+ active: flags.active === true || flags.active === "true" ? true : flags.active === "false" ? false : undefined,
102
102
  });
103
103
  if (Object.keys(patch).length <= 2) {
104
104
  throw new CliError(
@@ -156,7 +156,7 @@ async function authorsAssign(ctx) {
156
156
  author_id: authorId,
157
157
  article_ids: all ? undefined : articleIds,
158
158
  all: all ? true : undefined,
159
- rebuild: flags["no-rebuild"] ? false : undefined,
159
+ rebuild: flags["no-rebuild"] === true ? false : undefined,
160
160
  });
161
161
  const result = await client.callTool("assign_collection_authors", args);
162
162
  ok(ctx, authorId ? "Reassigned bylines to the chosen author." : "Auto-distributed bylines across active authors.");
@@ -79,12 +79,12 @@ export const ARGS = {
79
79
  must_include: { flag: "must-include", type: "list" },
80
80
  research_depth: { flag: "depth", enum: ["barebones", "fleshed_out"] },
81
81
  kernel_id: { flag: "kernel" },
82
- verify: { value: (flags) => (flags["no-verify"] === true ? false : undefined) },
82
+ verify: { flag: "no-verify", type: "bool", invert: true },
83
83
  shred: { flag: "shred", type: "bool" },
84
84
  mention_degree: { flag: "mention", enum: ["off", "subtle", "woven"] },
85
85
  approval_mode: { flag: "approval", enum: ["autopilot", "review_window", "manual"] },
86
- publish: { value: (flags) => (flags["no-publish"] === true ? false : undefined) },
87
- rebuild: { value: (flags) => (flags["no-rebuild"] === true ? false : undefined) },
86
+ publish: { flag: "no-publish", type: "bool", invert: true },
87
+ rebuild: { flag: "no-rebuild", type: "bool", invert: true },
88
88
  schedule_for: { flag: "at" },
89
89
  },
90
90
  get_collection_run: {
@@ -27,6 +27,12 @@ export const ARGS = {
27
27
  get_share_of_voice_detail: {
28
28
  period: { flag: "period", enum: PERIODS },
29
29
  },
30
+ get_share_of_voice_question: {
31
+ question_key: { positional: 0, label: "question-key", required: true },
32
+ period: { flag: "period", enum: PERIODS },
33
+ filter: { flag: "filter", enum: ["all", "named_you", "rival_not_you", "no_one"] },
34
+ limit: { flag: "limit", type: "num" },
35
+ },
30
36
  };
31
37
 
32
38
  export async function cmdInsights(ctx) {
@@ -47,9 +53,11 @@ export async function cmdInsights(ctx) {
47
53
  return call(rest, "get_measure_summary");
48
54
  case "competitors":
49
55
  return call(rest, "get_share_of_voice_detail");
56
+ case "question":
57
+ return call(rest, "get_share_of_voice_question");
50
58
  default:
51
59
  throw new CliError(
52
- `Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, competitors, site, post, top, bank, access`
60
+ `Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, competitors, question, site, post, top, bank, access`
53
61
  );
54
62
  }
55
63
  }
@@ -56,7 +56,7 @@ export const ARGS = {
56
56
  // Your own fallback line, returned (unbilled, still no_match) when nothing qualifies.
57
57
  default_lede: { flag: "default-lede" },
58
58
  // Enrichment is ON by default server-side; `--no-enrich` skips it for latency.
59
- enrich: { value: (flags) => (flags["no-enrich"] === true ? false : undefined) },
59
+ enrich: { flag: "no-enrich", type: "bool", invert: true },
60
60
  // A latency budget: optional evidence that cannot arrive in time is left out.
61
61
  deadline_ms: { flag: "deadline-ms", type: "num" },
62
62
  // Advanced: keep specific library lines out of this run (repeatable, by line id).
@@ -75,7 +75,7 @@ export const ARGS = {
75
75
  },
76
76
  // The state of the quality exercise: which evals still count above zero.
77
77
  lede_eval_briefing: {
78
- sweep: { value: (flags) => (flags.sweep === true ? true : undefined) },
78
+ sweep: { flag: "sweep", type: "bool" },
79
79
  },
80
80
  // What has already been said to an account, so the next send does not repeat it.
81
81
  lede_account_history: {
@@ -90,10 +90,10 @@ export const ARGS = {
90
90
  report_lede_outcome: {
91
91
  run_id: { positional: 0, label: "run-id", required: true },
92
92
  shipped_template_id: { flag: "shipped-template" },
93
- sent: { value: (flags) => (flags.sent === true ? true : undefined) },
94
- replied: { value: (flags) => (flags.replied === true ? true : undefined) },
93
+ sent: { flag: "sent", type: "bool" },
94
+ replied: { flag: "replied", type: "bool" },
95
95
  reply_sentiment: { flag: "sentiment", enum: ["positive", "neutral", "negative"] },
96
- flagged_wrong: { value: (flags) => (flags["flag-wrong"] === true ? true : undefined) },
96
+ flagged_wrong: { flag: "flag-wrong", type: "bool" },
97
97
  flagged_reason: { flag: "reason" },
98
98
  },
99
99
  // Per line: picked, sent, replied, positive, flagged wrong.
@@ -118,7 +118,7 @@ export function joinBundle(dir) {
118
118
 
119
119
  async function orgExport(ctx) {
120
120
  const { client, flags, io } = ctx;
121
- refuseUnknownFlags(ctx, ARGS, ["split", "file"]);
121
+ refuseUnknownFlags(ctx, ARGS.export_org_config, ["split", "file"]);
122
122
  const { config } = await client.callTool("export_org_config", argsFromFlags(ARGS.export_org_config, flags));
123
123
  const split = flagStr(flags.split);
124
124
  const file = flagStr(flags.file);
@@ -186,7 +186,7 @@ export function formatPlan(plan) {
186
186
 
187
187
  async function orgDiff(ctx) {
188
188
  const { client, flags, io } = ctx;
189
- refuseUnknownFlags(ctx, ARGS);
189
+ refuseUnknownFlags(ctx, ARGS.diff_org_config);
190
190
  const { plan } = await client.callTool(
191
191
  "diff_org_config",
192
192
  argsFromFlags(ARGS.diff_org_config, flags, ctx.positionals)
@@ -210,7 +210,7 @@ export function formatApply(result) {
210
210
 
211
211
  async function orgApply(ctx) {
212
212
  const { client, flags, io } = ctx;
213
- refuseUnknownFlags(ctx, ARGS, ["yes"]);
213
+ refuseUnknownFlags(ctx, ARGS.apply_org_config, ["yes"]);
214
214
  if (flagBool(flags.prune) && !flagBool(flags["dry-run"]) && !flagBool(flags.yes))
215
215
  throw new CliError("--prune deletes records missing from the bundle. Review with --dry-run, then add --yes.");
216
216
  const { result } = await client.callTool(
@@ -57,7 +57,7 @@ export const ARGS = {
57
57
  theme: "theme",
58
58
  collection_id: { flag: "collection" },
59
59
  customer_ref: { flag: "customer-ref" },
60
- configure: { value: (flags) => (flags["no-configure"] === true ? false : undefined) },
60
+ configure: { flag: "no-configure", type: "bool", invert: true },
61
61
  config: { value: (flags) => readJsonObjectFlag(flags, "config") },
62
62
  composition: { value: (flags) => readJsonObjectFlag(flags, "composition") },
63
63
  client_domain: { flag: "client-domain" },
@@ -39,7 +39,7 @@ export const ARGS = {
39
39
  run_seer: {
40
40
  seer_id: { positional: 0, label: "seer-id", required: true },
41
41
  dry_run: { flag: "dry-run", type: "bool" },
42
- produce: { value: (flags) => (flags["no-produce"] === true ? false : undefined) },
42
+ produce: { flag: "no-produce", type: "bool", invert: true },
43
43
  async: { flag: "async", type: "bool" },
44
44
  },
45
45
  get_seer_run: {
@@ -86,9 +86,35 @@ export const ARGS = {
86
86
  },
87
87
  },
88
88
  suggest_ranking_targets: { offering_id: offering },
89
+ list_ranking_subqueries: {
90
+ target_id: { positional: 0, label: "target-id" },
91
+ include_retired: { flag: "retired", type: "bool" },
92
+ },
93
+ add_ranking_subquery: {
94
+ target_id: { positional: 0, label: "target-id", required: true },
95
+ // The phrasing is every positional after the target, so it needs no quotes.
96
+ text: {
97
+ value: (_flags, positionals) => {
98
+ const text = positionals.slice(1).join(" ").trim();
99
+ if (!text) throw new CliError("Missing required <phrasing>");
100
+ return text;
101
+ },
102
+ },
103
+ },
104
+ update_ranking_subquery: {
105
+ subquery_id: { positional: 0, label: "subquery-id", required: true },
106
+ text: "text",
107
+ status: {
108
+ value: (flags) => {
109
+ if (flags.retire === true && flags.restore === true)
110
+ throw new CliError("Pass --retire or --restore, not both");
111
+ return flags.retire === true ? "retired" : flags.restore === true ? "active" : undefined;
112
+ },
113
+ },
114
+ },
89
115
  };
90
116
 
91
- const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest>
117
+ const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest|subqueries|subquery>
92
118
  targets list [--offering <offering-id|company>]
93
119
  targets add <phrase> [--framing <f>] [--offering <offering-id|company>] [--priority high|normal|low]
94
120
  targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
@@ -96,7 +122,10 @@ const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest>
96
122
  targets activate <target-id>
97
123
  targets deactivate <target-id>
98
124
  targets cap <offering-id|company> <n|default>
99
- targets suggest [--offering <offering-id|company>]`;
125
+ targets suggest [--offering <offering-id|company>]
126
+ targets subqueries [<target-id>] [--retired] the phrasings asked under each target
127
+ targets subquery add <target-id> <phrasing>
128
+ targets subquery update <subquery-id> [--text <t>] [--retire | --restore]`;
100
129
 
101
130
  export async function cmdTargets(ctx) {
102
131
  const sub = ctx.positionals[0];
@@ -117,6 +146,10 @@ export async function cmdTargets(ctx) {
117
146
  return targetsCap(rest);
118
147
  case "suggest":
119
148
  return targetsSuggest(rest);
149
+ case "subqueries":
150
+ return subqueriesList(rest);
151
+ case "subquery":
152
+ return subqueryWrite(rest);
120
153
  default:
121
154
  throw new CliError(USAGE);
122
155
  }
@@ -239,3 +272,56 @@ async function targetsSuggest(ctx) {
239
272
  });
240
273
  return 0;
241
274
  }
275
+
276
+ function formatSubquery(q) {
277
+ const who = q.source === "user" ? " (yours)" : "";
278
+ const retired = q.status === "retired" ? ` [retired by ${q.retired_by ?? "?"}: ${q.retired_reason ?? ""}]` : "";
279
+ return ` ${q.id} ${q.text}${who}${retired}`;
280
+ }
281
+
282
+ async function subqueriesList(ctx) {
283
+ const { client, flags, positionals, io } = ctx;
284
+ const result = await client.callTool(
285
+ "list_ranking_subqueries",
286
+ argsFromFlags(ARGS.list_ranking_subqueries, flags, positionals)
287
+ );
288
+ printResult(io, flags, result, (r) => {
289
+ const items = r.items ?? [];
290
+ if (!items.length)
291
+ return "No phrasings yet — the next GEO cycle proposes them, or add one: targets subquery add";
292
+ const byTarget = new Map();
293
+ for (const q of items) byTarget.set(q.target_id, [...(byTarget.get(q.target_id) ?? []), q]);
294
+ return [...byTarget]
295
+ .map(([target, qs]) => [`Target ${target}`, ...qs.map(formatSubquery)].join("\n"))
296
+ .join("\n\n");
297
+ });
298
+ return 0;
299
+ }
300
+
301
+ // `subquery add <target-id> <phrasing>` / `subquery update <subquery-id> [--text] [--retire|--restore]`
302
+ async function subqueryWrite(ctx) {
303
+ const { client, flags, io } = ctx;
304
+ const action = ctx.positionals[0];
305
+ const positionals = ctx.positionals.slice(1);
306
+ if (action === "add") {
307
+ refuseUnknownFlags(ctx, ARGS.add_ranking_subquery);
308
+ const result = await client.callTool(
309
+ "add_ranking_subquery",
310
+ argsFromFlags(ARGS.add_ranking_subquery, flags, positionals)
311
+ );
312
+ ok(ctx, "Added — it is asked from the next GEO cycle.");
313
+ printResult(io, flags, result, (r) => formatSubquery(r.subquery));
314
+ return 0;
315
+ }
316
+ if (action === "update") {
317
+ refuseUnknownFlags(ctx, ARGS.update_ranking_subquery, ["retire", "restore"]);
318
+ const args = argsFromFlags(ARGS.update_ranking_subquery, flags, positionals);
319
+ if (Object.keys(args).length === 1)
320
+ throw new CliError("Nothing to update — pass --text, --retire or --restore.");
321
+ const result = await client.callTool("update_ranking_subquery", args);
322
+ ok(ctx, `Saved — ${result.subquery?.status}.`);
323
+ printResult(io, flags, result, (r) => formatSubquery(r.subquery));
324
+ return 0;
325
+ }
326
+ throw new CliError(USAGE);
327
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.81.1",
3
+ "version": "0.82.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",