@letterstory/cli 0.136.0 → 0.138.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 +13 -2
- package/lib/commands/campaigns.mjs +159 -0
- package/lib/commands/insights.mjs +8 -2
- package/lib/commands/repo.mjs +34 -2
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -57,12 +57,13 @@ import {
|
|
|
57
57
|
cmdEvals,
|
|
58
58
|
cmdOfferings,
|
|
59
59
|
cmdEnclosures,
|
|
60
|
+
cmdCampaigns,
|
|
60
61
|
cmdTargets,
|
|
61
62
|
cmdVisits,
|
|
62
63
|
} from "./commands.mjs";
|
|
63
64
|
|
|
64
65
|
// Keep in sync with cli/package.json.
|
|
65
|
-
export const VERSION = "0.
|
|
66
|
+
export const VERSION = "0.138.0";
|
|
66
67
|
|
|
67
68
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
68
69
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -568,6 +569,11 @@ Enclosures (pictures, clips and sentences made for each prospect; each output is
|
|
|
568
569
|
enclosures apps connect <sign-in-url> [--no-remember] A sign-in link a person opens (no password passes through)
|
|
569
570
|
enclosures apps disconnect <app-id> --yes Remove it here and at the browser provider
|
|
570
571
|
|
|
572
|
+
Campaigns (one prompt, and Letter's agent builds the whole cold-email campaign; nothing sends until you approve):
|
|
573
|
+
campaigns build "<prompt>" [--limit <n>] [--watch] The list, the picture (tested on 3), the email, the review desk
|
|
574
|
+
campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
|
|
575
|
+
campaigns list | cancel <build-id> | suggest Its builds; stop one (what it built is kept); three to build
|
|
576
|
+
|
|
571
577
|
Offerings (context layered org → offering → collection; a layer sets only what it overrides):
|
|
572
578
|
offerings list The org's offerings (things you'd name when recommending them)
|
|
573
579
|
offerings show <offering-id> One offering + its competitor overrides
|
|
@@ -1323,6 +1329,7 @@ Insights:
|
|
|
1323
1329
|
insights reads [--period …] [--site all|own|phantoms|<key>]... [--path <path>] [--engine <operator>]
|
|
1324
1330
|
Measure → Reads: AI engines and people reading your sites, counted one way
|
|
1325
1331
|
insights questions [--period 14d|30d|90d] Measure → Questions: every question we ask, named / share / change / cited
|
|
1332
|
+
insights pages [--period 14d|30d|90d] [--site own|phantoms|<key>] Measure → Pages: your pages AI answers cited, won questions first
|
|
1326
1333
|
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
1327
1334
|
insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
|
|
1328
1335
|
One By-question row: the wordings asked and the AI answers
|
|
@@ -1395,6 +1402,9 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1395
1402
|
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1396
1403
|
One PR adding Letterprove's script and identify calls
|
|
1397
1404
|
(pick the repo where users sign in)
|
|
1405
|
+
repo install-visitors [--site <id>] [--repo <owner/name>] [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1406
|
+
One PR adding the Visitors script tag to the repo that
|
|
1407
|
+
builds the site (--repo: another repo)
|
|
1398
1408
|
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
|
1399
1409
|
repo set-site --repo <owner/name> --site <host> [--not] [--branch <name>|none]
|
|
1400
1410
|
Say which site a repo builds (--not: correct a wrong label)
|
|
@@ -1408,7 +1418,7 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1408
1418
|
repo status <run-id> Phase, spend, PR link and Claude's summary
|
|
1409
1419
|
repo activity <run-id> [--follow] What the sandbox is doing, line by line (--follow: until it stops)
|
|
1410
1420
|
repo list [--status running|succeeded|no_changes|failed|cancelled] [--limit n] [--audit <id>]
|
|
1411
|
-
[--source site_audit_issue|site_quick_check|letterprove_install]
|
|
1421
|
+
[--source site_audit_issue|site_quick_check|letterprove_install|visitors_install]
|
|
1412
1422
|
repo publish <run-id> Make PR on a preview in review
|
|
1413
1423
|
repo revise <run-id> --instructions <text>|--instructions-file <path|->
|
|
1414
1424
|
Ask for more on a preview; it takes over the preview's link
|
|
@@ -1494,6 +1504,7 @@ const CLIENT_COMMANDS = {
|
|
|
1494
1504
|
directives: cmdDirectives,
|
|
1495
1505
|
offerings: cmdOfferings,
|
|
1496
1506
|
enclosures: cmdEnclosures,
|
|
1507
|
+
campaigns: cmdCampaigns,
|
|
1497
1508
|
targets: cmdTargets,
|
|
1498
1509
|
probes: cmdProbes,
|
|
1499
1510
|
suggestions: cmdSuggestions,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// `campaigns` — one prompt, and Letter's agent builds the whole cold-email campaign: the list, the
|
|
2
|
+
// enclosure (tested on 3 and chosen), the sequence email that carries each person's picture, and
|
|
3
|
+
// the launch into review. Thin wrappers over the campaign build tools, which call the same
|
|
4
|
+
// functions the app's /api/campaign-builds routes do.
|
|
5
|
+
|
|
6
|
+
import { CliError } from "../client.mjs";
|
|
7
|
+
import { argsFromFlags, flagNum, pollUntil, printResult, refuseUnknownFlags, ok } from "./shared.mjs";
|
|
8
|
+
|
|
9
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
10
|
+
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
|
11
|
+
export const ARGS = {
|
|
12
|
+
build_campaign: {
|
|
13
|
+
prompt: {
|
|
14
|
+
value: (_flags, positionals) => {
|
|
15
|
+
const words = positionals.join(" ").trim();
|
|
16
|
+
if (!words)
|
|
17
|
+
throw new CliError(
|
|
18
|
+
'Say what to build, e.g. campaigns build "Cold-email 10 SaaS founders with an audit of their own site"'
|
|
19
|
+
);
|
|
20
|
+
return words;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
people_limit: { flag: "limit", type: "num" },
|
|
24
|
+
},
|
|
25
|
+
get_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
|
|
26
|
+
list_campaign_builds: { limit: { flag: "limit", type: "num" } },
|
|
27
|
+
cancel_campaign_build: { build_id: { positional: 0, label: "build-id", required: true } },
|
|
28
|
+
suggest_campaigns: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const USAGE = `Usage:
|
|
32
|
+
campaigns build "<prompt>" [--limit <n>] [--watch] Build a whole campaign from one prompt (at most 50 people)
|
|
33
|
+
campaigns show <build-id> [--watch] Its stage, what it built, and what it is doing
|
|
34
|
+
campaigns list [--limit <n>] This org's builds, newest first
|
|
35
|
+
campaigns cancel <build-id> Stop it; what it built is kept
|
|
36
|
+
campaigns suggest Three campaigns to build in one step`;
|
|
37
|
+
|
|
38
|
+
const STAGES = { who: "Who", picture: "The picture", test: "Tested on 3", email: "The email", desk: "On the desk" };
|
|
39
|
+
|
|
40
|
+
export async function cmdCampaigns(ctx) {
|
|
41
|
+
const sub = ctx.positionals[0];
|
|
42
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
43
|
+
switch (sub) {
|
|
44
|
+
case "build":
|
|
45
|
+
return campaignsBuild(rest);
|
|
46
|
+
case "show":
|
|
47
|
+
case "get":
|
|
48
|
+
return campaignsShow(rest);
|
|
49
|
+
case "list":
|
|
50
|
+
case "ls":
|
|
51
|
+
return campaignsList(rest);
|
|
52
|
+
case "cancel":
|
|
53
|
+
return campaignsCancel(rest);
|
|
54
|
+
case "suggest":
|
|
55
|
+
return campaignsSuggest(rest);
|
|
56
|
+
default:
|
|
57
|
+
throw new CliError(USAGE);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatBuild(b) {
|
|
62
|
+
const a = b.artifacts ?? {};
|
|
63
|
+
const lines = [`${b.id} [${b.status}, ${STAGES[b.stage] ?? b.stage}] ${b.prompt}`];
|
|
64
|
+
if (a.list)
|
|
65
|
+
lines.push(
|
|
66
|
+
` who: ${a.list.people} people on "${a.list.name}"${a.list.pulling ? " (still finding them)" : ""}`
|
|
67
|
+
);
|
|
68
|
+
if (a.enclosure) {
|
|
69
|
+
const p = a.enclosure.prints ?? {};
|
|
70
|
+
lines.push(
|
|
71
|
+
` picture: ${a.enclosure.name}${a.enclosure.current_version ? `, version ${a.enclosure.current_version}` : ", testing"}: ${p.done ?? 0} ready, ${p.making ?? 0} making, ${p.no_fit ?? 0} no fit, ${p.failed ?? 0} failed`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (a.sequence) lines.push(` email: ${a.sequence.name} [${a.sequence.status}]`);
|
|
75
|
+
if (a.desk)
|
|
76
|
+
lines.push(
|
|
77
|
+
` desk: ${a.desk.waiting} copies waiting (${a.desk.with_picture} with their picture), ${a.desk.on_the_way} on the way`
|
|
78
|
+
);
|
|
79
|
+
lines.push(` spent: $${Number(b.cost_usd).toFixed(2)} of $${Number(b.budget_usd).toFixed(2)}`);
|
|
80
|
+
if (b.summary) lines.push(` ${b.summary}`);
|
|
81
|
+
if (b.error) lines.push(` ${b.error}`);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const formatLine = (l) => `${l.at.slice(11, 19)} ${l.kind === "say" ? "" : `${l.kind}: `}${l.text}`;
|
|
86
|
+
|
|
87
|
+
// Follow a build: print each new activity line as it lands, until it stops.
|
|
88
|
+
async function follow(ctx, buildId) {
|
|
89
|
+
const { client, io } = ctx;
|
|
90
|
+
let shown = 0;
|
|
91
|
+
return pollUntil(
|
|
92
|
+
io,
|
|
93
|
+
async () => {
|
|
94
|
+
const { build } = await client.callTool("get_campaign_build", { build_id: buildId });
|
|
95
|
+
const lines = build.activity ?? [];
|
|
96
|
+
for (const line of lines.slice(shown)) io.log(formatLine(line));
|
|
97
|
+
shown = Math.max(shown, lines.length);
|
|
98
|
+
return { done: build.status !== "running", value: build };
|
|
99
|
+
},
|
|
100
|
+
{ intervalMs: 5000, timeoutMs: 60 * 60_000, label: "the campaign build" }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function campaignsBuild(ctx) {
|
|
105
|
+
const { client, flags, positionals, io } = ctx;
|
|
106
|
+
refuseUnknownFlags(ctx, ARGS.build_campaign, ["watch"]);
|
|
107
|
+
if (flags.limit !== undefined) flagNum(flags.limit);
|
|
108
|
+
const result = await client.callTool("build_campaign", argsFromFlags(ARGS.build_campaign, flags, positionals));
|
|
109
|
+
ok(ctx, `Building it (${result.build.id}). It takes 10 to 20 minutes; nothing sends until you approve a copy.`);
|
|
110
|
+
const build = flags.watch === true && !flags.json ? await follow(ctx, result.build.id) : result.build;
|
|
111
|
+
printResult(io, flags, flags.watch === true ? { build } : result, (r) => formatBuild(r.build));
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function campaignsShow(ctx) {
|
|
116
|
+
const { client, flags, positionals, io } = ctx;
|
|
117
|
+
refuseUnknownFlags(ctx, ARGS.get_campaign_build, ["watch"]);
|
|
118
|
+
const args = argsFromFlags(ARGS.get_campaign_build, flags, positionals);
|
|
119
|
+
if (flags.watch === true && !flags.json) {
|
|
120
|
+
const build = await follow(ctx, args.build_id);
|
|
121
|
+
io.log(formatBuild(build));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const result = await client.callTool("get_campaign_build", args);
|
|
125
|
+
printResult(io, flags, result, (r) =>
|
|
126
|
+
[formatBuild(r.build), ...(r.build.activity ?? []).slice(-15).map(formatLine)].join("\n")
|
|
127
|
+
);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function campaignsList(ctx) {
|
|
132
|
+
const { client, flags, io } = ctx;
|
|
133
|
+
if (flags.limit !== undefined) flagNum(flags.limit);
|
|
134
|
+
const result = await client.callTool("list_campaign_builds", argsFromFlags(ARGS.list_campaign_builds, flags));
|
|
135
|
+
printResult(io, flags, result, (r) =>
|
|
136
|
+
r.builds?.length ? r.builds.map(formatBuild).join("\n\n") : "No campaign builds yet."
|
|
137
|
+
);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function campaignsCancel(ctx) {
|
|
142
|
+
const { client, flags, positionals, io } = ctx;
|
|
143
|
+
const result = await client.callTool(
|
|
144
|
+
"cancel_campaign_build",
|
|
145
|
+
argsFromFlags(ARGS.cancel_campaign_build, flags, positionals)
|
|
146
|
+
);
|
|
147
|
+
ok(ctx, "Stopped. What it built is kept.");
|
|
148
|
+
printResult(io, flags, result, (r) => formatBuild(r.build));
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function campaignsSuggest(ctx) {
|
|
153
|
+
const { client, flags, io } = ctx;
|
|
154
|
+
const result = await client.callTool("suggest_campaigns", {});
|
|
155
|
+
printResult(io, flags, result, (r) =>
|
|
156
|
+
(r.suggestions ?? []).map((s) => `${s.title}\n letterstory campaigns build "${s.prompt}"`).join("\n\n")
|
|
157
|
+
);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// network, single-post, and top-posts levels (ported from phantomstory-cli's
|
|
3
3
|
// insights.ts); plus Measure's Overview (`summary`), GEO → Competitors
|
|
4
4
|
// (`competitors`), AI Visibility Bank (`bank`) and phantom Page Access (`access`);
|
|
5
|
-
// Measure → Reads (`reads`)
|
|
5
|
+
// Measure → Reads (`reads`), Questions (`questions`) and Pages (`pages`), and the live feed of what
|
|
6
6
|
// is reading the phantom sites (`activity`) — the same reads the web app renders.
|
|
7
7
|
|
|
8
8
|
import { CliError } from "../client.mjs";
|
|
@@ -38,6 +38,10 @@ export const ARGS = {
|
|
|
38
38
|
list_measure_questions: {
|
|
39
39
|
period: { flag: "period", enum: PERIODS },
|
|
40
40
|
},
|
|
41
|
+
list_cited_pages: {
|
|
42
|
+
period: { flag: "period", enum: PERIODS },
|
|
43
|
+
sites: { flag: "site", type: "list" },
|
|
44
|
+
},
|
|
41
45
|
get_recent_activity: {
|
|
42
46
|
phantom_id: { flag: "phantom" },
|
|
43
47
|
limit: { flag: "limit", type: "num" },
|
|
@@ -76,11 +80,13 @@ export async function cmdInsights(ctx) {
|
|
|
76
80
|
return call(rest, "get_reads");
|
|
77
81
|
case "questions":
|
|
78
82
|
return call(rest, "list_measure_questions");
|
|
83
|
+
case "pages":
|
|
84
|
+
return call(rest, "list_cited_pages");
|
|
79
85
|
case "activity":
|
|
80
86
|
return call(rest, "get_recent_activity");
|
|
81
87
|
default:
|
|
82
88
|
throw new CliError(
|
|
83
|
-
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, reads, questions, competitors, question, detection, activity, site, post, top, bank, access`
|
|
89
|
+
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, reads, questions, pages, competitors, question, detection, activity, site, post, top, bank, access`
|
|
84
90
|
);
|
|
85
91
|
}
|
|
86
92
|
}
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// repo repos → list_github_app_repos repo branches → list_repo_branches
|
|
9
9
|
// repo prs → list_open_pull_requests (every open PR, whoever opened it)
|
|
10
10
|
// repo install-letterprove → letterprove_open_install_pr
|
|
11
|
+
// repo install-visitors → visitors_open_install_pr
|
|
11
12
|
// repo secrets → list_repo_preview_secrets repo set-secret → set_repo_preview_secret
|
|
12
13
|
// repo delete-secret → delete_repo_preview_secret
|
|
13
14
|
// repo suggestions → list_suggested_fixes
|
|
@@ -117,6 +118,13 @@ const ARGS = {
|
|
|
117
118
|
base_branch: "branch",
|
|
118
119
|
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
119
120
|
},
|
|
121
|
+
visitors_open_install_pr: {
|
|
122
|
+
site_id: "site",
|
|
123
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
124
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
125
|
+
base_branch: "branch",
|
|
126
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
127
|
+
},
|
|
120
128
|
get_repo_change_settings: {},
|
|
121
129
|
list_suggested_fixes: {},
|
|
122
130
|
list_slack_fix_targets: {},
|
|
@@ -187,7 +195,13 @@ const ARGS = {
|
|
|
187
195
|
audit_id: "audit",
|
|
188
196
|
source_kind: {
|
|
189
197
|
flag: "source",
|
|
190
|
-
enum: [
|
|
198
|
+
enum: [
|
|
199
|
+
"site_audit_issue",
|
|
200
|
+
"site_quick_check",
|
|
201
|
+
"letterprove_install",
|
|
202
|
+
"visitors_install",
|
|
203
|
+
"collection_publish",
|
|
204
|
+
],
|
|
191
205
|
},
|
|
192
206
|
site_host: "site",
|
|
193
207
|
},
|
|
@@ -247,6 +261,8 @@ export async function cmdRepo(ctx) {
|
|
|
247
261
|
return repoFixAudit(rest);
|
|
248
262
|
case "install-letterprove":
|
|
249
263
|
return repoInstallLetterprove(rest);
|
|
264
|
+
case "install-visitors":
|
|
265
|
+
return repoInstallVisitors(rest);
|
|
250
266
|
case "sites":
|
|
251
267
|
return repoSites(rest);
|
|
252
268
|
case "set-site":
|
|
@@ -283,7 +299,7 @@ export async function cmdRepo(ctx) {
|
|
|
283
299
|
return repoSlackTest(rest);
|
|
284
300
|
default:
|
|
285
301
|
throw new CliError(
|
|
286
|
-
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test`
|
|
302
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, install-visitors, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test`
|
|
287
303
|
);
|
|
288
304
|
}
|
|
289
305
|
}
|
|
@@ -434,6 +450,22 @@ async function repoInstallLetterprove(ctx) {
|
|
|
434
450
|
return followRun(ctx, result.run);
|
|
435
451
|
}
|
|
436
452
|
|
|
453
|
+
/** `repo install-visitors [--site id] [--repo owner/name] [--branch b] [--wait]`: the Visitors script tag → one PR. */
|
|
454
|
+
async function repoInstallVisitors(ctx) {
|
|
455
|
+
const { client, flags, io } = ctx;
|
|
456
|
+
refuseUnknownFlags(ctx, {}, ["site", "repo", "owner", "name", "branch", "action", "wait"]);
|
|
457
|
+
const result = await client.callTool(
|
|
458
|
+
"visitors_open_install_pr",
|
|
459
|
+
argsFromFlags(ARGS.visitors_open_install_pr, flags, ctx.positionals)
|
|
460
|
+
);
|
|
461
|
+
ok(ctx, result.message);
|
|
462
|
+
if (!flags.wait) {
|
|
463
|
+
printResult(io, flags, result.run, describe);
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
return followRun(ctx, result.run);
|
|
467
|
+
}
|
|
468
|
+
|
|
437
469
|
/** `repo sites [--detect] [--force]`: which website each repo builds, and why. */
|
|
438
470
|
async function repoSites(ctx) {
|
|
439
471
|
const { client, flags, io } = ctx;
|
package/lib/commands.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export * from "./commands/landings.mjs";
|
|
|
39
39
|
export * from "./commands/outbound.mjs";
|
|
40
40
|
export * from "./commands/offerings.mjs";
|
|
41
41
|
export * from "./commands/enclosures.mjs";
|
|
42
|
+
export * from "./commands/campaigns.mjs";
|
|
42
43
|
export * from "./commands/targets.mjs";
|
|
43
44
|
export * from "./commands/admin.mjs";
|
|
44
45
|
export * from "./commands/feedback.mjs";
|