@letterstory/cli 0.90.0 → 0.92.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 -3
- package/lib/commands/onboard.mjs +60 -0
- package/lib/commands/outbound.mjs +12 -13
- package/lib/commands/shared.mjs +31 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -57,7 +57,7 @@ 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.92.0";
|
|
61
61
|
|
|
62
62
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
63
63
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -121,6 +121,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
121
121
|
// `phantom-onboard collection --enable-planner`, `strategy sitemap --posts-only`, and the
|
|
122
122
|
// `strategy positioning set --clear-*` switches: value-less, and each sits next to a value.
|
|
123
123
|
"enable-planner",
|
|
124
|
+
// `phantom-onboard primitives --retry [<domain>]` re-reads a domain whose read failed.
|
|
125
|
+
"retry",
|
|
124
126
|
"posts-only",
|
|
125
127
|
"clear-topics",
|
|
126
128
|
"clear-stances",
|
|
@@ -135,9 +137,10 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
135
137
|
"no-enrich",
|
|
136
138
|
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
137
139
|
"none",
|
|
138
|
-
// `prospects import|source --one-per-company`, `prospects source --enrich`.
|
|
140
|
+
// `prospects import|source --one-per-company`, `prospects source --enrich`, `prospects import --find-emails`.
|
|
139
141
|
"one-per-company",
|
|
140
142
|
"enrich",
|
|
143
|
+
"find-emails",
|
|
141
144
|
// `targets update <id> --clear-framing`; `targets subqueries --retired`,
|
|
142
145
|
// `targets subquery update <id> --retire` / `--restore`.
|
|
143
146
|
"clear-framing",
|
|
@@ -580,6 +583,9 @@ Propose lane (the Fleet wizard's Plan and Name screens; persists nothing, spends
|
|
|
580
583
|
(--plan '<json>' inline, or - for stdin).
|
|
581
584
|
|
|
582
585
|
What does this org still need? (the router — reads only, decides nothing):
|
|
586
|
+
${bin === "phantom" ? "onboard" : "phantom-onboard"} primitives [<domain>] [--retry] The primitives that fit a domain (default:
|
|
587
|
+
yours), each with the exact call that sets
|
|
588
|
+
it up — the plan Instances shows
|
|
583
589
|
${bin === "phantom" ? "onboard" : "phantom-onboard"} plan Every onboarding step (context, main blog,
|
|
584
590
|
Planner, CMS, phantom) with its status:
|
|
585
591
|
done / running / ready / blocked, and what
|
|
@@ -992,9 +998,10 @@ Sequences (multi-step cold email sent from your mailers; approving or enrolling
|
|
|
992
998
|
[--[no-]opt-out-footer] [--[no-]postal-address-footer] [--[no-]unsubscribe-links] [--[no-]detect-reply-opt-outs]
|
|
993
999
|
[--[no-]test-mode] [--default-lede <fallback opener line>]
|
|
994
1000
|
[--send-window-start <HH:MM>] [--send-window-end <HH:MM>] [--send-window-timezone <IANA tz>]
|
|
995
|
-
[--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none]
|
|
1001
|
+
[--enroll-on-cake paid|scheduled|out_for_delivery|delivered|none] [--enroll-on-cake-list <list-id>|none]
|
|
996
1002
|
--test-mode (internal accounts only): walks as live, sends nothing
|
|
997
1003
|
--enroll-on-cake: start it when a recipient's Daymaker cake reaches that stage
|
|
1004
|
+
--enroll-on-cake-list: only for prospects in that list (none = any)
|
|
998
1005
|
sequences pause|resume <sequence-id> Stop / restart sending
|
|
999
1006
|
sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
|
|
1000
1007
|
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
|
@@ -1023,6 +1030,9 @@ Prospects (who you write to):
|
|
|
1023
1030
|
prospects get <prospect-id>
|
|
1024
1031
|
prospects create-list --name <n>
|
|
1025
1032
|
prospects import --csv-file <path|-> [--list <list-id>|--list-name <n>] [--one-per-company]
|
|
1033
|
+
[--find-emails [--max-email-lookups <1..100>]]
|
|
1034
|
+
--find-emails: look up the business email of rows with a
|
|
1035
|
+
LinkedIn URL and no email (paid; default 25 lookups)
|
|
1026
1036
|
prospects source --description <who you want> [--list <list-id>|--list-name <n>] [--limit <1..100>]
|
|
1027
1037
|
[--one-per-company] [--enrich] Fill a list from paid data providers (background);
|
|
1028
1038
|
--enrich starts enrichment as soon as the pull lands
|
package/lib/commands/onboard.mjs
CHANGED
|
@@ -174,6 +174,12 @@ export const ARGS = {
|
|
|
174
174
|
get_phantom_launch_status: {
|
|
175
175
|
job_id: { internal: "the job id plan_phantom just returned" },
|
|
176
176
|
},
|
|
177
|
+
// The primitive plan: a domain (default: the org's own) read into the calls that would
|
|
178
|
+
// stand each primitive up. Printed, never run — approving an item is running its call.
|
|
179
|
+
propose_primitive_plan: {
|
|
180
|
+
domain: { positional: 0, label: "domain" },
|
|
181
|
+
retry: { flag: "retry", type: "bool" },
|
|
182
|
+
},
|
|
177
183
|
propose_phantom_plan: {
|
|
178
184
|
client_domain: { positional: 0, label: "client-domain" },
|
|
179
185
|
direction: ["about", "direction"],
|
|
@@ -752,8 +758,62 @@ async function cmdOnboardNext(ctx) {
|
|
|
752
758
|
return 0;
|
|
753
759
|
}
|
|
754
760
|
|
|
761
|
+
// `onboard primitives [<domain>]` — what Letterstory would stand up for this company: one
|
|
762
|
+
// read of propose_primitive_plan, printed as a table whose last column is the exact
|
|
763
|
+
// `call` that sets each item up. Nothing runs; approving an item is running its line.
|
|
764
|
+
// A first read crawls the site; `pending` means one is already in flight, so wait and ask.
|
|
765
|
+
function shellQuote(json) {
|
|
766
|
+
return `'${json.replace(/'/g, "'\\''")}'`;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function formatPrimitivePlan(ctx) {
|
|
770
|
+
return (res) => {
|
|
771
|
+
if (res.status !== "complete") {
|
|
772
|
+
return res.status === "pending"
|
|
773
|
+
? `Still reading ${res.domain} — run this again in a few seconds.`
|
|
774
|
+
: `Couldn't read ${res.domain}: ${res.error ?? "unknown error"} (retry with --retry)`;
|
|
775
|
+
}
|
|
776
|
+
const { plan } = res;
|
|
777
|
+
const lines = [`${plan.company_name} (${plan.domain})`, ` ${plan.priority_topics.join(" · ")}`, ""];
|
|
778
|
+
for (const item of plan.items) {
|
|
779
|
+
// A part of its parent's setup prints indented under it.
|
|
780
|
+
const pad = item.parent ? " " : "";
|
|
781
|
+
const mark = item.status === "exists" ? "running " : "proposed";
|
|
782
|
+
lines.push(`${pad}${mark} ${item.title}`);
|
|
783
|
+
lines.push(
|
|
784
|
+
item.status === "exists"
|
|
785
|
+
? `${pad} already: ${item.existing.map((e) => e.name).join(", ")}`
|
|
786
|
+
: `${pad} ${item.detail}`
|
|
787
|
+
);
|
|
788
|
+
if (item.call) {
|
|
789
|
+
lines.push(
|
|
790
|
+
`${pad} ${ctx.bin} call ${item.call.tool} --args ${shellQuote(JSON.stringify(item.call.args))}`
|
|
791
|
+
);
|
|
792
|
+
for (const [arg, ref] of Object.entries(item.refs ?? {})) {
|
|
793
|
+
lines.push(`${pad} with ${arg} = ${ref.field} from ${ref.item}`);
|
|
794
|
+
}
|
|
795
|
+
if (item.after?.length) lines.push(`${pad} after: ${item.after.join(", ")}`);
|
|
796
|
+
if (item.poll) lines.push(`${pad} then poll: ${item.poll.tool}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return lines.join("\n");
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function cmdOnboardPrimitives(ctx) {
|
|
804
|
+
const args = argsFromFlags(ARGS.propose_primitive_plan, ctx.flags, ctx.positionals);
|
|
805
|
+
ok(ctx, "Reading the domain…");
|
|
806
|
+
const res = await ctx.client.callTool("propose_primitive_plan", args);
|
|
807
|
+
printResult(ctx.io, ctx.flags, res, formatPrimitivePlan(ctx));
|
|
808
|
+
return 0;
|
|
809
|
+
}
|
|
810
|
+
|
|
755
811
|
export async function cmdOnboard(ctx) {
|
|
756
812
|
const { client, flags, io } = ctx;
|
|
813
|
+
// `onboard primitives [<domain>]` — the primitive plan and the call behind each item.
|
|
814
|
+
if (ctx.positionals[0] === "primitives") {
|
|
815
|
+
return cmdOnboardPrimitives({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
816
|
+
}
|
|
757
817
|
// `onboard plan` / `onboard next` — the router: what does this org still need?
|
|
758
818
|
if (ctx.positionals[0] === "plan") return cmdOnboardPlan(ctx);
|
|
759
819
|
if (ctx.positionals[0] === "next") return cmdOnboardNext(ctx);
|
|
@@ -93,6 +93,13 @@ const SEQUENCE_WINDOW_ARGS = {
|
|
|
93
93
|
// reaches it; `none` clears it.
|
|
94
94
|
const CAKE_TRIGGER_STAGES = ["paid", "scheduled", "out_for_delivery", "delivered"];
|
|
95
95
|
const CAKE_STAGES = ["draft", ...CAKE_TRIGGER_STAGES, "delivery_failed", "cancelled"];
|
|
96
|
+
const enrollOnCakeList = {
|
|
97
|
+
value: (flags) => {
|
|
98
|
+
const raw = flagStr(flags["enroll-on-cake-list"]);
|
|
99
|
+
if (raw === undefined) return undefined;
|
|
100
|
+
return raw === "none" ? null : raw;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
96
103
|
const enrollOnCake = {
|
|
97
104
|
value: (flags) => {
|
|
98
105
|
const raw = flagStr(flags["enroll-on-cake"]);
|
|
@@ -163,6 +170,7 @@ export const ARGS = {
|
|
|
163
170
|
...SEQUENCE_RAIL_ARGS,
|
|
164
171
|
...SEQUENCE_WINDOW_ARGS,
|
|
165
172
|
enroll_on_cake: enrollOnCake,
|
|
173
|
+
enroll_on_cake_list_id: enrollOnCakeList,
|
|
166
174
|
},
|
|
167
175
|
update_sequence: {
|
|
168
176
|
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
@@ -173,6 +181,7 @@ export const ARGS = {
|
|
|
173
181
|
...SEQUENCE_RAIL_ARGS,
|
|
174
182
|
...SEQUENCE_WINDOW_ARGS,
|
|
175
183
|
enroll_on_cake: enrollOnCake,
|
|
184
|
+
enroll_on_cake_list_id: enrollOnCakeList,
|
|
176
185
|
},
|
|
177
186
|
list_cakes: {
|
|
178
187
|
stage: { flag: "stage", enum: CAKE_STAGES },
|
|
@@ -265,6 +274,8 @@ export const ARGS = {
|
|
|
265
274
|
return csv;
|
|
266
275
|
},
|
|
267
276
|
},
|
|
277
|
+
find_emails: { flag: "find-emails", type: "bool" },
|
|
278
|
+
max_email_lookups: { flag: "max-email-lookups", type: "num" },
|
|
268
279
|
one_per_company: { flag: "one-per-company", type: "bool" },
|
|
269
280
|
list_id: "list",
|
|
270
281
|
list_name: "list-name",
|
|
@@ -378,20 +389,8 @@ export const ARGS = {
|
|
|
378
389
|
},
|
|
379
390
|
};
|
|
380
391
|
|
|
381
|
-
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
382
|
-
const VALUE_FLAGS = {
|
|
383
|
-
create_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
384
|
-
update_sequence: ["flow", "flow-file", "mailer", "send-window-start", "send-window-end"],
|
|
385
|
-
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
386
|
-
update_review_item: ["body", "file", "personalize", "personalize-mode"],
|
|
387
|
-
delete_prospects: ["id"],
|
|
388
|
-
import_prospects_csv: ["csv", "csv-file"],
|
|
389
|
-
update_prospect: ["custom"],
|
|
390
|
-
set_mailer_forwarding: ["to", "clear"],
|
|
391
|
-
};
|
|
392
|
-
|
|
393
392
|
async function run(ctx, tool, message) {
|
|
394
|
-
refuseUnknownFlags(ctx, ARGS[tool]
|
|
393
|
+
refuseUnknownFlags(ctx, ARGS[tool]);
|
|
395
394
|
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
396
395
|
if (message) ok(ctx, message);
|
|
397
396
|
printResult(ctx.io, ctx.flags, result);
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -83,6 +83,35 @@ export const GLOBAL_FLAGS = new Set([
|
|
|
83
83
|
"profile",
|
|
84
84
|
]);
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The flags a computed `{ value }` source reads, found by running it against
|
|
88
|
+
* no flags and recording each key it looks up. A value source names its flags
|
|
89
|
+
* inside code the map can't see, so `sequences update --enroll-on-cake` was
|
|
90
|
+
* refused by a command whose help and parser both took it.
|
|
91
|
+
*/
|
|
92
|
+
export function valueSourceFlags(value) {
|
|
93
|
+
const read = new Set();
|
|
94
|
+
const flags = new Proxy(
|
|
95
|
+
{},
|
|
96
|
+
{
|
|
97
|
+
get: (_target, key) => {
|
|
98
|
+
if (typeof key === "string") read.add(key);
|
|
99
|
+
return undefined;
|
|
100
|
+
},
|
|
101
|
+
has: (_target, key) => {
|
|
102
|
+
if (typeof key === "string") read.add(key);
|
|
103
|
+
return false;
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
value(flags, []);
|
|
109
|
+
} catch {
|
|
110
|
+
// A required source throws when its flag is absent; the read was recorded first.
|
|
111
|
+
}
|
|
112
|
+
return read;
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
/**
|
|
87
116
|
* Refuse a flag this command does not understand.
|
|
88
117
|
*
|
|
@@ -105,6 +134,8 @@ export function refuseUnknownFlags(ctx, argsMap, own = []) {
|
|
|
105
134
|
else if (source && typeof source === "object" && source.flag) {
|
|
106
135
|
allowed.add(source.flag);
|
|
107
136
|
if (source.negate) allowed.add(source.negate);
|
|
137
|
+
} else if (source && typeof source === "object" && typeof source.value === "function") {
|
|
138
|
+
valueSourceFlags(source.value).forEach((f) => allowed.add(f));
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
const unknown = Object.keys(ctx.flags).filter((f) => !allowed.has(f));
|