argorant 0.14.1 → 0.15.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 -9
- package/bin/argorant.js +294 -132
- package/bin/search-filters.json +753 -0
- package/package.json +1 -1
package/bin/argorant.js
CHANGED
|
@@ -54,28 +54,138 @@ function resolveKey() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// ---- arg parsing ----
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
// ---- one filter vocabulary (0.15.0) ----
|
|
58
|
+
// Every filter-taking command (count, search, reveal, export, list create, lists create, lists add,
|
|
59
|
+
// companies, company people, campaigns leads add) reads its filters from ONE map, built from the
|
|
60
|
+
// filter schema the API publishes (bin/search-filters.json, the same file the API, the app and the
|
|
61
|
+
// connector use). Each list filter has an include flag and an exclude flag, usable together.
|
|
62
|
+
const FILTER_SCHEMA = (() => {
|
|
63
|
+
try {
|
|
64
|
+
return require("./search-filters.json");
|
|
65
|
+
} catch {
|
|
66
|
+
return { filters: [], modes: {}, cli_flags: {} };
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
// Older spellings that keep working.
|
|
70
|
+
const FILTER_ALIASES = {
|
|
69
71
|
"--keyword": "keywords",
|
|
70
|
-
"--
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
"--exclude-keyword": "exclude_keywords",
|
|
73
|
+
"--departments": "departments",
|
|
74
|
+
"--exclude-departments": "exclude_departments",
|
|
73
75
|
"--geography": "country",
|
|
74
76
|
"--region": "country",
|
|
75
|
-
"--
|
|
76
|
-
"--
|
|
77
|
+
"--exclude-geography": "exclude_country",
|
|
78
|
+
"--exclude-region": "exclude_country",
|
|
77
79
|
"--company": "company_name",
|
|
78
|
-
"--domain": "company_domain",
|
|
80
|
+
"--company-domain": "company_domain",
|
|
81
|
+
"--exclude-company-domain": "exclude_company_domain",
|
|
82
|
+
"--employee-range": "employee_range",
|
|
83
|
+
"--technologies": "technologies",
|
|
84
|
+
"--technology": "technologies",
|
|
85
|
+
"--near-place": "near_place",
|
|
86
|
+
};
|
|
87
|
+
const BOOL_PARAMS = new Set(FILTER_SCHEMA.filters.filter((f) => f.kind === "boolean").map((f) => f.include));
|
|
88
|
+
// flag -> API parameter, value flags only (booleans below). --near takes a place (a city or postal code such as
|
|
89
|
+
// "Denver, Colorado", or a place id) or "lat,lon".
|
|
90
|
+
const FILTER_VALUE_FLAGS = (() => {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [param, flag] of Object.entries(FILTER_SCHEMA.cli_flags || {})) {
|
|
93
|
+
if (!BOOL_PARAMS.has(param)) out[flag] = param;
|
|
94
|
+
}
|
|
95
|
+
out["--near"] = "near";
|
|
96
|
+
return { ...out, ...FILTER_ALIASES };
|
|
97
|
+
})();
|
|
98
|
+
const FILTER_BOOL_FLAGS = (() => {
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [param, flag] of Object.entries(FILTER_SCHEMA.cli_flags || {})) if (BOOL_PARAMS.has(param)) out[flag] = param;
|
|
101
|
+
return out;
|
|
102
|
+
})();
|
|
103
|
+
// Finite values (checked here, before any request): parameter -> [{value, label}].
|
|
104
|
+
const FILTER_ENUMS = (() => {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const f of FILTER_SCHEMA.filters || []) {
|
|
107
|
+
if (f.kind === "enum_list" && Array.isArray(f.values)) {
|
|
108
|
+
for (const p of [f.include, f.exclude]) if (p) out[p] = f.values;
|
|
109
|
+
}
|
|
110
|
+
if (f.kind === "radius") {
|
|
111
|
+
if (Array.isArray(f.units)) out.radius_unit = f.units;
|
|
112
|
+
if (Array.isArray(f.targets)) out.radius_target = f.targets;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [name, m] of Object.entries(FILTER_SCHEMA.modes || {})) if (Array.isArray(m.values)) out[name] = m.values;
|
|
116
|
+
return out;
|
|
117
|
+
})();
|
|
118
|
+
const SINGLE_VALUE = new Set(["title_match", "exclude_title_match", "keyword_scope", "radius_unit", "radius_target"]);
|
|
119
|
+
// Filters for company records (people-only ones are refused by the API on company searches).
|
|
120
|
+
const COMPANY_PARAMS = new Set([
|
|
121
|
+
"q", "industry", "exclude_industry", "industries", "exclude_industries", "country", "exclude_country", "state",
|
|
122
|
+
"exclude_state", "city", "exclude_city", "company_name", "company_domain", "exclude_company_domain", "employee_range",
|
|
123
|
+
"employees_min", "employees_max", "revenue_band", "revenue_min", "revenue_max", "keywords", "exclude_keywords",
|
|
124
|
+
"keyword_scope", "technologies", "near", "near_place", "near_lat", "near_lon", "radius", "radius_unit", "radius_target",
|
|
125
|
+
]);
|
|
126
|
+
// What `company <domain>` passes on (the people at one company).
|
|
127
|
+
const COMPANY_PEOPLE_PARAMS = new Set([
|
|
128
|
+
"title", "exclude_title", "title_match", "exclude_title_match", "seniority", "exclude_seniority", "departments",
|
|
129
|
+
"exclude_departments", "country", "exclude_country", "exclude_state", "exclude_city", "gender", "income_band",
|
|
130
|
+
]);
|
|
131
|
+
function flagOfParam(param) {
|
|
132
|
+
return (FILTER_SCHEMA.cli_flags || {})[param] || `--${String(param).replace(/_/g, "-")}`;
|
|
133
|
+
}
|
|
134
|
+
function fold(v) {
|
|
135
|
+
return String(v).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
136
|
+
}
|
|
137
|
+
// Check and spell finite values the canonical way; plain error listing what is allowed.
|
|
138
|
+
function checkEnum(param, raw) {
|
|
139
|
+
const allowed = FILTER_ENUMS[param];
|
|
140
|
+
if (!allowed) return raw;
|
|
141
|
+
const match = (v) => allowed.find((a) => fold(a.value) === fold(v) || fold(a.label) === fold(v) || fold(a.value).replace(/[\s,]/g, "") === fold(v).replace(/[\s,]/g, ""));
|
|
142
|
+
const whole = match(String(raw).trim());
|
|
143
|
+
if (whole) return whole.value; // a label with a comma in it, such as 501-1,000
|
|
144
|
+
const parts = SINGLE_VALUE.has(param) ? [String(raw)] : String(raw).split(",");
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const part of parts) {
|
|
147
|
+
const v = part.trim();
|
|
148
|
+
if (!v) continue;
|
|
149
|
+
const hit = match(v);
|
|
150
|
+
if (!hit) die(`${flagOfParam(param)} takes: ${allowed.map((a) => a.value).join(", ")} (got "${v}").`);
|
|
151
|
+
if (!out.includes(hit.value)) out.push(hit.value);
|
|
152
|
+
}
|
|
153
|
+
return out.join(",");
|
|
154
|
+
}
|
|
155
|
+
// The filters of one command, ready to send: values checked, --near split.
|
|
156
|
+
function finishFilters(filters) {
|
|
157
|
+
const out = {};
|
|
158
|
+
for (const [k, v] of Object.entries(filters || {})) {
|
|
159
|
+
if (v === undefined || v === null || v === "") continue;
|
|
160
|
+
out[k] = typeof v === "string" ? checkEnum(k, v) : v;
|
|
161
|
+
}
|
|
162
|
+
if (out.near !== undefined) {
|
|
163
|
+
const near = String(out.near).trim();
|
|
164
|
+
delete out.near;
|
|
165
|
+
const m = near.match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/);
|
|
166
|
+
if (m) {
|
|
167
|
+
out.near_lat = m[1];
|
|
168
|
+
out.near_lon = m[2];
|
|
169
|
+
} else {
|
|
170
|
+
out.near_place = near;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const hasPlace = out.near_place || (out.near_lat !== undefined && out.near_lon !== undefined);
|
|
174
|
+
if (out.radius !== undefined && !hasPlace) die("--radius needs a place: --near \"Denver, Colorado\" or --near <lat,lon>.");
|
|
175
|
+
if (hasPlace && out.radius === undefined) die("--near needs a distance: add --radius, for example --radius 25.");
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
// Collect the filters a readFlags() result holds for a flag map.
|
|
179
|
+
function collectFilters(args, valueFlags, boolFlags = FILTER_BOOL_FLAGS) {
|
|
180
|
+
const out = {};
|
|
181
|
+
for (const field of new Set(Object.values(valueFlags))) if (args[field] !== undefined && args[field] !== "") out[field] = args[field];
|
|
182
|
+
for (const field of new Set(Object.values(boolFlags))) if (args[field]) out[field] = "true";
|
|
183
|
+
return finishFilters(out);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Flags that map to API filter params for the generic parser. Value flags take the next token.
|
|
187
|
+
const VALUE_FLAGS = {
|
|
188
|
+
...FILTER_VALUE_FLAGS,
|
|
79
189
|
"--website": "website",
|
|
80
190
|
// Used by `enrich` (email → full profile). Harmless on the other commands,
|
|
81
191
|
// which simply ignore an unknown query field.
|
|
@@ -85,9 +195,7 @@ const VALUE_FLAGS = {
|
|
|
85
195
|
// only (deliverable contacts); there is deliberately NO flag to query invalid or
|
|
86
196
|
// any raw verification status - that is never exposed on any surface.
|
|
87
197
|
const BOOL_FLAGS = {
|
|
88
|
-
|
|
89
|
-
"--has-linkedin": "has_linkedin",
|
|
90
|
-
"--has-email": "has_email",
|
|
198
|
+
...FILTER_BOOL_FLAGS,
|
|
91
199
|
"--verified-only": "verified_only",
|
|
92
200
|
};
|
|
93
201
|
|
|
@@ -105,16 +213,6 @@ function setGrade(out, v) {
|
|
|
105
213
|
out.gradeExplicit = true;
|
|
106
214
|
}
|
|
107
215
|
|
|
108
|
-
// --exclude-title is fully applied by `export` and `list create` today (the
|
|
109
|
-
// platform forwards it into the title-exclusion query on those two paths).
|
|
110
|
-
// `count`, `search`, and `reveal` go through a separate read path that does
|
|
111
|
-
// not yet apply it (see GODMODE-PLAN.md). Warn instead of silently dropping
|
|
112
|
-
// a filter the user asked for.
|
|
113
|
-
function warnExcludeTitleGap(filters) {
|
|
114
|
-
if (filters.exclude_title) {
|
|
115
|
-
warn(`--exclude-title is not applied by this command yet (platform-side gap) - it works with \`export\` and \`list create\`.`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
216
|
function warnGradeGap(scope) {
|
|
119
217
|
if (scope === "browse") warn(`--grade has no effect on count/search - grading only applies at reveal/export time.`);
|
|
120
218
|
else if (scope === "reveal") warn(`--grade is coming soon for reveal - it currently always returns the platform's standard deliverable set.`);
|
|
@@ -178,6 +276,7 @@ function parseArgs(argv) {
|
|
|
178
276
|
}
|
|
179
277
|
// Free-text positional → q
|
|
180
278
|
if (out._.length) out.filters.q = out._.join(" ");
|
|
279
|
+
out.filters = finishFilters(out.filters);
|
|
181
280
|
return out;
|
|
182
281
|
}
|
|
183
282
|
|
|
@@ -432,6 +531,8 @@ function need(res, what) {
|
|
|
432
531
|
const wait = Number.isFinite(ra) && ra > 0 ? ` Try again in ${ra === 1 ? "1 second" : fmtWait(Math.ceil(ra))}.` : "";
|
|
433
532
|
die(`${msg || "too many requests right now, or today's limit is used up."}${wait}${ref}`, EXIT.RATE_LIMIT);
|
|
434
533
|
}
|
|
534
|
+
const notReady = res.res && res.res.headers && res.res.headers["x-argorant-filter-not-ready"];
|
|
535
|
+
if (notReady) die(`${msg || "This filter is not ready yet."}\nRun \`argorant filters\` to see which filters can be used now.${ref}`);
|
|
435
536
|
die(`${msg || `${what} failed (HTTP ${res.status}).`}${ref}`);
|
|
436
537
|
}
|
|
437
538
|
|
|
@@ -561,7 +662,6 @@ async function cmdWhoami(args) {
|
|
|
561
662
|
|
|
562
663
|
async function cmdCount(args) {
|
|
563
664
|
const key = requireKey();
|
|
564
|
-
warnExcludeTitleGap(args.filters);
|
|
565
665
|
if (args.gradeExplicit) warnGradeGap("browse");
|
|
566
666
|
const res = await request("GET", args.base, "/api/mcp/people/count", { key, query: args.filters });
|
|
567
667
|
const r = need(res, "count");
|
|
@@ -580,13 +680,13 @@ async function cmdCompany(args) {
|
|
|
580
680
|
if (!domain || !domain.includes(".")) {
|
|
581
681
|
die("usage: argorant company <company.com> [--title <role>] [-n 5] [--json]");
|
|
582
682
|
}
|
|
583
|
-
const query = {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
};
|
|
683
|
+
const query = { limit: args.limit || 5 };
|
|
684
|
+
const ignored = [];
|
|
685
|
+
for (const [k, v] of Object.entries(args.filters)) {
|
|
686
|
+
if (COMPANY_PEOPLE_PARAMS.has(k)) query[k] = v;
|
|
687
|
+
else if (!["company_domain", "q", "website"].includes(k)) ignored.push(flagOfParam(k));
|
|
688
|
+
}
|
|
689
|
+
if (ignored.length) die(`\`company\` looks at one company's people; it does not take ${ignored.join(", ")}. Use \`count\` or \`search\` with --domain instead.`);
|
|
590
690
|
const res = await request(
|
|
591
691
|
"GET",
|
|
592
692
|
args.base,
|
|
@@ -615,7 +715,6 @@ async function cmdCompany(args) {
|
|
|
615
715
|
|
|
616
716
|
async function cmdSearch(args) {
|
|
617
717
|
const key = requireKey();
|
|
618
|
-
warnExcludeTitleGap(args.filters);
|
|
619
718
|
if (args.gradeExplicit) warnGradeGap("browse");
|
|
620
719
|
const query = { ...args.filters, limit: args.limit || 5 };
|
|
621
720
|
const res = await request("GET", args.base, "/api/mcp/people/preview", { key, query });
|
|
@@ -768,7 +867,6 @@ function phoneLine(p) {
|
|
|
768
867
|
|
|
769
868
|
async function cmdReveal(args) {
|
|
770
869
|
const key = requireKey();
|
|
771
|
-
warnExcludeTitleGap(args.filters);
|
|
772
870
|
if (args.gradeExplicit) warnGradeGap("reveal");
|
|
773
871
|
const limit = args.limit || 10;
|
|
774
872
|
const pricing = args.phones ? await phonePricing(args, key) : null;
|
|
@@ -1222,9 +1320,16 @@ async function cmdList(args) {
|
|
|
1222
1320
|
const res = await request("POST", args.base, "/api/mcp/lists/create", { key, body });
|
|
1223
1321
|
const r = need(res, "list create");
|
|
1224
1322
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1323
|
+
const fill = await listFill(args.base, key, r.list_id, r);
|
|
1324
|
+
if (fill && fill.filling) {
|
|
1325
|
+
console.log(green("✓") + ` Created list ${bold("#" + r.list_id)} ${dim("“" + r.name + "”")}`);
|
|
1326
|
+
console.log(` ${fillLine(fill)}`);
|
|
1327
|
+
console.log(dim(`Check it: argorant lists show ${r.list_id}`));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1225
1330
|
const total = Number(r.snapshot_total || 0);
|
|
1226
1331
|
console.log(green("✓") + ` Created list ${bold("#" + r.list_id)} ${dim("“" + r.name + "”")} - ${bold(total.toLocaleString())} matching contacts`);
|
|
1227
|
-
console.log(dim(`Export it with: argorant export ${Object.entries(args.filters).filter(([, v]) => v).map(([k, v]) =>
|
|
1332
|
+
console.log(dim(`Export it with: argorant export ${Object.entries(args.filters).filter(([, v]) => v).map(([k, v]) => (k === "q" ? (/\s/.test(String(v)) ? `"${v}"` : String(v)) : FILTER_BOOL_FLAGS[flagOfParam(k)] ? flagOfParam(k) : `${flagOfParam(k)} ${/\s/.test(String(v)) ? `"${v}"` : v}`)).join(" ")} -o leads.csv`));
|
|
1228
1333
|
return;
|
|
1229
1334
|
}
|
|
1230
1335
|
if (sub === "status" || sub === "show" || sub === "get") {
|
|
@@ -1239,7 +1344,9 @@ async function cmdList(args) {
|
|
|
1239
1344
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1240
1345
|
const total = Number(r.snapshot_total ?? r.item_count ?? 0);
|
|
1241
1346
|
console.log(`${bold("List #" + (r.list_id ?? id))} ${dim("“" + (r.name || "-") + "”")}`);
|
|
1242
|
-
|
|
1347
|
+
const fill = await listFill(args.base, key, String(id).trim(), null);
|
|
1348
|
+
if (fill && (fill.filling || (fill.fill && fill.fill.status === "failed"))) console.log(` ${fillLine(fill)}`);
|
|
1349
|
+
else console.log(` ${bold(total.toLocaleString())} contacts · ${dim((r.selection_mode || "filtered") + " · " + (r.record_type || "person"))}`);
|
|
1243
1350
|
return;
|
|
1244
1351
|
}
|
|
1245
1352
|
die("usage: argorant list create --name \"…\" [filters] | argorant list status <id>");
|
|
@@ -1270,29 +1377,8 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
1270
1377
|
// filter-enroll endpoint doesn't accept, e.g. --verified-only), plus --query
|
|
1271
1378
|
// as an explicit alias for free-text `q` (clearer than a bare positional in a
|
|
1272
1379
|
// command that already takes a campaign name/id as its first positional).
|
|
1273
|
-
const CAMPAIGN_FILTER_VALUE_FLAGS = {
|
|
1274
|
-
|
|
1275
|
-
"--title": "title",
|
|
1276
|
-
"--exclude-title": "exclude_title",
|
|
1277
|
-
"--seniority": "seniority",
|
|
1278
|
-
"--department": "departments",
|
|
1279
|
-
"--departments": "departments",
|
|
1280
|
-
"--industry": "industry",
|
|
1281
|
-
"--keywords": "keywords",
|
|
1282
|
-
"--keyword": "keywords",
|
|
1283
|
-
"--country": "country",
|
|
1284
|
-
"--geography": "country",
|
|
1285
|
-
"--region": "country",
|
|
1286
|
-
"--state": "state",
|
|
1287
|
-
"--city": "city",
|
|
1288
|
-
"--company": "company_name",
|
|
1289
|
-
"--domain": "company_domain",
|
|
1290
|
-
};
|
|
1291
|
-
const CAMPAIGN_FILTER_BOOL_FLAGS = {
|
|
1292
|
-
"--has-phone": "has_phone",
|
|
1293
|
-
"--has-linkedin": "has_linkedin",
|
|
1294
|
-
"--has-email": "has_email",
|
|
1295
|
-
};
|
|
1380
|
+
const CAMPAIGN_FILTER_VALUE_FLAGS = { "--query": "q", ...FILTER_VALUE_FLAGS };
|
|
1381
|
+
const CAMPAIGN_FILTER_BOOL_FLAGS = { ...FILTER_BOOL_FLAGS };
|
|
1296
1382
|
|
|
1297
1383
|
// Minimal flag reader shared by every `campaigns` subcommand: pulls out the
|
|
1298
1384
|
// universal --json/--yes/--base plus whatever value/bool flags the caller
|
|
@@ -1721,9 +1807,7 @@ async function campaignsLeads(argv) {
|
|
|
1721
1807
|
}
|
|
1722
1808
|
|
|
1723
1809
|
// Filter enrollment straight from the database: operator keys.
|
|
1724
|
-
const filters =
|
|
1725
|
-
for (const field of Object.values(CAMPAIGN_FILTER_VALUE_FLAGS)) if (args[field]) filters[field] = args[field];
|
|
1726
|
-
for (const field of Object.values(CAMPAIGN_FILTER_BOOL_FLAGS)) if (args[field]) filters[field] = "true";
|
|
1810
|
+
const filters = collectFilters(args, CAMPAIGN_FILTER_VALUE_FLAGS, CAMPAIGN_FILTER_BOOL_FLAGS);
|
|
1727
1811
|
if (!Object.keys(filters).length) die(usage);
|
|
1728
1812
|
const body = { filters };
|
|
1729
1813
|
if (args.limit) body.limit = parseLimit(args.limit, "-n");
|
|
@@ -3003,25 +3087,11 @@ async function cmdFind(argv) {
|
|
|
3003
3087
|
|
|
3004
3088
|
const V1 = "/api/v1";
|
|
3005
3089
|
const PAGE_FLAGS = { "-n": "limit", "--limit": "limit", "--cursor": "cursor", "--offset": "offset" };
|
|
3006
|
-
const COMPANY_FILTER_FLAGS = {
|
|
3007
|
-
"--query": "q",
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
"--country": "country",
|
|
3012
|
-
"--geography": "country",
|
|
3013
|
-
"--region": "country",
|
|
3014
|
-
"--state": "state",
|
|
3015
|
-
"--city": "city",
|
|
3016
|
-
"--company": "company_name",
|
|
3017
|
-
"--domain": "company_domain",
|
|
3018
|
-
"--employees": "employee_range",
|
|
3019
|
-
"--employee-range": "employee_range",
|
|
3020
|
-
"--keywords": "keywords",
|
|
3021
|
-
"--keyword": "keywords",
|
|
3022
|
-
"--technologies": "technologies",
|
|
3023
|
-
"--technology": "technologies",
|
|
3024
|
-
};
|
|
3090
|
+
const COMPANY_FILTER_FLAGS = (() => {
|
|
3091
|
+
const out = { "--query": "q", "--q": "q" };
|
|
3092
|
+
for (const [flag, param] of Object.entries(FILTER_VALUE_FLAGS)) if (COMPANY_PARAMS.has(param)) out[flag] = param;
|
|
3093
|
+
return out;
|
|
3094
|
+
})();
|
|
3025
3095
|
const LOOKUP_KINDS = ["industries", "countries", "keywords", "technologies", "titles"];
|
|
3026
3096
|
const LOOKUP_ALIASES = { industry: "industries", country: "countries", keyword: "keywords", technology: "technologies", tech: "technologies", title: "titles" };
|
|
3027
3097
|
|
|
@@ -3192,29 +3262,14 @@ ${bold("Argorant lists")} - saved lists of people (free to create and browse)
|
|
|
3192
3262
|
${p} export <id> [-n 1000] [-o leads.csv] [--include-exported] [--include-phones] [--no-wait] [--yes]
|
|
3193
3263
|
${dim("1 credit per valid contact; with --include-phones 10 credits per phone number returned")}
|
|
3194
3264
|
|
|
3195
|
-
filters
|
|
3265
|
+
${p} add <id> [filters] ${dim("adds every match of a search; people already in it are skipped")}
|
|
3266
|
+
|
|
3267
|
+
filters for create and add: the same as everywhere (argorant help, FILTERS), include and exclude
|
|
3196
3268
|
${dim("Add --json to any command for the raw answer.")}
|
|
3197
3269
|
`);
|
|
3198
3270
|
}
|
|
3199
3271
|
|
|
3200
|
-
const LIST_FILTER_FLAGS = {
|
|
3201
|
-
"--query": "q",
|
|
3202
|
-
"--title": "title",
|
|
3203
|
-
"--exclude-title": "exclude_title",
|
|
3204
|
-
"--seniority": "seniority",
|
|
3205
|
-
"--department": "departments",
|
|
3206
|
-
"--departments": "departments",
|
|
3207
|
-
"--industry": "industry",
|
|
3208
|
-
"--keywords": "keywords",
|
|
3209
|
-
"--keyword": "keywords",
|
|
3210
|
-
"--country": "country",
|
|
3211
|
-
"--geography": "country",
|
|
3212
|
-
"--region": "country",
|
|
3213
|
-
"--state": "state",
|
|
3214
|
-
"--city": "city",
|
|
3215
|
-
"--company": "company_name",
|
|
3216
|
-
"--domain": "company_domain",
|
|
3217
|
-
};
|
|
3272
|
+
const LIST_FILTER_FLAGS = { "--query": "q", ...FILTER_VALUE_FLAGS };
|
|
3218
3273
|
|
|
3219
3274
|
function listLine(l) {
|
|
3220
3275
|
const size = Number(l.snapshot_total || l.item_count || 0);
|
|
@@ -3245,20 +3300,40 @@ async function cmdLists(argv) {
|
|
|
3245
3300
|
return;
|
|
3246
3301
|
}
|
|
3247
3302
|
if (sub === "create" || sub === "new") {
|
|
3248
|
-
const args = readFlags(rest, { ...LIST_FILTER_FLAGS, "--name": "name" },
|
|
3303
|
+
const args = readFlags(rest, { ...LIST_FILTER_FLAGS, "--name": "name" }, FILTER_BOOL_FLAGS);
|
|
3249
3304
|
const name = String(args.name || "").trim();
|
|
3250
3305
|
if (!name) die('usage: argorant lists create --name "My list" [filters] (e.g. --title CEO --country Germany)');
|
|
3251
|
-
const filters =
|
|
3252
|
-
for (const field of Object.values(LIST_FILTER_FLAGS)) if (args[field]) filters[field] = args[field];
|
|
3253
|
-
for (const field of ["has_phone", "has_linkedin", "has_email"]) if (args[field]) filters[field] = "true";
|
|
3306
|
+
const filters = collectFilters(args, LIST_FILTER_FLAGS);
|
|
3254
3307
|
if (args._.length) filters.q = args._.join(" ");
|
|
3255
3308
|
if (!Object.keys(filters).length) die("give at least one filter, for example --title CEO --country Germany");
|
|
3256
3309
|
const body = { name, filters };
|
|
3257
3310
|
const r = need(await request("POST", args.base, `${V1}/lists`, { key, body }), "lists create");
|
|
3258
3311
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
3259
3312
|
const l = r.list && typeof r.list === "object" ? r.list : r;
|
|
3260
|
-
|
|
3261
|
-
|
|
3313
|
+
const lid = l.list_id ?? l.id;
|
|
3314
|
+
const fill = await listFill(args.base, key, lid, r);
|
|
3315
|
+
if (fill && fill.filling) {
|
|
3316
|
+
console.log(green("✓") + ` Saved list ${bold("#" + lid)} ${dim("“" + (l.name || name) + "”")}`);
|
|
3317
|
+
console.log(` ${fillLine(fill)}`);
|
|
3318
|
+
console.log(dim(`Check it: argorant lists show ${lid}`));
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
console.log(green("✓") + ` Saved list ${bold("#" + lid)} ${dim("“" + (l.name || name) + "”")} · ${bold(Number(l.snapshot_total || 0).toLocaleString())} matching people`);
|
|
3322
|
+
console.log(dim(`Export it: argorant lists export ${lid} -n 500 -o leads.csv`));
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
if (sub === "add") {
|
|
3326
|
+
const args = readFlags(rest, LIST_FILTER_FLAGS, FILTER_BOOL_FLAGS);
|
|
3327
|
+
const id = numericId(args._[0], "list id", "usage: argorant lists add <id> [filters] (adds every match; people already in the list are skipped)");
|
|
3328
|
+
const filters = collectFilters(args, LIST_FILTER_FLAGS);
|
|
3329
|
+
if (args._.length > 1) filters.q = args._.slice(1).join(" ");
|
|
3330
|
+
if (!Object.keys(filters).length) die("give at least one filter, for example --title CEO --country Germany");
|
|
3331
|
+
const r = need(await request("POST", args.base, `${V1}/lists/${id}/items`, { key, body: { filters } }), "lists add");
|
|
3332
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
3333
|
+
if (r.summary) console.log(green("✓") + " " + r.summary);
|
|
3334
|
+
else console.log(green("✓") + ` Added ${Number(r.added || 0).toLocaleString()} people to list #${id}`);
|
|
3335
|
+
const fill = await listFill(args.base, key, id, r);
|
|
3336
|
+
if (fill && fill.filling) console.log(` ${fillLine(fill)}`);
|
|
3262
3337
|
return;
|
|
3263
3338
|
}
|
|
3264
3339
|
if (sub === "show" || sub === "get" || sub === "status") {
|
|
@@ -3267,7 +3342,14 @@ async function cmdLists(argv) {
|
|
|
3267
3342
|
const r = need(await request("GET", args.base, `${V1}/lists/${id}`, { key }), "list");
|
|
3268
3343
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
3269
3344
|
const l = r.list && typeof r.list === "object" ? r.list : r;
|
|
3270
|
-
|
|
3345
|
+
const fill = await listFill(args.base, key, id, null);
|
|
3346
|
+
if (fill && fill.filling) {
|
|
3347
|
+
console.log(`${bold("#" + (l.list_id ?? l.id ?? id))} ${l.name || "-"}`);
|
|
3348
|
+
console.log(` ${fillLine(fill)}`);
|
|
3349
|
+
} else {
|
|
3350
|
+
console.log(listLine(l));
|
|
3351
|
+
if (fill && fill.fill && fill.fill.status === "failed") console.log(` ${fillLine(fill)}`);
|
|
3352
|
+
}
|
|
3271
3353
|
const f = l.filters && typeof l.filters === "object" ? Object.entries(l.filters).filter(([k, v]) => v && k !== "record_type") : [];
|
|
3272
3354
|
if (f.length) console.log(dim(` filters: ${f.map(([k, v]) => `${k}=${v}`).join(", ")}`));
|
|
3273
3355
|
console.log(dim(` argorant lists people ${id} · argorant lists export ${id} -o leads.csv`));
|
|
@@ -3308,7 +3390,34 @@ async function cmdLists(argv) {
|
|
|
3308
3390
|
return console.log(green("✓") + ` Deleted list #${id}`);
|
|
3309
3391
|
}
|
|
3310
3392
|
if (sub === "export") return listsExport(rest, key);
|
|
3311
|
-
die(`unknown lists subcommand: ${sub} (ls, create, show, people, rename, rm, export)`);
|
|
3393
|
+
die(`unknown lists subcommand: ${sub} (ls, create, add, show, people, rename, rm, export)`);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
// A list that is still filling (saved from a search, or grown with `lists add`) is never reported as
|
|
3397
|
+
// complete: GET /api/v1/lists/{id}/fill says how far it is. Null when the API has no fill status.
|
|
3398
|
+
async function listFill(base, key, id, created) {
|
|
3399
|
+
// After a save or an add: only when the answer says it is still filling (queued or processing).
|
|
3400
|
+
if (created && !created.async && !(created.materialize && ["queued", "processing"].includes(created.materialize.status))) return null;
|
|
3401
|
+
let res;
|
|
3402
|
+
try {
|
|
3403
|
+
res = await request("GET", base, `${V1}/lists/${encodeURIComponent(String(id))}/fill`, { key });
|
|
3404
|
+
} catch {
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
if (res.status >= 400 || !res.json) return null;
|
|
3408
|
+
return res.json;
|
|
3409
|
+
}
|
|
3410
|
+
function fillLine(fill) {
|
|
3411
|
+
const job = (fill && fill.fill) || {};
|
|
3412
|
+
const list = (fill && fill.list) || {};
|
|
3413
|
+
const sofar = Math.max(Number(job.inserted_rows || 0), Number(list.item_count || 0));
|
|
3414
|
+
if (job.status === "processing") return `filling: ${sofar.toLocaleString()} people so far${job.target_rows ? ` of about ${Number(job.target_rows).toLocaleString()}` : ""}`;
|
|
3415
|
+
if (job.status === "queued") {
|
|
3416
|
+
const ahead = Number(job.queue_position || 0);
|
|
3417
|
+
return `waiting for its turn${ahead ? ` (${ahead} before it)` : ""}; ${Number(list.item_count || 0).toLocaleString()} people in it now`;
|
|
3418
|
+
}
|
|
3419
|
+
if (job.status === "failed") return `the last fill did not finish; ${Number(list.item_count || 0).toLocaleString()} people in it. Save or add the search again.`;
|
|
3420
|
+
return `ready with ${Number(list.item_count || list.snapshot_total || 0).toLocaleString()} people`;
|
|
3312
3421
|
}
|
|
3313
3422
|
|
|
3314
3423
|
async function listsExport(rest, key) {
|
|
@@ -3623,7 +3732,9 @@ async function cmdCompanies(argv) {
|
|
|
3623
3732
|
${bold("argorant companies")} count [filters] ${dim("free")}
|
|
3624
3733
|
${bold("argorant companies")} search [filters] [-n 25] [--cursor <c>] [--all] ${dim("free")}
|
|
3625
3734
|
${bold("argorant companies")} people <company.com> [--title <role>] [-n 5] ${dim("free; same as argorant company")}
|
|
3626
|
-
filters: --industry --country --state --city --
|
|
3735
|
+
filters: --industry --country --state --city --domain (each with --exclude-...) --company --keywords
|
|
3736
|
+
--exclude-keywords --keyword-scope --technologies --employees 11-50,51-200 --employees-min/max
|
|
3737
|
+
--revenue --revenue-min/max --near <place> --radius <n> [free text]
|
|
3627
3738
|
`);
|
|
3628
3739
|
}
|
|
3629
3740
|
const key = requireKey();
|
|
@@ -3657,12 +3768,48 @@ ${bold("argorant companies")} people <company.com> [--title <role>] [-n 5]
|
|
|
3657
3768
|
}
|
|
3658
3769
|
|
|
3659
3770
|
function companyFilters(args) {
|
|
3660
|
-
const q = {};
|
|
3661
|
-
for (const field of new Set(Object.values(COMPANY_FILTER_FLAGS))) if (args[field]) q[field] = args[field];
|
|
3771
|
+
const q = collectFilters(args, COMPANY_FILTER_FLAGS, {});
|
|
3662
3772
|
if (args._.length) q.q = [q.q, ...args._].filter(Boolean).join(" ");
|
|
3773
|
+
if (q.radius !== undefined && !q.radius_target) q.radius_target = "company";
|
|
3663
3774
|
return q;
|
|
3664
3775
|
}
|
|
3665
3776
|
|
|
3777
|
+
// ---- filters: every filter, its values, and whether it can be used now ----
|
|
3778
|
+
async function cmdFilters(args) {
|
|
3779
|
+
const key = resolveKey();
|
|
3780
|
+
let schema = null;
|
|
3781
|
+
let live = false;
|
|
3782
|
+
if (key) {
|
|
3783
|
+
try {
|
|
3784
|
+
const res = await request("GET", args.base, "/api/v1/people/filters", { key });
|
|
3785
|
+
if (res.status < 400 && res.json && Array.isArray(res.json.filters)) {
|
|
3786
|
+
schema = res.json;
|
|
3787
|
+
live = true;
|
|
3788
|
+
} else if (res.status === 401) need(res, "filters");
|
|
3789
|
+
} catch {
|
|
3790
|
+
/* offline: the bundled list below */
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
if (!schema) schema = FILTER_SCHEMA;
|
|
3794
|
+
if (args.json) return console.log(JSON.stringify(schema, null, 2));
|
|
3795
|
+
const flags = schema.cli_flags || FILTER_SCHEMA.cli_flags || {};
|
|
3796
|
+
console.log(bold("Filters") + dim(live ? " (ready now, from your account)" : " (bundled list; log in to see what is ready now)"));
|
|
3797
|
+
for (const f of schema.filters || []) {
|
|
3798
|
+
const names = [f.include, f.exclude, ...(f.kind === "range" || f.kind === "radius" ? f.params : [])]
|
|
3799
|
+
.filter(Boolean)
|
|
3800
|
+
.filter((p, i, a) => a.indexOf(p) === i)
|
|
3801
|
+
.map((p) => flags[p] || `--${p.replace(/_/g, "-")}`);
|
|
3802
|
+
const state = !live ? "" : f.ready ? green(" ready") : red(" not ready yet") + dim(f.reason ? ` ${f.reason}` : "");
|
|
3803
|
+
console.log(` ${bold(f.label)} ${dim(names.join(" "))}${state}`);
|
|
3804
|
+
if (Array.isArray(f.values) && f.values.length) console.log(dim(` values: ${f.values.map((v) => v.value).join(", ")}`));
|
|
3805
|
+
}
|
|
3806
|
+
for (const [name, m] of Object.entries(schema.modes || {})) {
|
|
3807
|
+
console.log(` ${bold(m.label)} ${dim(flags[name] || "--" + name.replace(/_/g, "-"))}`);
|
|
3808
|
+
console.log(dim(` values: ${(m.values || []).map((v) => v.value).join(", ")}${m.default ? ` (default ${m.default})` : ""}`));
|
|
3809
|
+
}
|
|
3810
|
+
console.log(dim(" Every list filter takes several values; a comma means any of them. Include and exclude work together."));
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3666
3813
|
// ---- lookup ----
|
|
3667
3814
|
async function cmdLookup(argv) {
|
|
3668
3815
|
const raw = String(argv[0] || "").toLowerCase();
|
|
@@ -3718,7 +3865,8 @@ ${bold("COMMANDS")}
|
|
|
3718
3865
|
${cyan("export status")} <job_id> Status of an existing export ${dim("(free; add --batch for >50k)")}
|
|
3719
3866
|
${cyan("export download")} <job_id> -o leads.csv Re-download a finished export ${dim("(free)")}
|
|
3720
3867
|
${cyan("list create")} --name "<n>" [filters] Save a reusable list ${dim("(free)")}
|
|
3721
|
-
${cyan("list status")} <id> Show a saved list's size
|
|
3868
|
+
${cyan("list status")} <id> Show a saved list's size, or how far it has filled ${dim("(free)")}
|
|
3869
|
+
${cyan("filters")} Every filter, its values, and which are ready now ${dim("(free)")}
|
|
3722
3870
|
${cyan("verify")} <email> Verify one of your own emails ${dim("(verification pool)")}
|
|
3723
3871
|
${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
|
|
3724
3872
|
${cyan("find")} "<first last>" --domain <d> Find a person's work email ${dim("(1 credit found, 0.25 credit nothing found)")}
|
|
@@ -3732,24 +3880,35 @@ ${bold("COMMANDS")}
|
|
|
3732
3880
|
${cyan("inbox")} list | read | reply | forward | classify Replies across campaigns; answer in-thread or forward
|
|
3733
3881
|
${cyan("blocklist")} list | add | remove Addresses and domains never contacted
|
|
3734
3882
|
${cyan("account")} Plan, credits, request limit, campaign emails ${dim("(free)")}
|
|
3735
|
-
${cyan("lists")} ls | create | show | people | rename | rm | export Saved lists ${dim("(argorant lists help)")}
|
|
3883
|
+
${cyan("lists")} ls | create | add | show | people | rename | rm | export Saved lists ${dim("(argorant lists help)")}
|
|
3736
3884
|
${cyan("exports")} ls | status | download Your exports, a page at a time ${dim("(free)")}
|
|
3737
3885
|
${cyan("companies")} count | search | people Companies that match your filters ${dim("(free)")}
|
|
3738
3886
|
${cyan("lookup")} industries | countries | keywords | technologies | titles [text] Exact filter labels ${dim("(free)")}
|
|
3739
3887
|
${cyan("credits")} packs | quote | buy | purchases Buy credit packs ${dim("(asks first; saved card or payment link)")}
|
|
3740
3888
|
${cyan("webhooks")} ls | add | set | test | deliveries | rm Get events sent to your server ${dim("(Pro plan and up)")}
|
|
3741
3889
|
|
|
3742
|
-
${bold("FILTERS")}
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
--title <t>
|
|
3746
|
-
--
|
|
3747
|
-
--
|
|
3748
|
-
--
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3890
|
+
${bold("FILTERS")} ${dim("(the same on every command: count, search, reveal, export, lists, companies, campaigns)")}
|
|
3891
|
+
Each list filter has an include flag and an exclude flag; use both together.
|
|
3892
|
+
A comma means any of them.
|
|
3893
|
+
--title <t> --exclude-title <t> --title-match words|exact|contains
|
|
3894
|
+
--keywords <k> --exclude-keywords <k> --keyword-scope company|people|any
|
|
3895
|
+
--industry <i> --exclude-industry <i>
|
|
3896
|
+
--country <c> --exclude-country <c> ${dim("(regions too: Europe, EMEA, DACH, Nordics, APAC, LATAM, GCC)")}
|
|
3897
|
+
--state <s> --exclude-state <s> --city <c> --exclude-city <c>
|
|
3898
|
+
--seniority <s> --exclude-seniority <s>
|
|
3899
|
+
--department <d> --exclude-department <d>
|
|
3900
|
+
--domain <d> --exclude-domain <d> --company <name>
|
|
3901
|
+
--employees <bands> --employees-min <n> --employees-max <n>
|
|
3902
|
+
--revenue <bands> --revenue-min <usd> --revenue-max <usd>
|
|
3903
|
+
--near <place or lat,lon> --radius <n> [--radius-unit mi|km] [--radius-target person|company]
|
|
3904
|
+
--gender female|male --income <bands> ${dim("(estimates; not for lending audiences)")}
|
|
3905
|
+
--has-phone --has-linkedin --has-email --verified-only
|
|
3906
|
+
${dim("--title-match words (default): whole words, so Data does not match Database. exact: the whole")}
|
|
3907
|
+
${dim("job title, so CEO does not match Founder & CEO. contains: anywhere in the title.")}
|
|
3908
|
+
${dim("--keyword-scope company (default): company tags, industry and description. people: skills and")}
|
|
3909
|
+
${dim("profile text. any: both, plus job title and company name.")}
|
|
3910
|
+
${dim("--title is abbreviation-aware (CFO and Chief Financial Officer find each other).")}
|
|
3911
|
+
${dim("Run `argorant filters` for every value and to see which filters are ready now.")}
|
|
3753
3912
|
|
|
3754
3913
|
${bold("OPTIONS")}
|
|
3755
3914
|
-n, --limit <n> Max rows -o, --output <file> CSV path (export)
|
|
@@ -3830,7 +3989,7 @@ const OWN_FLAG_COMMANDS = {
|
|
|
3830
3989
|
lookup: (a) => cmdLookup(a),
|
|
3831
3990
|
lookups: (a) => cmdLookup(a),
|
|
3832
3991
|
};
|
|
3833
|
-
const LISTS_SUBCOMMANDS = new Set(["ls", "people", "items", "rename", "rm", "delete", "remove", "export", "show", "get", "help"]);
|
|
3992
|
+
const LISTS_SUBCOMMANDS = new Set(["ls", "add", "people", "items", "rename", "rm", "delete", "remove", "export", "show", "get", "help"]);
|
|
3834
3993
|
|
|
3835
3994
|
async function main() {
|
|
3836
3995
|
const argv = takeIdempotencyFlag(process.argv.slice(2));
|
|
@@ -3863,6 +4022,7 @@ async function main() {
|
|
|
3863
4022
|
export: cmdExport,
|
|
3864
4023
|
list: cmdList,
|
|
3865
4024
|
verify: cmdVerify,
|
|
4025
|
+
filters: cmdFilters,
|
|
3866
4026
|
};
|
|
3867
4027
|
const fn = table[cmd];
|
|
3868
4028
|
if (!fn) die(`unknown command: ${cmd}\nRun \`argorant help\` for usage.`);
|
|
@@ -3883,5 +4043,7 @@ if (require.main === module) {
|
|
|
3883
4043
|
errorMessage, errorCode, requestIdOf, detailMsg, need, EXIT, request, pageQuery, pickItems, fetchPages, retryWaitSeconds, RATE_RETRY,
|
|
3884
4044
|
companyFilters, numericId, fmtPrice, packLine, quoteLines, LOOKUP_KINDS, OWN_FLAG_COMMANDS, VERSION,
|
|
3885
4045
|
parseCsv, mailboxesFromCsv, mailboxBody, normalizeSecurity, SMTP_BULK_CHUNK,
|
|
4046
|
+
FILTER_SCHEMA, FILTER_VALUE_FLAGS, FILTER_BOOL_FLAGS, FILTER_ENUMS, VALUE_FLAGS, BOOL_FLAGS, CAMPAIGN_FILTER_VALUE_FLAGS,
|
|
4047
|
+
LIST_FILTER_FLAGS, COMPANY_FILTER_FLAGS, COMPANY_PARAMS, finishFilters, collectFilters, checkEnum, fillLine,
|
|
3886
4048
|
};
|
|
3887
4049
|
}
|