@letterstory/cli 0.176.92 → 0.176.94
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 +16 -8
- package/lib/commands/outbound.mjs +19 -0
- package/lib/commands/replicate.mjs +47 -6
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
87
87
|
|
|
88
88
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
89
|
-
export const VERSION = "0.176.
|
|
89
|
+
export const VERSION = "0.176.94";
|
|
90
90
|
|
|
91
91
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
92
92
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -354,6 +354,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
354
354
|
"consent",
|
|
355
355
|
"no-redesign",
|
|
356
356
|
"no-test-mode",
|
|
357
|
+
"first-touch-only",
|
|
358
|
+
"no-first-touch-only",
|
|
357
359
|
"no-unsubscribe-links",
|
|
358
360
|
"now",
|
|
359
361
|
"opt-out-footer",
|
|
@@ -1640,6 +1642,7 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
1640
1642
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
1641
1643
|
[--[no-]test-mode] [--default-lede <fallback opener line>]
|
|
1642
1644
|
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
1645
|
+
[--daily-send-cap <n>|none] [--[no-]first-touch-only]
|
|
1643
1646
|
New draft sequence (nothing sends yet; rails default on, opt-out line off;
|
|
1644
1647
|
sends 9:00-17:00 in each recipient's inferred timezone, else the sender's)
|
|
1645
1648
|
sequences update <sequence-id> [--name <n>] [--flow '<json>'|--flow-file <path>]
|
|
@@ -1648,6 +1651,7 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
1648
1651
|
[--[no-]test-mode] [--default-lede <fallback opener line>]
|
|
1649
1652
|
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
1650
1653
|
[--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none] [--enroll-on-cake-list <list-id>|none]
|
|
1654
|
+
[--daily-send-cap <n>|none] [--[no-]first-touch-only]
|
|
1651
1655
|
--test-mode (internal accounts only): walks as live, sends nothing
|
|
1652
1656
|
--enroll-on-cake: start it when a recipient's Daymaker cake reaches that stage
|
|
1653
1657
|
--enroll-on-cake-list: only for prospects in that list (none = any)
|
|
@@ -1686,8 +1690,10 @@ Prospects (who you write to):
|
|
|
1686
1690
|
--find-emails: look up the business email of rows with a
|
|
1687
1691
|
LinkedIn URL and no email (paid; default 25 lookups)
|
|
1688
1692
|
prospects source --description <who you want> [--list <list-id>|--list-name <n>] [--limit <1..100>]
|
|
1689
|
-
[--one-per-company] [--enrich]
|
|
1690
|
-
|
|
1693
|
+
[--one-per-company] [--enrich] [--exclude-platform webflow]...
|
|
1694
|
+
Fill a list from paid data providers (background);
|
|
1695
|
+
--enrich starts enrichment as soon as the pull lands;
|
|
1696
|
+
--exclude-platform leaves out sites built on it
|
|
1691
1697
|
prospects enrich [--list <list-id>] [--status <s>] [--q <search>]
|
|
1692
1698
|
Funding + competitors for un-enriched rows
|
|
1693
1699
|
prospects openers <list-id> [--group <ledes-group-id>|--new-group <name>]
|
|
@@ -2025,15 +2031,17 @@ Bench (Benches: open-source benchmarks on their own domains, kept running; @lett
|
|
|
2025
2031
|
bench cite <id> The reference line a post cites
|
|
2026
2032
|
bench replicate <id> REPLICATE.md: how an agent reproduces the results
|
|
2027
2033
|
|
|
2028
|
-
Replicate (the Replicator: a website rebuilt as a native Next.js repo, page for page;
|
|
2034
|
+
Replicate (the Replicator: a website rebuilt as a native Next.js repo, page for page; free, within fair-use limits):
|
|
2029
2035
|
replicate <https://site> [--max-pages <n>] [--refine-budget <usd>] [--repo-name <r>] [--repo-owner <o>]
|
|
2030
2036
|
Capture every page, rebuild it, compare it pixel by pixel
|
|
2031
|
-
replicate [list] Your replications: stage, how closely each matches, preview link
|
|
2037
|
+
replicate [list] Your replications: stage, how closely each matches, preview link;
|
|
2038
|
+
the limits (in progress, pages this month, preview days)
|
|
2032
2039
|
replicate show <id> [--failing] Pages with their routes and pixel match; collections; events
|
|
2033
2040
|
replicate attach <id> <repo-name> [--repo-owner <o>] The repo to commit it into (yours; Letter never creates it)
|
|
2034
|
-
replicate advance <id> [--retry] [--again] [--preview]
|
|
2035
|
-
Move it on now; --retry once blocked_on is done
|
|
2036
|
-
--preview deploys its staging link again
|
|
2041
|
+
replicate advance <id> [--retry] [--again] [--preview] [--extend-preview <days>]
|
|
2042
|
+
Move it on now; --retry once blocked_on is done (checks the tag or TXT record);
|
|
2043
|
+
--again replicates from the start; --preview deploys its staging link again;
|
|
2044
|
+
--extend-preview keeps the preview up longer (Letter's team only)
|
|
2037
2045
|
replicate archive <id> Stop it and take its preview down; the project stays in storage and in the repo
|
|
2038
2046
|
|
|
2039
2047
|
Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
|
|
@@ -89,6 +89,22 @@ const SEQUENCE_WINDOW_ARGS = {
|
|
|
89
89
|
send_window_end_minute: { value: windowMinute("send-window-end") },
|
|
90
90
|
send_window_timezone: "send-window-timezone",
|
|
91
91
|
};
|
|
92
|
+
// --daily-send-cap <n|none>: the most emails the sequence sends per UTC day across its mailers;
|
|
93
|
+
// --first-touch-only: only people no other sequence has written to.
|
|
94
|
+
const SEQUENCE_LIMIT_ARGS = {
|
|
95
|
+
daily_send_cap: {
|
|
96
|
+
value: (flags) => {
|
|
97
|
+
const raw = flagStr(flags["daily-send-cap"]);
|
|
98
|
+
if (raw === undefined) return undefined;
|
|
99
|
+
if (raw === "none") return null;
|
|
100
|
+
const n = Number(raw);
|
|
101
|
+
if (!Number.isInteger(n) || n < 1 || n > 500)
|
|
102
|
+
throw new CliError(`--daily-send-cap takes 1–500 or none (got ${raw})`);
|
|
103
|
+
return n;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
first_touch_only: { flag: "first-touch-only", type: "bool", negate: "no-first-touch-only" },
|
|
107
|
+
};
|
|
92
108
|
// --enroll-on-cake <stage>: start the sequence when a recipient's Daymaker cake
|
|
93
109
|
// reaches it; `none` clears it.
|
|
94
110
|
const CAKE_TRIGGER_STAGES = ["paid", "scheduled", "out_for_delivery", "delivered"];
|
|
@@ -169,6 +185,7 @@ export const ARGS = {
|
|
|
169
185
|
mailer_ids: mailerIds,
|
|
170
186
|
...SEQUENCE_RAIL_ARGS,
|
|
171
187
|
...SEQUENCE_WINDOW_ARGS,
|
|
188
|
+
...SEQUENCE_LIMIT_ARGS,
|
|
172
189
|
enroll_on_cake: enrollOnCake,
|
|
173
190
|
enroll_on_cake_list_id: enrollOnCakeList,
|
|
174
191
|
},
|
|
@@ -180,6 +197,7 @@ export const ARGS = {
|
|
|
180
197
|
mailer_ids: mailerIds,
|
|
181
198
|
...SEQUENCE_RAIL_ARGS,
|
|
182
199
|
...SEQUENCE_WINDOW_ARGS,
|
|
200
|
+
...SEQUENCE_LIMIT_ARGS,
|
|
183
201
|
enroll_on_cake: enrollOnCake,
|
|
184
202
|
enroll_on_cake_list_id: enrollOnCakeList,
|
|
185
203
|
},
|
|
@@ -291,6 +309,7 @@ export const ARGS = {
|
|
|
291
309
|
list_name: "list-name",
|
|
292
310
|
limit: { flag: "limit", type: "num" },
|
|
293
311
|
one_per_company: { flag: "one-per-company", type: "bool" },
|
|
312
|
+
exclude_platforms: { flag: "exclude-platform", type: "list" },
|
|
294
313
|
enrich: { flag: "enrich", type: "bool" },
|
|
295
314
|
},
|
|
296
315
|
enrich_prospects: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `replicate` — the Replicator from a terminal (docs/architecture/replicator.md): a website rebuilt
|
|
2
2
|
// as a native Next.js repo, page for page, and committed to a repo on your organization's GitHub.
|
|
3
|
-
//
|
|
3
|
+
// Who may use it is one switch in the app (lib/replicator/access.ts): until it opens, the tools answer
|
|
4
|
+
// only a key an @letterstory.com person made. Free, within fair-use limits. Thin wrappers over the
|
|
4
5
|
// replicator tools (create_replication, list_replications, get_replication,
|
|
5
6
|
// attach_replication_repo, advance_replication, archive_replication), the same lib functions the
|
|
6
7
|
// app calls.
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
// letterstory replicate list
|
|
10
11
|
// letterstory replicate show <id> [--failing]
|
|
11
12
|
// letterstory replicate attach <id> <repo-name> [--repo-owner <account>]
|
|
12
|
-
// letterstory replicate advance <id> [--retry] [--again] [--preview]
|
|
13
|
+
// letterstory replicate advance <id> [--retry] [--again] [--preview] [--extend-preview <days>]
|
|
13
14
|
// letterstory replicate archive <id>
|
|
14
15
|
|
|
15
16
|
import { CliError } from "../client.mjs";
|
|
@@ -40,6 +41,7 @@ export const ARGS = {
|
|
|
40
41
|
retry: { flag: "retry", type: "bool" },
|
|
41
42
|
again: { flag: "again", type: "bool" },
|
|
42
43
|
preview: { flag: "preview", type: "bool" },
|
|
44
|
+
extend_preview_days: { flag: "extend-preview", type: "num" },
|
|
43
45
|
},
|
|
44
46
|
archive_replication: { replication_id: replicationId },
|
|
45
47
|
};
|
|
@@ -81,7 +83,9 @@ export async function cmdReplicate(ctx) {
|
|
|
81
83
|
async function replicationList(ctx) {
|
|
82
84
|
const result = await call(ctx, "list_replications");
|
|
83
85
|
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
84
|
-
|
|
86
|
+
const limits = limitsLines(r.limits);
|
|
87
|
+
if (!r.replications?.length)
|
|
88
|
+
return ["No replications yet. Start one: replicate https://example.com", ...limits].join("\n");
|
|
85
89
|
return r.replications
|
|
86
90
|
.map(
|
|
87
91
|
(x) =>
|
|
@@ -91,17 +95,40 @@ async function replicationList(ctx) {
|
|
|
91
95
|
: ""
|
|
92
96
|
}${x.staging_url ? ` ${x.staging_url}` : ""}`
|
|
93
97
|
)
|
|
98
|
+
.concat(limits.length ? ["", ...limits] : [])
|
|
94
99
|
.join("\n");
|
|
95
100
|
});
|
|
96
101
|
return 0;
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// The fair-use limits (replicating is free): in progress, pages this month, how long a preview stays up.
|
|
105
|
+
function limitsLines(l) {
|
|
106
|
+
if (!l || l.exempt) return [];
|
|
107
|
+
const p = l.pages_this_month;
|
|
108
|
+
return [
|
|
109
|
+
`Limits ${l.concurrent.used} of ${l.concurrent.max} in progress; ${p.used} of ${p.max} pages this month (resets ${p.resets_at.slice(0, 10)}); previews stay up ${l.preview_days} days`,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// What it used, in quantities (free: never a charge).
|
|
114
|
+
function usageLine(u) {
|
|
115
|
+
if (!u) return [];
|
|
116
|
+
const parts = [
|
|
117
|
+
u.sandbox_minutes ? `${u.sandbox_minutes} sandbox min` : null,
|
|
118
|
+
u.pages_captured ? `${u.pages_captured} pages` : null,
|
|
119
|
+
u.preview_hours ? `${u.preview_hours} preview hours` : null,
|
|
120
|
+
u.media_bytes ? `${(u.media_bytes / 1e6).toFixed(1)} MB media` : null,
|
|
121
|
+
].filter(Boolean);
|
|
122
|
+
return parts.length ? [` Used ${parts.join(", ")} (free)`] : [];
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
function headline(x) {
|
|
100
126
|
return [
|
|
101
127
|
`${x.host} — ${x.status}, stage ${x.stage}`,
|
|
102
128
|
...(x.blocked_on ? [` Blocked: ${x.blocked_on}`, ` Then: replicate advance ${x.id} --retry`] : []),
|
|
103
129
|
...(x.stage_note && !x.blocked_on ? [` ${x.stage_note}`] : []),
|
|
104
130
|
...previewLines(x),
|
|
131
|
+
...usageLine(x.usage),
|
|
105
132
|
...(x.summary
|
|
106
133
|
? [
|
|
107
134
|
` ${x.summary.replicated} of ${x.summary.pages} pages replicated; ${x.summary.passing} match the original at every width (mean ${pct(x.summary.meanPixel)})`,
|
|
@@ -155,9 +182,11 @@ function motionLines(m) {
|
|
|
155
182
|
|
|
156
183
|
// The staging link: the replica live on a Letter domain, to share before (or without) a repo.
|
|
157
184
|
function previewLines(x) {
|
|
185
|
+
if (x.staging_state === "queued" || x.staging_state === "expired")
|
|
186
|
+
return [` Preview ${x.staging_note ?? x.staging_state}`];
|
|
158
187
|
if (x.staging_url)
|
|
159
188
|
return [
|
|
160
|
-
` Preview ${x.staging_url}${x.staging_state === "deploying" || x.staging_state === "pending" ? " (redeploying)" : ""}`,
|
|
189
|
+
` Preview ${x.staging_url}${x.staging_state === "deploying" || x.staging_state === "pending" ? " (redeploying)" : ""}${x.staging_expires_at ? ` (online until ${x.staging_expires_at.slice(0, 10)})` : ""}`,
|
|
161
190
|
...(x.staging_state === "failed" && x.staging_error
|
|
162
191
|
? [` Preview redeploy failed: ${x.staging_error}`]
|
|
163
192
|
: []),
|
|
@@ -173,7 +202,11 @@ function previewLines(x) {
|
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
function replicationShow(ctx, result) {
|
|
176
|
-
printResult(ctx.io, ctx.flags, result, (r) =>
|
|
205
|
+
printResult(ctx.io, ctx.flags, result, (r) =>
|
|
206
|
+
[...headline(r.replication), ...(r.notice ? [` Note ${r.notice}`] : []), ...limitsLines(r.limits)].join(
|
|
207
|
+
"\n"
|
|
208
|
+
)
|
|
209
|
+
);
|
|
177
210
|
return 0;
|
|
178
211
|
}
|
|
179
212
|
|
|
@@ -181,12 +214,20 @@ function replicationDetail(ctx, result) {
|
|
|
181
214
|
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
182
215
|
const lines = headline(r.replication);
|
|
183
216
|
// Waiting for its domain to be verified: the tag for the homepage (then advance --retry checks it).
|
|
184
|
-
if (r.domain_verification)
|
|
217
|
+
if (r.domain_verification) {
|
|
185
218
|
lines.push(
|
|
186
219
|
"",
|
|
187
220
|
`Add this tag to the <head> of ${r.domain_verification.host}'s homepage:`,
|
|
188
221
|
r.domain_verification.tag
|
|
189
222
|
);
|
|
223
|
+
if (r.domain_verification.dns)
|
|
224
|
+
lines.push(
|
|
225
|
+
"",
|
|
226
|
+
"Or add this TXT record to its DNS:",
|
|
227
|
+
` Name ${r.domain_verification.dns.name}`,
|
|
228
|
+
` Value ${r.domain_verification.dns.value}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
190
231
|
if (r.pages?.length) {
|
|
191
232
|
lines.push("", "Pages:");
|
|
192
233
|
for (const p of r.pages) {
|