@series-a/cli 0.24.0 → 1.0.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.
@@ -1,175 +0,0 @@
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
- }