@letterstory/cli 0.81.2 → 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 +71 -4
- package/lib/commands/authors.mjs +2 -2
- package/lib/commands/collection.mjs +3 -3
- package/lib/commands/insights.mjs +9 -1
- package/lib/commands/lede.mjs +5 -5
- package/lib/commands/phantom.mjs +1 -1
- package/lib/commands/seers.mjs +1 -1
- package/lib/commands/targets.mjs +88 -2
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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",
|
|
@@ -129,8 +130,68 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
129
130
|
// `prospects import|source --one-per-company`, `prospects source --enrich`.
|
|
130
131
|
"one-per-company",
|
|
131
132
|
"enrich",
|
|
132
|
-
// `targets update <id> --clear-framing
|
|
133
|
+
// `targets update <id> --clear-framing`; `targets subqueries --retired`,
|
|
134
|
+
// `targets subquery update <id> --retire` / `--restore`.
|
|
133
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",
|
|
134
195
|
]);
|
|
135
196
|
|
|
136
197
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -381,6 +442,9 @@ Ranking targets (what the org wants to be THE answer for — company-wide or on
|
|
|
381
442
|
targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
|
|
382
443
|
inherit the company's cap / back to the preset)
|
|
383
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]
|
|
384
448
|
|
|
385
449
|
Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
|
|
386
450
|
arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
|
|
@@ -930,6 +994,9 @@ Mail:
|
|
|
930
994
|
|
|
931
995
|
Insights:
|
|
932
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
|
|
933
1000
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
934
1001
|
insights post <article-id> [--period 14d|30d|90d]
|
|
935
1002
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
package/lib/commands/authors.mjs
CHANGED
|
@@ -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:
|
|
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: {
|
|
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: {
|
|
87
|
-
rebuild: {
|
|
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
|
}
|
package/lib/commands/lede.mjs
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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: {
|
|
94
|
-
replied: {
|
|
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: {
|
|
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.
|
package/lib/commands/phantom.mjs
CHANGED
|
@@ -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: {
|
|
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" },
|
package/lib/commands/seers.mjs
CHANGED
|
@@ -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: {
|
|
42
|
+
produce: { flag: "no-produce", type: "bool", invert: true },
|
|
43
43
|
async: { flag: "async", type: "bool" },
|
|
44
44
|
},
|
|
45
45
|
get_seer_run: {
|
package/lib/commands/targets.mjs
CHANGED
|
@@ -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
|
+
}
|