@letterstory/cli 0.81.2 → 0.82.2

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/README.md CHANGED
@@ -437,7 +437,9 @@ letterstory admin crm payment-link <account-id> # Stripe Payment Link at
437
437
 
438
438
  `onboard` runs org → link → phantom sites → Slack channel → checklist → app invites →
439
439
  stage in one call. Each site is a real (not test) fleet build; `--site ""` lets the engine
440
- infer its beat from the client's homepage. `onboard-plan` reads the account's recorded calls
440
+ infer its beat from the client's homepage. `--disclose 2` (or `--disclose all`) turns on sponsor
441
+ disclosure ("safe mode") for that site, and `--disclose-note "…"` records the client's words
442
+ as the reason; `onboard-plan` flags the sites whose calls asked for it. `onboard-plan` reads the account's recorded calls
441
443
  (newest first, cached per set of calls; `--refresh` re-reads) and says how many sites, what each
442
444
  is about, who to invite, and whether they've paid. `payment-link` stamps the account's org on
443
445
  the Stripe subscription, so paying through it binds and marks the account paid by itself.
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.2";
60
+ export const VERSION = "0.82.2";
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",
@@ -80,6 +81,8 @@ const BOOLEAN_FLAGS = new Set([
80
81
  "rebuild",
81
82
  "print-key",
82
83
  "stdin",
84
+ // `authors generate <id> --with-bios`.
85
+ "with-bios",
83
86
  // `covers generate <id> --regenerate` and `arena run --gate --article <id>`
84
87
  // both put a value-less flag next to a value: without these entries the parser
85
88
  // hands the following token to the flag and the real argument disappears.
@@ -129,8 +132,68 @@ const BOOLEAN_FLAGS = new Set([
129
132
  // `prospects import|source --one-per-company`, `prospects source --enrich`.
130
133
  "one-per-company",
131
134
  "enrich",
132
- // `targets update <id> --clear-framing`.
135
+ // `targets update <id> --clear-framing`; `targets subqueries --retired`,
136
+ // `targets subquery update <id> --retire` / `--restore`.
133
137
  "clear-framing",
138
+ "retired",
139
+ "retire",
140
+ "restore",
141
+ // Bool flags from command ARGS maps that were missing here.
142
+ "apply-rules",
143
+ "async",
144
+ "configure",
145
+ "detect-reply-opt-outs",
146
+ "experience-claims",
147
+ "maverick",
148
+ "no-detect-reply-opt-outs",
149
+ "no-maverick",
150
+ "no-notify-staff",
151
+ "no-opt-out-footer",
152
+ "no-postal-address-footer",
153
+ "no-redesign",
154
+ "no-test-mode",
155
+ "no-unsubscribe-links",
156
+ "now",
157
+ "opt-out-footer",
158
+ "postal-address-footer",
159
+ "redesign",
160
+ "release-mailbox",
161
+ "rival",
162
+ "shred",
163
+ "shred-judge",
164
+ "shred-skip-voice-bearing",
165
+ "shred-turnover-avoid-list",
166
+ "test-mode",
167
+ "unsubscribe-links",
168
+ // Switches read directly (`flags["x"] === true` / `flagBool(flags["x"])`) or once read
169
+ // through `value:` functions.
170
+ "active-only",
171
+ "clear-seer",
172
+ "flag-wrong",
173
+ "ledger-scope",
174
+ "lift-budget",
175
+ "no-configure",
176
+ "no-produce",
177
+ "no-publish",
178
+ "no-rebuild",
179
+ "null-check",
180
+ "replied",
181
+ "sent",
182
+ "sweep",
183
+ // Switches read in dot form (`flags.x === true` / `flagBool(flags.x)`).
184
+ "all",
185
+ "byok",
186
+ "clear",
187
+ "closed",
188
+ "create",
189
+ "live",
190
+ "regrade",
191
+ "rule",
192
+ "sql",
193
+ "steps",
194
+ "suggested",
195
+ "sync",
196
+ "texts",
134
197
  ]);
135
198
 
136
199
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -277,7 +340,8 @@ Collections:
277
340
 
278
341
  Authors (the per-collection author bank — recurring bylines):
279
342
  authors list <collection-id> List the bank + byline distribution
280
- authors generate <collection-id> [--count <n>] Generate a believable masthead
343
+ authors generate <collection-id> [--count <n>] [--with-bios]
344
+ Generate a masthead (names only; --with-bios adds a one-line bio + role)
281
345
  authors add <collection-id> --name <n> [--bio <t>] [--role <t>] [--expertise a,b] [--started-at YYYY-MM-DD]
282
346
  authors edit <collection-id> <author-id> [--name] [--bio] [--role] [--expertise a,b] [--started-at] [--active true|false]
283
347
  authors retire <collection-id> <author-id> Stop assigning new posts to them
@@ -381,6 +445,9 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
381
445
  targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
382
446
  inherit the company's cap / back to the preset)
383
447
  targets suggest [--offering <offering-id|company>] Evidence-informed suggestions (nothing saved)
448
+ targets subqueries [<target-id>] [--retired] The phrasings asked under each target (who retired one, and why)
449
+ targets subquery add <target-id> <phrasing> Another way your buyers ask a target's question
450
+ targets subquery update <subquery-id> [--text <t>] [--retire | --restore]
384
451
 
385
452
  Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
386
453
  arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
@@ -457,6 +524,7 @@ Phantom launch recipe (a CLIENT domain -> a live phantom with its first posts; d
457
524
  ${bin === "phantom" ? "onboard" : "phantom-onboard"} <domain> [--about "the beat"] [--name X]
458
525
  [--posts N] [--cadence N] [--cover editorial_image|full_ai|line_art] [--theme T]
459
526
  [--real] [--domain host] [--domain-mode subdomain|buy_now|connect_later]
527
+ [--disclose [surfaces]] [--sponsor-label X] [--disclose-note "why"]
460
528
  [--accept] [--dry-run]
461
529
  Plan (read-only) then, with --accept
462
530
  or an override flag, provision + configure
@@ -930,6 +998,9 @@ Mail:
930
998
 
931
999
  Insights:
932
1000
  insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
1001
+ insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
1002
+ insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
1003
+ One By-question row: the wordings asked and the AI answers
933
1004
  insights site [--period 14d|30d|90d] [--collection <uuid>]
934
1005
  insights post <article-id> [--period 14d|30d|90d]
935
1006
  insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
@@ -2405,7 +2405,11 @@ function planLines(p) {
2405
2405
  `domain: ${p.domain ?? "(unknown)"}`,
2406
2406
  `sites: ${p.sites.length}`,
2407
2407
  ...p.sites.map(
2408
- (s, i) => ` ${i + 1}. ${s.direction || "(infer from their homepage)"}${s.why ? ` — ${s.why}` : ""}`
2408
+ (s, i) =>
2409
+ ` ${i + 1}. ${s.direction || "(infer from their homepage)"}${s.why ? ` — ${s.why}` : ""}` +
2410
+ (s.disclose_sponsor?.recommended
2411
+ ? `\n disclose the sponsor (--disclose ${i + 1})${s.disclose_sponsor.quote ? `: “${s.disclose_sponsor.quote}”` : ""}`
2412
+ : "")
2409
2413
  ),
2410
2414
  `slack: ${p.slackEmails.join(", ") || "(nobody found)"}`,
2411
2415
  `invites: ${p.invites.map((i) => `${i.email}:${i.role}`).join(", ") || "(none)"}`,
@@ -2418,19 +2422,35 @@ function planLines(p) {
2418
2422
  /**
2419
2423
  * `admin crm onboard` body from flags — the same CrmOnboardBodySchema the desk sends.
2420
2424
  * --site "<direction>" per site (repeatable; "" = infer from the homepage) and/or
2421
- * --sites N (pads with inferred sites); --slack a@x,b@x (bare --slack = the channel, no
2422
- * invites yet); --invite email[:admin|editor] (repeatable); --no-checklist.
2425
+ * --sites N (pads with inferred sites); --disclose N (repeatable, 1-based; "all") turns on
2426
+ * sponsor disclosure ("safe mode": the line after each article + schema.org sponsor) for
2427
+ * that site, with --disclose-note "<the client's words>" as the reason; --slack a@x,b@x
2428
+ * (bare --slack = the channel, no invites yet); --invite email[:admin|editor] (repeatable);
2429
+ * --no-checklist.
2423
2430
  */
2424
2431
  function onboardBody(flags) {
2425
2432
  const domain = flagStr(flags.domain);
2426
2433
  if (!domain) {
2427
2434
  throw new CliError(
2428
- 'Usage: admin crm onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N] [--slack a@x,b@x] [--invite a@x[:admin]]… [--no-checklist]'
2435
+ 'Usage: admin crm onboard <account-id> --domain acme.com [--name "Acme"] [--site "<direction>"]… [--sites N] [--disclose N|all]… [--disclose-note "…"] [--slack a@x,b@x] [--invite a@x[:admin]]… [--no-checklist]'
2429
2436
  );
2430
2437
  }
2431
2438
  const sites = flagList(flags.site).map((direction) => ({ direction: direction.trim() || null }));
2432
2439
  const total = flagNum(flags.sites);
2433
2440
  while (total !== undefined && sites.length < total) sites.push({ direction: null });
2441
+ const disclose = flagList(flags.disclose)
2442
+ .flatMap((v) => String(v).split(","))
2443
+ .map((v) => v.trim())
2444
+ .filter(Boolean);
2445
+ const note = flagStr(flags["disclose-note"]);
2446
+ for (const which of disclose) {
2447
+ const targets =
2448
+ which === "all"
2449
+ ? sites.map((_, i) => i)
2450
+ : [Number(which) - 1].filter((i) => Number.isInteger(i) && i >= 0 && i < sites.length);
2451
+ if (targets.length === 0) throw new CliError(`--disclose ${which}: no such site (1–${sites.length}, or "all")`);
2452
+ for (const i of targets) sites[i] = { ...sites[i], disclosure: compact({ note }) };
2453
+ }
2434
2454
  const slackEmails = flagList(flags.slack)
2435
2455
  .flatMap((v) => v.split(","))
2436
2456
  .map((e) => e.trim())
@@ -61,7 +61,11 @@ async function authorsList(ctx) {
61
61
 
62
62
  async function authorsGenerate(ctx) {
63
63
  const { client, flags, io } = ctx;
64
- const args = compact({ collection_id: collectionId(ctx), count: flagNum(flags.count) });
64
+ const args = compact({
65
+ collection_id: collectionId(ctx),
66
+ count: flagNum(flags.count),
67
+ with_bios: flagBool(flags["with-bios"]) || undefined,
68
+ });
65
69
  const result = await client.callTool("generate_collection_authors", args);
66
70
  ok(ctx, "Generated authors.");
67
71
  printResult(io, flags, result);
@@ -98,7 +102,7 @@ async function authorsEdit(ctx) {
98
102
  role: flagStr(flags.role),
99
103
  expertise: flagList(flags.expertise),
100
104
  started_at: flagStr(flags["started-at"]),
101
- active: flagBool(flags.active),
105
+ active: flags.active === true || flags.active === "true" ? true : flags.active === "false" ? false : undefined,
102
106
  });
103
107
  if (Object.keys(patch).length <= 2) {
104
108
  throw new CliError(
@@ -156,7 +160,7 @@ async function authorsAssign(ctx) {
156
160
  author_id: authorId,
157
161
  article_ids: all ? undefined : articleIds,
158
162
  all: all ? true : undefined,
159
- rebuild: flags["no-rebuild"] ? false : undefined,
163
+ rebuild: flags["no-rebuild"] === true ? false : undefined,
160
164
  });
161
165
  const result = await client.callTool("assign_collection_authors", args);
162
166
  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.
@@ -151,6 +151,23 @@ export const ARGS = {
151
151
  is_test: { flag: "real", type: "bool", invert: true },
152
152
  // A self-serve integrator's customers launch their own sites: no staff pings.
153
153
  notify_staff: { flag: "no-notify-staff", type: "bool", invert: true },
154
+ // Sponsor disclosure ("safe mode"): bare --disclose = the safe-mode surfaces (the
155
+ // server's default); --disclose article_footer,cover_badge picks them. Off unless asked.
156
+ disclosure: {
157
+ value: (flags) => {
158
+ if (flags.disclose === undefined || flags.disclose === false) return undefined;
159
+ const listed = typeof flags.disclose === "string" ? flags.disclose : "";
160
+ const surfaces = listed
161
+ .split(",")
162
+ .map((s) => s.trim())
163
+ .filter(Boolean);
164
+ return compact({
165
+ surfaces: surfaces.length ? surfaces : undefined,
166
+ sponsor_label: flagStr(flags["sponsor-label"]),
167
+ note: flagStr(flags["disclose-note"]),
168
+ });
169
+ },
170
+ },
154
171
  },
155
172
  },
156
173
  },
@@ -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.2",
3
+ "version": "0.82.2",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",