@series-a/cli 1.3.0 → 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 +184 -51
- package/LICENSE +7 -0
- package/README.md +249 -115
- package/SKILLS.md +359 -0
- package/bin/advisoros.mjs +2 -0
- package/package.json +50 -15
- package/src/api.mjs +102 -0
- package/src/commands/auth.mjs +193 -0
- package/src/commands/brand.mjs +439 -0
- package/src/commands/email.mjs +130 -0
- package/src/commands/flow.mjs +175 -0
- package/src/commands/insights.mjs +1813 -0
- package/src/commands/knowledge-sync.mjs +519 -0
- package/src/commands/resources.mjs +2214 -0
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +212 -0
- package/src/config.mjs +82 -0
- package/src/index.mjs +79 -0
- package/src/ui.mjs +104 -0
- package/bin/setu.mjs +0 -797
- package/lib/api.mjs +0 -84
- package/lib/config.mjs +0 -42
- package/lib/ui.mjs +0 -68
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { call } from "../api.mjs";
|
|
3
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
|
|
4
|
+
|
|
5
|
+
function parseCsv(text) {
|
|
6
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
|
|
7
|
+
if (!lines.length) return [];
|
|
8
|
+
const split = (line) => {
|
|
9
|
+
const out = []; let cur = ""; let q = false;
|
|
10
|
+
for (let i = 0; i < line.length; i++) {
|
|
11
|
+
const c = line[i];
|
|
12
|
+
if (q) {
|
|
13
|
+
if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
|
|
14
|
+
else if (c === '"') { q = false; }
|
|
15
|
+
else cur += c;
|
|
16
|
+
} else {
|
|
17
|
+
if (c === ",") { out.push(cur); cur = ""; }
|
|
18
|
+
else if (c === '"') { q = true; }
|
|
19
|
+
else cur += c;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
out.push(cur);
|
|
23
|
+
return out.map((s) => s.trim());
|
|
24
|
+
};
|
|
25
|
+
const headers = split(lines[0]).map((h) => h.toLowerCase());
|
|
26
|
+
return lines.slice(1).map((line) => {
|
|
27
|
+
const cells = split(line);
|
|
28
|
+
const row = {};
|
|
29
|
+
headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
|
|
30
|
+
return row;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadRows(file) {
|
|
35
|
+
const text = await readFile(file, "utf8");
|
|
36
|
+
if (file.endsWith(".jsonl")) {
|
|
37
|
+
return text.split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l));
|
|
38
|
+
}
|
|
39
|
+
if (file.endsWith(".json")) {
|
|
40
|
+
const parsed = JSON.parse(text);
|
|
41
|
+
return Array.isArray(parsed) ? parsed : (parsed.leads ?? []);
|
|
42
|
+
}
|
|
43
|
+
return parseCsv(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function registerFlow(program) {
|
|
47
|
+
withCommonOpts(program.command("campaign-flow"))
|
|
48
|
+
.description("End-to-end: import leads → create/attach sequence → launch → show status")
|
|
49
|
+
.requiredOption("--name <name>", "campaign name")
|
|
50
|
+
.requiredOption("--file <path>", "leads CSV / JSON / JSONL")
|
|
51
|
+
.option("--description <text>", "campaign description")
|
|
52
|
+
.option("--goal <text>", "campaign goal")
|
|
53
|
+
.option("--persona <tag>", "default persona_tag for imported leads")
|
|
54
|
+
.option("--source <s>", "default lead source", "cli-flow")
|
|
55
|
+
.option("--sequence <id>", "attach an existing sequence (otherwise picks most recent active)")
|
|
56
|
+
.option("--senders <csv>", "comma-separated sender_account_ids")
|
|
57
|
+
.option("--daily-limit <n>", "daily send cap", "25")
|
|
58
|
+
.option("--launch", "activate the campaign immediately after setup", false)
|
|
59
|
+
.option("--dry-run", "print plan only, do not call the API", false)
|
|
60
|
+
.action(async function (opts) {
|
|
61
|
+
const ctx = ctxFrom(this);
|
|
62
|
+
const steps = [];
|
|
63
|
+
const log = (msg) => { if (!this.optsWithGlobals().json) process.stdout.write(msg); };
|
|
64
|
+
try {
|
|
65
|
+
// 1. Load + import leads
|
|
66
|
+
const rows = await loadRows(opts.file);
|
|
67
|
+
if (!rows.length) return fail(new Error(`No leads found in ${opts.file}`));
|
|
68
|
+
if (opts.persona) rows.forEach((r) => { r.persona_tag = r.persona_tag || opts.persona; });
|
|
69
|
+
rows.forEach((r) => { r.source = r.source || opts.source; });
|
|
70
|
+
|
|
71
|
+
if (opts.dryRun) {
|
|
72
|
+
log(`DRY RUN — would import ${rows.length} leads and create campaign "${opts.name}"\n`);
|
|
73
|
+
return emit(this, { dry_run: true, lead_count: rows.length, name: opts.name }, () => {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
log(`→ Importing ${rows.length} leads…\n`);
|
|
77
|
+
let imported = 0;
|
|
78
|
+
const importedIds = [];
|
|
79
|
+
for (let i = 0; i < rows.length; i += 200) {
|
|
80
|
+
const chunk = rows.slice(i, i + 200);
|
|
81
|
+
const r = await call("leads", "bulk_upsert", { leads: chunk }, ctx);
|
|
82
|
+
imported += r.count ?? 0;
|
|
83
|
+
(r.leads || r.upserted || []).forEach((l) => l?.id && importedIds.push(l.id));
|
|
84
|
+
}
|
|
85
|
+
steps.push({ step: "import_leads", imported, total: rows.length });
|
|
86
|
+
log(` ✓ Upserted ${imported}/${rows.length} leads\n`);
|
|
87
|
+
|
|
88
|
+
// Fallback: re-fetch ids if backend didn't return them
|
|
89
|
+
let leadIds = importedIds;
|
|
90
|
+
if (!leadIds.length) {
|
|
91
|
+
const list = await call("leads", "list", {
|
|
92
|
+
persona_tag: opts.persona, limit: Math.min(rows.length, 500),
|
|
93
|
+
}, ctx);
|
|
94
|
+
leadIds = (list.leads || []).map((l) => l.id).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Resolve sequence
|
|
98
|
+
let sequenceId = opts.sequence;
|
|
99
|
+
if (!sequenceId) {
|
|
100
|
+
const seqs = await call("sequences", "list", { limit: 50 }, ctx);
|
|
101
|
+
const active = (seqs.sequences || []).find((s) => s.status === "active")
|
|
102
|
+
|| (seqs.sequences || [])[0];
|
|
103
|
+
if (!active) return fail(new Error("No sequence found — pass --sequence <id> or create one first"));
|
|
104
|
+
sequenceId = active.id;
|
|
105
|
+
log(`→ Using sequence: ${active.name} (${truncate(sequenceId, 8)})\n`);
|
|
106
|
+
}
|
|
107
|
+
steps.push({ step: "resolve_sequence", sequence_id: sequenceId });
|
|
108
|
+
|
|
109
|
+
// 3. Create campaign
|
|
110
|
+
log(`→ Creating campaign "${opts.name}"…\n`);
|
|
111
|
+
const created = await call("campaigns", "create", {
|
|
112
|
+
name: opts.name,
|
|
113
|
+
description: opts.description,
|
|
114
|
+
goal: opts.goal,
|
|
115
|
+
sequence_id: sequenceId,
|
|
116
|
+
sender_accounts: opts.senders ? opts.senders.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
117
|
+
daily_send_limit: Number(opts.dailyLimit),
|
|
118
|
+
}, ctx);
|
|
119
|
+
const campaignId = created.campaign?.id;
|
|
120
|
+
if (!campaignId) return fail(new Error("Campaign create returned no id"));
|
|
121
|
+
steps.push({ step: "create_campaign", campaign_id: campaignId });
|
|
122
|
+
log(` ✓ Campaign ${truncate(campaignId, 8)} created\n`);
|
|
123
|
+
|
|
124
|
+
// 4. Attach leads
|
|
125
|
+
if (leadIds.length) {
|
|
126
|
+
log(`→ Attaching ${leadIds.length} leads to campaign…\n`);
|
|
127
|
+
const add = await call("campaigns", "add_leads", {
|
|
128
|
+
campaign_id: campaignId, lead_ids: leadIds,
|
|
129
|
+
}, ctx);
|
|
130
|
+
steps.push({ step: "add_leads", added: add.added, requested: add.requested });
|
|
131
|
+
log(` ✓ Added ${add.added}/${add.requested} (${add.valid} valid in workspace)\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. Launch (optional)
|
|
135
|
+
let launched = null;
|
|
136
|
+
if (opts.launch) {
|
|
137
|
+
log(`→ Launching campaign…\n`);
|
|
138
|
+
launched = await call("campaigns", "launch", { campaign_id: campaignId }, ctx);
|
|
139
|
+
steps.push({ step: "launch", status: launched.campaign?.status });
|
|
140
|
+
log(` ✓ Status → ${launched.campaign?.status}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 6. Final status
|
|
144
|
+
const finalShow = await call("campaigns", "get", { id: campaignId }, ctx);
|
|
145
|
+
const c = finalShow.campaign || {};
|
|
146
|
+
const summary = {
|
|
147
|
+
campaign_id: campaignId,
|
|
148
|
+
name: c.name,
|
|
149
|
+
status: c.status,
|
|
150
|
+
sequence_id: c.sequence_id,
|
|
151
|
+
daily_send_limit: c.daily_send_limit,
|
|
152
|
+
sender_accounts: c.sender_accounts || [],
|
|
153
|
+
leads_attached: c.lead_count ?? leadIds.length,
|
|
154
|
+
steps,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
emit(this, summary, (d) => {
|
|
158
|
+
process.stdout.write("\n" + table(
|
|
159
|
+
["field", "value"],
|
|
160
|
+
[
|
|
161
|
+
["campaign", `${d.name} (${truncate(d.campaign_id, 8)})`],
|
|
162
|
+
["status", d.status],
|
|
163
|
+
["sequence", truncate(d.sequence_id, 8)],
|
|
164
|
+
["leads", d.leads_attached],
|
|
165
|
+
["senders", (d.sender_accounts || []).length],
|
|
166
|
+
["daily cap", d.daily_send_limit],
|
|
167
|
+
],
|
|
168
|
+
) + "\n");
|
|
169
|
+
process.stdout.write(opts.launch
|
|
170
|
+
? `✓ Campaign live — monitor with: advisoros campaigns show ${campaignId}\n`
|
|
171
|
+
: `✓ Campaign ready (draft) — launch with: advisoros campaigns launch ${campaignId}\n`);
|
|
172
|
+
});
|
|
173
|
+
} catch (e) { fail(e); }
|
|
174
|
+
});
|
|
175
|
+
}
|