@series-a/cli 1.3.1 → 1.4.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/CHANGELOG.md +23 -0
- package/SKILLS.md +15 -0
- package/package.json +1 -1
- package/src/commands/email.mjs +130 -0
- package/src/commands/resources.mjs +32 -0
- package/src/index.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
## 1.4.0 — Company news, brand imagery fix, API reliability
|
|
8
|
+
|
|
9
|
+
Version jumps from 0.28.0 to 1.4.0 because npm already has releases up to 1.3.2.
|
|
10
|
+
|
|
11
|
+
- New `advisoros leads company-refresh <ids...>` fetches company details and recent company news for contacts through Brave Search. It only works when the workspace's "Company news" switch is on, each company is refreshed at most once every 14 days, and every lookup is billed as `web_search` credits.
|
|
12
|
+
- `brand set-imagery` works again for workspaces whose owner is stored in the older `ws_…` format. Before, it failed with "One of the supplied ids is not a valid identifier".
|
|
13
|
+
- API reliability: the CLI, MCP and Agent API briefly returned 503 because the server function crashed on startup. That is fixed. No change is needed on the CLI side.
|
|
14
|
+
- Server-side agent fixes also reach CLI-triggered runs: Market Research, News Monitor, Competitive Intel, Social Listening and the Intelligence Synthesizer no longer crash when the AI sends its lists in a different format.
|
|
15
|
+
- Campaigns: "Stop if replied (else continue)" checks only that contact's own saved replies (it never spends a LinkedIn read), and each contact's branches and waits are tracked separately.
|
|
16
|
+
|
|
17
|
+
## 0.28.0 — Instantly email campaigns
|
|
18
|
+
|
|
19
|
+
- New `advisoros email` command group (alias `instantly`) drives the workspace's Instantly account through its own connected key — the key never leaves the server.
|
|
20
|
+
- `email status`, `email actions`, and `email call <action> --data '{…}'` for any supported action.
|
|
21
|
+
- `email campaigns list|get|create|update|activate|pause|duplicate|delete|sending-status|analytics|steps` — `steps` shows sent, delivered, opened and replied per email.
|
|
22
|
+
- `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).
|
|
23
|
+
- `email accounts list|get|pause|resume|mark-fixed|warmup on/off|limit|test` for sending mailboxes.
|
|
24
|
+
- `email leads list|add|import (CSV, max 500)|remove|move|status|merge|lists|list-create|list-delete|blocklist|block|unblock`.
|
|
25
|
+
- `email tags` and `email labels`.
|
|
26
|
+
- Agent API: new `instantly` resource (alias `email`) with snake_case actions plus `status`, `actions`, `sequence_history`, `sequence_restore`.
|
|
27
|
+
- Destructive commands (delete, remove, restore) ask for confirmation; pass `--yes` in scripts.
|
|
28
|
+
- Changes require an Instantly key on the workspace (Integrations → Instantly); without one, commands return a clear "connect Instantly" message.
|
|
29
|
+
|
|
7
30
|
## 0.27.0 — Campaign engine reliability
|
|
8
31
|
|
|
9
32
|
- 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
|
+
}
|
|
@@ -68,6 +68,20 @@ export function registerLeads(program) {
|
|
|
68
68
|
} catch (e) { fail(e); }
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
withCommonOpts(leads.command("company-refresh <ids...>"))
|
|
72
|
+
.description("Refresh company news (Brave via Madnion) for up to 25 leads")
|
|
73
|
+
.option("--force", "Ignore the 14-day freshness window")
|
|
74
|
+
.action(async function (ids, opts) {
|
|
75
|
+
try {
|
|
76
|
+
const r = await call("leads", "company_refresh", { ids, force: !!opts.force }, ctxFrom(this));
|
|
77
|
+
emit(this, r, (d) => {
|
|
78
|
+
const c = d.company_refresh || {};
|
|
79
|
+
if (c.skipped) return process.stdout.write(`Skipped: ${c.reason ?? "company news is off for this workspace"}\n`);
|
|
80
|
+
process.stdout.write(`✓ ${c.enriched ?? 0} companies refreshed, ${c.skipped_fresh ?? 0} already fresh, ${(c.errors || []).length} errors\n`);
|
|
81
|
+
});
|
|
82
|
+
} catch (e) { fail(e); }
|
|
83
|
+
});
|
|
84
|
+
|
|
71
85
|
withCommonOpts(leads.command("set-status <id> <status>"))
|
|
72
86
|
.description("Update a lead's status")
|
|
73
87
|
.action(async function (id, status) {
|
|
@@ -77,6 +91,24 @@ export function registerLeads(program) {
|
|
|
77
91
|
} catch (e) { fail(e); }
|
|
78
92
|
});
|
|
79
93
|
|
|
94
|
+
withCommonOpts(leads.command("import-engagers"))
|
|
95
|
+
.description("Import people who recently commented on your LinkedIn posts (via Taplio)")
|
|
96
|
+
.option("--max-posts <n>", "recent posts to scan (max 50)", "20")
|
|
97
|
+
.action(async function () {
|
|
98
|
+
try {
|
|
99
|
+
const o = this.optsWithGlobals();
|
|
100
|
+
const r = await call("leads", "import_engagers", {
|
|
101
|
+
max_posts: Number(o.maxPosts),
|
|
102
|
+
}, ctxFrom(this));
|
|
103
|
+
emit(this, r, (d) => {
|
|
104
|
+
if (d.reason) return process.stdout.write(`${d.reason}\n`);
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`✓ ${d.imported} new lead(s) imported · ${d.already_known} already known · ${d.posts_scanned ?? 0} posts scanned\n`,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
} catch (e) { fail(e); }
|
|
110
|
+
});
|
|
111
|
+
|
|
80
112
|
withCommonOpts(leads.command("import"))
|
|
81
113
|
.description("Bulk upsert leads from a CSV or JSON/JSONL file")
|
|
82
114
|
.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);
|