@letterstory/cli 0.37.0 → 0.38.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 +83 -1
- package/lib/commands/lede.mjs +147 -2
- package/lib/commands/outbound.mjs +324 -0
- package/lib/commands.mjs +1 -0
- package/lib/documents.mjs +2 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -41,13 +41,16 @@ import {
|
|
|
41
41
|
cmdShredder,
|
|
42
42
|
cmdLede,
|
|
43
43
|
cmdLandings,
|
|
44
|
+
cmdSequences,
|
|
45
|
+
cmdProspects,
|
|
46
|
+
cmdMail,
|
|
44
47
|
cmdAdmin,
|
|
45
48
|
cmdFeedback,
|
|
46
49
|
cmdOfferings,
|
|
47
50
|
} from "./commands.mjs";
|
|
48
51
|
|
|
49
52
|
// Keep in sync with cli/package.json.
|
|
50
|
-
export const VERSION = "0.
|
|
53
|
+
export const VERSION = "0.38.0";
|
|
51
54
|
|
|
52
55
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
53
56
|
// can't accidentally swallow the id as --json's value.
|
|
@@ -101,8 +104,18 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
101
104
|
"clear-topics",
|
|
102
105
|
"clear-stances",
|
|
103
106
|
"clear-avoid",
|
|
107
|
+
// `lede run --diverse`, `--high-temp`, their `--no-*` negations and `--no-enrich`
|
|
108
|
+
// are value-less and can sit next to the endpoint id or another value; without
|
|
109
|
+
// these the parser eats the following token.
|
|
110
|
+
"diverse",
|
|
111
|
+
"no-diverse",
|
|
112
|
+
"high-temp",
|
|
113
|
+
"no-high-temp",
|
|
114
|
+
"no-enrich",
|
|
104
115
|
// `offerings set-collection <id> --none` (the org itself is the subject again).
|
|
105
116
|
"none",
|
|
117
|
+
// `prospects import|source --one-per-company`.
|
|
118
|
+
"one-per-company",
|
|
106
119
|
]);
|
|
107
120
|
|
|
108
121
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -642,6 +655,8 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
642
655
|
[--years <n>] [--linkedin <url>] [--notes <text>]
|
|
643
656
|
[--candidates <1..10>] [--tone warm|direct|playful|formal] [--no-enrich]
|
|
644
657
|
[--deadline-ms <2000..120000>]
|
|
658
|
+
[--default-lede <line>] [--disable-lede <id>]... [--diverse [--recent-lede <id>]...]
|
|
659
|
+
[--high-temp|--no-high-temp]
|
|
645
660
|
Synchronous: prints the candidate lines with the
|
|
646
661
|
top choice flagged. Needs at least one of
|
|
647
662
|
--first-name, --name, --email, --company or
|
|
@@ -650,10 +665,39 @@ Ledes (opening lines for cold email, chosen per prospect from a hand-approved li
|
|
|
650
665
|
lookup when latency matters more than fit.
|
|
651
666
|
--deadline-ms leaves out evidence that cannot
|
|
652
667
|
arrive in time (fewer lines may qualify).
|
|
668
|
+
--disable-lede keeps a library line out (by id,
|
|
669
|
+
repeatable). --diverse gently rotates away from
|
|
670
|
+
recently used lines; pass the ids your sequence
|
|
671
|
+
already sent with --recent-lede (repeatable), or
|
|
672
|
+
omit them to count the endpoint's own recent runs.
|
|
673
|
+
--high-temp writes one strictly checked opener
|
|
674
|
+
(marked generated) when no library line qualifies;
|
|
675
|
+
the --no-* spellings override endpoint defaults.
|
|
653
676
|
lede runs [--endpoint <id>] [--limit <n>] Recent lede calls (telemetry), newest first
|
|
654
677
|
lede history [<domain>] [--days <n>] [--limit <n>] Openers already sent to an account
|
|
655
678
|
lede evals [--sweep] Lede quality evals still above zero
|
|
656
679
|
lede balance Ledes left on this org
|
|
680
|
+
lede library list The org's opener library: pre-built + custom lines
|
|
681
|
+
lede library create --text <line> [--alts <v>]... [--peer-alts <v>]... [--requires <trigger>]...
|
|
682
|
+
[--threshold <0..100>] [--tone warm|direct|playful|formal] [--tag <t>]...
|
|
683
|
+
Add a custom line. A {{placeholder}} must appear in
|
|
684
|
+
--requires; proprietary-engine triggers stay
|
|
685
|
+
reserved for the pre-built lines. --threshold
|
|
686
|
+
nudges the trigger's gate strictness around the
|
|
687
|
+
engine default: 50 = default, higher = pickier,
|
|
688
|
+
lower = looser.
|
|
689
|
+
lede library update <lede-id> [--text <line>] [--alts <v>]... [--peer-alts <v>]...
|
|
690
|
+
[--requires <trigger>]... [--threshold <0..100>] [--tone <t>] [--tag <t>]...
|
|
691
|
+
Edit a line; only the flags you pass change
|
|
692
|
+
lede library enable <lede-id> Make a line a run candidate again
|
|
693
|
+
lede library disable <lede-id> Keep a line out of runs (reversible; the way to
|
|
694
|
+
retire a pre-built line)
|
|
695
|
+
lede library delete <lede-id> --yes Delete a line (a deleted pre-built re-seeds at its
|
|
696
|
+
seed wording on the next list)
|
|
697
|
+
lede library rewrite --voice <description> [--ids <lede-id>]...
|
|
698
|
+
Propose brand-voice rewrites of the wording only.
|
|
699
|
+
Applies nothing: accept a proposal with
|
|
700
|
+
lede library update --text/--alts
|
|
657
701
|
|
|
658
702
|
Landings (a link + context; auto-mentioned in articles by collections you enable):
|
|
659
703
|
landings list Your landing pages + mention counts
|
|
@@ -663,6 +707,41 @@ Landings (a link + context; auto-mentioned in articles by collections you enable
|
|
|
663
707
|
landings update <landing-id> [--link <url>] [--context <text>|--file <path>]
|
|
664
708
|
landings delete <landing-id> --yes
|
|
665
709
|
|
|
710
|
+
Sequences (multi-step cold email sent from your mailers; approving or enrolling SENDS):
|
|
711
|
+
sequences list Your sequences + per-step funnel
|
|
712
|
+
sequences get <sequence-id> Flow, enrollments, who replied at each step
|
|
713
|
+
sequences create --name <n> (--flow '<json>'|--flow-file <path>) [--mailer <mailer-id>]...
|
|
714
|
+
New draft sequence (nothing sends yet)
|
|
715
|
+
sequences update <sequence-id> [--name <n>] [--flow '<json>'|--flow-file <path>]
|
|
716
|
+
[--status draft|active|paused|archived] [--mailer <mailer-id>]...
|
|
717
|
+
sequences pause|resume <sequence-id> Stop / restart sending
|
|
718
|
+
sequences enroll <sequence-id> (--list <list-id> | --email <addr>... | --recipients '<json>'|--recipients-file <path>)
|
|
719
|
+
sequences stop <sequence-id> <enrollment-id> Stop one recipient
|
|
720
|
+
sequences review list Emails waiting for a decision
|
|
721
|
+
sequences review edit <item-id> [--subject <s>] [--body <text>|--file <path>]
|
|
722
|
+
sequences review approve|skip|delete <item-id> Decide one (approve sends now)
|
|
723
|
+
sequences links list Live Pocket Dispatch links (no tokens)
|
|
724
|
+
sequences links create --days <1..3> Mint a no-login review link (shown once)
|
|
725
|
+
sequences links revoke <link-id> --yes
|
|
726
|
+
|
|
727
|
+
Prospects (who you write to):
|
|
728
|
+
prospects lists Your lists + counts
|
|
729
|
+
prospects list <list-id> One list
|
|
730
|
+
prospects ls [--list <list-id>] [--status <s>] [--q <search>] [--limit <n>]
|
|
731
|
+
prospects get <prospect-id>
|
|
732
|
+
prospects create-list --name <n>
|
|
733
|
+
prospects import --csv-file <path|-> [--list <list-id>|--list-name <n>] [--one-per-company]
|
|
734
|
+
prospects source --description <who you want> [--list <list-id>|--list-name <n>] [--limit <1..100>]
|
|
735
|
+
[--one-per-company] Fill a list from paid data providers (background)
|
|
736
|
+
prospects enrich [--list <list-id>] [--status <s>] [--q <search>]
|
|
737
|
+
Funding + competitors for un-enriched rows
|
|
738
|
+
prospects update <prospect-id> [--status new|contacted|replied|unsubscribed|archived] [--name <n>]
|
|
739
|
+
[--first-name] [--last-name] [--title] [--company] [--website] [--linkedin] [--school]
|
|
740
|
+
[--city] [--country] [--years] [--notes] [--custom key=value]...
|
|
741
|
+
|
|
742
|
+
Mail:
|
|
743
|
+
mail connections Connected mailboxes, Google connect + Vercel key status
|
|
744
|
+
|
|
666
745
|
Insights:
|
|
667
746
|
insights site [--period 14d|30d|90d] [--collection <uuid>]
|
|
668
747
|
insights post <article-id> [--period 14d|30d|90d]
|
|
@@ -740,6 +819,9 @@ const CLIENT_COMMANDS = {
|
|
|
740
819
|
lede: cmdLede,
|
|
741
820
|
ledes: cmdLede,
|
|
742
821
|
landings: cmdLandings,
|
|
822
|
+
sequences: cmdSequences,
|
|
823
|
+
prospects: cmdProspects,
|
|
824
|
+
mail: cmdMail,
|
|
743
825
|
feedback: cmdFeedback,
|
|
744
826
|
// Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
|
|
745
827
|
admin: cmdAdmin,
|
package/lib/commands/lede.mjs
CHANGED
|
@@ -59,6 +59,15 @@ export const ARGS = {
|
|
|
59
59
|
enrich: { value: (flags) => (flags["no-enrich"] === true ? false : undefined) },
|
|
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
|
+
// Advanced: keep specific library lines out of this run (repeatable, by line id).
|
|
63
|
+
disabled_ledes: { flag: "disable-lede", type: "list" },
|
|
64
|
+
// Gently rotate away from openers this endpoint (or --recent-lede) already used.
|
|
65
|
+
// The negate spelling exists so a call can override an endpoint default to OFF.
|
|
66
|
+
optimize_diversity: { flag: "diverse", type: "bool", negate: "no-diverse" },
|
|
67
|
+
// The line ids this sequence already used (repeatable); counted by --diverse.
|
|
68
|
+
recent_ledes: { flag: "recent-lede", type: "list" },
|
|
69
|
+
// When no library line qualifies, WRITE one (strictly checked, marked generated).
|
|
70
|
+
high_temperature: { flag: "high-temp", type: "bool", negate: "no-high-temp" },
|
|
62
71
|
},
|
|
63
72
|
list_lede_runs: {
|
|
64
73
|
endpoint_id: "endpoint",
|
|
@@ -108,6 +117,42 @@ export const ARGS = {
|
|
|
108
117
|
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
109
118
|
config: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
110
119
|
},
|
|
120
|
+
// The editable library (`lede library ...`): the org's own opening lines, pre-built
|
|
121
|
+
// and custom. Reads seed the pre-built set idempotently; writes are validated server-
|
|
122
|
+
// side (known triggers, placeholder coverage, the custom-line engine ban).
|
|
123
|
+
list_ledes: {},
|
|
124
|
+
create_lede: {
|
|
125
|
+
text: { flag: "text", required: true },
|
|
126
|
+
alts: { flag: "alts", type: "list" },
|
|
127
|
+
peer_alts: { flag: "peer-alts", type: "list" },
|
|
128
|
+
requires: { flag: "requires", type: "list" },
|
|
129
|
+
// 0-100 strictness around the engine default (50): higher = pickier, lower = looser.
|
|
130
|
+
threshold: { flag: "threshold", type: "num" },
|
|
131
|
+
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
132
|
+
tags: { flag: "tag", type: "list" },
|
|
133
|
+
},
|
|
134
|
+
update_lede: {
|
|
135
|
+
id: { positional: 0, label: "lede-id", required: true },
|
|
136
|
+
text: "text",
|
|
137
|
+
alts: { flag: "alts", type: "list" },
|
|
138
|
+
peer_alts: { flag: "peer-alts", type: "list" },
|
|
139
|
+
requires: { flag: "requires", type: "list" },
|
|
140
|
+
threshold: { flag: "threshold", type: "num" },
|
|
141
|
+
tone: { flag: "tone", enum: ["warm", "direct", "playful", "formal"] },
|
|
142
|
+
tags: { flag: "tag", type: "list" },
|
|
143
|
+
},
|
|
144
|
+
set_lede_enabled: {
|
|
145
|
+
id: { positional: 0, label: "lede-id", required: true },
|
|
146
|
+
// `lede library enable|disable <id>` decides this; the subcommand passes it in.
|
|
147
|
+
enabled: { value: (_flags, _positionals, extra) => extra.enabled },
|
|
148
|
+
},
|
|
149
|
+
delete_lede: {
|
|
150
|
+
id: { positional: 0, label: "lede-id", required: true },
|
|
151
|
+
},
|
|
152
|
+
rewrite_ledes_voice: {
|
|
153
|
+
voice: { flag: "voice", required: true },
|
|
154
|
+
ids: { flag: "ids", type: "list" },
|
|
155
|
+
},
|
|
111
156
|
};
|
|
112
157
|
|
|
113
158
|
export async function cmdLede(ctx) {
|
|
@@ -125,6 +170,9 @@ export async function cmdLede(ctx) {
|
|
|
125
170
|
return ledeDelete(rest);
|
|
126
171
|
case "endpoint":
|
|
127
172
|
return ledeEndpoint(rest);
|
|
173
|
+
case "library":
|
|
174
|
+
case "lib":
|
|
175
|
+
return ledeLibrary(rest);
|
|
128
176
|
case "run":
|
|
129
177
|
return ledeRun(rest);
|
|
130
178
|
case "runs":
|
|
@@ -145,7 +193,103 @@ export async function cmdLede(ctx) {
|
|
|
145
193
|
return ledeFunnel(rest);
|
|
146
194
|
default:
|
|
147
195
|
throw new CliError(
|
|
148
|
-
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, run, runs, history, evals, balance, outcome, funnel`
|
|
196
|
+
`Unknown lede subcommand: ${sub ?? "(none)"}. Try: list, create, delete, endpoint, library, run, runs, history, evals, balance, outcome, funnel`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `lede library list|create|update|enable|disable|delete|rewrite` — the org's own opener
|
|
203
|
+
* library (pre-built lines seeded on first read, plus lines the org authors). Runs pick
|
|
204
|
+
* only from enabled lines; `rewrite` only PROPOSES new wordings, and an accepted proposal
|
|
205
|
+
* is applied with `lede library update`.
|
|
206
|
+
*/
|
|
207
|
+
async function ledeLibrary(ctx) {
|
|
208
|
+
const { client, positionals, flags, io } = ctx;
|
|
209
|
+
const action = positionals[0];
|
|
210
|
+
const rest = { ...ctx, positionals: positionals.slice(1) };
|
|
211
|
+
switch (action) {
|
|
212
|
+
case "list":
|
|
213
|
+
case "ls": {
|
|
214
|
+
const result = await client.callTool("list_ledes", argsFromFlags(ARGS.list_ledes, flags));
|
|
215
|
+
// One line per row: state, id, provenance, then the wording. `--json` for the rest
|
|
216
|
+
// (alts, requires, threshold, tags) when editing.
|
|
217
|
+
printResult(io, flags, result, (r) =>
|
|
218
|
+
(r?.ledes ?? [])
|
|
219
|
+
.map(
|
|
220
|
+
(l) =>
|
|
221
|
+
`${l.enabled ? "on " : "off"} ${l.id} [${l.source}${l.engine ? `/${l.engine}` : ""}] ${l.text}`
|
|
222
|
+
)
|
|
223
|
+
.join("\n")
|
|
224
|
+
);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
case "create":
|
|
228
|
+
case "new":
|
|
229
|
+
case "add": {
|
|
230
|
+
const args = argsFromFlags(ARGS.create_lede, flags, rest.positionals);
|
|
231
|
+
const result = await client.callTool("create_lede", args);
|
|
232
|
+
ok(ctx, "Added the line to the library.");
|
|
233
|
+
printResult(io, flags, result);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
case "update":
|
|
237
|
+
case "edit": {
|
|
238
|
+
const args = argsFromFlags(ARGS.update_lede, flags, rest.positionals);
|
|
239
|
+
const result = await client.callTool("update_lede", args);
|
|
240
|
+
ok(ctx, `Updated line ${args.id}.`);
|
|
241
|
+
printResult(io, flags, result);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
case "enable":
|
|
245
|
+
case "disable": {
|
|
246
|
+
const enabled = action === "enable";
|
|
247
|
+
const args = argsFromFlags(ARGS.set_lede_enabled, flags, rest.positionals, { enabled });
|
|
248
|
+
const result = await client.callTool("set_lede_enabled", args);
|
|
249
|
+
ok(ctx, `${enabled ? "Enabled" : "Disabled"} line ${args.id}.`);
|
|
250
|
+
printResult(io, flags, result);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
case "delete":
|
|
254
|
+
case "rm": {
|
|
255
|
+
const id = requirePositional(rest.positionals, 0, "lede-id");
|
|
256
|
+
if (!flags.yes) {
|
|
257
|
+
io.error(`Re-run with --yes to confirm: ${ctx.bin} lede library delete ${id} --yes`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
const result = await client.callTool(
|
|
261
|
+
"delete_lede",
|
|
262
|
+
argsFromFlags(ARGS.delete_lede, flags, rest.positionals)
|
|
263
|
+
);
|
|
264
|
+
ok(
|
|
265
|
+
ctx,
|
|
266
|
+
"Line deleted. A pre-built line re-seeds at its seed wording on the next list; disable instead to keep one out."
|
|
267
|
+
);
|
|
268
|
+
printResult(io, flags, result);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
case "rewrite": {
|
|
272
|
+
const args = argsFromFlags(ARGS.rewrite_ledes_voice, flags, rest.positionals);
|
|
273
|
+
const result = await client.callTool("rewrite_ledes_voice", args);
|
|
274
|
+
// Nothing is applied: print the proposals for a human to vouch for, with the
|
|
275
|
+
// command that applies an accepted one.
|
|
276
|
+
printResult(io, flags, result, (r) => {
|
|
277
|
+
const proposals = (r?.proposals ?? []).map((p) => `${p.id}\n ${p.text}`);
|
|
278
|
+
const rejected = (r?.rejected ?? []).map((x) => `${x.id}: ${(x.reasons ?? []).join("; ")}`);
|
|
279
|
+
const parts = [];
|
|
280
|
+
parts.push(
|
|
281
|
+
proposals.length
|
|
282
|
+
? `Proposed rewrites (nothing applied; accept one with: ${ctx.bin} lede library update <lede-id> --text "..."):\n${proposals.join("\n")}`
|
|
283
|
+
: "No rewrite cleared the checks; the originals stand."
|
|
284
|
+
);
|
|
285
|
+
if (rejected.length) parts.push(`Rejected (original stands):\n${rejected.join("\n")}`);
|
|
286
|
+
return parts.join("\n\n");
|
|
287
|
+
});
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
default:
|
|
291
|
+
throw new CliError(
|
|
292
|
+
`Unknown lede library action: ${action ?? "(none)"}. Try: list, create, update, enable, disable, delete, rewrite`
|
|
149
293
|
);
|
|
150
294
|
}
|
|
151
295
|
}
|
|
@@ -257,7 +401,8 @@ async function ledeRun(ctx) {
|
|
|
257
401
|
return "No line cleared the confidence floor for this person (not charged). More fields usually turn this into a match.";
|
|
258
402
|
}
|
|
259
403
|
const lines = (r?.candidates ?? []).map(
|
|
260
|
-
(c, i) =>
|
|
404
|
+
(c, i) =>
|
|
405
|
+
`${i + 1}. ${c.text} [${Math.round((c.confidence ?? 0) * 100)}%${c.top ? ", top choice" : ""}${c.generated ? ", written for this prospect" : ""}]`
|
|
261
406
|
);
|
|
262
407
|
const prefix =
|
|
263
408
|
r?.status === "no_match"
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// `sequences`, `prospects` and `mail` — the outbound email stack, as thin wrappers
|
|
2
|
+
// over the MCP tools, which call the same lib functions the app's /api/sequences
|
|
3
|
+
// and /api/prospects routes do. Every call is built from the ARGS map below, so
|
|
4
|
+
// `scripts/parity-check.ts` can hold each command to its tool's input schema.
|
|
5
|
+
|
|
6
|
+
import { CliError } from "../client.mjs";
|
|
7
|
+
import {
|
|
8
|
+
flagStr,
|
|
9
|
+
flagList,
|
|
10
|
+
requirePositional,
|
|
11
|
+
printResult,
|
|
12
|
+
argsFromFlags,
|
|
13
|
+
refuseUnknownFlags,
|
|
14
|
+
readTextInput,
|
|
15
|
+
ok,
|
|
16
|
+
} from "./shared.mjs";
|
|
17
|
+
|
|
18
|
+
// A JSON document from --<name> '<json>' or --<name>-file <path|->.
|
|
19
|
+
function jsonInput(flag) {
|
|
20
|
+
return (flags) => {
|
|
21
|
+
const raw = readTextInput(flags, flag, `${flag}-file`);
|
|
22
|
+
if (raw === undefined) return undefined;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new CliError(`--${flag} is not valid JSON: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recipients from repeatable --email <addr>, or a JSON array via --recipients/--recipients-file.
|
|
32
|
+
function readRecipients(flags) {
|
|
33
|
+
const fromJson = jsonInput("recipients")(flags);
|
|
34
|
+
if (fromJson !== undefined) return fromJson;
|
|
35
|
+
const emails = flagList(flags.email);
|
|
36
|
+
return emails.length ? emails.map((email) => ({ email })) : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `--custom key=value` (repeatable) → { key: value }.
|
|
40
|
+
function readCustom(flags) {
|
|
41
|
+
const entries = flagList(flags.custom);
|
|
42
|
+
if (!entries.length) return undefined;
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const i = entry.indexOf("=");
|
|
46
|
+
if (i <= 0) throw new CliError(`--custom takes key=value (got ${entry})`);
|
|
47
|
+
out[entry.slice(0, i)] = entry.slice(i + 1);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SEQUENCE_STATUSES = ["draft", "active", "paused", "archived"];
|
|
53
|
+
const PROSPECT_STATUSES = ["new", "contacted", "replied", "unsubscribed", "archived"];
|
|
54
|
+
const mailerIds = {
|
|
55
|
+
value: (flags) => {
|
|
56
|
+
const ids = flagList(flags.mailer);
|
|
57
|
+
return ids.length ? ids : undefined;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
62
|
+
export const ARGS = {
|
|
63
|
+
list_sequences: {},
|
|
64
|
+
get_sequence: {
|
|
65
|
+
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
66
|
+
},
|
|
67
|
+
create_sequence: {
|
|
68
|
+
name: { flag: "name", required: true },
|
|
69
|
+
flow: {
|
|
70
|
+
value: (flags) => {
|
|
71
|
+
const flow = jsonInput("flow")(flags);
|
|
72
|
+
if (flow === undefined) throw new CliError("Missing required --flow '<json>' (or --flow-file <path>)");
|
|
73
|
+
return flow;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
mailer_ids: mailerIds,
|
|
77
|
+
},
|
|
78
|
+
update_sequence: {
|
|
79
|
+
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
80
|
+
name: "name",
|
|
81
|
+
flow: { value: jsonInput("flow") },
|
|
82
|
+
status: { flag: "status", enum: SEQUENCE_STATUSES },
|
|
83
|
+
mailer_ids: mailerIds,
|
|
84
|
+
},
|
|
85
|
+
enroll_in_sequence: {
|
|
86
|
+
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
87
|
+
recipients: { value: readRecipients },
|
|
88
|
+
list_id: "list",
|
|
89
|
+
},
|
|
90
|
+
stop_sequence_enrollment: {
|
|
91
|
+
sequence_id: { positional: 0, label: "sequence-id", required: true },
|
|
92
|
+
enrollment_id: { positional: 1, label: "enrollment-id", required: true },
|
|
93
|
+
},
|
|
94
|
+
list_review_items: {},
|
|
95
|
+
update_review_item: {
|
|
96
|
+
item_id: { positional: 0, label: "item-id", required: true },
|
|
97
|
+
subject: "subject",
|
|
98
|
+
body: { value: (flags) => readTextInput(flags, "body", "file") },
|
|
99
|
+
},
|
|
100
|
+
decide_review_item: {
|
|
101
|
+
item_id: { positional: 0, label: "item-id", required: true },
|
|
102
|
+
action: { flag: "action", required: true, enum: ["approve", "skip", "delete"] },
|
|
103
|
+
},
|
|
104
|
+
list_dispatch_links: {},
|
|
105
|
+
create_dispatch_link: {
|
|
106
|
+
days: { flag: "days", type: "num", required: true },
|
|
107
|
+
},
|
|
108
|
+
revoke_dispatch_link: {
|
|
109
|
+
link_id: { positional: 0, label: "link-id", required: true },
|
|
110
|
+
},
|
|
111
|
+
list_prospect_lists: {},
|
|
112
|
+
get_prospect_list: {
|
|
113
|
+
list_id: { positional: 0, label: "list-id", required: true },
|
|
114
|
+
},
|
|
115
|
+
list_prospects: {
|
|
116
|
+
list_id: "list",
|
|
117
|
+
status: { flag: "status", enum: PROSPECT_STATUSES },
|
|
118
|
+
q: "q",
|
|
119
|
+
limit: { flag: "limit", type: "num" },
|
|
120
|
+
},
|
|
121
|
+
get_prospect: {
|
|
122
|
+
prospect_id: { positional: 0, label: "prospect-id", required: true },
|
|
123
|
+
},
|
|
124
|
+
create_prospect_list: {
|
|
125
|
+
name: { flag: "name", required: true },
|
|
126
|
+
},
|
|
127
|
+
import_prospects_csv: {
|
|
128
|
+
csv_text: {
|
|
129
|
+
value: (flags) => {
|
|
130
|
+
const csv = readTextInput(flags, "csv", "csv-file");
|
|
131
|
+
if (csv === undefined)
|
|
132
|
+
throw new CliError("Missing CSV: pass --csv-file <path> (or --csv-file - for stdin)");
|
|
133
|
+
return csv;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
one_per_company: { flag: "one-per-company", type: "bool" },
|
|
137
|
+
list_id: "list",
|
|
138
|
+
list_name: "list-name",
|
|
139
|
+
},
|
|
140
|
+
create_prospect_list_from_prompt: {
|
|
141
|
+
description: { flag: "description", required: true },
|
|
142
|
+
list_id: "list",
|
|
143
|
+
list_name: "list-name",
|
|
144
|
+
limit: { flag: "limit", type: "num" },
|
|
145
|
+
one_per_company: { flag: "one-per-company", type: "bool" },
|
|
146
|
+
},
|
|
147
|
+
enrich_prospects: {
|
|
148
|
+
list_id: "list",
|
|
149
|
+
status: { flag: "status", enum: PROSPECT_STATUSES },
|
|
150
|
+
q: "q",
|
|
151
|
+
},
|
|
152
|
+
update_prospect: {
|
|
153
|
+
prospect_id: { positional: 0, label: "prospect-id", required: true },
|
|
154
|
+
status: { flag: "status", enum: PROSPECT_STATUSES },
|
|
155
|
+
name: "name",
|
|
156
|
+
first_name: "first-name",
|
|
157
|
+
last_name: "last-name",
|
|
158
|
+
title: "title",
|
|
159
|
+
company: "company",
|
|
160
|
+
website: "website",
|
|
161
|
+
linkedin_url: "linkedin",
|
|
162
|
+
school: "school",
|
|
163
|
+
city: "city",
|
|
164
|
+
country: "country",
|
|
165
|
+
years_at_company: "years",
|
|
166
|
+
notes: "notes",
|
|
167
|
+
custom: { value: readCustom },
|
|
168
|
+
},
|
|
169
|
+
get_mail_connections: {},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Flags the computed `value` sources read (refuseUnknownFlags can't see them).
|
|
173
|
+
const VALUE_FLAGS = {
|
|
174
|
+
create_sequence: ["flow", "flow-file", "mailer"],
|
|
175
|
+
update_sequence: ["flow", "flow-file", "mailer"],
|
|
176
|
+
enroll_in_sequence: ["recipients", "recipients-file", "email"],
|
|
177
|
+
update_review_item: ["body", "file"],
|
|
178
|
+
import_prospects_csv: ["csv", "csv-file"],
|
|
179
|
+
update_prospect: ["custom"],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
async function run(ctx, tool, message) {
|
|
183
|
+
refuseUnknownFlags(ctx, ARGS[tool], VALUE_FLAGS[tool] ?? []);
|
|
184
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
185
|
+
if (message) ok(ctx, message);
|
|
186
|
+
printResult(ctx.io, ctx.flags, result);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const SEQUENCES_USAGE = `Usage: sequences <list|get|create|update|pause|resume|enroll|stop|review|links>`;
|
|
191
|
+
|
|
192
|
+
export async function cmdSequences(ctx) {
|
|
193
|
+
const sub = ctx.positionals[0];
|
|
194
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
195
|
+
switch (sub) {
|
|
196
|
+
case "list":
|
|
197
|
+
case "ls":
|
|
198
|
+
return run(rest, "list_sequences");
|
|
199
|
+
case "get":
|
|
200
|
+
case "show":
|
|
201
|
+
return run(rest, "get_sequence");
|
|
202
|
+
case "create":
|
|
203
|
+
case "new":
|
|
204
|
+
return run(rest, "create_sequence", "Created sequence (a draft: set --status active to send).");
|
|
205
|
+
case "update":
|
|
206
|
+
return run(rest, "update_sequence", "Updated sequence.");
|
|
207
|
+
case "pause":
|
|
208
|
+
case "resume": {
|
|
209
|
+
const id = requirePositional(rest.positionals, 0, "sequence-id");
|
|
210
|
+
const status = sub === "pause" ? "paused" : "active";
|
|
211
|
+
const result = await ctx.client.callTool(
|
|
212
|
+
"update_sequence",
|
|
213
|
+
argsFromFlags(ARGS.update_sequence, { status }, [id])
|
|
214
|
+
);
|
|
215
|
+
ok(ctx, sub === "pause" ? "Paused: nothing further sends." : "Resumed: sending again.");
|
|
216
|
+
printResult(ctx.io, ctx.flags, result);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
case "enroll":
|
|
220
|
+
return run(rest, "enroll_in_sequence", "Enrolled.");
|
|
221
|
+
case "stop":
|
|
222
|
+
return run(rest, "stop_sequence_enrollment", "Enrollment stopped.");
|
|
223
|
+
case "review":
|
|
224
|
+
return sequencesReview({ ...rest, positionals: rest.positionals.slice(1) }, rest.positionals[0]);
|
|
225
|
+
case "links":
|
|
226
|
+
return sequencesLinks({ ...rest, positionals: rest.positionals.slice(1) }, rest.positionals[0]);
|
|
227
|
+
default:
|
|
228
|
+
throw new CliError(`Unknown sequences subcommand: ${sub ?? "(none)"}. ${SEQUENCES_USAGE}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function sequencesReview(ctx, sub) {
|
|
233
|
+
switch (sub) {
|
|
234
|
+
case "list":
|
|
235
|
+
case "ls":
|
|
236
|
+
case undefined:
|
|
237
|
+
return run(ctx, "list_review_items");
|
|
238
|
+
case "edit":
|
|
239
|
+
return run(ctx, "update_review_item", "Saved the edit.");
|
|
240
|
+
case "decide":
|
|
241
|
+
return run(ctx, "decide_review_item", "Decided.");
|
|
242
|
+
case "approve":
|
|
243
|
+
case "skip":
|
|
244
|
+
case "delete": {
|
|
245
|
+
const id = requirePositional(ctx.positionals, 0, "item-id");
|
|
246
|
+
const result = await ctx.client.callTool(
|
|
247
|
+
"decide_review_item",
|
|
248
|
+
argsFromFlags(ARGS.decide_review_item, { action: sub }, [id])
|
|
249
|
+
);
|
|
250
|
+
ok(ctx, sub === "approve" ? "Approved: sending now." : sub === "skip" ? "Skipped." : "Deleted.");
|
|
251
|
+
printResult(ctx.io, ctx.flags, result);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
throw new CliError(`Unknown review subcommand: ${sub}. Try: list, edit, decide, approve, skip, delete`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function sequencesLinks(ctx, sub) {
|
|
260
|
+
switch (sub) {
|
|
261
|
+
case "list":
|
|
262
|
+
case "ls":
|
|
263
|
+
case undefined:
|
|
264
|
+
return run(ctx, "list_dispatch_links");
|
|
265
|
+
case "create":
|
|
266
|
+
return run(ctx, "create_dispatch_link", "Link minted. Anyone holding it can send; it is shown only once.");
|
|
267
|
+
case "revoke": {
|
|
268
|
+
if (!ctx.flags.yes) {
|
|
269
|
+
const id = flagStr(ctx.positionals[0]) ?? "<link-id>";
|
|
270
|
+
ctx.io.error(`Re-run with --yes to confirm: ${ctx.bin} sequences links revoke ${id} --yes`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
return run(ctx, "revoke_dispatch_link", "Link revoked.");
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
throw new CliError(`Unknown links subcommand: ${sub}. Try: list, create, revoke`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const PROSPECTS_USAGE = `Usage: prospects <lists|list|ls|get|create-list|import|source|enrich|update>`;
|
|
281
|
+
|
|
282
|
+
export async function cmdProspects(ctx) {
|
|
283
|
+
const sub = ctx.positionals[0];
|
|
284
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
285
|
+
switch (sub) {
|
|
286
|
+
case "lists":
|
|
287
|
+
return run(rest, "list_prospect_lists");
|
|
288
|
+
case "list":
|
|
289
|
+
return run(rest, "get_prospect_list");
|
|
290
|
+
case "ls":
|
|
291
|
+
return run(rest, "list_prospects");
|
|
292
|
+
case "get":
|
|
293
|
+
case "show":
|
|
294
|
+
return run(rest, "get_prospect");
|
|
295
|
+
case "create-list":
|
|
296
|
+
return run(rest, "create_prospect_list", "List ready.");
|
|
297
|
+
case "import":
|
|
298
|
+
return run(rest, "import_prospects_csv", "Imported.");
|
|
299
|
+
case "source":
|
|
300
|
+
return run(
|
|
301
|
+
rest,
|
|
302
|
+
"create_prospect_list_from_prompt",
|
|
303
|
+
"Sourcing started; rows land in the list as they arrive."
|
|
304
|
+
);
|
|
305
|
+
case "enrich":
|
|
306
|
+
return run(rest, "enrich_prospects", "Enrichment queued.");
|
|
307
|
+
case "update":
|
|
308
|
+
return run(rest, "update_prospect", "Updated prospect.");
|
|
309
|
+
default:
|
|
310
|
+
throw new CliError(`Unknown prospects subcommand: ${sub ?? "(none)"}. ${PROSPECTS_USAGE}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function cmdMail(ctx) {
|
|
315
|
+
const sub = ctx.positionals[0];
|
|
316
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
317
|
+
switch (sub) {
|
|
318
|
+
case "connections":
|
|
319
|
+
case "status":
|
|
320
|
+
return run(rest, "get_mail_connections");
|
|
321
|
+
default:
|
|
322
|
+
throw new CliError(`Unknown mail subcommand: ${sub ?? "(none)"}. Try: connections`);
|
|
323
|
+
}
|
|
324
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -32,6 +32,7 @@ export * from "./commands/onboard.mjs";
|
|
|
32
32
|
export * from "./commands/seers.mjs";
|
|
33
33
|
export * from "./commands/shredder.mjs";
|
|
34
34
|
export * from "./commands/landings.mjs";
|
|
35
|
+
export * from "./commands/outbound.mjs";
|
|
35
36
|
export * from "./commands/offerings.mjs";
|
|
36
37
|
export * from "./commands/admin.mjs";
|
|
37
38
|
export * from "./commands/feedback.mjs";
|
package/lib/documents.mjs
CHANGED
|
@@ -77,6 +77,8 @@ export const JSON_ARG_FLAG_PAIRS = [
|
|
|
77
77
|
["plan", "plan-file"],
|
|
78
78
|
// A reviewed plan handed back to be applied verbatim (onboard.mjs readAcceptedPlan).
|
|
79
79
|
["accept-plan-json", "accept-plan"],
|
|
80
|
+
// A prospects CSV, parsed and header-mapped server-side (outbound.mjs prospects import).
|
|
81
|
+
["csv", "csv-file"],
|
|
80
82
|
];
|
|
81
83
|
|
|
82
84
|
/** Lower-case extension without the dot. */
|