argorant 0.5.0 → 0.6.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/README.md +21 -33
- package/bin/argorant.js +358 -160
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,43 +122,31 @@ needs a paid plan" apart from "something broke".
|
|
|
122
122
|
command always wins over the stored one, `ARGORANT_API_BASE` overrides both,
|
|
123
123
|
and `argorant logout` removes the file.
|
|
124
124
|
|
|
125
|
-
## Campaigns
|
|
125
|
+
## Campaigns and inboxes
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
terminal (create → copy → inboxes → leads → start), in about two minutes.
|
|
129
|
-
This group talks to a different, internal surface than everything above and
|
|
130
|
-
only works for an `ag_live_` key that belongs to an **owner/admin** account
|
|
131
|
-
and carries the `argorant:operator` scope — a normal customer key gets a
|
|
132
|
-
401/403, same as a browser session would without outbound access.
|
|
127
|
+
Email outreach from the terminal, with any Argorant API key. Nothing sends until you launch.
|
|
133
128
|
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
```
|
|
130
|
+
argorant campaigns list
|
|
131
|
+
argorant campaigns create --name "Q4 CFO outreach" [--daily-limit 40] [--timezone Europe/Berlin] [--gap 60-120] [--start 2026-10-01]
|
|
132
|
+
argorant campaigns emails set <campaign> --step 1 --subject "Quick question, {{first_name}}" --body-file ./step1.txt
|
|
133
|
+
argorant campaigns emails set <campaign> --step 2 --body "Bumping this." --delay-days 3
|
|
134
|
+
argorant campaigns emails set <campaign> --file emails.json # whole sequence at once
|
|
135
|
+
argorant campaigns leads add <campaign> --list <list-id> # a saved list
|
|
136
|
+
argorant campaigns leads add <campaign> --csv leads.csv # email, first_name, last_name, company, title
|
|
137
|
+
argorant campaigns senders set <campaign> --emails a@x.com,b@y.com # or --all
|
|
138
|
+
argorant campaigns launch <campaign> [-y] # asks for confirmation
|
|
139
|
+
argorant campaigns pause | stop | status <campaign>
|
|
140
|
+
|
|
141
|
+
argorant inboxes list
|
|
142
|
+
argorant inboxes connect-google --admin admin@yourdomain.com # lists mailboxes (or the delegation setup to do first)
|
|
143
|
+
argorant inboxes connect-google --admin admin@yourdomain.com --emails a@x.com,b@x.com # or --all
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
| `campaigns steps set <campaign> --step <n> --subject "..." (--body-file <path> \| --body <text\|->) [--approve]` | Upsert one sequence step's copy. The CLI never writes copy for you. |
|
|
149
|
-
| `campaigns inboxes attach <campaign> --count <n> [--pool <brand>]` | Attach N healthy, unattached sending inboxes (explicit fleet change; prints exactly which ones) |
|
|
150
|
-
| `campaigns leads add <campaign> --csv <file>` | Import leads from a CSV (email + optional first_name/last_name/company/title/...) |
|
|
151
|
-
| `campaigns leads add <campaign> --query "..." [filters]` | Enroll leads straight from a server-side search (same filters as above) |
|
|
152
|
-
| `campaigns start <campaign>` / `pause <campaign>` | Start (auto-approves draft copy + campaign, background-schedules sends) / pause |
|
|
153
|
-
| `campaigns status <campaign>` | Setup completeness (steps/inboxes/leads) + launch blockers |
|
|
154
|
-
|
|
155
|
-
`<campaign>` accepts a raw id or an unambiguous case-insensitive name prefix;
|
|
156
|
-
an ambiguous prefix lists every match instead of guessing.
|
|
157
|
-
|
|
158
|
-
**Known gap:** `campaigns leads add --query` has no server-side row cap yet —
|
|
159
|
-
`-n/--limit` is accepted for a familiar CLI surface but not forwarded/honored
|
|
160
|
-
(it enrolls every valid match up to the platform's own cap). The CLI warns
|
|
161
|
-
when you pass it. See `cli/GODMODE-PLAN.md`.
|
|
146
|
+
`<campaign>` accepts an id or an unambiguous case-insensitive name prefix. Every address is verified before
|
|
147
|
+
it is enrolled; only valid ones send unless `--include-catch-all`. Variables: `{{first_name}}`, `{{last_name}}`,
|
|
148
|
+
`{{company}}`, `{{title}}`; spintax `{a|b}` varies phrasing. `--query "..."` filter enrollment stays available
|
|
149
|
+
for operator keys.
|
|
162
150
|
|
|
163
151
|
## Releasing
|
|
164
152
|
|
package/bin/argorant.js
CHANGED
|
@@ -26,6 +26,7 @@ const dim = (s) => c("2", s);
|
|
|
26
26
|
const green = (s) => c("32", s);
|
|
27
27
|
const red = (s) => c("31", s);
|
|
28
28
|
const cyan = (s) => c("36", s);
|
|
29
|
+
const yellow = (s) => c("33", s);
|
|
29
30
|
|
|
30
31
|
function die(msg, code = 1) {
|
|
31
32
|
process.stderr.write(red("error: ") + msg + "\n");
|
|
@@ -75,6 +76,9 @@ const VALUE_FLAGS = {
|
|
|
75
76
|
"--company": "company_name",
|
|
76
77
|
"--domain": "company_domain",
|
|
77
78
|
"--website": "website",
|
|
79
|
+
// Used by `enrich` (email → full profile). Harmless on the other commands,
|
|
80
|
+
// which simply ignore an unknown query field.
|
|
81
|
+
"--email": "email",
|
|
78
82
|
};
|
|
79
83
|
// Boolean filter flags (presence => "true"). --verified-only is positive intent
|
|
80
84
|
// only (deliverable contacts); there is deliberately NO flag to query invalid or
|
|
@@ -348,6 +352,14 @@ async function cmdLogin(args) {
|
|
|
348
352
|
if (!key) die("no key provided.");
|
|
349
353
|
if (!/^ag_(live|test)_/.test(key)) process.stderr.write(dim("note: keys normally start with ag_live_ — continuing anyway.\n"));
|
|
350
354
|
const res = await request("GET", args.base, "/api/mcp/account", { key });
|
|
355
|
+
if (res.status === 428 && res.json?.detail?.error === "access_key_activation_required") {
|
|
356
|
+
const url = res.json.detail.claim_url;
|
|
357
|
+
process.stdout.write(cyan("Your access key is reserved. Confirm your email to activate it:\n"));
|
|
358
|
+
process.stdout.write(`${url}\n\n`);
|
|
359
|
+
process.stdout.write(dim("After confirmation, run the same login command again.\n"));
|
|
360
|
+
process.exitCode = 3;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
351
363
|
if (res.status === 401) die("that key was rejected (401). Double-check you copied the whole ag_live_ key.", 2);
|
|
352
364
|
const acct = need(res, "login");
|
|
353
365
|
saveConfig({ apiKey: key, base: args.baseExplicit && args.base !== DEFAULT_BASE ? args.base : undefined });
|
|
@@ -570,6 +582,96 @@ async function cmdReveal(args) {
|
|
|
570
582
|
}
|
|
571
583
|
}
|
|
572
584
|
|
|
585
|
+
// ---- enrich: one record in, one record out. Three modes, one endpoint:
|
|
586
|
+
// --email <addr> → the person behind an address (billed like a reveal)
|
|
587
|
+
// --name "<n>" --domain <d> → find that person and reveal a verified email
|
|
588
|
+
// --domain <d> → the company profile, free
|
|
589
|
+
// Exit code is part of the contract: a miss returns 0 rows and exits 1, so a
|
|
590
|
+
// script or agent can branch on it without parsing the payload.
|
|
591
|
+
function normalizeDomain(raw) {
|
|
592
|
+
return String(raw || "")
|
|
593
|
+
.trim()
|
|
594
|
+
.replace(/^https?:\/\//i, "")
|
|
595
|
+
.replace(/^www\./i, "")
|
|
596
|
+
.split(/[/?#]/, 1)[0]
|
|
597
|
+
.toLowerCase();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function cmdEnrich(args) {
|
|
601
|
+
const key = requireKey();
|
|
602
|
+
const usage =
|
|
603
|
+
'usage: argorant enrich --email <name@company.com>\n' +
|
|
604
|
+
' argorant enrich --name "Jane Doe" --domain <company.com>\n' +
|
|
605
|
+
" argorant enrich --domain <company.com>";
|
|
606
|
+
const positional = (args._[0] || "").trim();
|
|
607
|
+
let email = String(args.filters.email || "").trim().toLowerCase();
|
|
608
|
+
let domain = String(args.filters.company_domain || "").trim();
|
|
609
|
+
const name = String(args.name || "").trim();
|
|
610
|
+
// Bare argument: an address is an email, anything else is a domain.
|
|
611
|
+
if (!email && !domain && positional) {
|
|
612
|
+
if (positional.includes("@")) email = positional.toLowerCase();
|
|
613
|
+
else domain = positional;
|
|
614
|
+
}
|
|
615
|
+
domain = domain ? normalizeDomain(domain) : "";
|
|
616
|
+
if (email && !email.includes("@")) die(`--email needs a full address (got "${email}").`);
|
|
617
|
+
if (!email && !domain) die(usage);
|
|
618
|
+
if (name && !domain) die(`--name needs --domain (the company the person works at).\n${usage}`);
|
|
619
|
+
const body = {};
|
|
620
|
+
if (email) body.email = email;
|
|
621
|
+
else if (name) { body.name = name; body.domain = domain; }
|
|
622
|
+
else body.domain = domain;
|
|
623
|
+
const personMode = Boolean(body.email || body.name);
|
|
624
|
+
// Company mode is free, so it never asks. Person mode is billed exactly like
|
|
625
|
+
// a reveal, so it gets the same interactive-only confirmation as `reveal`.
|
|
626
|
+
if (personMode && !args.yes && !args.json && process.stdin.isTTY) {
|
|
627
|
+
const ans = await prompt(
|
|
628
|
+
`Enrich this contact? A match costs 1 credit; a miss and a non-deliverable address are free. [y/N] `
|
|
629
|
+
);
|
|
630
|
+
if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
|
|
631
|
+
}
|
|
632
|
+
const res = await request("POST", args.base, "/api/v1/enrich", { key, body });
|
|
633
|
+
const r = need(res, "enrich");
|
|
634
|
+
if (args.json) {
|
|
635
|
+
console.log(JSON.stringify(r, null, 2));
|
|
636
|
+
if (!r.found) process.exit(EXIT.ERROR);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (!r.found) {
|
|
640
|
+
console.log(dim(`no ${r.type || (personMode ? "person" : "company")} found · 0 credits charged`));
|
|
641
|
+
process.exit(EXIT.ERROR);
|
|
642
|
+
}
|
|
643
|
+
if (r.type === "company") {
|
|
644
|
+
const co = r.company || {};
|
|
645
|
+
console.log(`${bold(co.company || domain)} ${dim(co.company_domain || domain)}`);
|
|
646
|
+
const where = [co.city, co.state, co.country].filter(Boolean).join(", ");
|
|
647
|
+
const what = co.industry || (co.industries || [])[0];
|
|
648
|
+
if (what) console.log(` ${what}`);
|
|
649
|
+
if (where) console.log(` ${dim(where)}`);
|
|
650
|
+
console.log(dim(" company profile · 0 credits charged"));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const p = r.person || {};
|
|
654
|
+
const who = [p.full_name || [p.first_name, p.last_name].filter(Boolean).join(" "), p.title].filter(Boolean).join(" · ");
|
|
655
|
+
console.log(` ${bold(who || "—")}`);
|
|
656
|
+
const bits = [
|
|
657
|
+
p.email && cyan(p.email),
|
|
658
|
+
p.phone,
|
|
659
|
+
p.linkedin_url,
|
|
660
|
+
[p.current_company_name, p.current_company_domain, p.country].filter(Boolean).join(", "),
|
|
661
|
+
].filter(Boolean);
|
|
662
|
+
if (bits.length) console.log(" " + bits.join(dim(" · ")));
|
|
663
|
+
if (r.deliverable === false) {
|
|
664
|
+
console.log(" " + dim(r.message || "This address did not pass live verification, so nothing was charged."));
|
|
665
|
+
}
|
|
666
|
+
const charged = Number(r.charged || 0);
|
|
667
|
+
console.log(
|
|
668
|
+
dim(
|
|
669
|
+
` ${charged === 0 ? "0 credits charged" : `${charged} credit${charged === 1 ? "" : "s"} charged`}` +
|
|
670
|
+
(r.already_revealed ? " · already in your workspace" : "")
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
573
675
|
const EXPORT_TERMINAL_OK = ["completed", "done", "ready", "succeeded"];
|
|
574
676
|
const EXPORT_TERMINAL_FAIL = ["failed", "error", "cancelled", "canceled"];
|
|
575
677
|
|
|
@@ -967,36 +1069,34 @@ function readStdin() {
|
|
|
967
1069
|
});
|
|
968
1070
|
}
|
|
969
1071
|
|
|
970
|
-
async function fetchCampaigns(base, key
|
|
971
|
-
const
|
|
972
|
-
if (brand) query.brand = brand;
|
|
973
|
-
const res = await request("GET", base, "/api/sequencer/campaigns", { key, query });
|
|
1072
|
+
async function fetchCampaigns(base, key) {
|
|
1073
|
+
const res = await request("GET", base, "/api/v1/campaigns", { key, query: { limit: "500" } });
|
|
974
1074
|
const r = need(res, "campaigns list");
|
|
975
1075
|
return r.campaigns || [];
|
|
976
1076
|
}
|
|
977
1077
|
|
|
978
|
-
// <campaign> accepts a raw id (uuid) or an unambiguous case-insensitive name
|
|
979
|
-
// prefix. Errors listing every match when the prefix is ambiguous.
|
|
980
1078
|
async function resolveCampaign(base, key, identifier) {
|
|
981
|
-
if (!identifier) die("campaign id or name is required.");
|
|
982
1079
|
if (UUID_RE.test(identifier)) return identifier;
|
|
983
|
-
const
|
|
984
|
-
const
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
if (matches.length
|
|
1080
|
+
const needle = String(identifier || "").toLowerCase();
|
|
1081
|
+
const campaigns = await fetchCampaigns(base, key);
|
|
1082
|
+
const exact = campaigns.filter((c) => String(c.name || "").toLowerCase() === needle);
|
|
1083
|
+
const matches = exact.length ? exact : campaigns.filter((c) => String(c.name || "").toLowerCase().startsWith(needle));
|
|
1084
|
+
if (!matches.length) {
|
|
988
1085
|
die(`no campaign matching "${identifier}". Run \`argorant campaigns list\` to see names.`);
|
|
989
1086
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
)
|
|
1087
|
+
if (matches.length > 1) {
|
|
1088
|
+
die(
|
|
1089
|
+
`"${identifier}" matches ${matches.length} campaigns — be more specific:\n` +
|
|
1090
|
+
matches.map((c) => ` ${c.name} ${dim(c.id)}`).join("\n")
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return matches[0].id;
|
|
994
1094
|
}
|
|
995
1095
|
|
|
996
1096
|
async function campaignsList(argv) {
|
|
997
1097
|
const key = requireKey();
|
|
998
|
-
const args = readFlags(argv
|
|
999
|
-
const campaigns = await fetchCampaigns(args.base, key
|
|
1098
|
+
const args = readFlags(argv);
|
|
1099
|
+
const campaigns = await fetchCampaigns(args.base, key);
|
|
1000
1100
|
if (args.json) return console.log(JSON.stringify(campaigns, null, 2));
|
|
1001
1101
|
if (!campaigns.length) {
|
|
1002
1102
|
return console.log(dim('No campaigns yet. Create one with `argorant campaigns create --name "..."`.'));
|
|
@@ -1008,7 +1108,9 @@ async function campaignsList(argv) {
|
|
|
1008
1108
|
console.log(`${bold(c.name || "—")} ${dim(c.id)}`);
|
|
1009
1109
|
console.log(
|
|
1010
1110
|
` ${c.status}` +
|
|
1011
|
-
dim(" ·
|
|
1111
|
+
dim(" · leads ") + Number(c.lead_count || 0).toLocaleString() +
|
|
1112
|
+
dim(" · senders ") + Number(c.inbox_count || 0).toLocaleString() +
|
|
1113
|
+
dim(" · sent ") + sent.toLocaleString() +
|
|
1012
1114
|
dim(" · replies ") + replied.toLocaleString() +
|
|
1013
1115
|
dim(" · reply rate ") + rate
|
|
1014
1116
|
);
|
|
@@ -1019,43 +1121,102 @@ async function campaignsCreate(argv) {
|
|
|
1019
1121
|
const key = requireKey();
|
|
1020
1122
|
const args = readFlags(
|
|
1021
1123
|
argv,
|
|
1022
|
-
{ "--name": "name", "--
|
|
1023
|
-
{ "--
|
|
1124
|
+
{ "--name": "name", "--timezone": "timezone", "--daily-limit": "dailyLimit", "--gap": "gap", "--start": "start" },
|
|
1125
|
+
{ "--html": "html" }
|
|
1024
1126
|
);
|
|
1025
1127
|
const name = (args.name || "").trim();
|
|
1026
1128
|
if (!name) {
|
|
1027
|
-
die(
|
|
1028
|
-
'usage: argorant campaigns create --name "<name>" [--brand <key>] [--timezone <tz>] ' +
|
|
1029
|
-
"[--window HH:MM-HH:MM] [--skip-weekends | --no-skip-weekends]"
|
|
1030
|
-
);
|
|
1129
|
+
die('usage: argorant campaigns create --name "<name>" [--daily-limit 100] [--timezone America/New_York] [--gap 60-120] [--start YYYY-MM-DD] [--html]');
|
|
1031
1130
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (
|
|
1043
|
-
body.
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (args.noSkipWeekends) body.skip_weekends = false;
|
|
1048
|
-
const res = await request("POST", args.base, "/api/sequencer/campaigns", { key, body });
|
|
1131
|
+
const body = { name };
|
|
1132
|
+
if (args.timezone) body.timezone = args.timezone;
|
|
1133
|
+
if (args.dailyLimit) body.daily_limit = parseLimit(args.dailyLimit, "--daily-limit");
|
|
1134
|
+
if (args.gap) {
|
|
1135
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(args.gap);
|
|
1136
|
+
if (!m) die("--gap must look like 60-120 (minutes between two sends from one mailbox)");
|
|
1137
|
+
body.gap_minutes_min = Number(m[1]);
|
|
1138
|
+
body.gap_minutes_max = Number(m[2] || m[1]);
|
|
1139
|
+
}
|
|
1140
|
+
if (args.start) {
|
|
1141
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(args.start)) die("--start must be YYYY-MM-DD");
|
|
1142
|
+
body.planned_start_date = args.start;
|
|
1143
|
+
}
|
|
1144
|
+
if (args.html) body.plain_text = false;
|
|
1145
|
+
const res = await request("POST", args.base, "/api/v1/campaigns", { key, body });
|
|
1049
1146
|
const r = need(res, "campaigns create");
|
|
1050
|
-
const c = r.campaign ||
|
|
1147
|
+
const c = r.campaign || r;
|
|
1051
1148
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1052
|
-
console.log(green("✓") + ` Created campaign ${bold(c.name)} ${dim(c.id)}`);
|
|
1053
|
-
console.log(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1149
|
+
console.log(green("✓") + ` Created campaign ${bold(c.name || name)} ${dim(c.id)}`);
|
|
1150
|
+
console.log(dim(` ${c.status || "draft"} · ${c.default_timezone || body.timezone || "America/New_York"} · ${c.daily_limit || body.daily_limit || 100} a day`));
|
|
1151
|
+
console.log(dim(`Next: argorant campaigns emails set ${c.id} --step 1 --subject "..." --body-file ./copy.txt`));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// emails: the whole sequence at once (--file emails.json, or - for stdin) or
|
|
1155
|
+
// one step merged into what is already there (--step n ...).
|
|
1156
|
+
async function campaignsEmails(argv) {
|
|
1157
|
+
const sub = argv[0];
|
|
1158
|
+
if (sub !== "set") {
|
|
1159
|
+
die(
|
|
1160
|
+
'usage: argorant campaigns emails set <campaign> --file emails.json\n' +
|
|
1161
|
+
' or: argorant campaigns emails set <campaign> --step <n> --subject "..." (--body-file <path> | --body <text|->) [--delay-days <d>] [--new-thread]\n' +
|
|
1162
|
+
' emails.json: [{"subject":"...","body":"...","delay_days":0},{"body":"...","delay_days":3}]'
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
const key = requireKey();
|
|
1166
|
+
const args = readFlags(
|
|
1167
|
+
argv.slice(1),
|
|
1168
|
+
{ "--file": "file", "--step": "step", "--subject": "subject", "--body": "body", "--body-file": "bodyFile", "--delay-days": "delayDays" },
|
|
1169
|
+
{ "--new-thread": "newThread" }
|
|
1057
1170
|
);
|
|
1058
|
-
|
|
1171
|
+
const identifier = args._[0];
|
|
1172
|
+
if (!identifier) die("usage: argorant campaigns emails set <campaign> ...");
|
|
1173
|
+
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1174
|
+
let emails;
|
|
1175
|
+
if (args.file) {
|
|
1176
|
+
const text = args.file === "-" ? await readStdin() : (() => { try { return fs.readFileSync(args.file, "utf8"); } catch { die(`cannot read file: ${args.file}`); } })();
|
|
1177
|
+
try { emails = JSON.parse(text); } catch (e) { die(`${args.file}: not valid JSON (${e.message})`); }
|
|
1178
|
+
if (!Array.isArray(emails) || !emails.length) die("emails.json must be a non-empty array");
|
|
1179
|
+
} else {
|
|
1180
|
+
const step = Number(args.step);
|
|
1181
|
+
if (!Number.isInteger(step) || step < 1) die("--step must be 1, 2, 3 …");
|
|
1182
|
+
let body = args.body;
|
|
1183
|
+
if (args.bodyFile) { try { body = fs.readFileSync(args.bodyFile, "utf8"); } catch { die(`cannot read file: ${args.bodyFile}`); } }
|
|
1184
|
+
else if (body === "-") body = await readStdin();
|
|
1185
|
+
if (!body || !body.trim()) die("give the email text with --body or --body-file");
|
|
1186
|
+
if (step === 1 && !(args.subject || "").trim()) die("the first email needs --subject");
|
|
1187
|
+
// merge into the current sequence
|
|
1188
|
+
const cur = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}`, { key }), "campaign");
|
|
1189
|
+
const existing = (cur.emails || []).map((e) => ({ subject: e.subject || "", body: e.body || "", delay_days: e.delay_days || 0, same_thread: e.same_thread !== false }));
|
|
1190
|
+
while (existing.length < step) existing.push({ subject: "", body: "", delay_days: 3, same_thread: true });
|
|
1191
|
+
existing[step - 1] = {
|
|
1192
|
+
subject: (args.subject || existing[step - 1].subject || "").trim(),
|
|
1193
|
+
body,
|
|
1194
|
+
delay_days: args.delayDays !== undefined ? Number(args.delayDays) : (step === 1 ? 0 : existing[step - 1].delay_days || 3),
|
|
1195
|
+
same_thread: !args.newThread,
|
|
1196
|
+
};
|
|
1197
|
+
emails = existing.filter((e) => e.body && e.body.trim());
|
|
1198
|
+
}
|
|
1199
|
+
const res = await request("POST", args.base, `/api/v1/campaigns/${campaignId}/emails`, { key, body: { emails } });
|
|
1200
|
+
const r = need(res, "campaigns emails set");
|
|
1201
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1202
|
+
console.log(green("✓") + ` Saved ${(r.emails || emails).length} email(s) for ${dim(campaignId)}`);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// senders: which connected mailboxes send this campaign.
|
|
1206
|
+
async function campaignsSenders(argv) {
|
|
1207
|
+
const sub = argv[0];
|
|
1208
|
+
if (sub !== "set") die("usage: argorant campaigns senders set <campaign> --emails a@x.com,b@y.com | --all");
|
|
1209
|
+
const key = requireKey();
|
|
1210
|
+
const args = readFlags(argv.slice(1), { "--emails": "emails" }, { "--all": "all" });
|
|
1211
|
+
const identifier = args._[0];
|
|
1212
|
+
if (!identifier) die("usage: argorant campaigns senders set <campaign> --emails a@x.com,b@y.com | --all");
|
|
1213
|
+
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1214
|
+
const emails = String(args.emails || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
1215
|
+
if (!emails.length && !args.all) die("give --emails <a,b,c> or --all (see: argorant inboxes list)");
|
|
1216
|
+
const res = await request("POST", args.base, `/api/v1/campaigns/${campaignId}/senders`, { key, body: { emails, all_connected: !!args.all } });
|
|
1217
|
+
const r = need(res, "campaigns senders set");
|
|
1218
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1219
|
+
console.log(green("✓") + ` ${r.attached || 0} sender(s) attached` + (r.senders && r.senders.length ? dim(` ${r.senders.join(", ")}`) : ""));
|
|
1059
1220
|
}
|
|
1060
1221
|
|
|
1061
1222
|
async function campaignsSteps(argv) {
|
|
@@ -1165,153 +1326,133 @@ async function campaignsInboxes(argv) {
|
|
|
1165
1326
|
|
|
1166
1327
|
async function campaignsLeads(argv) {
|
|
1167
1328
|
const sub = argv[0];
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1329
|
+
const usage =
|
|
1330
|
+
'usage: argorant campaigns leads add <campaign> --list <list-id>\n' +
|
|
1331
|
+
' or: argorant campaigns leads add <campaign> --csv <file> (columns: email, first_name, last_name, company, title)\n' +
|
|
1332
|
+
' or: argorant campaigns leads add <campaign> --query "..." [filters] -n <n> (operator keys)';
|
|
1333
|
+
if (sub !== "add") die(usage);
|
|
1174
1334
|
const key = requireKey();
|
|
1175
|
-
const valueFlags = { ...CAMPAIGN_FILTER_VALUE_FLAGS, "--csv": "csv", "-n": "limit", "--limit": "limit" };
|
|
1176
|
-
const args = readFlags(argv.slice(1), valueFlags, CAMPAIGN_FILTER_BOOL_FLAGS);
|
|
1335
|
+
const valueFlags = { ...CAMPAIGN_FILTER_VALUE_FLAGS, "--csv": "csv", "--file": "csv", "--list": "list", "-n": "limit", "--limit": "limit" };
|
|
1336
|
+
const args = readFlags(argv.slice(1), valueFlags, { ...CAMPAIGN_FILTER_BOOL_FLAGS, "--include-catch-all": "includeCatchAll" });
|
|
1177
1337
|
const identifier = args._[0];
|
|
1178
|
-
if (!identifier)
|
|
1179
|
-
die(
|
|
1180
|
-
'usage: argorant campaigns leads add <campaign> --csv <file>\n' +
|
|
1181
|
-
' or: argorant campaigns leads add <campaign> --query "..." [filters] -n <n>'
|
|
1182
|
-
);
|
|
1183
|
-
}
|
|
1338
|
+
if (!identifier) die(usage);
|
|
1184
1339
|
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1185
|
-
|
|
1186
|
-
if (args.csv) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
die(`cannot read file: ${args.csv}`);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
"--seniority --department --state --city --company --domain --has-phone --has-linkedin --has-email"
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
// GAP (see GODMODE-PLAN.md): the sequencer's filter-enroll endpoint has no
|
|
1206
|
-
// per-request row cap — it enrolls every valid match up to its own
|
|
1207
|
-
// server-side limit (currently 50,000). -n/--limit is accepted here for a
|
|
1208
|
-
// familiar CLI surface but is NOT forwarded/honored; warn rather than
|
|
1209
|
-
// silently ignoring a flag the caller explicitly set.
|
|
1210
|
-
if (args.limit) {
|
|
1211
|
-
warn("-n/--limit is not honored by campaign lead enrollment yet (platform-side gap, see GODMODE-PLAN.md) — it enrolls every valid match.");
|
|
1340
|
+
|
|
1341
|
+
if (args.list || args.csv) {
|
|
1342
|
+
const body = { include_catch_all: !!args.includeCatchAll };
|
|
1343
|
+
if (args.list) body.list_id = parseLimit(args.list, "--list");
|
|
1344
|
+
else {
|
|
1345
|
+
let text;
|
|
1346
|
+
try { text = fs.readFileSync(args.csv, "utf8"); } catch { die(`cannot read file: ${args.csv}`); }
|
|
1347
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
1348
|
+
if (lines.length < 2) die(`file has no rows: ${args.csv}`);
|
|
1349
|
+
const head = lines.shift().split(",").map((h) => h.trim().toLowerCase().replace(/\s+/g, "_"));
|
|
1350
|
+
if (!head.includes("email")) die("the CSV needs an email column");
|
|
1351
|
+
body.rows = lines.map((ln) => {
|
|
1352
|
+
const cells = ln.split(",");
|
|
1353
|
+
const row = {};
|
|
1354
|
+
head.forEach((h, i) => { if (h) row[h] = (cells[i] || "").trim(); });
|
|
1355
|
+
return row;
|
|
1356
|
+
});
|
|
1212
1357
|
}
|
|
1213
|
-
|
|
1358
|
+
const res = await request("POST", args.base, `/api/v1/campaigns/${campaignId}/leads`, { key, body });
|
|
1359
|
+
const r = need(res, "campaigns leads add");
|
|
1360
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1361
|
+
const v = r.verification_counts || {};
|
|
1362
|
+
console.log(green("✓") + ` Enrolled ${Number(r.inserted || 0).toLocaleString()} of ${Number(r.input_rows || 0).toLocaleString()}`);
|
|
1363
|
+
console.log(dim(` duplicates ${r.duplicates_skipped || 0} · not valid ${r.skipped_not_valid || 0} (invalid ${v.invalid || 0}, catch-all ${v.catch_all || 0}) · checking in background ${r.queued_for_verification || 0}`));
|
|
1364
|
+
return;
|
|
1214
1365
|
}
|
|
1366
|
+
|
|
1367
|
+
// Filter enrollment straight from the database: operator keys.
|
|
1368
|
+
const filters = {};
|
|
1369
|
+
for (const field of Object.values(CAMPAIGN_FILTER_VALUE_FLAGS)) if (args[field]) filters[field] = args[field];
|
|
1370
|
+
for (const field of Object.values(CAMPAIGN_FILTER_BOOL_FLAGS)) if (args[field]) filters[field] = "true";
|
|
1371
|
+
if (!Object.keys(filters).length) die(usage);
|
|
1372
|
+
const body = { filters };
|
|
1373
|
+
if (args.limit) body.limit = parseLimit(args.limit, "-n");
|
|
1215
1374
|
const res = await request("POST", args.base, `/api/sequencer/campaigns/${campaignId}/leads/import`, { key, body });
|
|
1216
1375
|
const r = need(res, "campaigns leads add");
|
|
1217
1376
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1218
|
-
console.log(
|
|
1219
|
-
green("✓") +
|
|
1220
|
-
` ${bold(r.inserted || 0)} lead(s) added` +
|
|
1221
|
-
(r.duplicates_skipped ? dim(` (${r.duplicates_skipped} duplicate already in campaign)`) : "")
|
|
1222
|
-
);
|
|
1223
|
-
if (r.queued_for_verification) {
|
|
1224
|
-
console.log(dim(` ${r.queued_for_verification} queued for verification — will join once confirmed deliverable`));
|
|
1225
|
-
}
|
|
1226
|
-
if (r.skipped_not_valid) console.log(dim(` ${r.skipped_not_valid} skipped (not deliverable)`));
|
|
1227
|
-
if (r.invalid_email_rows) console.log(dim(` ${r.invalid_email_rows} row(s) had no usable email`));
|
|
1377
|
+
console.log(green("✓") + ` Imported ${Number(r.inserted || 0).toLocaleString()} lead(s)` + dim(` (skipped ${r.duplicates_skipped || 0} duplicates, ${r.invalid_rows || 0} invalid)`));
|
|
1228
1378
|
}
|
|
1229
1379
|
|
|
1230
|
-
async function campaignsSetStatus(argv,
|
|
1380
|
+
async function campaignsSetStatus(argv, action) {
|
|
1231
1381
|
const key = requireKey();
|
|
1232
|
-
const args = readFlags(argv
|
|
1382
|
+
const args = readFlags(argv);
|
|
1233
1383
|
const identifier = args._[0];
|
|
1234
|
-
if (!identifier) die(`usage: argorant campaigns ${
|
|
1384
|
+
if (!identifier) die(`usage: argorant campaigns ${action} <campaign>`);
|
|
1235
1385
|
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
die(
|
|
1241
|
-
`cannot ${verb} campaign: ${d.message || "blocked"}` +
|
|
1242
|
-
(blockers.length ? "\n" + blockers.map((b) => ` - ${b}`).join("\n") : "")
|
|
1243
|
-
);
|
|
1386
|
+
if (action === "launch" && !args.yes) {
|
|
1387
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1388
|
+
const ok = await new Promise((r) => rl.question(`Launch ${campaignId} and start sending real email? [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
|
|
1389
|
+
if (!ok) return console.log(dim("aborted"));
|
|
1244
1390
|
}
|
|
1245
|
-
const
|
|
1391
|
+
const res = await request("POST", args.base, `/api/v1/campaigns/${campaignId}/${action}`, { key, body: {} });
|
|
1392
|
+
const r = need(res, `campaigns ${action}`);
|
|
1246
1393
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1247
1394
|
const c = r.campaign || {};
|
|
1248
|
-
console.log(green("✓") + `
|
|
1395
|
+
console.log(green("✓") + ` ${bold(c.name || campaignId)} is now ${bold(c.status || action)}`);
|
|
1249
1396
|
}
|
|
1250
1397
|
|
|
1251
1398
|
async function campaignsStatus(argv) {
|
|
1252
1399
|
const key = requireKey();
|
|
1253
|
-
const args = readFlags(argv
|
|
1400
|
+
const args = readFlags(argv);
|
|
1254
1401
|
const identifier = args._[0];
|
|
1255
1402
|
if (!identifier) die("usage: argorant campaigns status <campaign>");
|
|
1256
1403
|
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1404
|
+
const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}`, { key }), "campaign");
|
|
1405
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1259
1406
|
const c = r.campaign || {};
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
console.log(` ${mark(inboxesOk)} inboxes attached ${dim(String(c.inbox_count || 0))}`);
|
|
1268
|
-
console.log(
|
|
1269
|
-
` ${mark(leadsOk)} leads queued ${dim(`${c.queued_count || 0} queued / ${c.lead_count || 0} total`)}`
|
|
1270
|
-
);
|
|
1271
|
-
const blockers = c.launch_blockers || [];
|
|
1272
|
-
if (blockers.length) {
|
|
1273
|
-
console.log(bold("\nLaunch blockers:"));
|
|
1274
|
-
for (const b of blockers) console.log(` - ${b}`);
|
|
1275
|
-
} else {
|
|
1276
|
-
console.log(green("\n✓ Ready to launch") + dim(` — argorant campaigns start ${c.id}`));
|
|
1407
|
+
console.log(`${bold(c.name || "—")} ${dim(c.id)}`);
|
|
1408
|
+
console.log(` status ${bold(c.status)} · leads ${Number(c.lead_count || 0).toLocaleString()} · sent ${Number(c.sent_count || 0).toLocaleString()} · replies ${Number(c.replied_count || 0).toLocaleString()} · bounced ${Number(c.bounced_count || 0).toLocaleString()}`);
|
|
1409
|
+
console.log(` emails ${(r.emails || []).length} · senders ${(r.senders || []).length} · ${c.daily_limit || "?"} a day · ${c.default_timezone || ""}`);
|
|
1410
|
+
for (const e of r.emails || []) console.log(dim(` ${e.step}. ${e.subject || "(same thread)"}${e.step > 1 ? ` · +${e.delay_days || 0} days` : ""}`));
|
|
1411
|
+
if (r.launch_blockers && r.launch_blockers.length) {
|
|
1412
|
+
console.log(yellow(" before launch:"));
|
|
1413
|
+
for (const b of r.launch_blockers) console.log(yellow(` · ${b}`));
|
|
1277
1414
|
}
|
|
1415
|
+
if (c.url) console.log(dim(` ${c.url}`));
|
|
1278
1416
|
}
|
|
1279
1417
|
|
|
1280
1418
|
function campaignsHelp() {
|
|
1281
1419
|
const p = bold("argorant campaigns");
|
|
1282
1420
|
console.log(`
|
|
1283
|
-
${bold("Argorant Campaigns")} —
|
|
1421
|
+
${bold("Argorant Campaigns")} — email outreach from the terminal
|
|
1284
1422
|
|
|
1285
|
-
|
|
1286
|
-
in about two minutes from the terminal. Requires an ag_live_ key belonging to
|
|
1287
|
-
an owner/admin account with the ${bold("argorant:operator")} scope; any other key gets
|
|
1288
|
-
a 401/403, same as it would in a browser without outbound access.
|
|
1423
|
+
Works with any Argorant API key. Nothing is sent until you launch.
|
|
1289
1424
|
|
|
1290
1425
|
${bold("USAGE")}
|
|
1291
|
-
${p} list
|
|
1292
|
-
${p} create --name "<name>" [--
|
|
1293
|
-
${p}
|
|
1294
|
-
${p}
|
|
1295
|
-
${p} leads add <campaign> --csv <file>
|
|
1296
|
-
${p}
|
|
1297
|
-
${p}
|
|
1426
|
+
${p} list
|
|
1427
|
+
${p} create --name "<name>" [--daily-limit 100] [--timezone <tz>] [--gap 60-120] [--start YYYY-MM-DD]
|
|
1428
|
+
${p} emails set <campaign> --file emails.json
|
|
1429
|
+
${p} emails set <campaign> --step <n> --subject "..." (--body-file <path> | --body <text|->) [--delay-days <d>]
|
|
1430
|
+
${p} leads add <campaign> --list <list-id> | --csv <file> [--include-catch-all]
|
|
1431
|
+
${p} senders set <campaign> --emails a@x.com,b@y.com | --all
|
|
1432
|
+
${p} launch <campaign> [-y] ${dim("asks for confirmation; sends real email")}
|
|
1298
1433
|
${p} pause <campaign>
|
|
1434
|
+
${p} stop <campaign>
|
|
1299
1435
|
${p} status <campaign>
|
|
1300
1436
|
|
|
1301
|
-
${
|
|
1302
|
-
${
|
|
1437
|
+
${bold("argorant inboxes")} list
|
|
1438
|
+
${bold("argorant inboxes")} connect-google --admin <admin@yourdomain.com> [--emails a,b | --all]
|
|
1439
|
+
|
|
1440
|
+
${dim("<campaign> accepts an id or an unambiguous case-insensitive name prefix.")}
|
|
1441
|
+
${dim("Variables in emails: {{first_name}} {{last_name}} {{company}} {{title}}. Spintax {a|b} varies phrasing.")}
|
|
1442
|
+
${dim("Every address is verified before it is enrolled; only valid ones send unless --include-catch-all.")}
|
|
1303
1443
|
|
|
1304
|
-
${bold("EXAMPLE
|
|
1305
|
-
${p} create --name "
|
|
1306
|
-
${p}
|
|
1307
|
-
${p}
|
|
1308
|
-
${p} leads add "
|
|
1309
|
-
${p}
|
|
1444
|
+
${bold("EXAMPLE")}
|
|
1445
|
+
${p} create --name "Q4 CFO outreach" --daily-limit 40
|
|
1446
|
+
${p} emails set "Q4 CFO" --step 1 --subject "Quick question, {{first_name}}" --body-file ./step1.txt
|
|
1447
|
+
${p} emails set "Q4 CFO" --step 2 --body "Bumping this up." --delay-days 3
|
|
1448
|
+
${p} leads add "Q4 CFO" --list 1240
|
|
1449
|
+
${p} senders set "Q4 CFO" --all
|
|
1450
|
+
${p} launch "Q4 CFO"
|
|
1310
1451
|
|
|
1311
1452
|
${bold("OPTIONS")}
|
|
1312
1453
|
--json Raw JSON output --base <url> Override API base
|
|
1313
1454
|
|
|
1314
|
-
Docs: ${cyan("https://argorant.com/docs/cli")}
|
|
1455
|
+
Docs: ${cyan("https://argorant.com/docs/cli")}
|
|
1315
1456
|
`);
|
|
1316
1457
|
}
|
|
1317
1458
|
|
|
@@ -1322,18 +1463,65 @@ async function cmdCampaigns(argv) {
|
|
|
1322
1463
|
const table = {
|
|
1323
1464
|
list: campaignsList,
|
|
1324
1465
|
create: campaignsCreate,
|
|
1466
|
+
emails: campaignsEmails,
|
|
1325
1467
|
steps: campaignsSteps,
|
|
1468
|
+
senders: campaignsSenders,
|
|
1326
1469
|
inboxes: campaignsInboxes,
|
|
1327
1470
|
leads: campaignsLeads,
|
|
1328
|
-
|
|
1329
|
-
|
|
1471
|
+
launch: (a) => campaignsSetStatus(a, "launch"),
|
|
1472
|
+
start: (a) => campaignsSetStatus(a, "launch"),
|
|
1473
|
+
pause: (a) => campaignsSetStatus(a, "pause"),
|
|
1474
|
+
stop: (a) => campaignsSetStatus(a, "stop"),
|
|
1330
1475
|
status: campaignsStatus,
|
|
1476
|
+
get: campaignsStatus,
|
|
1331
1477
|
};
|
|
1332
1478
|
const fn = table[sub];
|
|
1333
1479
|
if (!fn) die(`unknown campaigns subcommand: ${sub}\nRun \`argorant campaigns help\` for usage.`);
|
|
1334
1480
|
await fn(rest);
|
|
1335
1481
|
}
|
|
1336
1482
|
|
|
1483
|
+
async function cmdInboxes(argv) {
|
|
1484
|
+
const sub = argv[0] || "list";
|
|
1485
|
+
const key = requireKey();
|
|
1486
|
+
if (sub === "list") {
|
|
1487
|
+
const args = readFlags(argv.slice(1));
|
|
1488
|
+
const r = need(await request("GET", args.base, "/api/v1/inboxes", { key }), "inboxes list");
|
|
1489
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1490
|
+
const rows = r.inboxes || [];
|
|
1491
|
+
if (!rows.length) return console.log(dim("No mailboxes connected yet. Connect Google Workspace with `argorant inboxes connect-google --admin ...` or Microsoft 365 in the app."));
|
|
1492
|
+
for (const i of rows) {
|
|
1493
|
+
console.log(`${bold(i.email)} ${dim(i.name || "")}`);
|
|
1494
|
+
console.log(dim(` ${i.provider} · ${i.status}${i.health ? ` · ${i.health}` : ""} · today ${i.sent_today || 0}/${i.daily_limit || "?"}`));
|
|
1495
|
+
}
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (sub === "connect-google") {
|
|
1499
|
+
const args = readFlags(argv.slice(1), { "--admin": "admin", "--emails": "emails" }, { "--all": "all" });
|
|
1500
|
+
if (!args.admin) die("usage: argorant inboxes connect-google --admin admin@yourdomain.com [--emails a@x.com,b@x.com | --all]");
|
|
1501
|
+
const body = { admin_email: args.admin };
|
|
1502
|
+
if (args.all) body.emails = [];
|
|
1503
|
+
else if (args.emails) body.emails = String(args.emails).split(",").map((x) => x.trim()).filter(Boolean);
|
|
1504
|
+
const r = need(await request("POST", args.base, "/api/v1/inboxes/google-workspace", { key, body }), "inboxes connect-google");
|
|
1505
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1506
|
+
if (r.ok === false && r.setup) {
|
|
1507
|
+
console.log(yellow("Delegation is not active yet.") + ` ${r.reason || ""}`);
|
|
1508
|
+
console.log(`In Google Admin > Security > API controls > Domain-wide delegation add:\n Client ID: ${bold(r.setup.client_id)}\n Scopes: ${r.setup.scopes_csv || (r.setup.scopes || []).join(",")}`);
|
|
1509
|
+
if (r.setup.delegation_url) console.log(dim(` ${r.setup.delegation_url}`));
|
|
1510
|
+
console.log(dim("Then run this command again."));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (r.mailboxes) {
|
|
1514
|
+
for (const m of r.mailboxes) console.log(`${m.connected ? green("✓") : " "} ${m.email} ${dim(m.name || "")}${m.alias ? dim(" alias") : ""}`);
|
|
1515
|
+
console.log(dim("\nconnect with: --emails a@x.com,b@x.com or --all"));
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
console.log(green("✓") + ` Connected ${(r.connected || []).length} mailbox(es)`);
|
|
1519
|
+
for (const c of r.connected || []) console.log(dim(` ${c.email} · ${c.health || "ok"}`));
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
die(`unknown inboxes subcommand: ${sub} (list, connect-google)`);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1337
1525
|
function help() {
|
|
1338
1526
|
const p = bold("argorant");
|
|
1339
1527
|
console.log(`
|
|
@@ -1351,6 +1539,9 @@ ${bold("COMMANDS")}
|
|
|
1351
1539
|
${cyan("search")} "<query>" -n 10 Preview matches, details redacted ${dim("(0 contact credits)")}
|
|
1352
1540
|
${cyan("sample")} <company.com> Build 25 distinct, live-valid company leads ${dim("(free sample)")}
|
|
1353
1541
|
${cyan("reveal")} "<query>" -n 25 Reveal full contact details ${dim("(uses credits; live-verified, pay only for deliverable)")}
|
|
1542
|
+
${cyan("enrich")} --email <a@b.com> One address → the full person profile ${dim("(1 credit per match; miss = free)")}
|
|
1543
|
+
${cyan("enrich")} --name "<n>" --domain <d> Find that person + reveal a verified email ${dim("(1 credit per match)")}
|
|
1544
|
+
${cyan("enrich")} --domain <d> Company profile ${dim("(0 contact credits)")}
|
|
1354
1545
|
${cyan("export")} "<query>" -n 1000 -o leads.csv Verified CSV export ${dim("(uses credits)")}
|
|
1355
1546
|
${cyan("export status")} <job_id> Status of an existing export ${dim("(free; add --batch for >50k)")}
|
|
1356
1547
|
${cyan("export download")} <job_id> -o leads.csv Re-download a finished export ${dim("(free)")}
|
|
@@ -1358,7 +1549,8 @@ ${bold("COMMANDS")}
|
|
|
1358
1549
|
${cyan("list status")} <id> Show a saved list's size ${dim("(free)")}
|
|
1359
1550
|
${cyan("verify")} <email> Verify one of your own emails ${dim("(verification pool)")}
|
|
1360
1551
|
${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
|
|
1361
|
-
${cyan("campaigns")} ...
|
|
1552
|
+
${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
|
|
1553
|
+
${cyan("inboxes")} list | connect-google Connected mailboxes; connect a Google Workspace
|
|
1362
1554
|
|
|
1363
1555
|
${bold("FILTERS")}
|
|
1364
1556
|
--keywords <k> Comma = OR. The widest, most reliable filter - prefer it
|
|
@@ -1390,6 +1582,7 @@ ${bold("NON-INTERACTIVE USE")} ${dim("(agents, CI, pipes)")}
|
|
|
1390
1582
|
${bold("EXIT CODES")}
|
|
1391
1583
|
0 ok · 1 error · 2 not authenticated · 3 forbidden (missing scope)
|
|
1392
1584
|
4 rate limit / daily quota · 5 plan upgrade required
|
|
1585
|
+
${dim("`enrich` exits 1 when nothing matched, so scripts can branch without parsing the payload.")}
|
|
1393
1586
|
|
|
1394
1587
|
${bold("EXAMPLES")}
|
|
1395
1588
|
${p} count "fintech CFOs in germany"
|
|
@@ -1397,6 +1590,9 @@ ${bold("EXAMPLES")}
|
|
|
1397
1590
|
${p} search "heads of procurement" --country Germany -n 10
|
|
1398
1591
|
${p} sample recruitcrm.io -o sample.csv
|
|
1399
1592
|
${p} export --industry fintech --title CFO --country Germany -n 500 -o cfos.csv
|
|
1593
|
+
${p} enrich --email patrick@stripe.com --json
|
|
1594
|
+
${p} enrich --name "Patrick Collison" --domain stripe.com
|
|
1595
|
+
${p} enrich --domain stripe.com
|
|
1400
1596
|
${p} verify ceo@stripe.com
|
|
1401
1597
|
${p} verify --file my-list.csv -o verified.csv
|
|
1402
1598
|
|
|
@@ -1412,9 +1608,10 @@ async function main() {
|
|
|
1412
1608
|
// `campaigns` has its own flag vocabulary (--step, --count, --pool, --csv, ...)
|
|
1413
1609
|
// handled by readFlags — it never goes through the generic filter parser
|
|
1414
1610
|
// below, which would reject those flags as unknown.
|
|
1415
|
-
if (cmd === "campaigns") {
|
|
1611
|
+
if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "inbox") {
|
|
1416
1612
|
try {
|
|
1417
|
-
await cmdCampaigns(argv.slice(1));
|
|
1613
|
+
if (cmd.startsWith("campaign")) await cmdCampaigns(argv.slice(1));
|
|
1614
|
+
else await cmdInboxes(argv.slice(1));
|
|
1418
1615
|
} catch (e) {
|
|
1419
1616
|
die(e && e.message ? e.message : String(e));
|
|
1420
1617
|
}
|
|
@@ -1430,6 +1627,7 @@ async function main() {
|
|
|
1430
1627
|
search: cmdSearch,
|
|
1431
1628
|
sample: cmdSample,
|
|
1432
1629
|
reveal: cmdReveal,
|
|
1630
|
+
enrich: cmdEnrich,
|
|
1433
1631
|
export: cmdExport,
|
|
1434
1632
|
list: cmdList,
|
|
1435
1633
|
verify: cmdVerify,
|
package/package.json
CHANGED