@series-a/cli 1.3.1 → 1.3.2
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/CHANGELOG.md +13 -0
- package/SKILLS.md +15 -0
- package/package.json +1 -1
- package/src/commands/email.mjs +130 -0
- package/src/commands/resources.mjs +18 -0
- package/src/index.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this package are documented here. This project follows
|
|
|
4
4
|
[Semantic Versioning](https://semver.org/) and uses
|
|
5
5
|
[Changesets](https://github.com/changesets/changesets) to manage releases.
|
|
6
6
|
|
|
7
|
+
## 0.28.0 — Instantly email campaigns
|
|
8
|
+
|
|
9
|
+
- New `advisoros email` command group (alias `instantly`) drives the workspace's Instantly account through its own connected key — the key never leaves the server.
|
|
10
|
+
- `email status`, `email actions`, and `email call <action> --data '{…}'` for any supported action.
|
|
11
|
+
- `email campaigns list|get|create|update|activate|pause|duplicate|delete|sending-status|analytics|steps` — `steps` shows sent, delivered, opened and replied per email.
|
|
12
|
+
- `email sequence show|set|history|restore|test|branches|branch-pause|branch-resume|branch-delete|variables` — `history` lists every saved version from the app, `restore` puts one back live (the current sequence is saved to history first).
|
|
13
|
+
- `email accounts list|get|pause|resume|mark-fixed|warmup on/off|limit|test` for sending mailboxes.
|
|
14
|
+
- `email leads list|add|import (CSV, max 500)|remove|move|status|merge|lists|list-create|list-delete|blocklist|block|unblock`.
|
|
15
|
+
- `email tags` and `email labels`.
|
|
16
|
+
- Agent API: new `instantly` resource (alias `email`) with snake_case actions plus `status`, `actions`, `sequence_history`, `sequence_restore`.
|
|
17
|
+
- Destructive commands (delete, remove, restore) ask for confirmation; pass `--yes` in scripts.
|
|
18
|
+
- Changes require an Instantly key on the workspace (Integrations → Instantly); without one, commands return a clear "connect Instantly" message.
|
|
19
|
+
|
|
7
20
|
## 0.27.0 — Campaign engine reliability
|
|
8
21
|
|
|
9
22
|
- Campaigns no longer restart from the beginning. Finished runs were being relaunched roughly every minute and began again at step one, so the same people were actioned repeatedly (one lead recorded 875 profile views). Each enrolled lead now keeps one permanent progress record for the lifetime of the campaign.
|
package/SKILLS.md
CHANGED
|
@@ -342,3 +342,18 @@ Deletion is allowed but always two-step:
|
|
|
342
342
|
|
|
343
343
|
When in doubt: `advisoros <command> --help`, `docs/cli.md`, and
|
|
344
344
|
`docs/agent-api.md` are the canonical references.
|
|
345
|
+
|
|
346
|
+
## Email campaigns (Instantly)
|
|
347
|
+
|
|
348
|
+
Needs an Instantly key on the workspace (Integrations → Instantly).
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
advisoros email status
|
|
352
|
+
advisoros email campaigns list
|
|
353
|
+
advisoros email campaigns steps <campaignId> # sent / delivered / opened / replied per email
|
|
354
|
+
advisoros email sequence history <campaignId>
|
|
355
|
+
advisoros email sequence restore <versionId> --yes # current sequence is saved to history first
|
|
356
|
+
advisoros email accounts list
|
|
357
|
+
advisoros email leads import <campaignId> leads.csv # email,first_name,last_name,company_name
|
|
358
|
+
advisoros email call <action> --data '{"id":"..."}' # any action from `email actions`
|
|
359
|
+
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// `advisoros email` — Instantly email campaigns, accounts, leads and sequences.
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { call } from "../api.mjs";
|
|
4
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
|
|
5
|
+
|
|
6
|
+
const STATUS = { "-99": "suspended", "-2": "account paused", "-1": "archived", 0: "draft", 1: "active", 2: "paused", 3: "completed", 4: "subsequence" };
|
|
7
|
+
const items = (r) => r?.result?.items ?? (Array.isArray(r?.result) ? r.result : []);
|
|
8
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
9
|
+
const daysAgo = (n) => { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString().slice(0, 10); };
|
|
10
|
+
const range = (o) => ({ start_date: o.from || (o.days === "all" ? "2020-01-01" : daysAgo(Number(o.days || 30))), end_date: o.to || today() });
|
|
11
|
+
const parseJson = (s, label) => { try { return JSON.parse(s); } catch { throw new Error(`${label} must be valid JSON`); } };
|
|
12
|
+
|
|
13
|
+
/** Run one Instantly action and print the result. */
|
|
14
|
+
function run(cmd, action, params, human, destructive) {
|
|
15
|
+
return async function (...args) {
|
|
16
|
+
try {
|
|
17
|
+
const self = this;
|
|
18
|
+
const p = typeof params === "function" ? params(...args, self.optsWithGlobals()) : params;
|
|
19
|
+
if (destructive) await confirmDestructive(self, typeof destructive === "function" ? destructive(...args) : destructive);
|
|
20
|
+
const r = await call("instantly", action, p, ctxFrom(self));
|
|
21
|
+
emit(self, r, human || ((d) => process.stdout.write(`✓ ${d.action ?? action} done\n`)));
|
|
22
|
+
} catch (e) { fail(e); }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerEmail(program) {
|
|
27
|
+
const email = program.command("email").alias("instantly").description("Instantly email campaigns — campaigns, senders, leads, sequences");
|
|
28
|
+
|
|
29
|
+
withCommonOpts(email.command("status")).description("Is an Instantly key connected for this workspace?")
|
|
30
|
+
.action(run(null, "status", {}, (d) => process.stdout.write(d.configured ? `✓ Instantly connected (${d.key_preview})\n` : "✖ No Instantly key — connect it in Integrations → Instantly\n")));
|
|
31
|
+
withCommonOpts(email.command("actions")).description("List every supported Instantly action (for `email call`)")
|
|
32
|
+
.action(run(null, "actions", {}, (d) => process.stdout.write(d.actions.join("\n") + "\n")));
|
|
33
|
+
withCommonOpts(email.command("call <action>")).description("Run any supported action with raw JSON data")
|
|
34
|
+
.option("--data <json>", "JSON payload", "{}")
|
|
35
|
+
.action(run(null, "call", (action, o) => ({ op: action, data: parseJson(o.data, "--data") }), (d) => process.stdout.write(JSON.stringify(d.result, null, 2) + "\n")));
|
|
36
|
+
|
|
37
|
+
// ── Campaigns ──
|
|
38
|
+
const c = email.command("campaigns").description("Email campaigns");
|
|
39
|
+
withCommonOpts(c.command("list")).option("--search <q>").option("--status <n>", "0 draft · 1 active · 2 paused · 3 completed").option("--limit <n>", "", "50")
|
|
40
|
+
.action(run(null, "list_campaigns", (o) => ({ search: o.search, status: o.status, limit: Number(o.limit) }), (d) =>
|
|
41
|
+
process.stdout.write(table(["id", "name", "status", "daily limit", "senders"], items(d).map((x) => [x.id, truncate(x.name, 44), STATUS[x.status] ?? x.status, x.daily_limit ?? "—", (x.email_list || []).length])))));
|
|
42
|
+
withCommonOpts(c.command("get <id>")).action(run(null, "get_campaign", (id) => ({ id }), (d) => process.stdout.write(JSON.stringify(d.result, null, 2) + "\n")));
|
|
43
|
+
withCommonOpts(c.command("create <name>")).option("--daily-limit <n>").option("--senders <emails>", "comma-separated sending accounts")
|
|
44
|
+
.action(run(null, "create_campaign", (name, o) => ({ name, daily_limit: o.dailyLimit ? Number(o.dailyLimit) : undefined, email_list: o.senders?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
45
|
+
campaign_schedule: { schedules: [{ name: "Default", timing: { from: "09:00", to: "17:00" }, days: { 1: true, 2: true, 3: true, 4: true, 5: true }, timezone: "America/Chicago" }] } }),
|
|
46
|
+
(d) => process.stdout.write(`✓ Created ${d.result?.id}\n`)));
|
|
47
|
+
withCommonOpts(c.command("update <id>")).option("--data <json>", "fields to change (name, daily_limit, email_list, stop_on_reply, …)", "{}")
|
|
48
|
+
.action(run(null, "update_campaign", (id, o) => ({ id, ...parseJson(o.data, "--data") })));
|
|
49
|
+
withCommonOpts(c.command("activate <id>")).action(run(null, "activate_campaign", (id) => ({ id })));
|
|
50
|
+
withCommonOpts(c.command("pause <id>")).action(run(null, "pause_campaign", (id) => ({ id })));
|
|
51
|
+
withCommonOpts(c.command("duplicate <id>")).action(run(null, "duplicate_campaign", (id) => ({ id }), (d) => process.stdout.write(`✓ Duplicated → ${d.result?.id ?? "new campaign"}\n`)));
|
|
52
|
+
withCommonOpts(c.command("delete <id>")).option("-y, --yes", "skip confirmation")
|
|
53
|
+
.action(run(null, "delete_campaign", (id) => ({ id }), undefined, (id) => `Delete email campaign ${id} in Instantly. This cannot be undone.`));
|
|
54
|
+
withCommonOpts(c.command("sending-status <id>")).action(run(null, "sending_status", (id) => ({ id }), (d) => process.stdout.write(JSON.stringify(d.result, null, 2) + "\n")));
|
|
55
|
+
withCommonOpts(c.command("analytics <id>")).option("--days <n>", "7 | 30 | 90 | all", "30").option("--from <date>").option("--to <date>")
|
|
56
|
+
.action(run(null, "analytics", (id, o) => ({ id, ...range(o) }), (d) => { const a = Array.isArray(d.result) ? d.result[0] : d.result; process.stdout.write(table(["metric", "value"], ["leads_count", "contacted_count", "emails_sent_count", "open_count_unique", "link_click_count_unique", "reply_count_unique", "bounced_count", "unsubscribed_count", "total_interested", "total_meeting_booked", "total_opportunity_value"].map((k) => [k, a?.[k] ?? 0]))); }));
|
|
57
|
+
withCommonOpts(c.command("steps <id>")).description("Per-email results: sent, delivered, opened, replied").option("--days <n>", "", "all")
|
|
58
|
+
.action(run(null, "step_analytics", (id, o) => ({ campaign_id: id, ...range(o) }), (d) => {
|
|
59
|
+
const rows = Array.isArray(d.result) ? d.result : items(d);
|
|
60
|
+
process.stdout.write(table(["step", "variant", "sent", "delivered", "opened", "replied"], rows.map((r) => [Number(r.step ?? 0) + 1, r.variant ?? "—", r.sent ?? 0, Math.max(0, (r.sent ?? 0) - (r.bounced ?? 0)), r.unique_opened ?? r.opened ?? 0, r.unique_replies ?? r.replies ?? 0])));
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// ── Sequence ──
|
|
64
|
+
const s = email.command("sequence").description("Email steps, test sends, history and restore");
|
|
65
|
+
withCommonOpts(s.command("show <campaignId>")).action(run(null, "get_campaign", (id) => ({ id }), (d) => {
|
|
66
|
+
const steps = d.result?.sequences?.[0]?.steps ?? [];
|
|
67
|
+
process.stdout.write(table(["#", "wait", "subject", "variants"], steps.map((st, i) => [i + 1, `${st.delay ?? 0} ${st.delay_unit || "days"}`, truncate(st.variants?.[0]?.subject || "", 50), (st.variants || []).length])));
|
|
68
|
+
}));
|
|
69
|
+
withCommonOpts(s.command("set <campaignId> <file>")).description("Replace the sequence from a JSON file: [{delay, delay_unit, variants:[{subject, body}]}]")
|
|
70
|
+
.action(async function (id, file) { try { const steps = parseJson(await readFile(file, "utf8"), file); const r = await call("instantly", "update_campaign", { id, sequences: [{ steps: steps.map((st) => ({ type: "email", ...st })) }] }, ctxFrom(this)); emit(this, r, () => process.stdout.write(`✓ Sequence saved (${steps.length} emails)\n`)); } catch (e) { fail(e); } });
|
|
71
|
+
withCommonOpts(s.command("history <campaignId>")).option("--limit <n>", "", "20")
|
|
72
|
+
.action(run(null, "sequence_history", (id, o) => ({ campaign_id: id, limit: Number(o.limit) }), (d) =>
|
|
73
|
+
process.stdout.write(table(["version", "saved", "note", "emails"], d.versions.map((v) => [v.id, v.created_at.slice(0, 16).replace("T", " "), v.note || "Saved", (v.steps || []).filter((x) => x.type !== "wait").length])))));
|
|
74
|
+
withCommonOpts(s.command("restore <versionId>")).option("-y, --yes", "skip confirmation")
|
|
75
|
+
.action(run(null, "sequence_restore", (version_id) => ({ version_id }), (d) => process.stdout.write(`✓ Restored version ${d.version_id} on campaign ${d.campaign_id}\n`), (v) => `Replace the live Instantly sequence with version ${v}. The current sequence is saved to history first.`));
|
|
76
|
+
withCommonOpts(s.command("test")).requiredOption("--from <email>", "sending account").requiredOption("--to <email>").requiredOption("--subject <s>").requiredOption("--body <b>")
|
|
77
|
+
.action(run(null, "send_test_email", (o) => ({ eaccount: o.from, to_address_email_list: o.to, subject: o.subject, body: o.body })));
|
|
78
|
+
withCommonOpts(s.command("branches <campaignId>")).description("List follow-up branches (subsequences)")
|
|
79
|
+
.action(run(null, "list_subsequences", (id) => ({ parent_campaign: id, limit: 100 }), (d) => process.stdout.write(table(["id", "name", "status"], items(d).map((x) => [x.id, x.name, STATUS[x.status] ?? x.status])))));
|
|
80
|
+
for (const [name, action] of [["branch-pause", "pause_subsequence"], ["branch-resume", "resume_subsequence"]]) withCommonOpts(s.command(`${name} <id>`)).action(run(null, action, (id) => ({ id })));
|
|
81
|
+
withCommonOpts(s.command("branch-delete <id>")).option("-y, --yes").action(run(null, "delete_subsequence", (id) => ({ id }), undefined, (id) => `Delete follow-up branch ${id}.`));
|
|
82
|
+
withCommonOpts(s.command("variables <campaignId> <json>")).description("Set campaign variables").action(run(null, "set_variables", (id, j) => ({ id, variables: parseJson(j, "variables") })));
|
|
83
|
+
|
|
84
|
+
// ── Sending accounts ──
|
|
85
|
+
const a = email.command("accounts").description("Sending mailboxes");
|
|
86
|
+
withCommonOpts(a.command("list")).option("--search <q>").option("--limit <n>", "", "100")
|
|
87
|
+
.action(run(null, "list_accounts", (o) => ({ search: o.search, limit: Number(o.limit) }), (d) => {
|
|
88
|
+
const rows = items(d); if (!rows.length) return process.stdout.write("No sending mailboxes in this Instantly workspace.\n");
|
|
89
|
+
process.stdout.write(table(["email", "status", "daily limit", "warm-up"], rows.map((x) => [x.email, x.status ?? "—", x.daily_limit ?? "—", x.warmup_status ?? "—"])));
|
|
90
|
+
}));
|
|
91
|
+
withCommonOpts(a.command("get <email>")).action(run(null, "get_account", (e) => ({ email: e }), (d) => process.stdout.write(JSON.stringify(d.result, null, 2) + "\n")));
|
|
92
|
+
for (const [name, action] of [["pause", "pause_account"], ["resume", "resume_account"], ["mark-fixed", "mark_account_fixed"]]) withCommonOpts(a.command(`${name} <email>`)).action(run(null, action, (e) => ({ email: e })));
|
|
93
|
+
withCommonOpts(a.command("warmup <on|off> <emails...>")).action(run(null, "call", (mode, emails) => ({ op: mode === "off" ? "disableWarmup" : "enableWarmup", data: { emails } })));
|
|
94
|
+
withCommonOpts(a.command("limit <email> <n>")).description("Set a mailbox's daily send limit").action(run(null, "update_account", (e, n) => ({ email: e, daily_limit: Number(n) })));
|
|
95
|
+
withCommonOpts(a.command("test <emails...>")).description("Run a health (vitals) test").action(run(null, "test_account_vitals", (emails) => ({ accounts: emails }), (d) => process.stdout.write(JSON.stringify(d.result, null, 2) + "\n")));
|
|
96
|
+
|
|
97
|
+
// ── Leads ──
|
|
98
|
+
const l = email.command("leads").description("Campaign leads, lists and block list");
|
|
99
|
+
withCommonOpts(l.command("list <campaignId>")).option("--search <q>").option("--limit <n>", "", "100")
|
|
100
|
+
.action(run(null, "list_leads", (id, o) => ({ campaign: id, search: o.search, limit: Number(o.limit) }), (d) =>
|
|
101
|
+
process.stdout.write(table(["id", "email", "name", "company", "status"], items(d).map((x) => [x.id, x.email, [x.first_name, x.last_name].filter(Boolean).join(" "), truncate(x.company_name || "", 24), x.status ?? "—"])))));
|
|
102
|
+
withCommonOpts(l.command("add <campaignId> <email>")).option("--first <n>").option("--last <n>").option("--company <c>")
|
|
103
|
+
.action(run(null, "create_lead", (id, e, o) => ({ campaign: id, email: e, first_name: o.first, last_name: o.last, company_name: o.company, skip_if_in_campaign: true })));
|
|
104
|
+
withCommonOpts(l.command("import <campaignId> <csv>")).description("Bulk add from CSV (email,first_name,last_name,company_name), max 500")
|
|
105
|
+
.action(async function (id, file) {
|
|
106
|
+
try {
|
|
107
|
+
const lines = (await readFile(file, "utf8")).split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
108
|
+
const header = lines[0].toLowerCase().includes("email") ? lines.shift().toLowerCase().split(",").map((x) => x.trim()) : ["email", "first_name", "last_name", "company_name"];
|
|
109
|
+
const rows = lines.slice(0, 500).map((line) => Object.fromEntries(line.split(",").map((v, i) => [header[i], v.trim()])));
|
|
110
|
+
let ok = 0; const failed = [];
|
|
111
|
+
for (const row of rows) { if (!row.email) continue; try { await call("instantly", "create_lead", { campaign: id, email: row.email, first_name: row.first_name, last_name: row.last_name, company_name: row.company_name, skip_if_in_campaign: true }, ctxFrom(this)); ok++; } catch (e) { failed.push(`${row.email}: ${e.message}`); } }
|
|
112
|
+
emit(this, { added: ok, failed }, (d) => process.stdout.write(`✓ Added ${d.added} lead(s)${d.failed.length ? `, ${d.failed.length} failed:\n ${d.failed.join("\n ")}` : ""}\n`));
|
|
113
|
+
} catch (e) { fail(e); }
|
|
114
|
+
});
|
|
115
|
+
withCommonOpts(l.command("remove <ids...>")).option("-y, --yes").action(run(null, "call", (ids) => ids.length === 1 ? { op: "deleteLead", data: { id: ids[0] } } : { op: "deleteLeadsBulk", data: { ids } }, undefined, (ids) => `Remove ${ids.length} lead(s) from Instantly.`));
|
|
116
|
+
withCommonOpts(l.command("move <toCampaignId> <ids...>")).action(run(null, "move_leads", (to, ids) => ({ ids, to_campaign_id: to })));
|
|
117
|
+
withCommonOpts(l.command("status <email> <interest>")).description("Set interest: 1 interested, 2 meeting booked, 3 meeting completed, 4 won, 0 out of office, -1 not interested, -2 wrong person, -3 lost").option("--campaign <id>")
|
|
118
|
+
.action(run(null, "update_lead_status", (e, v, o) => ({ lead_email: e, interest_value: Number(v), campaign_id: o.campaign })));
|
|
119
|
+
withCommonOpts(l.command("merge <leadId> <intoLeadId>")).action(run(null, "merge_leads", (a1, b) => ({ lead_id: a1, destination_lead_id: b })));
|
|
120
|
+
withCommonOpts(l.command("lists")).action(run(null, "list_lead_lists", { limit: 100 }, (d) => process.stdout.write(table(["id", "name"], items(d).map((x) => [x.id, x.name])))));
|
|
121
|
+
withCommonOpts(l.command("list-create <name>")).action(run(null, "create_lead_list", (name) => ({ name })));
|
|
122
|
+
withCommonOpts(l.command("list-delete <id>")).option("-y, --yes").action(run(null, "delete_lead_list", (id) => ({ id }), undefined, (id) => `Delete lead list ${id}.`));
|
|
123
|
+
withCommonOpts(l.command("blocklist")).action(run(null, "list_block_list", { limit: 100 }, (d) => process.stdout.write(table(["id", "blocked"], items(d).map((x) => [x.id, x.bl_value])))));
|
|
124
|
+
withCommonOpts(l.command("block <emailOrDomain>")).action(run(null, "add_block_list", (v) => ({ bl_value: v })));
|
|
125
|
+
withCommonOpts(l.command("unblock <id>")).action(run(null, "remove_block_list", (id) => ({ id })));
|
|
126
|
+
|
|
127
|
+
// ── Tags ──
|
|
128
|
+
withCommonOpts(email.command("tags")).action(run(null, "list_tags", { limit: 100 }, (d) => process.stdout.write(table(["id", "label"], items(d).map((x) => [x.id, x.label])))));
|
|
129
|
+
withCommonOpts(email.command("labels")).action(run(null, "list_labels", { limit: 100 }, (d) => process.stdout.write(table(["id", "label"], items(d).map((x) => [x.id, x.label ?? x.name])))));
|
|
130
|
+
}
|
|
@@ -77,6 +77,24 @@ export function registerLeads(program) {
|
|
|
77
77
|
} catch (e) { fail(e); }
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
withCommonOpts(leads.command("import-engagers"))
|
|
81
|
+
.description("Import people who recently commented on your LinkedIn posts (via Taplio)")
|
|
82
|
+
.option("--max-posts <n>", "recent posts to scan (max 50)", "20")
|
|
83
|
+
.action(async function () {
|
|
84
|
+
try {
|
|
85
|
+
const o = this.optsWithGlobals();
|
|
86
|
+
const r = await call("leads", "import_engagers", {
|
|
87
|
+
max_posts: Number(o.maxPosts),
|
|
88
|
+
}, ctxFrom(this));
|
|
89
|
+
emit(this, r, (d) => {
|
|
90
|
+
if (d.reason) return process.stdout.write(`${d.reason}\n`);
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
`✓ ${d.imported} new lead(s) imported · ${d.already_known} already known · ${d.posts_scanned ?? 0} posts scanned\n`,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
} catch (e) { fail(e); }
|
|
96
|
+
});
|
|
97
|
+
|
|
80
98
|
withCommonOpts(leads.command("import"))
|
|
81
99
|
.description("Bulk upsert leads from a CSV or JSON/JSONL file")
|
|
82
100
|
.requiredOption("--file <path>", "leads.csv | leads.json | leads.jsonl")
|
package/src/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
registerExport, registerIntegrations, registerAudit, registerReports,
|
|
18
18
|
} from "./commands/insights.mjs";
|
|
19
19
|
import { registerFlow } from "./commands/flow.mjs";
|
|
20
|
+
import { registerEmail } from "./commands/email.mjs";
|
|
20
21
|
import { registerVaultSync } from "./commands/knowledge-sync.mjs";
|
|
21
22
|
|
|
22
23
|
const program = new Command();
|
|
@@ -43,6 +44,7 @@ registerStrategies(program);
|
|
|
43
44
|
registerVoice(program);
|
|
44
45
|
registerBrand(program);
|
|
45
46
|
registerAuthority(program);
|
|
47
|
+
registerEmail(program);
|
|
46
48
|
registerBriefs(program);
|
|
47
49
|
registerAgents(program);
|
|
48
50
|
registerIntegrations(program);
|