argorant 0.9.0 → 0.11.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 +99 -22
- package/bin/argorant.js +565 -80
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# argorant
|
|
2
2
|
|
|
3
3
|
Search, count, reveal, export, and verify B2B contacts from the Argorant
|
|
4
|
-
database
|
|
4
|
+
database, and find work emails by name and company domain, from your
|
|
5
|
+
terminal, scripts, or coding agent. No install required.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
8
|
npx argorant count "fintech CFOs in germany"
|
|
@@ -9,7 +10,7 @@ npx argorant count "fintech CFOs in germany"
|
|
|
9
10
|
|
|
10
11
|
Company lookups, counts, and masked searches spend **zero contact credits on
|
|
11
12
|
an active plan**. Reveals and exports draw on your Argorant workspace
|
|
12
|
-
quota/credits
|
|
13
|
+
quota/credits - the same pool as the app, API, and MCP server.
|
|
13
14
|
|
|
14
15
|
## Authenticate
|
|
15
16
|
|
|
@@ -19,15 +20,15 @@ Create an API key at **app.argorant.com/profile** (API keys), then:
|
|
|
19
20
|
npx argorant login # paste your ag_live_ key (stored in ~/.argorant)
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
Or set `ARGORANT_API_KEY=ag_live_…` in your environment
|
|
23
|
+
Or set `ARGORANT_API_KEY=ag_live_…` in your environment - ideal for scripts,
|
|
23
24
|
CI, and agents.
|
|
24
25
|
|
|
25
26
|
## Commands
|
|
26
27
|
|
|
27
28
|
| Command | What it does | Cost |
|
|
28
29
|
| --- | --- | --- |
|
|
29
|
-
| `login [key]` | Save an API key |
|
|
30
|
-
| `logout` | Forget the saved key and base (`~/.argorant/config.json`) |
|
|
30
|
+
| `login [key]` | Save an API key | - |
|
|
31
|
+
| `logout` | Forget the saved key and base (`~/.argorant/config.json`) | - |
|
|
31
32
|
| `whoami` | Account, scopes, daily quota | free |
|
|
32
33
|
| `count "<query>"` | Count matching contacts | free |
|
|
33
34
|
| `company <company.com> -n 5` | Count people at a company, split business-email coverage, preview masked roles | free |
|
|
@@ -40,13 +41,77 @@ CI, and agents.
|
|
|
40
41
|
| `list create --name "<n>" [filters]` | Save a reusable filtered list (server counts it) | free |
|
|
41
42
|
| `list status <id>` | A saved list's size and mode | free |
|
|
42
43
|
| `verify <email>` / `verify --file emails.csv -o out.csv` | Verify your own addresses (verification pool; recent re-checks free) | pool |
|
|
43
|
-
| `
|
|
44
|
+
| `find "<first last>" --domain <d>` / `find --file people.csv -o found.csv` | Find a person's work email from a name and company domain, one or a whole list ([details](#find-emails)) | 1 credit found, 0.25 nothing found |
|
|
45
|
+
| `find status \| download \| resume \| cancel <job_id>`, `find pricing` | Manage a find job; show prices, balance and limits | free |
|
|
46
|
+
| `campaigns …` | Live outbound campaigns - **operator keys only**, see below | - |
|
|
44
47
|
|
|
45
48
|
Exports above 50,000 rows are created as a multi-chunk batch: the CLI polls
|
|
46
49
|
`export status --batch` for you and writes one file per chunk
|
|
47
50
|
(`leads-part1.csv`, `leads-part2.csv`, …). The batch id is printed so you can
|
|
48
51
|
re-download any time with `export download <batch_id> --batch`.
|
|
49
52
|
|
|
53
|
+
## Find emails
|
|
54
|
+
|
|
55
|
+
Give a name and the company's domain, get the person's work email.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx argorant find "Jane Doe" --domain acme.com
|
|
59
|
+
npx argorant find --first Jane --last Doe --domain acme.com
|
|
60
|
+
# jane.doe@acme.com · Confirmed · confidence 95 · 1 credit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each lookup prints one line: the email (or `no email found`), its status, the
|
|
64
|
+
confidence and what it cost. Statuses:
|
|
65
|
+
|
|
66
|
+
| Status | Meaning | Cost |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Confirmed | Address found and confirmed | 1 credit |
|
|
69
|
+
| Unconfirmed, catch-all domain | The domain accepts every address, so the best address is returned without confirmation | 1 credit |
|
|
70
|
+
| Not found | No address found for this person | 0.25 credit |
|
|
71
|
+
| No mail server | The domain does not receive email | free |
|
|
72
|
+
| Try again later | No answer for this domain right now; run it again later | free |
|
|
73
|
+
|
|
74
|
+
Invalid rows and every failed request (out of credits, limits, a busy
|
|
75
|
+
server) are free too. A 0.25 charge takes one whole credit and keeps the rest
|
|
76
|
+
as prepaid no-result lookups, which cover your next three misses. Run
|
|
77
|
+
`argorant find pricing` for the live prices, your balance, prepaid lookups and
|
|
78
|
+
limits.
|
|
79
|
+
|
|
80
|
+
A whole list runs as a job. The file is a CSV with a header row and the
|
|
81
|
+
columns `first_name`, `last_name` and `domain`; an optional `ref` column is
|
|
82
|
+
passed through to the results:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npx argorant find --file people.csv -o found.csv --max-credits 200 --name "Q4 list"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The CLI prints the job id, shows progress (processed/total, found, charged)
|
|
89
|
+
and saves the results CSV when the job is done (`argorant-found.csv` by
|
|
90
|
+
default). Columns: `row, ref, first_name, last_name, domain, email, status,
|
|
91
|
+
confirmed, confidence, credits_charged`. The output path is checked before the
|
|
92
|
+
job is created. On a terminal the CLI asks first, showing the prices and the
|
|
93
|
+
maximum charge; with `--yes`, `--json` or no TTY it starts right away.
|
|
94
|
+
Duplicate rows are removed before anything is charged. `--max-credits` caps
|
|
95
|
+
the job: it pauses at the cap instead of spending more.
|
|
96
|
+
|
|
97
|
+
A job pauses when your credits run out, when it reaches `--max-credits`, at
|
|
98
|
+
the daily limit, or when almost none of the recent lookups found anything. The
|
|
99
|
+
CLI says why and still saves what was found so far. Ctrl-C only stops
|
|
100
|
+
watching; the job keeps running.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
argorant find status <job_id> # progress and counts
|
|
104
|
+
argorant find download <job_id> -o found.csv # the results CSV, any time
|
|
105
|
+
argorant find resume <job_id> [--max-credits 300] # continue a paused job
|
|
106
|
+
argorant find cancel <job_id> # stop; rows not processed yet are not charged
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Exit codes: a single `find` exits `1` when no email came back, so scripts can
|
|
110
|
+
branch without parsing. A `find --file` job that pauses exits `5` when out of
|
|
111
|
+
credits and `4` at the daily limit or when slowed down; a job that stops at
|
|
112
|
+
your own `--max-credits` cap exits `0`. Errors use the codes below (`5` out of
|
|
113
|
+
credits, `4` rate limit with the wait time in the message).
|
|
114
|
+
|
|
50
115
|
## Filters
|
|
51
116
|
|
|
52
117
|
Combine free text with structured filters:
|
|
@@ -57,7 +122,7 @@ Combine free text with structured filters:
|
|
|
57
122
|
--has-phone --has-linkedin --has-email --verified-only
|
|
58
123
|
```
|
|
59
124
|
|
|
60
|
-
`--keywords` is the widest, most reliable filter (comma = OR)
|
|
125
|
+
`--keywords` is the widest, most reliable filter (comma = OR) - prefer it over
|
|
61
126
|
`--industry`. `--country`/`--geography` accept regions (Europe, EMEA, DACH,
|
|
62
127
|
Nordics, APAC, LATAM, GCC…).
|
|
63
128
|
|
|
@@ -84,13 +149,14 @@ npx argorant company stripe.com --json
|
|
|
84
149
|
npx argorant reveal "heads of procurement" --country Germany -n 25 --json --yes
|
|
85
150
|
```
|
|
86
151
|
|
|
87
|
-
### Non-interactive behaviour
|
|
152
|
+
### Non-interactive behaviour - read this before scripting `reveal`/`export`
|
|
88
153
|
|
|
89
|
-
`reveal`, `export`, and `
|
|
90
|
-
stdin is a TTY and neither `-y/--yes` nor `--json` was passed.
|
|
91
|
-
pipe, or inside an agent loop there is **no prompt at all
|
|
92
|
-
spend credits immediately.
|
|
93
|
-
|
|
154
|
+
`reveal`, `export`, `verify --file` and `find --file` ask for confirmation
|
|
155
|
+
**only** when stdin is a TTY and neither `-y/--yes` nor `--json` was passed.
|
|
156
|
+
In CI, in a pipe, or inside an agent loop there is **no prompt at all**, these
|
|
157
|
+
commands spend credits immediately. A single `find` never asks; each lookup
|
|
158
|
+
costs at most 1 credit. The prompt is a convenience for humans at a terminal,
|
|
159
|
+
never a safety net. Check your `-n` and `--max-credits` before you run them.
|
|
94
160
|
|
|
95
161
|
Related guardrails, so a mistake stays cheap:
|
|
96
162
|
|
|
@@ -108,9 +174,9 @@ Related guardrails, so a mistake stays cheap:
|
|
|
108
174
|
| `0` | success |
|
|
109
175
|
| `1` | generic error (bad usage, network, failed job) |
|
|
110
176
|
| `2` | not authenticated (no key, or the key was rejected) |
|
|
111
|
-
| `3` | forbidden
|
|
177
|
+
| `3` | forbidden - the key lacks the required scope |
|
|
112
178
|
| `4` | rate limit or daily quota reached |
|
|
113
|
-
| `5` | plan upgrade required (HTTP 402); the message carries the upgrade URL |
|
|
179
|
+
| `5` | plan upgrade required or out of credits (HTTP 402); the message carries the upgrade URL |
|
|
114
180
|
|
|
115
181
|
Exit `5` is deliberately distinct from `1`: an agent can tell "this account
|
|
116
182
|
needs a paid plan" apart from "something broke".
|
|
@@ -153,18 +219,29 @@ argorant inboxes connect-google --admin admin@yourdomain.com --emails a@x.com,b@
|
|
|
153
219
|
```
|
|
154
220
|
|
|
155
221
|
New mailboxes on their own new domains, ordered from the terminal. The quote shows every domain with its
|
|
156
|
-
first-year and renewal price, the mailboxes
|
|
157
|
-
|
|
158
|
-
|
|
222
|
+
first-year and renewal price, the mailboxes, the total and how it is paid. When your workspace saved its card
|
|
223
|
+
for API and agent purchases (you ticked that box when you paid an earlier order, and the setting in
|
|
224
|
+
Settings > Billing is on), the order is charged to that card right away, within any limit you set there, and a
|
|
225
|
+
receipt is emailed to the workspace. Otherwise the order returns a payment link that you pay yourself.
|
|
226
|
+
Nothing is bought before the payment, and setup starts once it went through. Warm-up is included in every
|
|
227
|
+
plan; switch it on per mailbox with `argorant inboxes set <mailbox> --warmup`.
|
|
228
|
+
|
|
229
|
+
Domains can end in .com, .net, .org, .co, .sbs, .biz or .info (`argorant inboxes endings` prints the live list).
|
|
159
230
|
|
|
160
231
|
```
|
|
161
|
-
argorant inboxes suggest acme.com
|
|
232
|
+
argorant inboxes suggest acme.com [--ending com] # free domain names with prices
|
|
162
233
|
argorant inboxes quote --mailboxes 100 --company acme.com # or --domains getacme.com,tryacme.co
|
|
163
|
-
argorant inboxes order <quote-id> --senders "Elena Vaughn" # asks first;
|
|
164
|
-
argorant inboxes
|
|
165
|
-
argorant inboxes
|
|
234
|
+
argorant inboxes order <quote-id> --senders "Elena Vaughn" # asks first; saved card or a payment link
|
|
235
|
+
argorant inboxes order <quote-id> --senders "Elena Vaughn" --pay-with link # always a payment link
|
|
236
|
+
argorant inboxes status [<order-id>] # steps, domains, renewal, bundles
|
|
237
|
+
argorant inboxes renewal <order-id> <domain> --off # Stop renewal (nothing is charged now)
|
|
238
|
+
argorant inboxes renewal <order-id> <domain> --on # Turn renewal back on (billed shortly before it renews)
|
|
239
|
+
argorant inboxes cancel-bundle <order-id> <bundle-id> --reason too_expensive [--note "..."] [--keep-domains]
|
|
166
240
|
```
|
|
167
241
|
|
|
242
|
+
Cancel reasons: `too_expensive`, `not_sending_enough`, `deliverability`, `switching_provider`,
|
|
243
|
+
`campaign_finished`, `other`. `cancel-domain` still works as the old name of `renewal --off`.
|
|
244
|
+
|
|
168
245
|
`<campaign>` accepts an id or an unambiguous case-insensitive name prefix. Every address is verified before
|
|
169
246
|
it is enrolled; only valid ones send unless `--include-catch-all`. Variables: `{{first_name}}`, `{{last_name}}`,
|
|
170
247
|
`{{company}}`, `{{title}}`; spintax `{a|b}` varies phrasing. `--query "..."` filter enrollment stays available
|
package/bin/argorant.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// Argorant CLI
|
|
5
|
-
// Search, count, reveal, export, and verify B2B contacts from the terminal.
|
|
4
|
+
// Argorant CLI - a thin, dependency-free wrapper over the Argorant REST API.
|
|
5
|
+
// Search, count, reveal, export, and verify B2B contacts, and find work emails, from the terminal.
|
|
6
6
|
// Auth: an Argorant API key (ag_live_*) via `argorant login`, or ARGORANT_API_KEY.
|
|
7
7
|
|
|
8
8
|
const crypto = require("crypto");
|
|
@@ -83,7 +83,7 @@ const VALUE_FLAGS = {
|
|
|
83
83
|
};
|
|
84
84
|
// Boolean filter flags (presence => "true"). --verified-only is positive intent
|
|
85
85
|
// only (deliverable contacts); there is deliberately NO flag to query invalid or
|
|
86
|
-
// any raw verification status
|
|
86
|
+
// any raw verification status - that is never exposed on any surface.
|
|
87
87
|
const BOOL_FLAGS = {
|
|
88
88
|
"--has-phone": "has_phone",
|
|
89
89
|
"--has-linkedin": "has_linkedin",
|
|
@@ -124,7 +124,7 @@ function warnGradeGap(scope) {
|
|
|
124
124
|
// A value flag must be followed by an actual value. Silently swallowing the
|
|
125
125
|
// NEXT FLAG (`search "CFO" --title --base http://…` → title="--base") or a
|
|
126
126
|
// missing trailing value (→ undefined, dropped by request()) sends a request
|
|
127
|
-
// the user never asked for
|
|
127
|
+
// the user never asked for - against the wrong host, with the wrong filters.
|
|
128
128
|
// Fail loud instead. "-" stays legal: it is the documented stdin sentinel.
|
|
129
129
|
function flagValue(argv, i, flag) {
|
|
130
130
|
const v = argv[i + 1];
|
|
@@ -134,7 +134,7 @@ function flagValue(argv, i, flag) {
|
|
|
134
134
|
}
|
|
135
135
|
return v;
|
|
136
136
|
}
|
|
137
|
-
// -n/--limit drives billed row counts on reveal/export
|
|
137
|
+
// -n/--limit drives billed row counts on reveal/export - a typo must never
|
|
138
138
|
// fall through to the default (a mistyped `-n` used to become a 1000-row
|
|
139
139
|
// billed export).
|
|
140
140
|
function parseLimit(raw, flag) {
|
|
@@ -252,7 +252,7 @@ function downloadTo(base, urlPath, key, dest) {
|
|
|
252
252
|
// `detail` is a plain string on most endpoints, a structured object on some
|
|
253
253
|
// (plan_required, campaign launch blockers, native-delivery readiness) and a
|
|
254
254
|
// LIST of validation errors on any FastAPI 422. All three have to render as
|
|
255
|
-
// something a human or an agent can read
|
|
255
|
+
// something a human or an agent can read - never "[object Object]".
|
|
256
256
|
function detailMsg(detail) {
|
|
257
257
|
if (detail == null) return null;
|
|
258
258
|
if (typeof detail === "string") return detail;
|
|
@@ -292,7 +292,7 @@ function need(res, what) {
|
|
|
292
292
|
const url = d.upgrade_url || d.url || "https://argorant.com/pricing";
|
|
293
293
|
die(`${msg}${msg.includes(url) ? "" : `\nUpgrade: ${url}`}`, EXIT.UPGRADE);
|
|
294
294
|
}
|
|
295
|
-
if (res.status === 403) die(detailMsg(detail) || `forbidden
|
|
295
|
+
if (res.status === 403) die(detailMsg(detail) || `forbidden - your key lacks the scope for ${what}.`, EXIT.FORBIDDEN);
|
|
296
296
|
if (res.status === 429) die(detailMsg(detail) || "rate limit / daily quota reached.", EXIT.RATE_LIMIT);
|
|
297
297
|
if (res.status >= 400) die(detailMsg(detail) || `${what} failed (HTTP ${res.status}).`);
|
|
298
298
|
return res.json || {};
|
|
@@ -351,7 +351,7 @@ async function cmdLogin(args) {
|
|
|
351
351
|
let key = args._[0];
|
|
352
352
|
if (!key) key = await prompt("Paste your Argorant API key (ag_live_…): ", { hidden: true });
|
|
353
353
|
if (!key) die("no key provided.");
|
|
354
|
-
if (!/^ag_(live|test)_/.test(key)) process.stderr.write(dim("note: keys normally start with ag_live_
|
|
354
|
+
if (!/^ag_(live|test)_/.test(key)) process.stderr.write(dim("note: keys normally start with ag_live_ - continuing anyway.\n"));
|
|
355
355
|
const res = await request("GET", args.base, "/api/mcp/account", { key });
|
|
356
356
|
if (res.status === 428 && res.json?.detail?.error === "access_key_activation_required") {
|
|
357
357
|
const url = res.json.detail.claim_url;
|
|
@@ -370,7 +370,7 @@ async function cmdLogin(args) {
|
|
|
370
370
|
|
|
371
371
|
async function cmdLogout() {
|
|
372
372
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
373
|
-
console.log(dim(`Nothing to do
|
|
373
|
+
console.log(dim(`Nothing to do - no saved credentials at ${CONFIG_PATH}.`));
|
|
374
374
|
} else {
|
|
375
375
|
try {
|
|
376
376
|
fs.unlinkSync(CONFIG_PATH);
|
|
@@ -380,7 +380,7 @@ async function cmdLogout() {
|
|
|
380
380
|
console.log(green("✓") + ` Removed saved key and base from ${bold(CONFIG_PATH)}`);
|
|
381
381
|
}
|
|
382
382
|
if (process.env.ARGORANT_API_KEY) {
|
|
383
|
-
warn("ARGORANT_API_KEY is still set in this environment and takes precedence
|
|
383
|
+
warn("ARGORANT_API_KEY is still set in this environment and takes precedence - unset it too.");
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
@@ -389,8 +389,8 @@ async function cmdWhoami(args) {
|
|
|
389
389
|
const res = await request("GET", args.base, "/api/mcp/account", { key });
|
|
390
390
|
const a = need(res, "whoami");
|
|
391
391
|
if (args.json) return console.log(JSON.stringify(a, null, 2));
|
|
392
|
-
console.log(`${bold("Account")} ${a.email || "
|
|
393
|
-
console.log(`${bold("Scopes")} ${(a.scopes || []).join(", ") || "
|
|
392
|
+
console.log(`${bold("Account")} ${a.email || "-"} ${dim("(" + (a.role || "member") + ")")}`);
|
|
393
|
+
console.log(`${bold("Scopes")} ${(a.scopes || []).join(", ") || "-"}`);
|
|
394
394
|
const u = a.usage || {};
|
|
395
395
|
// Keys/fields must match _mcp_usage_summary: actions are count_requests /
|
|
396
396
|
// preview_rows / reveal_rows / export_rows and each entry carries
|
|
@@ -471,7 +471,7 @@ async function cmdCompany(args) {
|
|
|
471
471
|
for (const person of r.results) {
|
|
472
472
|
const who = [person.preview, person.title].filter(Boolean).join(" · ");
|
|
473
473
|
const where = [person.country].filter(Boolean).join(", ");
|
|
474
|
-
console.log(` ${bold(who || "
|
|
474
|
+
console.log(` ${bold(who || "-")}${where ? dim(" " + where) : ""}`);
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
}
|
|
@@ -484,13 +484,13 @@ async function cmdSearch(args) {
|
|
|
484
484
|
const res = await request("GET", args.base, "/api/mcp/people/preview", { key, query });
|
|
485
485
|
const r = need(res, "search");
|
|
486
486
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
487
|
-
console.log(dim(`${Number(r.total).toLocaleString()} total · showing ${r.returned} (details redacted
|
|
487
|
+
console.log(dim(`${Number(r.total).toLocaleString()} total · showing ${r.returned} (details redacted - use \`reveal\` or \`export\`)`));
|
|
488
488
|
for (const p of r.results || []) {
|
|
489
489
|
// _redacted_preview returns the masked identity as `preview` (e.g. "A* P"),
|
|
490
|
-
// never `name`
|
|
490
|
+
// never `name` - without this the row rendered as title-only.
|
|
491
491
|
const who = [p.preview || p.name, p.title].filter(Boolean).join(" · ");
|
|
492
492
|
const where = [p.company || p.company_name, p.country].filter(Boolean).join(", ");
|
|
493
|
-
console.log(` ${bold(who || "
|
|
493
|
+
console.log(` ${bold(who || "-")}${where ? dim(" " + where) : ""}`);
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
@@ -546,9 +546,9 @@ async function cmdSample(args) {
|
|
|
546
546
|
`${bold(Number(job.total_companies || 0).toLocaleString())} matching companies`
|
|
547
547
|
);
|
|
548
548
|
for (const lead of job.results || []) {
|
|
549
|
-
console.log(` ${bold(lead.full_name || "
|
|
550
|
-
console.log(` ${(lead.company || "
|
|
551
|
-
console.log(` ${cyan(lead.email || "
|
|
549
|
+
console.log(` ${bold(lead.full_name || "-")} ${dim("· " + (lead.title || "-"))}`);
|
|
550
|
+
console.log(` ${(lead.company || "-")} ${dim("· " + (lead.company_domain || "-"))}`);
|
|
551
|
+
console.log(` ${cyan(lead.email || "-")} ${green("✓ Valid")}`);
|
|
552
552
|
}
|
|
553
553
|
if (args.output) console.log(green("✓") + ` Saved CSV → ${bold(args.output)}`);
|
|
554
554
|
console.log(dim(`These 25 are the sample; the full pool contains ${Number(job.total_companies || 0).toLocaleString()} companies.`));
|
|
@@ -573,11 +573,11 @@ async function cmdReveal(args) {
|
|
|
573
573
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
574
574
|
console.log(dim(`${Number(r.total).toLocaleString()} total · revealed ${r.returned}`));
|
|
575
575
|
for (const p of r.results || []) {
|
|
576
|
-
// _revealed_contact returns full_name / first_name / last_name
|
|
576
|
+
// _revealed_contact returns full_name / first_name / last_name - there is
|
|
577
577
|
// no `name` key. The customer just paid for this row; print who it is.
|
|
578
578
|
const name = p.full_name || [p.first_name, p.last_name].filter(Boolean).join(" ") || p.name;
|
|
579
579
|
const who = [name, p.title].filter(Boolean).join(" · ");
|
|
580
|
-
console.log(` ${bold(who || "
|
|
580
|
+
console.log(` ${bold(who || "-")}`);
|
|
581
581
|
const bits = [p.email && cyan(p.email), p.phone, p.linkedin_url, [p.company || p.company_name, p.country].filter(Boolean).join(", ")].filter(Boolean);
|
|
582
582
|
if (bits.length) console.log(" " + bits.join(dim(" · ")));
|
|
583
583
|
}
|
|
@@ -653,7 +653,7 @@ async function cmdEnrich(args) {
|
|
|
653
653
|
}
|
|
654
654
|
const p = r.person || {};
|
|
655
655
|
const who = [p.full_name || [p.first_name, p.last_name].filter(Boolean).join(" "), p.title].filter(Boolean).join(" · ");
|
|
656
|
-
console.log(` ${bold(who || "
|
|
656
|
+
console.log(` ${bold(who || "-")}`);
|
|
657
657
|
const bits = [
|
|
658
658
|
p.email && cyan(p.email),
|
|
659
659
|
p.phone,
|
|
@@ -683,7 +683,7 @@ function partPath(dest, n) {
|
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
// >50k rows come back as {type:"batch", batch_id, status_api_path:
|
|
686
|
-
// /api/mcp/export-batches/{id}} with NO job_id and NO download_api_path
|
|
686
|
+
// /api/mcp/export-batches/{id}} with NO job_id and NO download_api_path - the
|
|
687
687
|
// old code fell through to /api/mcp/exports/undefined/download, so large
|
|
688
688
|
// exports were simply impossible from the CLI. Poll the batch, then download
|
|
689
689
|
// each completed chunk.
|
|
@@ -740,10 +740,10 @@ async function exportStatusCmd(args, key, id) {
|
|
|
740
740
|
const s = need(res, "export status");
|
|
741
741
|
if (args.json) return console.log(JSON.stringify(s, null, 2));
|
|
742
742
|
if (isBatch) {
|
|
743
|
-
console.log(`${bold("Batch #" + (s.batch_id ?? id))} ${dim(s.status || "
|
|
743
|
+
console.log(`${bold("Batch #" + (s.batch_id ?? id))} ${dim(s.status || "-")}`);
|
|
744
744
|
console.log(` ${s.completed_chunks || 0}/${s.total_chunks || 0} chunks · ${Number(s.verified_rows || 0).toLocaleString()} rows · ${s.progress_pct || 0}%`);
|
|
745
745
|
} else {
|
|
746
|
-
console.log(`${bold("Export #" + (s.job_id ?? id))} ${dim(s.status || "
|
|
746
|
+
console.log(`${bold("Export #" + (s.job_id ?? id))} ${dim(s.status || "-")}`);
|
|
747
747
|
console.log(` ${Number(s.verified_rows || 0).toLocaleString()}/${Number(s.total_rows || 0).toLocaleString()} rows · ${s.progress_pct || 0}%${s.downloadable ? green(" · ready to download") : ""}`);
|
|
748
748
|
}
|
|
749
749
|
if (s.error_message) console.log(red(" " + s.error_message));
|
|
@@ -756,7 +756,7 @@ async function exportDownloadCmd(args, key, id) {
|
|
|
756
756
|
const res = await request("GET", args.base, `/api/mcp/export-batches/${id}`, { key });
|
|
757
757
|
const b = need(res, "export batch status");
|
|
758
758
|
if (!EXPORT_TERMINAL_OK.includes(String(b.status || "").toLowerCase())) {
|
|
759
|
-
die(`export batch #${id} is ${b.status || "not ready"}
|
|
759
|
+
die(`export batch #${id} is ${b.status || "not ready"} - run \`argorant export status ${id} --batch\`.`);
|
|
760
760
|
}
|
|
761
761
|
const files = await downloadBatchChunks(args, key, b, dest);
|
|
762
762
|
if (args.json) return console.log(JSON.stringify({ ok: true, batch_id: id, files }, null, 2));
|
|
@@ -766,7 +766,7 @@ async function exportDownloadCmd(args, key, id) {
|
|
|
766
766
|
const res = await request("GET", args.base, `/api/mcp/exports/${id}`, { key });
|
|
767
767
|
const s = need(res, "export status");
|
|
768
768
|
if (!s.downloadable && !s.download_api_path) {
|
|
769
|
-
die(`export #${id} is ${s.status || "not ready"}
|
|
769
|
+
die(`export #${id} is ${s.status || "not ready"} - run \`argorant export status ${id}\`.`);
|
|
770
770
|
}
|
|
771
771
|
await downloadTo(args.base, s.download_api_path || `/api/mcp/exports/${id}/download`, key, dest);
|
|
772
772
|
if (args.json) return console.log(JSON.stringify({ ok: true, file: dest, job_id: id }, null, 2));
|
|
@@ -776,7 +776,7 @@ async function exportDownloadCmd(args, key, id) {
|
|
|
776
776
|
|
|
777
777
|
async function cmdExport(args) {
|
|
778
778
|
const key = requireKey();
|
|
779
|
-
// `export status <id>` / `export download <id>`
|
|
779
|
+
// `export status <id>` / `export download <id>` - recovery subcommands, no
|
|
780
780
|
// job is created and nothing is billed.
|
|
781
781
|
const sub = (args._[0] || "").toLowerCase();
|
|
782
782
|
if (sub === "status" || sub === "download") {
|
|
@@ -817,7 +817,7 @@ async function cmdExport(args) {
|
|
|
817
817
|
const batchPath = job.status_api_path || `/api/mcp/export-batches/${job.batch_id}`;
|
|
818
818
|
if (!args.json) {
|
|
819
819
|
process.stdout.write(
|
|
820
|
-
dim(`Large export queued as batch #${job.batch_id}
|
|
820
|
+
dim(`Large export queued as batch #${job.batch_id} - ${job.total_chunks || "?"} chunk(s) of up to ${Number(job.chunk_size || 0).toLocaleString()} rows\n`)
|
|
821
821
|
);
|
|
822
822
|
}
|
|
823
823
|
const batch = await pollExportBatch(args, key, batchPath, { quiet: !!args.json });
|
|
@@ -833,7 +833,7 @@ async function cmdExport(args) {
|
|
|
833
833
|
if (args.json) return console.log(JSON.stringify(job, null, 2));
|
|
834
834
|
return console.log("Export queued. " + JSON.stringify(job));
|
|
835
835
|
}
|
|
836
|
-
if (!args.json) process.stdout.write(dim("Export queued
|
|
836
|
+
if (!args.json) process.stdout.write(dim("Export queued - verifying & building CSV"));
|
|
837
837
|
let downloadPath = job.download_api_path || null;
|
|
838
838
|
const started = Date.now();
|
|
839
839
|
// Poll until the job reports a terminal state.
|
|
@@ -851,7 +851,7 @@ async function cmdExport(args) {
|
|
|
851
851
|
}
|
|
852
852
|
if (EXPORT_TERMINAL_FAIL.includes(status)) die(`\nexport ${status}${s.error_message ? `: ${s.error_message}` : "."}`);
|
|
853
853
|
if (Date.now() - started > 1000 * 60 * 20) {
|
|
854
|
-
die(`\nexport timed out after 20 minutes. It is still running server-side
|
|
854
|
+
die(`\nexport timed out after 20 minutes. It is still running server-side - check with \`argorant export status ${job.job_id}\`.`);
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
857
|
if (!args.json) process.stdout.write("\n");
|
|
@@ -862,7 +862,7 @@ async function cmdExport(args) {
|
|
|
862
862
|
try {
|
|
863
863
|
await downloadTo(args.base, downloadPath, key, dest);
|
|
864
864
|
} catch (e) {
|
|
865
|
-
// The job is already paid for
|
|
865
|
+
// The job is already paid for - always tell the user how to get it back.
|
|
866
866
|
die(`${e.message}\nThe export itself completed. Retry the download with \`argorant export download ${job.job_id} -o ${dest}\`.`);
|
|
867
867
|
}
|
|
868
868
|
if (args.json) return console.log(JSON.stringify({ ok: true, file: dest, job_id: job.job_id }, null, 2));
|
|
@@ -870,7 +870,7 @@ async function cmdExport(args) {
|
|
|
870
870
|
console.log(green("✓") + ` Saved ${rows != null ? bold(rows.toLocaleString()) + " rows → " : ""}${bold(dest)}`);
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
-
// ---- verify: external email verification (own lists)
|
|
873
|
+
// ---- verify: external email verification (own lists) - the verification pool,
|
|
874
874
|
// separate from contact credits. 60-day re-checks are free. ----
|
|
875
875
|
const EMAIL_RE = /[^\s,;"']+@[^\s,;"']+\.[^\s,;"']+/;
|
|
876
876
|
|
|
@@ -911,7 +911,7 @@ async function cmdVerifyFile(args, key) {
|
|
|
911
911
|
emails = [...new Set(emails)];
|
|
912
912
|
if (!emails.length) die("no email addresses found in file (try --column <name>)");
|
|
913
913
|
const out = ensureWritable(args.output || "argorant-verified.csv");
|
|
914
|
-
// Interactive-only by design
|
|
914
|
+
// Interactive-only by design - see reveal/export.
|
|
915
915
|
if (!args.yes && !args.json && process.stdin.isTTY) {
|
|
916
916
|
const ans = await prompt(`Verify ${bold(emails.length.toLocaleString())} emails? Fresh checks cost half a credit each; recent re-checks are free. [y/N] `);
|
|
917
917
|
if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
|
|
@@ -938,7 +938,7 @@ async function cmdVerifyFile(args, key) {
|
|
|
938
938
|
}
|
|
939
939
|
|
|
940
940
|
// ---- list: save & inspect reusable lead lists (parity with MCP/app). Creating a
|
|
941
|
-
// filtered list is free and does NOT reveal contacts
|
|
941
|
+
// filtered list is free and does NOT reveal contacts - the server counts the
|
|
942
942
|
// matches itself, so the list reports its real size right away. ----
|
|
943
943
|
async function cmdList(args) {
|
|
944
944
|
const key = requireKey();
|
|
@@ -952,7 +952,7 @@ async function cmdList(args) {
|
|
|
952
952
|
const r = need(res, "list create");
|
|
953
953
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
954
954
|
const total = Number(r.snapshot_total || 0);
|
|
955
|
-
console.log(green("✓") + ` Created list ${bold("#" + r.list_id)} ${dim("“" + r.name + "”")}
|
|
955
|
+
console.log(green("✓") + ` Created list ${bold("#" + r.list_id)} ${dim("“" + r.name + "”")} - ${bold(total.toLocaleString())} matching contacts`);
|
|
956
956
|
console.log(dim(`Export it with: argorant export ${Object.entries(args.filters).filter(([, v]) => v).map(([k, v]) => `--${k.replace(/_/g, "-")} ${/\s/.test(String(v)) ? `"${v}"` : v}`).join(" ")} -o leads.csv`));
|
|
957
957
|
return;
|
|
958
958
|
}
|
|
@@ -960,14 +960,14 @@ async function cmdList(args) {
|
|
|
960
960
|
const id = args._[1] || args.name;
|
|
961
961
|
if (!id) die("usage: argorant list status <list_id>");
|
|
962
962
|
// Validate locally: the API path param is an int, so anything else came
|
|
963
|
-
// back as a FastAPI 422 whose detail is an array
|
|
963
|
+
// back as a FastAPI 422 whose detail is an array - a useless error for a
|
|
964
964
|
// plain typo.
|
|
965
965
|
if (!/^\d+$/.test(String(id).trim())) die(`list id must be a number (got "${id}").`);
|
|
966
966
|
const res = await request("GET", args.base, `/api/mcp/lists/${encodeURIComponent(String(id).trim())}`, { key });
|
|
967
967
|
const r = need(res, "list status");
|
|
968
968
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
969
969
|
const total = Number(r.snapshot_total ?? r.item_count ?? 0);
|
|
970
|
-
console.log(`${bold("List #" + (r.list_id ?? id))} ${dim("“" + (r.name || "
|
|
970
|
+
console.log(`${bold("List #" + (r.list_id ?? id))} ${dim("“" + (r.name || "-") + "”")}`);
|
|
971
971
|
console.log(` ${bold(total.toLocaleString())} contacts · ${dim((r.selection_mode || "filtered") + " · " + (r.record_type || "person"))}`);
|
|
972
972
|
return;
|
|
973
973
|
}
|
|
@@ -977,9 +977,9 @@ async function cmdList(args) {
|
|
|
977
977
|
// =============================================================================
|
|
978
978
|
// campaigns: god-mode native outbound campaign control from the terminal.
|
|
979
979
|
//
|
|
980
|
-
// OPERATOR KEYS ONLY. Every command above talks to /api/mcp/*
|
|
980
|
+
// OPERATOR KEYS ONLY. Every command above talks to /api/mcp/* - the
|
|
981
981
|
// customer-facing contact-data API, gated by plan scopes. Everything below
|
|
982
|
-
// talks to /api/sequencer/*
|
|
982
|
+
// talks to /api/sequencer/* - the internal Argorant Sequencer that runs live
|
|
983
983
|
// outbound sends. It authenticates via the SAME ag_live_ Bearer key, but only
|
|
984
984
|
// works for a key that (a) belongs to an owner/admin account and (b) carries
|
|
985
985
|
// the `argorant:operator` scope (see cli/GODMODE-PLAN.md). Any other key gets
|
|
@@ -987,14 +987,14 @@ async function cmdList(args) {
|
|
|
987
987
|
// access.
|
|
988
988
|
//
|
|
989
989
|
// Kept on its own tiny flag reader (readFlags) instead of the top-level
|
|
990
|
-
// parseArgs
|
|
990
|
+
// parseArgs - these subcommands have their own vocabulary (--step, --subject,
|
|
991
991
|
// --count, --pool, --csv, ...) that would otherwise collide with, or be
|
|
992
992
|
// rejected by, the generic filter-flag parser used for search/reveal/export.
|
|
993
993
|
// =============================================================================
|
|
994
994
|
|
|
995
995
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
996
996
|
|
|
997
|
-
// Filter flags for `campaigns leads add --query ...`
|
|
997
|
+
// Filter flags for `campaigns leads add --query ...` - the same names/mapping
|
|
998
998
|
// as the top-level VALUE_FLAGS/BOOL_FLAGS (minus the ones the sequencer's
|
|
999
999
|
// filter-enroll endpoint doesn't accept, e.g. --verified-only), plus --query
|
|
1000
1000
|
// as an explicit alias for free-text `q` (clearer than a bare positional in a
|
|
@@ -1053,7 +1053,7 @@ function readFlags(argv, valueFlags = {}, boolFlags = {}) {
|
|
|
1053
1053
|
// A saved base from `argorant login --base …` only applies when the caller did
|
|
1054
1054
|
// NOT pass --base. Inferring "no flag given" from the VALUE (=== DEFAULT_BASE)
|
|
1055
1055
|
// meant `--base https://argorant.com` was silently ignored after a staging
|
|
1056
|
-
// login
|
|
1056
|
+
// login - requests went to the wrong host with no indication.
|
|
1057
1057
|
function applySavedBase(args) {
|
|
1058
1058
|
if (args.baseExplicit || process.env.ARGORANT_API_BASE) return args;
|
|
1059
1059
|
const saved = loadConfig().base;
|
|
@@ -1088,7 +1088,7 @@ async function resolveCampaign(base, key, identifier) {
|
|
|
1088
1088
|
}
|
|
1089
1089
|
if (matches.length > 1) {
|
|
1090
1090
|
die(
|
|
1091
|
-
`"${identifier}" matches ${matches.length} campaigns
|
|
1091
|
+
`"${identifier}" matches ${matches.length} campaigns - be more specific:\n` +
|
|
1092
1092
|
matches.map((c) => ` ${c.name} ${dim(c.id)}`).join("\n")
|
|
1093
1093
|
);
|
|
1094
1094
|
}
|
|
@@ -1106,8 +1106,8 @@ async function campaignsList(argv) {
|
|
|
1106
1106
|
for (const c of campaigns) {
|
|
1107
1107
|
const sent = Number(c.sent_count || 0);
|
|
1108
1108
|
const replied = Number(c.replied_count || 0);
|
|
1109
|
-
const rate = sent > 0 ? `${((replied / sent) * 100).toFixed(1)}%` : "
|
|
1110
|
-
console.log(`${bold(c.name || "
|
|
1109
|
+
const rate = sent > 0 ? `${((replied / sent) * 100).toFixed(1)}%` : "-";
|
|
1110
|
+
console.log(`${bold(c.name || "-")} ${dim(c.id)}`);
|
|
1111
1111
|
console.log(
|
|
1112
1112
|
` ${c.status}` +
|
|
1113
1113
|
dim(" · leads ") + Number(c.lead_count || 0).toLocaleString() +
|
|
@@ -1248,7 +1248,7 @@ async function campaignsSteps(argv) {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
body = (body || "").trim();
|
|
1250
1250
|
if (!body) die("body is empty");
|
|
1251
|
-
// The CLI never generates copy
|
|
1251
|
+
// The CLI never generates copy - the operator/agent writes it; this command
|
|
1252
1252
|
// only upserts what it's given.
|
|
1253
1253
|
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1254
1254
|
const stepBody = { step_number: stepNumber, subject, body, copy_status: args.approve ? "approved" : "draft" };
|
|
@@ -1270,7 +1270,7 @@ async function campaignsInboxes(argv) {
|
|
|
1270
1270
|
if (!count || count < 1) die("--count must be a positive integer");
|
|
1271
1271
|
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1272
1272
|
|
|
1273
|
-
// Fleet changes only ever happen via this explicit command
|
|
1273
|
+
// Fleet changes only ever happen via this explicit command - never
|
|
1274
1274
|
// implicitly from create/start. Exclude whatever's already attached to THIS
|
|
1275
1275
|
// campaign (an inbox can serve multiple campaigns; "unattached" is relative
|
|
1276
1276
|
// to this one), then page through the healthy/usable pool for candidates.
|
|
@@ -1405,7 +1405,7 @@ async function campaignsStatus(argv) {
|
|
|
1405
1405
|
const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}`, { key }), "campaign");
|
|
1406
1406
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1407
1407
|
const c = r.campaign || {};
|
|
1408
|
-
console.log(`${bold(c.name || "
|
|
1408
|
+
console.log(`${bold(c.name || "-")} ${dim(c.id)}`);
|
|
1409
1409
|
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()}`);
|
|
1410
1410
|
console.log(` emails ${(r.emails || []).length} · senders ${(r.senders || []).length}`);
|
|
1411
1411
|
printSettings({ settings: r.settings });
|
|
@@ -1511,7 +1511,7 @@ async function campaignsReplies(argv) {
|
|
|
1511
1511
|
if (!(r.replies || []).length) return console.log(dim("No replies yet."));
|
|
1512
1512
|
for (const e of r.replies) {
|
|
1513
1513
|
console.log(`${bold(e.from_email || "")} ${dim([e.first_name, e.last_name].filter(Boolean).join(" "))} ${e.classification || ""} ${dim(e.created_at || "")}`);
|
|
1514
|
-
console.log(dim(` ${(e.subject || "").slice(0, 80)}
|
|
1514
|
+
console.log(dim(` ${(e.subject || "").slice(0, 80)} - ${(e.body || "").replace(/\s+/g, " ").slice(0, 140)}`));
|
|
1515
1515
|
}
|
|
1516
1516
|
}
|
|
1517
1517
|
|
|
@@ -1530,7 +1530,7 @@ async function cmdInboxThreads(argv) {
|
|
|
1530
1530
|
for (const e of r.events) {
|
|
1531
1531
|
console.log(`${dim(e.id)}`);
|
|
1532
1532
|
console.log(` ${bold(e.from_email || e.lead_email || "")} ${e.classification || ""} ${dim(e.campaign_name || "")} ${dim(e.created_at || "")}`);
|
|
1533
|
-
console.log(dim(` ${(e.subject || "").slice(0, 80)}
|
|
1533
|
+
console.log(dim(` ${(e.subject || "").slice(0, 80)} - ${(e.body || e.snippet || "").replace(/\s+/g, " ").slice(0, 140)}`));
|
|
1534
1534
|
}
|
|
1535
1535
|
return;
|
|
1536
1536
|
}
|
|
@@ -1624,7 +1624,7 @@ async function cmdBlocklist(argv) {
|
|
|
1624
1624
|
function campaignsHelp() {
|
|
1625
1625
|
const p = bold("argorant campaigns");
|
|
1626
1626
|
console.log(`
|
|
1627
|
-
${bold("Argorant Campaigns")}
|
|
1627
|
+
${bold("Argorant Campaigns")} - email outreach from the terminal
|
|
1628
1628
|
|
|
1629
1629
|
Works with any Argorant API key. Nothing is sent until you launch.
|
|
1630
1630
|
|
|
@@ -1654,11 +1654,14 @@ ${bold("USAGE")}
|
|
|
1654
1654
|
${bold("argorant inboxes")} set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--timezone <tz>]
|
|
1655
1655
|
${bold("argorant inboxes")} disconnect <mailbox>
|
|
1656
1656
|
${bold("argorant inboxes")} connect-google --admin <admin@yourdomain.com> [--emails a,b | --all]
|
|
1657
|
-
${bold("argorant inboxes")}
|
|
1657
|
+
${bold("argorant inboxes")} endings ${dim("the domain endings on offer (com, net, org, co, sbs, biz, info)")}
|
|
1658
|
+
${bold("argorant inboxes")} suggest <website|brand> [--ending com] [-n 20] ${dim("free domain names with prices")}
|
|
1658
1659
|
${bold("argorant inboxes")} quote --mailboxes 100 [--company acme.com] [--domains a.com,b.com] [--currency usd|eur] [--billing month|quarter]
|
|
1659
|
-
${bold("argorant inboxes")} order <quote-id> --senders "Elena Vaughn[,Mark Hill]" [--website acme.com] [-y] ${dim("
|
|
1660
|
+
${bold("argorant inboxes")} order <quote-id> --senders "Elena Vaughn[,Mark Hill]" [--website acme.com] [--pay-with link] [-y] ${dim("the saved card when your workspace allows it, else a payment link")}
|
|
1660
1661
|
${bold("argorant inboxes")} status [<order-id>]
|
|
1661
|
-
${bold("argorant inboxes")}
|
|
1662
|
+
${bold("argorant inboxes")} renewal <order-id> <domain> --off|--on [-y] ${dim("Stop renewal / Turn renewal back on; nothing is charged now")}
|
|
1663
|
+
${bold("argorant inboxes")} cancel-bundle <order-id> <bundle-id> --reason <reason> [--note "..."] [--keep-domains] [-y]
|
|
1664
|
+
${dim(" reasons: too_expensive, not_sending_enough, deliverability, switching_provider, campaign_finished, other")}
|
|
1662
1665
|
${bold("argorant blocklist")} list | add <email|domain> | remove <id>
|
|
1663
1666
|
|
|
1664
1667
|
${bold("argorant inbox")} list [--folder replies|interested|not_interested|out_of_office|bounces|unsubscribes|all] [--campaign <c>] [--q text]
|
|
@@ -1780,11 +1783,18 @@ async function cmdInboxes(argv) {
|
|
|
1780
1783
|
for (const c of r.connected || []) console.log(dim(` ${c.email} · ${c.health || "ok"}`));
|
|
1781
1784
|
return;
|
|
1782
1785
|
}
|
|
1786
|
+
if (sub === "endings") {
|
|
1787
|
+
const args = readFlags(argv.slice(1));
|
|
1788
|
+
const r = need(await request("GET", args.base, "/api/v1/inboxes/endings", { key }), "inboxes endings");
|
|
1789
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1790
|
+
return console.log((r.endings || []).map((e) => "." + e).join(" "));
|
|
1791
|
+
}
|
|
1783
1792
|
if (sub === "suggest") {
|
|
1784
|
-
const args = readFlags(argv.slice(1), { "-n": "limit", "--limit": "limit" });
|
|
1793
|
+
const args = readFlags(argv.slice(1), { "-n": "limit", "--limit": "limit", "--ending": "ending", "--tld": "ending" });
|
|
1785
1794
|
const company = args._.join(" ").trim();
|
|
1786
|
-
if (!company) die("usage: argorant inboxes suggest <website|brand> [-n 20]");
|
|
1795
|
+
if (!company) die("usage: argorant inboxes suggest <website|brand> [--ending com] [-n 20]");
|
|
1787
1796
|
const query = { company, limit: args.limit ? parseLimit(args.limit, "--limit") : 20 };
|
|
1797
|
+
if (args.ending) query.ending = String(args.ending).replace(/^\./, "").toLowerCase();
|
|
1788
1798
|
const r = need(await request("GET", args.base, "/api/v1/inboxes/domains/suggest", { key, query }), "inboxes suggest");
|
|
1789
1799
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1790
1800
|
for (const d of r.results || []) {
|
|
@@ -1816,32 +1826,47 @@ async function cmdInboxes(argv) {
|
|
|
1816
1826
|
console.log(bold(`Due today: ${money(q.due_today_cents, cur)}`) + ` then ${money(q.recurring_cents, cur)} per ${q.mailboxes.billing}; domains renew at ${money(q.renewal_per_year_cents, cur)} a year in total`);
|
|
1817
1827
|
console.log(dim(q.currency_rule));
|
|
1818
1828
|
for (const line of q.included || []) console.log(dim(` · ${line}`));
|
|
1829
|
+
const card = q.payment && q.payment.saved_card;
|
|
1830
|
+
if (card && card.usable) console.log(bold(`Paid with: the saved ${card.card}, charged right away when you order (within 15 minutes of this quote)`));
|
|
1831
|
+
else if (card) console.log(dim(`Paid with: a payment link${card.reason ? ` (saved card: ${card.reason})` : ""}`));
|
|
1819
1832
|
console.log(`\nQuote ${bold(q.quote_id)} ${dim(`(valid until ${String(q.expires_at).slice(0, 16).replace("T", " ")} UTC)`)}`);
|
|
1820
1833
|
if (q.can_order) console.log(dim(`Order it: argorant inboxes order ${q.quote_id} --senders "First Last"`));
|
|
1821
1834
|
else console.log(yellow(q.next_step));
|
|
1822
1835
|
return;
|
|
1823
1836
|
}
|
|
1824
1837
|
if (sub === "order") {
|
|
1825
|
-
const args = readFlags(argv.slice(1), { "--senders": "senders", "--sender": "senders", "--website": "website" });
|
|
1838
|
+
const args = readFlags(argv.slice(1), { "--senders": "senders", "--sender": "senders", "--website": "website", "--pay-with": "payWith" });
|
|
1826
1839
|
const quoteId = args._[0];
|
|
1827
|
-
if (!quoteId || !args.senders) die("usage: argorant inboxes order <quote-id> --senders \"Elena Vaughn[,Mark Hill]\" [--website acme.com] [-y]");
|
|
1840
|
+
if (!quoteId || !args.senders) die("usage: argorant inboxes order <quote-id> --senders \"Elena Vaughn[,Mark Hill]\" [--website acme.com] [--pay-with link] [-y]");
|
|
1841
|
+
const payWith = String(args.payWith || "auto").toLowerCase();
|
|
1842
|
+
if (!["auto", "link"].includes(payWith)) die("--pay-with is link (always a payment link); leave it out for the saved card when allowed");
|
|
1828
1843
|
const senders = String(args.senders).split(",").map((x) => x.trim()).filter(Boolean).map((full) => {
|
|
1829
1844
|
const parts = full.split(/\s+/);
|
|
1830
1845
|
if (parts.length < 2) die(`sender "${full}" needs a first and a last name`);
|
|
1831
1846
|
return { first_name: parts[0], last_name: parts.slice(1).join(" ") };
|
|
1832
1847
|
});
|
|
1833
1848
|
if (!args.yes) {
|
|
1849
|
+
const how = payWith === "link" ? "You get a payment link; nothing is bought before you pay it."
|
|
1850
|
+
: "If your workspace saved its card for API and agent purchases (the quote says so), that card is charged now; otherwise you get a payment link.";
|
|
1834
1851
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1835
|
-
const ok = await new Promise((r) => rl.question(`Order the inboxes in quote ${quoteId}?
|
|
1852
|
+
const ok = await new Promise((r) => rl.question(`Order the inboxes in quote ${quoteId}? ${how} [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
|
|
1836
1853
|
if (!ok) return console.log(dim("aborted"));
|
|
1837
1854
|
}
|
|
1838
1855
|
const body = { quote_id: quoteId, senders, website: args.website || "", confirmed: true };
|
|
1839
|
-
|
|
1856
|
+
if (payWith !== "auto") body.pay_with = payWith;
|
|
1857
|
+
// Same command, same key: running it again returns the same order (and never charges twice).
|
|
1840
1858
|
const idem = "cli-" + crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex").slice(0, 40);
|
|
1841
1859
|
const r = need(await request("POST", args.base, "/api/v1/inboxes/orders", { key, body, headers: { "Idempotency-Key": idem } }), "inboxes order");
|
|
1842
1860
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1861
|
+
if (r.status === "paid") {
|
|
1862
|
+
console.log(green("✓") + ` Order ${bold(r.order_id)} paid with the saved ${r.card || "card"}${r.replayed ? dim(" (already placed)") : ""}`);
|
|
1863
|
+
console.log(dim(r.message || ""));
|
|
1864
|
+
console.log(dim(`Track it: argorant inboxes status ${r.order_id}`));
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1843
1867
|
if (!r.checkout_url) return console.log(`Order ${r.order_id}: ${r.status}. ${dim(r.message || "")}`);
|
|
1844
1868
|
console.log(green("✓") + ` Order ${bold(r.order_id)}${r.existing || r.replayed ? dim(" (already placed)") : ""}`);
|
|
1869
|
+
if (r.saved_card && r.saved_card.reason) console.log(yellow(`Not charged to the saved card: ${r.saved_card.reason}`));
|
|
1845
1870
|
console.log(`Pay here: ${cyan(r.checkout_url)}`);
|
|
1846
1871
|
console.log(dim("Nothing is bought until you pay it; setup starts after the payment is confirmed."));
|
|
1847
1872
|
console.log(dim(`Track it: argorant inboxes status ${r.order_id}`));
|
|
@@ -1857,7 +1882,7 @@ async function cmdInboxes(argv) {
|
|
|
1857
1882
|
if (!rows.length) return console.log(dim("No inbox orders yet. Start with: argorant inboxes quote --mailboxes 100 --company <your website>"));
|
|
1858
1883
|
for (const o of rows) {
|
|
1859
1884
|
const mb = o.mailboxes || {};
|
|
1860
|
-
console.log(`${bold(o.order_id)} ${o.status}${o.step ? dim(` · ${o.step}`) : ""} ${dim(`${mb.created || 0}/${mb.total || 0} mailboxes`)}${o.aging_ends_at ? dim(` · aging ends ${o.aging_ends_at}`) : ""}`);
|
|
1885
|
+
console.log(`${bold(o.order_id)} ${o.status}${o.step ? dim(` · ${o.step}`) : ""} ${dim(`${mb.created || 0}/${mb.total || 0} mailboxes`)}${o.aging_ends_at ? dim(` · aging ends ${o.aging_ends_at}`) : ""}${o.ordered_via ? dim(` · ordered via API/agent${o.paid_with === "saved_card" ? ", saved card" : ""}`) : ""}`);
|
|
1861
1886
|
for (const d of o.domains || []) {
|
|
1862
1887
|
console.log(dim(` ${d.name}${d.expires_at ? ` · expires ${String(d.expires_at).slice(0, 10)}` : ""} · renews at ${money(d.renewal_per_year_cents, o.currency)}/year${d.auto_renew === false ? " · renewal off" : ""}`));
|
|
1863
1888
|
}
|
|
@@ -1867,26 +1892,475 @@ async function cmdInboxes(argv) {
|
|
|
1867
1892
|
}
|
|
1868
1893
|
return;
|
|
1869
1894
|
}
|
|
1870
|
-
if (sub === "
|
|
1871
|
-
|
|
1895
|
+
if (sub === "renewal" || sub === "cancel-domain") {
|
|
1896
|
+
// cancel-domain is the old name of `renewal --off` (kept working, deprecated).
|
|
1897
|
+
const args = readFlags(argv.slice(1), {}, { "--on": "on", "--off": "off" });
|
|
1898
|
+
const [orderId, domain] = args._;
|
|
1899
|
+
const on = sub === "renewal" ? (args.on ? true : args.off ? false : null) : false;
|
|
1900
|
+
if (!orderId || !domain || on === null || (args.on && args.off)) die("usage: argorant inboxes renewal <order-id> <domain> --off|--on [-y]");
|
|
1901
|
+
if (sub === "cancel-domain") console.error(dim("cancel-domain is deprecated: use `argorant inboxes renewal <order-id> <domain> --off`."));
|
|
1902
|
+
const question = on
|
|
1903
|
+
? `Turn renewal back on for ${domain}? Nothing is charged now; it renews at its expiry date, billed shortly before. [y/N] `
|
|
1904
|
+
: `Stop renewal for ${domain}? Nothing is charged now; it keeps working until its expiry date and is not renewed. [y/N] `;
|
|
1905
|
+
if (!args.yes) {
|
|
1906
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1907
|
+
const ok = await new Promise((r) => rl.question(question, (a) => { rl.close(); r(/^y/i.test(a)); }));
|
|
1908
|
+
if (!ok) return console.log(dim("aborted"));
|
|
1909
|
+
}
|
|
1910
|
+
const r = need(await request("POST", args.base, `/api/v1/inboxes/orders/${encodeURIComponent(orderId)}/domains/${encodeURIComponent(domain)}/renewal`,
|
|
1911
|
+
{ key, body: { on } }), "inboxes renewal");
|
|
1912
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1913
|
+
return console.log(green("✓") + ` ${r.message || (on ? `${domain} renews again` : `${domain} will not renew`)}`);
|
|
1914
|
+
}
|
|
1915
|
+
if (sub === "cancel-bundle") {
|
|
1916
|
+
const REASONS = ["too_expensive", "not_sending_enough", "deliverability", "switching_provider", "campaign_finished", "other"];
|
|
1917
|
+
const args = readFlags(argv.slice(1), { "--reason": "reason", "--note": "note" }, { "--keep-domains": "keepDomains" });
|
|
1872
1918
|
const [orderId, target] = args._;
|
|
1873
|
-
if (!orderId || !target) die(
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
|
|
1919
|
+
if (!orderId || !target || !args.reason) die("usage: argorant inboxes cancel-bundle <order-id> <bundle-id> --reason <reason> [--note \"...\"] [--keep-domains] [-y]\n reasons: " + REASONS.join(", "));
|
|
1920
|
+
const reason = String(args.reason).toLowerCase();
|
|
1921
|
+
if (!REASONS.includes(reason)) die(`--reason is one of: ${REASONS.join(", ")}`);
|
|
1922
|
+
const question = `Cancel bundle ${target}? Billing for its mailboxes stops now (no refund); they work until the paid period ends, then they are deleted${args.keepDomains ? "" : " and its domains stop renewing"}. [y/N] `;
|
|
1877
1923
|
if (!args.yes) {
|
|
1878
1924
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1879
1925
|
const ok = await new Promise((r) => rl.question(question, (a) => { rl.close(); r(/^y/i.test(a)); }));
|
|
1880
1926
|
if (!ok) return console.log(dim("aborted"));
|
|
1881
1927
|
}
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1928
|
+
const body = { keep_domains: !!args.keepDomains, reason };
|
|
1929
|
+
if (args.note) body.note = String(args.note);
|
|
1930
|
+
const r = need(await request("POST", args.base, `/api/v1/inboxes/orders/${encodeURIComponent(orderId)}/bundles/${encodeURIComponent(target)}/cancel`,
|
|
1931
|
+
{ key, body }), "inboxes cancel-bundle");
|
|
1886
1932
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1887
|
-
return console.log(green("✓") +
|
|
1933
|
+
return console.log(green("✓") + ` Bundle ${target} cancelled${r.existing ? dim(" (already)") : ""}`);
|
|
1934
|
+
}
|
|
1935
|
+
die(`unknown inboxes subcommand: ${sub} (list, set, disconnect, connect-google, endings, suggest, quote, order, status, renewal, cancel-bundle)`);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// =============================================================================
|
|
1939
|
+
// find: the email finder. A person's name plus a company domain in, a work
|
|
1940
|
+
// email out. Priced per outcome by the server (GET /api/v1/email-finder/pricing):
|
|
1941
|
+
// an address returned costs 1 credit, a lookup that finds nothing 0.25 credit,
|
|
1942
|
+
// and lookups that cannot run (no mail server, try again later, invalid input)
|
|
1943
|
+
// are free. Nothing is charged on any error response.
|
|
1944
|
+
//
|
|
1945
|
+
// Own flag reader (readFlags) like `campaigns`: --first/--last/--max-credits
|
|
1946
|
+
// are not search filters, and `--name` means the job name with --file but the
|
|
1947
|
+
// person's name on a single lookup (same habit as `enrich --name`).
|
|
1948
|
+
// Exit codes: a single lookup exits 1 when no email came back (like `enrich`);
|
|
1949
|
+
// a --file job that pauses exits 5 (out of credits) or 4 (daily limit or slowed
|
|
1950
|
+
// down), so scripts can branch without parsing output.
|
|
1951
|
+
// =============================================================================
|
|
1952
|
+
|
|
1953
|
+
const FIND_API = "/api/v1/email-finder";
|
|
1954
|
+
const FIND_DEFAULT_OUT = "argorant-found.csv";
|
|
1955
|
+
const FIND_POLL_MS = 2500;
|
|
1956
|
+
const FIND_VALUE_FLAGS = {
|
|
1957
|
+
"--domain": "domain",
|
|
1958
|
+
"--first": "first",
|
|
1959
|
+
"--first-name": "first",
|
|
1960
|
+
"--last": "last",
|
|
1961
|
+
"--last-name": "last",
|
|
1962
|
+
"--name": "name",
|
|
1963
|
+
"-f": "file",
|
|
1964
|
+
"--file": "file",
|
|
1965
|
+
"-o": "output",
|
|
1966
|
+
"--output": "output",
|
|
1967
|
+
"--max-credits": "maxCredits",
|
|
1968
|
+
};
|
|
1969
|
+
const FIND_STATUS_LABELS = {
|
|
1970
|
+
valid: "Confirmed",
|
|
1971
|
+
catch_all: "Unconfirmed, catch-all domain",
|
|
1972
|
+
not_found: "Not found",
|
|
1973
|
+
no_mail_domain: "No mail server",
|
|
1974
|
+
retry_later: "Try again later",
|
|
1975
|
+
invalid_input: "Invalid input",
|
|
1976
|
+
pending: "Pending",
|
|
1977
|
+
};
|
|
1978
|
+
const FIND_PAUSED_REASONS = {
|
|
1979
|
+
insufficient_credits: "your workspace ran out of credits. Add credits, then resume.",
|
|
1980
|
+
spend_limit: "the job reached its credit cap (--max-credits). Resume with a higher cap to continue.",
|
|
1981
|
+
daily_limit: "the daily lookup limit was reached.",
|
|
1982
|
+
miss_rate: "almost none of the recent lookups found an address, so lookups are slowed down for a while.",
|
|
1983
|
+
};
|
|
1984
|
+
const FIND_TERMINAL = ["done", "cancelled", "canceled", "paused"];
|
|
1985
|
+
const FIND_USAGE =
|
|
1986
|
+
'usage: argorant find "<first last>" --domain <company.com>\n' +
|
|
1987
|
+
" argorant find --first <first> --last <last> --domain <company.com>\n" +
|
|
1988
|
+
' argorant find --file people.csv [-o found.csv] [--max-credits N] [--name "Q4 list"]\n' +
|
|
1989
|
+
" argorant find status | download | resume | cancel <job_id>\n" +
|
|
1990
|
+
" argorant find pricing";
|
|
1991
|
+
|
|
1992
|
+
// 1 -> "1 credit", 0.25 -> "0.25 credit", 2 / 0 -> "2 credits" / "0 credits".
|
|
1993
|
+
function fmtCredits(n) {
|
|
1994
|
+
const v = Number(n || 0);
|
|
1995
|
+
return `${v.toLocaleString("en-US", { maximumFractionDigits: 2 })} credit${v > 0 && v <= 1 ? "" : "s"}`;
|
|
1996
|
+
}
|
|
1997
|
+
function fmtCharge(n) {
|
|
1998
|
+
return Number(n || 0) > 0 ? fmtCredits(n) : "not charged";
|
|
1999
|
+
}
|
|
2000
|
+
function fmtWait(sec) {
|
|
2001
|
+
if (sec < 90) return `${sec} seconds`;
|
|
2002
|
+
if (sec < 5400) return `${Math.round(sec / 60)} minutes`;
|
|
2003
|
+
return `${Math.round(sec / 3600)} hours`;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// The finder answers errors FastAPI-style under `detail`; tolerate a bare
|
|
2007
|
+
// {error, message} body too, and turn Retry-After into a plain wait time.
|
|
2008
|
+
function findNeed(res, what, { expect } = {}) {
|
|
2009
|
+
const j = res.json;
|
|
2010
|
+
let r = res;
|
|
2011
|
+
// need() lets any status below 400 through. A redirect (http -> https, a
|
|
2012
|
+
// wrong --base) must not read as "no email found" or "job resumed".
|
|
2013
|
+
if (res.status >= 300 && res.status < 400) {
|
|
2014
|
+
const to = res.res && res.res.headers && res.res.headers.location;
|
|
2015
|
+
die(`${what}: the server answered with a redirect (HTTP ${res.status}${to ? ` to ${to}` : ""}). Check --base / ARGORANT_API_BASE.`);
|
|
2016
|
+
}
|
|
2017
|
+
if (res.status >= 400 && j && typeof j === "object" && !Array.isArray(j) && j.detail === undefined && (j.error || j.message)) {
|
|
2018
|
+
r = { ...res, json: { detail: j } };
|
|
2019
|
+
}
|
|
2020
|
+
if (r.status === 429) {
|
|
2021
|
+
const ra = Number((res.res && res.res.headers && res.res.headers["retry-after"]) || 0);
|
|
2022
|
+
const msg = detailMsg(r.json && r.json.detail) || "the email finder limit was reached.";
|
|
2023
|
+
die(Number.isFinite(ra) && ra > 0 ? `${msg} Try again in ${fmtWait(Math.ceil(ra))}. Nothing was charged.` : `${msg} Nothing was charged.`, EXIT.RATE_LIMIT);
|
|
2024
|
+
}
|
|
2025
|
+
let out = need(r, what);
|
|
2026
|
+
// Job endpoints answer {ok, job: {...}}; the commands work on the job itself.
|
|
2027
|
+
if (out && typeof out === "object" && out.job && typeof out.job === "object" && !Array.isArray(out.job)) out = out.job;
|
|
2028
|
+
if (expect && (out === null || typeof out !== "object" || out[expect] === undefined || out[expect] === null)) {
|
|
2029
|
+
die(`${what}: unexpected response from the server (no "${expect}" field). Check --base / ARGORANT_API_BASE.`);
|
|
2030
|
+
}
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function parseMaxCredits(raw) {
|
|
2035
|
+
if (raw === undefined || raw === null) return null;
|
|
2036
|
+
const n = Number(raw);
|
|
2037
|
+
if (!Number.isFinite(n) || n <= 0) die(`--max-credits must be a positive number (got "${raw}").`);
|
|
2038
|
+
return n;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function findJobId(args, sub) {
|
|
2042
|
+
const id = String(args._[1] || "").trim();
|
|
2043
|
+
if (!id) die(`usage: argorant find ${sub} <job_id>${sub === "download" ? " [-o found.csv]" : sub === "resume" ? " [--max-credits N]" : ""}`);
|
|
2044
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(id)) die(`that does not look like a job id: ${id}`);
|
|
2045
|
+
return id;
|
|
2046
|
+
}
|
|
2047
|
+
const findJobPath = (id, suffix = "") => `${FIND_API}/jobs/${encodeURIComponent(id)}${suffix}`;
|
|
2048
|
+
|
|
2049
|
+
// Server prices, tolerant of a flat or nested shape. Fallbacks are the
|
|
2050
|
+
// documented defaults; the server stays the source of truth for charges.
|
|
2051
|
+
function findPrices(p) {
|
|
2052
|
+
const pr = (p && (p.prices || p.pricing)) || {};
|
|
2053
|
+
const pick = (...vals) => {
|
|
2054
|
+
for (let v of vals) {
|
|
2055
|
+
if (v && typeof v === "object") v = v.credits;
|
|
2056
|
+
if (v !== undefined && v !== null && v !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
2057
|
+
}
|
|
2058
|
+
return null;
|
|
2059
|
+
};
|
|
2060
|
+
const found = pick(pr.valid, pr.found, p && p.price_found);
|
|
2061
|
+
return {
|
|
2062
|
+
known: found !== null,
|
|
2063
|
+
found: found ?? 1,
|
|
2064
|
+
catchAll: pick(pr.catch_all, p && p.price_catch_all) ?? found ?? 1,
|
|
2065
|
+
notFound: pick(pr.not_found, p && p.price_not_found) ?? 0.25,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function findCounts(job) {
|
|
2070
|
+
const c = job.counts || {};
|
|
2071
|
+
const confirmed = Number(c.valid || 0);
|
|
2072
|
+
const catchAll = Number(c.catch_all || 0);
|
|
2073
|
+
return {
|
|
2074
|
+
confirmed,
|
|
2075
|
+
catchAll,
|
|
2076
|
+
found: confirmed + catchAll,
|
|
2077
|
+
notFound: Number(c.not_found || 0),
|
|
2078
|
+
free: Number(c.no_mail_domain || 0) + Number(c.retry_later || 0) + Number(c.invalid_input || 0),
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function findProgress(job) {
|
|
2082
|
+
const k = findCounts(job);
|
|
2083
|
+
return `${job.status || "queued"} ${Number(job.processed || 0).toLocaleString()}/${Number(job.total || 0).toLocaleString()} processed · ${k.found.toLocaleString()} found · ${fmtCredits(job.credits_charged)} charged`;
|
|
2084
|
+
}
|
|
2085
|
+
function findSummaryLines(job) {
|
|
2086
|
+
const k = findCounts(job);
|
|
2087
|
+
const lines = [
|
|
2088
|
+
` ${Number(job.processed || 0).toLocaleString()}/${Number(job.total || 0).toLocaleString()} processed · ` +
|
|
2089
|
+
`${bold(k.found.toLocaleString())} found ${dim(`(${k.confirmed.toLocaleString()} confirmed, ${k.catchAll.toLocaleString()} unconfirmed catch-all)`)} · ` +
|
|
2090
|
+
`${k.notFound.toLocaleString()} not found · ${k.free.toLocaleString()} not charged`,
|
|
2091
|
+
` ${fmtCredits(job.credits_charged)} charged` + (job.max_credits != null ? dim(` · cap ${fmtCredits(job.max_credits)}`) : ""),
|
|
2092
|
+
];
|
|
2093
|
+
if (Number(job.duplicates_removed || 0) > 0) lines.push(dim(` ${Number(job.duplicates_removed).toLocaleString()} duplicate rows removed`));
|
|
2094
|
+
const bad = Array.isArray(job.invalid_rows) ? job.invalid_rows : [];
|
|
2095
|
+
if (bad.length) {
|
|
2096
|
+
lines.push(dim(` ${bad.length.toLocaleString()} rows skipped as invalid (row ${bad.slice(0, 10).join(", ")}${bad.length > 10 ? ", ..." : ""})`));
|
|
2097
|
+
}
|
|
2098
|
+
return lines;
|
|
2099
|
+
}
|
|
2100
|
+
function findPausedLines(job, id) {
|
|
2101
|
+
const r = job.paused_reason;
|
|
2102
|
+
const lines = [yellow(`Paused: ${FIND_PAUSED_REASONS[r] || (r ? `reason "${r}".` : "no reason given.")}`)];
|
|
2103
|
+
if (job.resume_after) lines.push(dim(`It can continue after ${job.resume_after}.`));
|
|
2104
|
+
lines.push(`Resume: argorant find resume ${id} ${r === "spend_limit" ? "--max-credits <new cap>" : "[--max-credits N]"}`);
|
|
2105
|
+
return lines;
|
|
2106
|
+
}
|
|
2107
|
+
function findPausedExit(reason) {
|
|
2108
|
+
if (reason === "insufficient_credits") return EXIT.UPGRADE;
|
|
2109
|
+
if (reason === "daily_limit" || reason === "miss_rate") return EXIT.RATE_LIMIT;
|
|
2110
|
+
return EXIT.OK; // spend_limit: the job stopped at the cap the caller set.
|
|
2111
|
+
}
|
|
2112
|
+
function findStatusTag(status) {
|
|
2113
|
+
const s = String(status || "").toLowerCase();
|
|
2114
|
+
if (s === "done") return green(s);
|
|
2115
|
+
if (s === "paused") return yellow(s);
|
|
2116
|
+
return dim(s || "unknown");
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async function findSingle(args, key) {
|
|
2120
|
+
const raw = String(args.domain || "").trim();
|
|
2121
|
+
const domain = normalizeDomain(raw.includes("@") ? raw.split("@").pop() : raw);
|
|
2122
|
+
const first = String(args.first || "").trim();
|
|
2123
|
+
const last = String(args.last || "").trim();
|
|
2124
|
+
const name = (args._.join(" ") || String(args.name || "")).trim();
|
|
2125
|
+
if (!domain && !first && !last && !name) die(FIND_USAGE);
|
|
2126
|
+
if (!domain) die(`--domain is required (the company the person works at).\n${FIND_USAGE}`);
|
|
2127
|
+
if (!domain.includes(".")) die(`--domain needs a company domain like acme.com (got "${raw}").`);
|
|
2128
|
+
if ((first || last) && name) die("pass either a full name or --first/--last, not both.");
|
|
2129
|
+
if (!first && !last && !name) die(`a name is required.\n${FIND_USAGE}`);
|
|
2130
|
+
if (args.output) warn("-o is for --file jobs; a single lookup prints its result.");
|
|
2131
|
+
if (args.maxCredits !== undefined) warn("--max-credits is for --file jobs; a single lookup costs at most 1 credit.");
|
|
2132
|
+
const body = first || last ? { first_name: first, last_name: last, domain } : { name, domain };
|
|
2133
|
+
const r = findNeed(await request("POST", args.base, `${FIND_API}/find`, { key, body }), "find", { expect: "status" });
|
|
2134
|
+
const gotEmail = Boolean(r.email);
|
|
2135
|
+
if (args.json) {
|
|
2136
|
+
console.log(JSON.stringify(r, null, 2));
|
|
2137
|
+
if (!gotEmail) process.exit(EXIT.ERROR);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const label = FIND_STATUS_LABELS[r.status] || r.status || "unknown";
|
|
2141
|
+
const tag = r.status === "valid" ? green(label) : r.status === "catch_all" ? yellow(label) : dim(label);
|
|
2142
|
+
const parts = [gotEmail ? bold(r.email) : dim("no email found"), tag];
|
|
2143
|
+
if (gotEmail && r.confidence !== undefined && r.confidence !== null) parts.push(`confidence ${r.confidence}`);
|
|
2144
|
+
parts.push(fmtCharge(r.credits_charged));
|
|
2145
|
+
console.log(parts.join(dim(" · ")));
|
|
2146
|
+
if (!gotEmail) process.exit(EXIT.ERROR);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
async function pollFindJob(args, key, job) {
|
|
2150
|
+
const id = job.job_id;
|
|
2151
|
+
const tty = Boolean(process.stdout.isTTY);
|
|
2152
|
+
const recovery = `The job keeps running. Check it with \`argorant find status ${id}\` and download with \`argorant find download ${id} -o ${args.output || FIND_DEFAULT_OUT}\`.`;
|
|
2153
|
+
const onInt = () => {
|
|
2154
|
+
if (tty && !args.json) process.stdout.write("\n");
|
|
2155
|
+
process.stderr.write(dim(`Stopped watching. ${recovery}\n`));
|
|
2156
|
+
process.exit(130);
|
|
2157
|
+
};
|
|
2158
|
+
process.once("SIGINT", onInt);
|
|
2159
|
+
const started = Date.now();
|
|
2160
|
+
let shown = "";
|
|
2161
|
+
let failures = 0;
|
|
2162
|
+
try {
|
|
2163
|
+
for (;;) {
|
|
2164
|
+
if (!args.json) {
|
|
2165
|
+
const line = findProgress(job);
|
|
2166
|
+
if (line !== shown) {
|
|
2167
|
+
if (tty) process.stdout.write(`\r\x1b[K${dim(line)}`);
|
|
2168
|
+
else console.log(line);
|
|
2169
|
+
shown = line;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (FIND_TERMINAL.includes(String(job.status || "").toLowerCase())) break;
|
|
2173
|
+
if (Date.now() - started > 1000 * 60 * 60 * 12) die(`\nstopped watching after 12 hours. ${recovery}`);
|
|
2174
|
+
await new Promise((r) => setTimeout(r, FIND_POLL_MS));
|
|
2175
|
+
let res;
|
|
2176
|
+
try {
|
|
2177
|
+
res = await request("GET", args.base, findJobPath(id), { key });
|
|
2178
|
+
} catch (e) {
|
|
2179
|
+
res = { error: e };
|
|
2180
|
+
}
|
|
2181
|
+
// Ride out short network blips and gateway errors; the job is server-side.
|
|
2182
|
+
if (res.error || res.status >= 500) {
|
|
2183
|
+
failures += 1;
|
|
2184
|
+
if (failures > 5) die(`\n${res.error ? res.error.message : `job status failed (HTTP ${res.status})`}. ${recovery}`);
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
failures = 0;
|
|
2188
|
+
job = findNeed(res, "find job status", { expect: "status" });
|
|
2189
|
+
}
|
|
2190
|
+
} finally {
|
|
2191
|
+
process.removeListener("SIGINT", onInt);
|
|
2192
|
+
}
|
|
2193
|
+
if (tty && !args.json) process.stdout.write("\n");
|
|
2194
|
+
return job;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async function findFile(args, key) {
|
|
2198
|
+
if (args._.length) die("pass either a name or --file, not both.");
|
|
2199
|
+
if (args.domain || args.first || args.last) {
|
|
2200
|
+
die("--domain/--first/--last are for a single lookup; with --file every row carries its own name and domain.");
|
|
2201
|
+
}
|
|
2202
|
+
const maxCredits = parseMaxCredits(args.maxCredits);
|
|
2203
|
+
let text;
|
|
2204
|
+
try {
|
|
2205
|
+
text = fs.readFileSync(args.file, "utf8");
|
|
2206
|
+
} catch {
|
|
2207
|
+
die(`cannot read file: ${args.file}`);
|
|
2208
|
+
}
|
|
2209
|
+
text = text.replace(/^/, "");
|
|
2210
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
2211
|
+
if (!lines.length) die("file is empty");
|
|
2212
|
+
// Local count for the prompt only; the server parses the file and reports
|
|
2213
|
+
// the real total, duplicates and invalid rows on the job.
|
|
2214
|
+
const rows = lines.length - 1;
|
|
2215
|
+
if (rows < 1) die("file has a header but no rows.");
|
|
2216
|
+
// A paid job must never die on a path problem after it was created.
|
|
2217
|
+
const destArg = args.output || FIND_DEFAULT_OUT;
|
|
2218
|
+
const dest = ensureWritable(destArg);
|
|
2219
|
+
// Interactive-only by design, like reveal/export: with --yes, --json or a
|
|
2220
|
+
// non-TTY stdin this spends credits with no prompt.
|
|
2221
|
+
if (!args.yes && !args.json && process.stdin.isTTY) {
|
|
2222
|
+
const p = findNeed(await request("GET", args.base, `${FIND_API}/pricing`, { key }), "find pricing");
|
|
2223
|
+
const pr = findPrices(p);
|
|
2224
|
+
if (!pr.known) warn("the server did not return prices in a known shape; showing the standard prices.");
|
|
2225
|
+
let max = rows * Math.max(pr.found, pr.catchAll, pr.notFound);
|
|
2226
|
+
if (maxCredits !== null) max = Math.min(max, maxCredits);
|
|
2227
|
+
const ans = await prompt(
|
|
2228
|
+
`Find emails for ${bold(rows.toLocaleString())} rows? ${fmtCredits(pr.found)} per email found, ` +
|
|
2229
|
+
`${fmtCredits(pr.notFound)} when nothing is found (maximum ${fmtCredits(max)}). [y/N] `
|
|
2230
|
+
);
|
|
2231
|
+
if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
|
|
2232
|
+
}
|
|
2233
|
+
const body = { csv: text };
|
|
2234
|
+
if (args.name) body.name = String(args.name).trim();
|
|
2235
|
+
if (maxCredits !== null) body.max_credits = maxCredits;
|
|
2236
|
+
const created = findNeed(await request("POST", args.base, `${FIND_API}/jobs`, { key, body }), "find job", { expect: "job_id" });
|
|
2237
|
+
const id = created.job_id;
|
|
2238
|
+
if (args.json) {
|
|
2239
|
+
// stdout stays pure JSON; the id still reaches the terminal for recovery.
|
|
2240
|
+
process.stderr.write(dim(`job ${id}\n`));
|
|
2241
|
+
} else {
|
|
2242
|
+
const extra = [];
|
|
2243
|
+
if (Number(created.duplicates_removed || 0) > 0) extra.push(`${created.duplicates_removed} duplicates removed`);
|
|
2244
|
+
if (Array.isArray(created.invalid_rows) && created.invalid_rows.length) extra.push(`${created.invalid_rows.length} invalid rows skipped`);
|
|
2245
|
+
console.log(
|
|
2246
|
+
green("✓") + ` Job ${bold(id)} created: ${Number(created.total ?? rows).toLocaleString()} rows` + (extra.length ? dim(` (${extra.join(", ")})`) : "")
|
|
2247
|
+
);
|
|
2248
|
+
console.log(dim("Ctrl-C stops watching; the job keeps running."));
|
|
2249
|
+
}
|
|
2250
|
+
const job = await pollFindJob(args, key, created);
|
|
2251
|
+
const status = String(job.status || "").toLowerCase();
|
|
2252
|
+
try {
|
|
2253
|
+
await downloadTo(args.base, findJobPath(id, "/results?format=csv"), key, dest);
|
|
2254
|
+
} catch (e) {
|
|
2255
|
+
die(`${e.message}\nThe job itself is saved. Download it again with \`argorant find download ${id} -o ${destArg}\`.`);
|
|
1888
2256
|
}
|
|
1889
|
-
|
|
2257
|
+
const exitCode = status === "paused" ? findPausedExit(job.paused_reason) : EXIT.OK;
|
|
2258
|
+
if (args.json) {
|
|
2259
|
+
console.log(JSON.stringify({ ...job, file: dest }, null, 2));
|
|
2260
|
+
if (exitCode !== EXIT.OK) process.exit(exitCode);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const head = status === "done" ? green("✓") + " Done" : status === "paused" ? yellow("Paused") : dim(status || "stopped");
|
|
2264
|
+
console.log(`${head} ${bold(id)}`);
|
|
2265
|
+
for (const l of findSummaryLines(job)) console.log(l);
|
|
2266
|
+
const saved = countCsvRows(dest);
|
|
2267
|
+
console.log(
|
|
2268
|
+
` Saved ${saved !== null ? bold(saved.toLocaleString()) + " rows → " : ""}${bold(dest)}` +
|
|
2269
|
+
(status === "done" ? "" : dim(" (rows not processed yet show as pending)"))
|
|
2270
|
+
);
|
|
2271
|
+
if (status === "paused") for (const l of findPausedLines(job, id)) console.log(l);
|
|
2272
|
+
if (exitCode !== EXIT.OK) process.exit(exitCode);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async function findStatus(args, key) {
|
|
2276
|
+
const id = findJobId(args, "status");
|
|
2277
|
+
const job = findNeed(await request("GET", args.base, findJobPath(id), { key }), "find job status", { expect: "status" });
|
|
2278
|
+
if (args.json) return console.log(JSON.stringify(job, null, 2));
|
|
2279
|
+
const status = String(job.status || "").toLowerCase();
|
|
2280
|
+
console.log(`${bold("Job " + (job.job_id || id))}${job.name ? " " + dim(`"${job.name}"`) : ""} ${findStatusTag(status)}`);
|
|
2281
|
+
for (const l of findSummaryLines(job)) console.log(l);
|
|
2282
|
+
if (status === "paused") for (const l of findPausedLines(job, id)) console.log(" " + l);
|
|
2283
|
+
if (Number(job.processed || 0) > 0 || FIND_TERMINAL.includes(status)) {
|
|
2284
|
+
console.log(dim(` Download: argorant find download ${id} -o found.csv`));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
async function findDownload(args, key) {
|
|
2289
|
+
const id = findJobId(args, "download");
|
|
2290
|
+
const dest = ensureWritable(args.output || FIND_DEFAULT_OUT);
|
|
2291
|
+
const job = findNeed(await request("GET", args.base, findJobPath(id), { key }), "find job status", { expect: "status" });
|
|
2292
|
+
await downloadTo(args.base, findJobPath(id, "/results?format=csv"), key, dest);
|
|
2293
|
+
const status = String(job.status || "").toLowerCase();
|
|
2294
|
+
if (args.json) return console.log(JSON.stringify({ ok: true, job_id: id, status, file: dest }, null, 2));
|
|
2295
|
+
const rows = countCsvRows(dest);
|
|
2296
|
+
console.log(green("✓") + ` Saved ${rows !== null ? bold(rows.toLocaleString()) + " rows → " : ""}${bold(dest)}`);
|
|
2297
|
+
if (status !== "done") {
|
|
2298
|
+
console.log(dim(` Job is ${status || "not finished"}: ${Number(job.processed || 0).toLocaleString()}/${Number(job.total || 0).toLocaleString()} processed, the rest show as pending.`));
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
async function findResume(args, key) {
|
|
2303
|
+
const id = findJobId(args, "resume");
|
|
2304
|
+
const maxCredits = parseMaxCredits(args.maxCredits);
|
|
2305
|
+
const body = maxCredits !== null ? { max_credits: maxCredits } : {};
|
|
2306
|
+
const job = findNeed(await request("POST", args.base, findJobPath(id, "/resume"), { key, body }), "find job resume", { expect: "status" });
|
|
2307
|
+
if (args.json) return console.log(JSON.stringify(job, null, 2));
|
|
2308
|
+
console.log(green("✓") + ` Resumed job ${bold(job.job_id || id)} ${findStatusTag(job.status)}`);
|
|
2309
|
+
for (const l of findSummaryLines(job)) console.log(l);
|
|
2310
|
+
console.log(dim(` Follow it with: argorant find status ${id}`));
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
async function findCancel(args, key) {
|
|
2314
|
+
const id = findJobId(args, "cancel");
|
|
2315
|
+
const job = findNeed(await request("POST", args.base, findJobPath(id, "/cancel"), { key }), "find job cancel", { expect: "status" });
|
|
2316
|
+
if (args.json) return console.log(JSON.stringify(job, null, 2));
|
|
2317
|
+
console.log(green("✓") + ` Cancelled job ${bold(job.job_id || id)}. Remaining rows will not be looked up or charged.`);
|
|
2318
|
+
for (const l of findSummaryLines(job)) console.log(l);
|
|
2319
|
+
if (Number(job.processed || 0) > 0) console.log(dim(` Download what was found: argorant find download ${id} -o found.csv`));
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
async function findPricing(args, key) {
|
|
2323
|
+
const p = findNeed(await request("GET", args.base, `${FIND_API}/pricing`, { key }), "find pricing");
|
|
2324
|
+
if (args.json) return console.log(JSON.stringify(p, null, 2));
|
|
2325
|
+
const pr = findPrices(p);
|
|
2326
|
+
if (!pr.known) warn("the server did not return prices in a known shape; showing the standard prices (see --json for the raw answer).");
|
|
2327
|
+
console.log(bold("Email finder prices"));
|
|
2328
|
+
console.log(` Email found and confirmed ${fmtCredits(pr.found)}`);
|
|
2329
|
+
console.log(` Catch-all domain, unconfirmed ${fmtCredits(pr.catchAll)}`);
|
|
2330
|
+
console.log(` Nothing found ${fmtCredits(pr.notFound)}`);
|
|
2331
|
+
console.log(` No mail server, try again later, invalid input ${green("free")}`);
|
|
2332
|
+
const b = p.balance || {};
|
|
2333
|
+
const bal = [];
|
|
2334
|
+
if (b.credits !== undefined && b.credits !== null) bal.push(`${bold(Number(b.credits).toLocaleString("en-US", { maximumFractionDigits: 2 }))} credits`);
|
|
2335
|
+
const pre = b.prepaid_no_result_lookups;
|
|
2336
|
+
if (pre !== undefined && pre !== null) bal.push(`${Number(pre).toLocaleString()} prepaid no-result lookup${Number(pre) === 1 ? "" : "s"}`);
|
|
2337
|
+
if (bal.length) console.log(`${bold("Balance")} ${bal.join(" · ")}`);
|
|
2338
|
+
const l = p.limits || {};
|
|
2339
|
+
const perMin = l.per_minute ?? l.single_per_minute ?? l.minute;
|
|
2340
|
+
const perDay = l.per_day ?? l.daily ?? l.daily_limit;
|
|
2341
|
+
const used = l.used_today;
|
|
2342
|
+
const lim = [];
|
|
2343
|
+
if (perMin !== undefined && perMin !== null) lim.push(`${Number(perMin).toLocaleString()} lookups per minute`);
|
|
2344
|
+
if (perDay !== undefined && perDay !== null) lim.push(`${Number(perDay).toLocaleString()} per day` + (used !== undefined && used !== null ? ` (${Number(used).toLocaleString()} used today)` : ""));
|
|
2345
|
+
if (lim.length) console.log(`${bold("Limits")} ${lim.join(" · ")}`);
|
|
2346
|
+
if (p.billing_exempt) console.log(dim("Lookups on this account are not charged."));
|
|
2347
|
+
console.log(dim("Nothing is charged when a request fails. A 0.25 charge takes 1 whole credit and keeps the rest for your next no-result lookups."));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
async function cmdFind(argv) {
|
|
2351
|
+
const args = readFlags(argv, FIND_VALUE_FLAGS);
|
|
2352
|
+
const sub = String(args._[0] || "").toLowerCase();
|
|
2353
|
+
if (sub === "help" || (!args._.length && !args.file && !args.domain && !args.first && !args.last && !args.name)) {
|
|
2354
|
+
if (sub === "help") return console.log(FIND_USAGE);
|
|
2355
|
+
die(FIND_USAGE);
|
|
2356
|
+
}
|
|
2357
|
+
const subs = { status: findStatus, download: findDownload, resume: findResume, cancel: findCancel, pricing: findPricing, prices: findPricing };
|
|
2358
|
+
const key = requireKey();
|
|
2359
|
+
// A subcommand word only counts as one when no single-lookup flags are set,
|
|
2360
|
+
// so `find "Pricing" --domain x.com` still looks up a person.
|
|
2361
|
+
if (subs[sub] && !args.file && !args.domain && !args.first && !args.last) return subs[sub](args, key);
|
|
2362
|
+
if (args.file) return findFile(args, key);
|
|
2363
|
+
return findSingle(args, key);
|
|
1890
2364
|
}
|
|
1891
2365
|
|
|
1892
2366
|
function money(cents, currency) {
|
|
@@ -1898,7 +2372,7 @@ function money(cents, currency) {
|
|
|
1898
2372
|
function help() {
|
|
1899
2373
|
const p = bold("argorant");
|
|
1900
2374
|
console.log(`
|
|
1901
|
-
${bold("Argorant")}
|
|
2375
|
+
${bold("Argorant")} - verified B2B contacts from your terminal ${dim("v" + VERSION)}
|
|
1902
2376
|
|
|
1903
2377
|
${bold("USAGE")}
|
|
1904
2378
|
${p} <command> "<query>" [filters]
|
|
@@ -1922,9 +2396,14 @@ ${bold("COMMANDS")}
|
|
|
1922
2396
|
${cyan("list status")} <id> Show a saved list's size ${dim("(free)")}
|
|
1923
2397
|
${cyan("verify")} <email> Verify one of your own emails ${dim("(verification pool)")}
|
|
1924
2398
|
${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
|
|
2399
|
+
${cyan("find")} "<first last>" --domain <d> Find a person's work email ${dim("(1 credit found, 0.25 credit nothing found)")}
|
|
2400
|
+
${cyan("find")} --first <f> --last <l> --domain <d> Same, with the name in two parts
|
|
2401
|
+
${cyan("find")} --file people.csv -o found.csv Find emails for a list ${dim("(same prices; --max-credits caps the job)")}
|
|
2402
|
+
${cyan("find")} status | download | resume | cancel <job_id> Manage a find job ${dim("(free)")}
|
|
2403
|
+
${cyan("find pricing")} Finder prices, balance, limits ${dim("(free)")}
|
|
1925
2404
|
${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
|
|
1926
2405
|
${cyan("inboxes")} list | connect-google Connected mailboxes; connect a Google Workspace
|
|
1927
|
-
${cyan("inboxes")} quote | order | status New mailboxes on new domains ${dim("(
|
|
2406
|
+
${cyan("inboxes")} quote | order | status | renewal New mailboxes on new domains ${dim("(saved card or payment link)")}
|
|
1928
2407
|
${cyan("inbox")} list | read | reply | forward | classify Replies across campaigns; answer in-thread or forward
|
|
1929
2408
|
${cyan("blocklist")} list | add | remove Addresses and domains never contacted
|
|
1930
2409
|
|
|
@@ -1945,20 +2424,23 @@ ${bold("OPTIONS")}
|
|
|
1945
2424
|
--json Raw JSON output -y, --yes Skip confirmations
|
|
1946
2425
|
--base <url> Override API base (or ARGORANT_API_BASE)
|
|
1947
2426
|
--batch Treat the id in \`export status/download\` as a batch id
|
|
2427
|
+
--max-credits <n> Credit cap for a \`find --file\` job (it pauses at the cap)
|
|
1948
2428
|
--grade <g> valid (default) or valid-plus-catchall - which deliverable
|
|
1949
2429
|
grade to include on reveal/export. You only ever pay for
|
|
1950
2430
|
deliverable contacts; this is the one grade distinction
|
|
1951
2431
|
exposed anywhere. ${dim("(coming soon - currently a no-op; see docs)")}
|
|
1952
2432
|
|
|
1953
2433
|
${bold("NON-INTERACTIVE USE")} ${dim("(agents, CI, pipes)")}
|
|
1954
|
-
${red("reveal, export,
|
|
2434
|
+
${red("reveal, export, verify --file, and find SPEND CREDITS WITHOUT A PROMPT")} whenever
|
|
1955
2435
|
stdin is not a TTY, or when --yes / --json is passed. The confirmation is a
|
|
1956
2436
|
convenience for humans at a terminal, never a safety net. Check your -n.
|
|
2437
|
+
A single \`find\` never asks: each lookup costs at most 1 credit.
|
|
1957
2438
|
|
|
1958
2439
|
${bold("EXIT CODES")}
|
|
1959
2440
|
0 ok · 1 error · 2 not authenticated · 3 forbidden (missing scope)
|
|
1960
2441
|
4 rate limit / daily quota · 5 plan upgrade required
|
|
1961
2442
|
${dim("`enrich` exits 1 when nothing matched, so scripts can branch without parsing the payload.")}
|
|
2443
|
+
${dim("`find` exits 1 when no email came back; a paused `find --file` job exits 5 (out of credits) or 4 (daily limit).")}
|
|
1962
2444
|
|
|
1963
2445
|
${bold("EXAMPLES")}
|
|
1964
2446
|
${p} count "fintech CFOs in germany"
|
|
@@ -1971,6 +2453,8 @@ ${bold("EXAMPLES")}
|
|
|
1971
2453
|
${p} enrich --domain stripe.com
|
|
1972
2454
|
${p} verify ceo@stripe.com
|
|
1973
2455
|
${p} verify --file my-list.csv -o verified.csv
|
|
2456
|
+
${p} find "Patrick Collison" --domain stripe.com
|
|
2457
|
+
${p} find --file people.csv -o found.csv --max-credits 200
|
|
1974
2458
|
|
|
1975
2459
|
Docs: ${cyan("https://argorant.com/docs/cli")}
|
|
1976
2460
|
`);
|
|
@@ -1981,12 +2465,13 @@ async function main() {
|
|
|
1981
2465
|
const cmd = argv[0];
|
|
1982
2466
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") return help();
|
|
1983
2467
|
if (cmd === "version" || cmd === "--version" || cmd === "-v") return console.log(VERSION);
|
|
1984
|
-
// `campaigns`
|
|
1985
|
-
// handled by readFlags
|
|
2468
|
+
// `campaigns` and `find` have their own flag vocabulary (--step, --count, --first, --max-credits, ...)
|
|
2469
|
+
// handled by readFlags - it never goes through the generic filter parser
|
|
1986
2470
|
// below, which would reject those flags as unknown.
|
|
1987
|
-
if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "mailboxes" || cmd === "inbox" || cmd === "unibox" || cmd === "blocklist") {
|
|
2471
|
+
if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "mailboxes" || cmd === "inbox" || cmd === "unibox" || cmd === "blocklist" || cmd === "find") {
|
|
1988
2472
|
try {
|
|
1989
2473
|
if (cmd.startsWith("campaign")) await cmdCampaigns(argv.slice(1));
|
|
2474
|
+
else if (cmd === "find") await cmdFind(argv.slice(1));
|
|
1990
2475
|
else if (cmd === "blocklist") await cmdBlocklist(argv.slice(1));
|
|
1991
2476
|
else if (cmd === "inbox" || cmd === "unibox") await cmdInboxThreads(argv.slice(1));
|
|
1992
2477
|
else await cmdInboxes(argv.slice(1));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argorant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Search, count, reveal, export, and verify B2B contacts from the Argorant database
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Search, count, reveal, export, and verify B2B contacts from the Argorant database, and find work emails by name and company domain, from your terminal, scripts, or coding agent.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"argorant": "bin/argorant.js"
|
|
7
7
|
},
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"leads",
|
|
20
20
|
"prospecting",
|
|
21
21
|
"email-verification",
|
|
22
|
+
"email-finder",
|
|
22
23
|
"sales",
|
|
23
24
|
"contacts",
|
|
24
25
|
"cli"
|