@letterstory/cli 0.38.0 → 0.39.1
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 +14 -0
- package/lib/cli.mjs +24 -2
- package/lib/commands/insights.mjs +30 -4
- package/lib/commands/outbound.mjs +6 -1
- package/lib/commands/targets.mjs +223 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -590,6 +590,20 @@ letterstory insights post <article-id> [--period 14d|30d|90d]
|
|
|
590
590
|
letterstory insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
|
591
591
|
```
|
|
592
592
|
|
|
593
|
+
AI visibility and page access — the Measure tabs' reads, for scripts and agents:
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
# The AI Visibility Bank: which questions named you, cited your pages, or both (a
|
|
597
|
+
# breakthrough). All time unless --period; --source picks the phantom network
|
|
598
|
+
# (default), your marketing site, or the whole portfolio; --phantom narrows to
|
|
599
|
+
# specific phantom sites (repeatable); --answers caps receipts per question.
|
|
600
|
+
letterstory insights bank [--source phantoms|marketing|all] [--phantom <uuid>]... [--period 14d|30d|90d] [--answers <n>]
|
|
601
|
+
|
|
602
|
+
# Page access: AI answer engines, search crawlers and confirmed human readers on
|
|
603
|
+
# your phantom sites; --collection narrows to the site(s) a collection publishes.
|
|
604
|
+
letterstory insights access [--period 14d|30d|90d] [--collection <uuid>]
|
|
605
|
+
```
|
|
606
|
+
|
|
593
607
|
## Anything else
|
|
594
608
|
|
|
595
609
|
Every Letterstory tool is reachable, not just the ones with a dedicated command group:
|
package/lib/cli.mjs
CHANGED
|
@@ -47,10 +47,11 @@ import {
|
|
|
47
47
|
cmdAdmin,
|
|
48
48
|
cmdFeedback,
|
|
49
49
|
cmdOfferings,
|
|
50
|
+
cmdTargets,
|
|
50
51
|
} from "./commands.mjs";
|
|
51
52
|
|
|
52
53
|
// Keep in sync with cli/package.json.
|
|
53
|
-
export const VERSION = "0.
|
|
54
|
+
export const VERSION = "0.39.1";
|
|
54
55
|
|
|
55
56
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
56
57
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -116,6 +117,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
116
117
|
"none",
|
|
117
118
|
// `prospects import|source --one-per-company`.
|
|
118
119
|
"one-per-company",
|
|
120
|
+
// `targets update <id> --clear-framing`.
|
|
121
|
+
"clear-framing",
|
|
119
122
|
]);
|
|
120
123
|
|
|
121
124
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -344,6 +347,19 @@ Offerings (context layered org → offering → collection; a layer sets only wh
|
|
|
344
347
|
series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
|
|
345
348
|
series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
|
|
346
349
|
|
|
350
|
+
Ranking targets (what the org wants to be THE answer for — company-wide or on one offering):
|
|
351
|
+
targets list [--offering <offering-id|company>] Targets grouped by scope, each with its active cap and usage
|
|
352
|
+
targets add <phrase> [--framing <f>] [--offering <offering-id|company>]
|
|
353
|
+
Lands active while its scope has room, inactive after —
|
|
354
|
+
adding never fails on the cap
|
|
355
|
+
targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
|
|
356
|
+
[--position <n>] [--state active|inactive|achieved|retired]
|
|
357
|
+
Moving an ACTIVE target needs room in the new scope
|
|
358
|
+
targets activate | deactivate <target-id> Switch a target on/off; activating is refused at the cap
|
|
359
|
+
targets cap <offering-id|company> <n|default> Active targets allowed in that scope (preset 5; default =
|
|
360
|
+
inherit the company's cap / back to the preset)
|
|
361
|
+
targets suggest [--offering <offering-id|company>] Evidence-informed suggestions (nothing saved)
|
|
362
|
+
|
|
347
363
|
Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
|
|
348
364
|
arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
|
|
349
365
|
[--gate] [--no-verify] [--provider native|exa|youcom|firecrawl] [--mention off|subtle|woven]
|
|
@@ -740,12 +756,17 @@ Prospects (who you write to):
|
|
|
740
756
|
[--city] [--country] [--years] [--notes] [--custom key=value]...
|
|
741
757
|
|
|
742
758
|
Mail:
|
|
743
|
-
mail connections Connected mailboxes, Google connect
|
|
759
|
+
mail connections Connected mailboxes, Google connect, Vercel key + mailing address status
|
|
760
|
+
mail postal-address "<address>" Set the org's CAN-SPAM mailing address (email footers)
|
|
744
761
|
|
|
745
762
|
Insights:
|
|
746
763
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
747
764
|
insights post <article-id> [--period 14d|30d|90d]
|
|
748
765
|
insights top [--period 14d|30d|90d] [--collection <uuid>] [--limit <n>] [--sort clicks|impressions]
|
|
766
|
+
insights bank [--source phantoms|marketing|all] [--phantom <uuid>]... [--period 14d|30d|90d] [--answers <n>]
|
|
767
|
+
AI Visibility Bank: named / cited / breakthrough evidence (all time by default)
|
|
768
|
+
insights access [--period 14d|30d|90d] [--collection <uuid>]
|
|
769
|
+
Page access: who's reading your phantom sites
|
|
749
770
|
|
|
750
771
|
Feedback (tell us what's broken, missing, unclear, or wanted):
|
|
751
772
|
feedback submit --category bug|missing_feature|unclear|feature_request|praise|other
|
|
@@ -802,6 +823,7 @@ const CLIENT_COMMANDS = {
|
|
|
802
823
|
strategy: cmdStrategy,
|
|
803
824
|
directives: cmdDirectives,
|
|
804
825
|
offerings: cmdOfferings,
|
|
826
|
+
targets: cmdTargets,
|
|
805
827
|
probes: cmdProbes,
|
|
806
828
|
suggestions: cmdSuggestions,
|
|
807
829
|
onboarding: cmdOnboarding,
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
// `insights` — Search Console impressions/clicks across the phantom network, at the
|
|
2
|
-
// network, single-post, and top-posts levels
|
|
3
|
-
// insights.ts
|
|
2
|
+
// network, single-post, and top-posts levels (ported from phantomstory-cli's
|
|
3
|
+
// insights.ts); plus Measure's AI Visibility Bank (`bank`) and phantom Page
|
|
4
|
+
// Access (`access`) — the same reads the web app's Measure tabs render.
|
|
4
5
|
|
|
5
6
|
import { CliError } from "../client.mjs";
|
|
6
|
-
import { flagStr, flagNum, requirePositional, printResult, compact } from "./shared.mjs";
|
|
7
|
+
import { flagStr, flagNum, requirePositional, printResult, compact, argsFromFlags } from "./shared.mjs";
|
|
8
|
+
|
|
9
|
+
const PERIODS = ["14d", "30d", "90d"];
|
|
10
|
+
|
|
11
|
+
export const ARGS = {
|
|
12
|
+
get_ai_visibility_bank: {
|
|
13
|
+
source: { flag: "source", enum: ["phantoms", "marketing", "all"] },
|
|
14
|
+
phantom_ids: { flag: "phantom", type: "list" },
|
|
15
|
+
period: { flag: "period", enum: PERIODS },
|
|
16
|
+
answers_per_prompt: { flag: "answers", type: "num" },
|
|
17
|
+
},
|
|
18
|
+
get_page_access: {
|
|
19
|
+
period: { flag: "period", enum: PERIODS },
|
|
20
|
+
collection_id: { flag: "collection" },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
7
23
|
|
|
8
24
|
export async function cmdInsights(ctx) {
|
|
9
25
|
const sub = ctx.positionals[0];
|
|
@@ -15,8 +31,12 @@ export async function cmdInsights(ctx) {
|
|
|
15
31
|
return insightsPost(rest);
|
|
16
32
|
case "top":
|
|
17
33
|
return insightsTop(rest);
|
|
34
|
+
case "bank":
|
|
35
|
+
return call(rest, "get_ai_visibility_bank");
|
|
36
|
+
case "access":
|
|
37
|
+
return call(rest, "get_page_access");
|
|
18
38
|
default:
|
|
19
|
-
throw new CliError(`Unknown insights subcommand: ${sub ?? "(none)"}. Try: site, post, top`);
|
|
39
|
+
throw new CliError(`Unknown insights subcommand: ${sub ?? "(none)"}. Try: site, post, top, bank, access`);
|
|
20
40
|
}
|
|
21
41
|
}
|
|
22
42
|
|
|
@@ -49,3 +69,9 @@ async function insightsTop(ctx) {
|
|
|
49
69
|
printResult(io, flags, result);
|
|
50
70
|
return 0;
|
|
51
71
|
}
|
|
72
|
+
|
|
73
|
+
async function call(ctx, tool) {
|
|
74
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
75
|
+
printResult(ctx.io, ctx.flags, result);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
@@ -167,6 +167,9 @@ export const ARGS = {
|
|
|
167
167
|
custom: { value: readCustom },
|
|
168
168
|
},
|
|
169
169
|
get_mail_connections: {},
|
|
170
|
+
set_mail_postal_address: {
|
|
171
|
+
mail_postal_address: { positional: 0, label: "address", required: true },
|
|
172
|
+
},
|
|
170
173
|
};
|
|
171
174
|
|
|
172
175
|
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
@@ -318,7 +321,9 @@ export async function cmdMail(ctx) {
|
|
|
318
321
|
case "connections":
|
|
319
322
|
case "status":
|
|
320
323
|
return run(rest, "get_mail_connections");
|
|
324
|
+
case "postal-address":
|
|
325
|
+
return run(rest, "set_mail_postal_address", "Mailing address saved.");
|
|
321
326
|
default:
|
|
322
|
-
throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections`);
|
|
327
|
+
throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections, postal-address`);
|
|
323
328
|
}
|
|
324
329
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// `targets` — ranking targets: what the org wants to be THE answer for.
|
|
2
|
+
//
|
|
3
|
+
// A target belongs to the company (company-wide) or to one offering. Each scope — the
|
|
4
|
+
// company layer and every offering — holds its own ACTIVE targets up to its cap (a preset
|
|
5
|
+
// of 5, adjustable per scope with `targets cap`); inactive targets are never capped, so
|
|
6
|
+
// `add` never fails on the cap: it lands inactive when the scope is full. Thin wrappers
|
|
7
|
+
// over the MCP tools, which call the same store functions the app's /api/ranking-targets
|
|
8
|
+
// routes and staff Fleet do.
|
|
9
|
+
|
|
10
|
+
import { CliError } from "../client.mjs";
|
|
11
|
+
import { flagStr, flagNum, printResult, argsFromFlags, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
12
|
+
|
|
13
|
+
const STATUSES = ["active", "inactive", "achieved", "retired"];
|
|
14
|
+
const COMPANY = "company";
|
|
15
|
+
|
|
16
|
+
// `--offering <offering-id|company>`: company = the company layer (null). Absent = not sent.
|
|
17
|
+
function readOffering(flags) {
|
|
18
|
+
const v = flagStr(flags.offering);
|
|
19
|
+
if (v === undefined) {
|
|
20
|
+
if (flags.offering === true) throw new CliError("--offering takes an offering id, or `company`");
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return v === COMPANY ? null : v;
|
|
24
|
+
}
|
|
25
|
+
const offering = { value: readOffering };
|
|
26
|
+
|
|
27
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
28
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
29
|
+
export const ARGS = {
|
|
30
|
+
list_ranking_targets: { offering_id: offering },
|
|
31
|
+
add_ranking_target: {
|
|
32
|
+
// The phrase is every positional, so `targets add best payroll software` needs no quotes.
|
|
33
|
+
phrase: {
|
|
34
|
+
value: (_flags, positionals) => {
|
|
35
|
+
const phrase = positionals.join(" ").trim();
|
|
36
|
+
if (!phrase) throw new CliError("Missing required <phrase>");
|
|
37
|
+
return phrase;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
mention_framing: "framing",
|
|
41
|
+
offering_id: offering,
|
|
42
|
+
},
|
|
43
|
+
update_ranking_target: {
|
|
44
|
+
target_id: { positional: 0, label: "target-id", required: true },
|
|
45
|
+
phrase: "phrase",
|
|
46
|
+
mention_framing: {
|
|
47
|
+
value: (flags) => (flags["clear-framing"] === true ? null : flagStr(flags.framing)),
|
|
48
|
+
},
|
|
49
|
+
// `activate` / `deactivate` set this for you.
|
|
50
|
+
status: { flag: "state", enum: STATUSES },
|
|
51
|
+
offering_id: offering,
|
|
52
|
+
position: { flag: "position", type: "num" },
|
|
53
|
+
},
|
|
54
|
+
set_ranking_target_cap: {
|
|
55
|
+
offering_id: {
|
|
56
|
+
value: (_flags, positionals) => {
|
|
57
|
+
const scope = positionals[0];
|
|
58
|
+
if (scope === undefined) throw new CliError("Missing required <offering-id|company>");
|
|
59
|
+
return scope === COMPANY ? null : scope;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
// `default` = back to inheriting (an offering → the company's cap; the company → the preset).
|
|
63
|
+
cap: {
|
|
64
|
+
value: (_flags, positionals) => {
|
|
65
|
+
const raw = positionals[1];
|
|
66
|
+
if (raw === undefined) throw new CliError("Missing required <n|default>");
|
|
67
|
+
return raw === "default" ? null : flagNum(raw);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
suggest_ranking_targets: { offering_id: offering },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const USAGE = `Usage: targets <list|add|update|activate|deactivate|cap|suggest>
|
|
75
|
+
targets list [--offering <offering-id|company>]
|
|
76
|
+
targets add <phrase> [--framing <f>] [--offering <offering-id|company>]
|
|
77
|
+
targets update <target-id> [--phrase <p>] [--framing <f> | --clear-framing] [--offering <offering-id|company>]
|
|
78
|
+
[--position <n>] [--state active|inactive|achieved|retired]
|
|
79
|
+
targets activate <target-id>
|
|
80
|
+
targets deactivate <target-id>
|
|
81
|
+
targets cap <offering-id|company> <n|default>
|
|
82
|
+
targets suggest [--offering <offering-id|company>]`;
|
|
83
|
+
|
|
84
|
+
export async function cmdTargets(ctx) {
|
|
85
|
+
const sub = ctx.positionals[0];
|
|
86
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
87
|
+
switch (sub) {
|
|
88
|
+
case "list":
|
|
89
|
+
case "ls":
|
|
90
|
+
return targetsList(rest);
|
|
91
|
+
case "add":
|
|
92
|
+
return targetsAdd(rest);
|
|
93
|
+
case "update":
|
|
94
|
+
return targetsUpdate(rest);
|
|
95
|
+
case "activate":
|
|
96
|
+
return targetsSetStatus(rest, "active");
|
|
97
|
+
case "deactivate":
|
|
98
|
+
return targetsSetStatus(rest, "inactive");
|
|
99
|
+
case "cap":
|
|
100
|
+
return targetsCap(rest);
|
|
101
|
+
case "suggest":
|
|
102
|
+
return targetsSuggest(rest);
|
|
103
|
+
default:
|
|
104
|
+
throw new CliError(USAGE);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const scopeName = (s) => (s.offeringId === null ? "Company-wide" : s.label);
|
|
109
|
+
|
|
110
|
+
function formatScope(s) {
|
|
111
|
+
const where = s.capSetHere ? "set here" : s.offeringId === null ? "preset" : "company's cap";
|
|
112
|
+
return `${scopeName(s)} — ${s.active}/${s.cap} active (${where})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatTarget(t) {
|
|
116
|
+
const framing = t.mention_framing ? ` — as ${t.mention_framing}` : "";
|
|
117
|
+
return ` ${t.id} [${t.status}] ${t.phrase}${framing}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Targets grouped under their scope, each headed by its cap and usage. A target on an
|
|
121
|
+
// offering the scopes list doesn't carry (archived) still shows, under its id.
|
|
122
|
+
export function formatTargets(r) {
|
|
123
|
+
const scopes = r.scopes ?? [];
|
|
124
|
+
const items = r.items ?? [];
|
|
125
|
+
if (!items.length && !scopes.length) return "No ranking targets yet.";
|
|
126
|
+
const lines = [];
|
|
127
|
+
const shown = new Set();
|
|
128
|
+
for (const s of scopes) {
|
|
129
|
+
const mine = items.filter((t) => (t.offering_id ?? null) === s.offeringId);
|
|
130
|
+
lines.push(formatScope(s));
|
|
131
|
+
for (const t of mine) {
|
|
132
|
+
lines.push(formatTarget(t));
|
|
133
|
+
shown.add(t.id);
|
|
134
|
+
}
|
|
135
|
+
if (!mine.length) lines.push(" (none)");
|
|
136
|
+
}
|
|
137
|
+
const orphans = items.filter((t) => !shown.has(t.id));
|
|
138
|
+
for (const id of [...new Set(orphans.map((t) => t.offering_id))]) {
|
|
139
|
+
lines.push(`Offering ${id} (not active)`);
|
|
140
|
+
for (const t of orphans.filter((o) => o.offering_id === id)) lines.push(formatTarget(t));
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function targetsList(ctx) {
|
|
146
|
+
const { client, flags, io } = ctx;
|
|
147
|
+
const result = await client.callTool("list_ranking_targets", argsFromFlags(ARGS.list_ranking_targets, flags));
|
|
148
|
+
printResult(io, flags, result, formatTargets);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function targetsAdd(ctx) {
|
|
153
|
+
const { client, flags, positionals, io } = ctx;
|
|
154
|
+
refuseUnknownFlags(ctx, ARGS.add_ranking_target, ["offering"]);
|
|
155
|
+
const result = await client.callTool(
|
|
156
|
+
"add_ranking_target",
|
|
157
|
+
argsFromFlags(ARGS.add_ranking_target, flags, positionals)
|
|
158
|
+
);
|
|
159
|
+
const t = result.target;
|
|
160
|
+
ok(
|
|
161
|
+
ctx,
|
|
162
|
+
t?.status === "active"
|
|
163
|
+
? `Added "${t.phrase}" — active.`
|
|
164
|
+
: `Added "${t?.phrase}" as inactive: its scope is at its active cap. Deactivate one there, or raise the cap (targets cap), then: ${ctx.bin} targets activate ${t?.id}`
|
|
165
|
+
);
|
|
166
|
+
printResult(io, flags, result, (r) => formatTarget(r.target));
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function update(ctx, flags) {
|
|
171
|
+
const { client, positionals, io } = ctx;
|
|
172
|
+
const args = argsFromFlags(ARGS.update_ranking_target, flags, positionals);
|
|
173
|
+
if (Object.keys(args).length === 1) {
|
|
174
|
+
throw new CliError(
|
|
175
|
+
"Nothing to update — pass --phrase, --framing, --clear-framing, --offering, --position or --state."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const result = await client.callTool("update_ranking_target", args);
|
|
179
|
+
ok(ctx, `Updated "${result.target?.phrase}" — ${result.target?.status}.`);
|
|
180
|
+
printResult(io, ctx.flags, result, (r) => formatTarget(r.target));
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function targetsUpdate(ctx) {
|
|
185
|
+
refuseUnknownFlags(ctx, ARGS.update_ranking_target, ["framing", "clear-framing", "offering"]);
|
|
186
|
+
return update(ctx, ctx.flags);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Activate / deactivate are the first-class switch. Activating is refused when the
|
|
190
|
+
// target's scope is at its cap — the server's message names the scope.
|
|
191
|
+
async function targetsSetStatus(ctx, status) {
|
|
192
|
+
refuseUnknownFlags(ctx, {});
|
|
193
|
+
return update(ctx, { ...ctx.flags, state: status });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function targetsCap(ctx) {
|
|
197
|
+
const { client, flags, positionals, io } = ctx;
|
|
198
|
+
refuseUnknownFlags(ctx, {});
|
|
199
|
+
const result = await client.callTool(
|
|
200
|
+
"set_ranking_target_cap",
|
|
201
|
+
argsFromFlags(ARGS.set_ranking_target_cap, flags, positionals)
|
|
202
|
+
);
|
|
203
|
+
ok(ctx, "Cap saved.");
|
|
204
|
+
printResult(io, flags, result, (r) => (r.scopes ?? []).map(formatScope).join("\n"));
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function targetsSuggest(ctx) {
|
|
209
|
+
const { client, flags, io } = ctx;
|
|
210
|
+
const result = await client.callTool("suggest_ranking_targets", argsFromFlags(ARGS.suggest_ranking_targets, flags));
|
|
211
|
+
printResult(io, flags, result, (r) => {
|
|
212
|
+
const all = r.suggestions ?? [];
|
|
213
|
+
if (!all.length) return "No suggestions yet — add positioning first.";
|
|
214
|
+
const scope = r.offering_id ? ` --offering ${r.offering_id}` : "";
|
|
215
|
+
const lines = all.map(
|
|
216
|
+
(s) =>
|
|
217
|
+
` ${s.phrase} [${s.basis}]${s.suggestedFraming ? ` — as ${s.suggestedFraming}` : ""}\n ${s.rationale}`
|
|
218
|
+
);
|
|
219
|
+
lines.push(`Nothing saved. Keep one with: ${ctx.bin} targets add <phrase>${scope}`);
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
});
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -34,5 +34,6 @@ export * from "./commands/shredder.mjs";
|
|
|
34
34
|
export * from "./commands/landings.mjs";
|
|
35
35
|
export * from "./commands/outbound.mjs";
|
|
36
36
|
export * from "./commands/offerings.mjs";
|
|
37
|
+
export * from "./commands/targets.mjs";
|
|
37
38
|
export * from "./commands/admin.mjs";
|
|
38
39
|
export * from "./commands/feedback.mjs";
|