argorant 0.11.0 → 0.12.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 CHANGED
@@ -34,8 +34,11 @@ CI, and agents.
34
34
  | `company <company.com> -n 5` | Count people at a company, split business-email coverage, preview masked roles | free |
35
35
  | `search "<query>" -n 10` | Preview matches (masked identity, details redacted) | free |
36
36
  | `sample <company.com> [-o sample.csv]` | Read a website and build 25 distinct, live-valid company leads from it | free sample |
37
- | `reveal "<query>" -n 25` | Reveal full contact details (name, email, phone, LinkedIn) | quota |
38
- | `export "<query>" -n 1000 -o leads.csv` | Verified CSV export, polled until ready | quota |
37
+ | `reveal "<query>" -n 25` | Reveal full contact details (name, work email, LinkedIn) | 1 credit per new working email |
38
+ | `reveal "<query>" -n 25 --phones` | Same, plus each person's phone number (direct dial or mobile) | + 10 credits per number found |
39
+ | `enrich --email <a@b.com> [--phones]` | The person behind an address, optionally with their phone number | 1 credit per match, + 10 if a number is found |
40
+ | `export "<query>" -n 1000 -o leads.csv` | Verified CSV export, polled until ready | 1 credit per working email |
41
+ | `export "<query>" -n 1000 --phones -o leads.csv` | Same, with phone numbers in the file | + 10 credits per number in the file |
39
42
  | `export status <job_id> [--batch]` | Status of an export you already created | free |
40
43
  | `export download <job_id> [--batch] -o leads.csv` | Re-download a finished export | free |
41
44
  | `list create --name "<n>" [filters]` | Save a reusable filtered list (server counts it) | free |
@@ -45,6 +48,20 @@ CI, and agents.
45
48
  | `find status \| download \| resume \| cancel <job_id>`, `find pricing` | Manage a find job; show prices, balance and limits | free |
46
49
  | `campaigns …` | Live outbound campaigns - **operator keys only**, see below | - |
47
50
 
51
+ ## Phone numbers
52
+
53
+ Phone numbers are never included unless you ask for them with `--phones` (reveal, enrich, export).
54
+ A phone number costs 10 credits, and only when a number is actually returned. People without a number
55
+ cost nothing, and numbers you already unlocked (in the app, the API or the connector) are free. Phone
56
+ numbers come with the Pro plan and higher. The CLI shows the price before it spends anything, and the
57
+ result says per person whether a number came back and what it cost. Company and HQ numbers stay free.
58
+
59
+ ```sh
60
+ npx argorant reveal --title CFO --domain stripe.com -n 5 --phones
61
+ npx argorant enrich --email patrick@stripe.com --phones --json
62
+ npx argorant export --title CFO --country Germany -n 200 --phones -o cfos.csv
63
+ ```
64
+
48
65
  Exports above 50,000 rows are created as a multi-chunk batch: the CLI polls
49
66
  `export status --batch` for you and writes one file per chunk
50
67
  (`leads-part1.csv`, `leads-part2.csv`, …). The batch id is printed so you can
package/bin/argorant.js CHANGED
@@ -146,7 +146,7 @@ function parseLimit(raw, flag) {
146
146
  }
147
147
 
148
148
  function parseArgs(argv) {
149
- const out = { _: [], filters: {}, limit: null, output: null, file: null, column: null, json: false, yes: false, base: DEFAULT_BASE, baseExplicit: false, batch: false, name: null, includeExported: false, grade: "valid", gradeExplicit: false };
149
+ const out = { _: [], filters: {}, limit: null, output: null, file: null, column: null, json: false, yes: false, base: DEFAULT_BASE, baseExplicit: false, batch: false, name: null, includeExported: false, grade: "valid", gradeExplicit: false, phones: false };
150
150
  for (let i = 0; i < argv.length; i++) {
151
151
  const a = argv[i];
152
152
  if (a === "--json") out.json = true;
@@ -158,6 +158,9 @@ function parseArgs(argv) {
158
158
  else if (a === "--base") { out.base = flagValue(argv, i++, a); out.baseExplicit = true; }
159
159
  else if (a === "--name") out.name = flagValue(argv, i++, a);
160
160
  else if (a === "--include-exported") out.includeExported = true;
161
+ // Phone numbers are opt-in on every surface (2026-10-07): only with --phones, 10 credits per
162
+ // number actually returned, nothing when none is found or the account already has it.
163
+ else if (a === "--phones" || a === "--include-phones") out.phones = true;
161
164
  else if (a === "--batch") out.batch = true;
162
165
  else if (a === "--grade") setGrade(out, flagValue(argv, i++, a));
163
166
  else if (a in VALUE_FLAGS) out.filters[VALUE_FLAGS[a]] = flagValue(argv, i++, a);
@@ -554,22 +557,106 @@ async function cmdSample(args) {
554
557
  console.log(dim(`These 25 are the sample; the full pool contains ${Number(job.total_companies || 0).toLocaleString()} companies.`));
555
558
  }
556
559
 
560
+ // ---- phone numbers (opt-in, --phones) ----
561
+ const PHONE_FIELDS = ["phone", "phone_1", "phone_2", "phones_all", "additional_phones", "phone_masked"];
562
+ function stripPhones(rec) {
563
+ if (rec && typeof rec === "object") for (const k of PHONE_FIELDS) delete rec[k];
564
+ return rec;
565
+ }
566
+
567
+ // The price and whether the plan includes phone numbers, before anything is spent. An older server
568
+ // without the pricing route falls back to the published price.
569
+ async function phonePricing(args, key) {
570
+ try {
571
+ const res = await request("GET", args.base, "/api/v1/people/phones/pricing", { key });
572
+ if (res.status === 200 && res.json) return res.json;
573
+ } catch {
574
+ /* fall back below */
575
+ }
576
+ return { price_per_phone: 10, plan_includes_phones: null };
577
+ }
578
+
579
+ function phonePriceText(pricing) {
580
+ const price = Number((pricing && pricing.price_per_phone) || 10);
581
+ return `Phone numbers cost ${price} credits each, only when a number is found. Numbers you already have are free.`;
582
+ }
583
+
584
+ // Phone numbers for people already revealed (POST /api/v1/people/phones). A refusal never undoes the
585
+ // emails that were just revealed: it comes back as {included:false, message}.
586
+ async function addPhones(args, key, records) {
587
+ const ids = [];
588
+ for (const r of records) {
589
+ const id = String(r.person_id || r.record_id || "").trim();
590
+ if (id && !ids.includes(id)) ids.push(id);
591
+ }
592
+ records.forEach(stripPhones);
593
+ if (!ids.length) return { included: true, phones_returned: 0, credits_charged: 0, summary: "No people to look up phone numbers for, so nothing was charged." };
594
+ const res = await request("POST", args.base, "/api/v1/people/phones", {
595
+ key,
596
+ body: { record_ids: ids.slice(0, 100), channel: "cli" },
597
+ headers: { "X-Argorant-Channel": "cli" },
598
+ });
599
+ if (res.status !== 200 || !res.json) {
600
+ const detail = res.json && res.json.detail;
601
+ const code = detail && typeof detail === "object" ? detail.error : null;
602
+ let message = detailMsg(detail) || `phone numbers failed (HTTP ${res.status}).`;
603
+ if (code === "phones_not_in_plan") message = "Your plan does not include phone numbers, so none were added and nothing was charged for them.";
604
+ else if (code === "insufficient_credits") message = "Not enough credits for a phone number, so none were added and nothing was charged for them.";
605
+ else if (code === "mcp_quota_exceeded") message = "Today's reveal limit is used up, so no phone numbers were added. It resets at midnight UTC.";
606
+ for (const r of records) r.phone_status = "not_added";
607
+ return { included: false, status: res.status, error: code || "error", message, summary: message };
608
+ }
609
+ const byId = new Map((res.json.results || []).map((row) => [String(row.record_id), row]));
610
+ for (const r of records) {
611
+ const row = byId.get(String(r.person_id || r.record_id || "")) || {};
612
+ r.phone = row.phone || null;
613
+ if (row.additional_phones && row.additional_phones.length) r.additional_phones = row.additional_phones;
614
+ r.phone_status = row.phone_status || "not_found";
615
+ r.phone_credits_charged = Number(row.credits_charged || 0);
616
+ }
617
+ return { included: true, ...res.json, results: undefined };
618
+ }
619
+
620
+ function phoneLine(p) {
621
+ if (!p || !p.phone_status) return null;
622
+ if (p.phone) {
623
+ const extra = p.additional_phones && p.additional_phones.length ? dim(` (+${p.additional_phones.length} more)`) : "";
624
+ const cost = p.phone_credits_charged ? dim(` · ${p.phone_credits_charged} credits`) : p.phone_status === "already_unlocked" ? dim(" · already yours") : "";
625
+ return `${p.phone}${extra}${cost}`;
626
+ }
627
+ if (p.phone_status === "none_found" || p.phone_status === "not_found") return dim("no phone number on record · not charged");
628
+ if (p.phone_status === "not_enough_credits") return dim("phone not added · not enough credits");
629
+ if (p.phone_status === "not_in_plan") return dim("phone not added · not in your plan");
630
+ if (p.phone_status === "daily_limit_reached") return dim("phone not added · daily limit reached");
631
+ return null;
632
+ }
633
+
557
634
  async function cmdReveal(args) {
558
635
  const key = requireKey();
559
636
  warnExcludeTitleGap(args.filters);
560
637
  if (args.gradeExplicit) warnGradeGap("reveal");
561
638
  const limit = args.limit || 10;
639
+ const pricing = args.phones ? await phonePricing(args, key) : null;
640
+ if (pricing && pricing.plan_includes_phones === false) {
641
+ warn("Your plan does not include phone numbers. Emails are revealed as usual; no phone numbers will be added or charged.");
642
+ }
562
643
  // Confirmation is interactive-only by design: with --yes, --json, or a
563
644
  // non-TTY stdin (CI, agents, pipes) this spends credits with no prompt.
564
645
  if (!args.yes && !args.json && process.stdin.isTTY) {
565
- const ans = await prompt(`Reveal up to ${bold(limit)} contacts? This uses your quota/credits. [y/N] `);
646
+ const phonesText = args.phones ? ` ${phonePriceText(pricing)}` : "";
647
+ const ans = await prompt(`Reveal up to ${bold(limit)} contacts${args.phones ? " with phone numbers" : ""}? Each new working email costs 1 credit.${phonesText} [y/N] `);
566
648
  if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
649
+ } else if (args.phones && !args.json) {
650
+ process.stderr.write(dim(phonePriceText(pricing) + "\n"));
567
651
  }
568
652
  // Sent for forward-compatibility: the platform has no per-request grade
569
653
  // control on reveal yet (see GODMODE-PLAN.md), so this is a no-op today.
570
654
  const query = { ...args.filters, limit, grade: args.grade === "valid-plus-catchall" ? "valid_plus_catchall" : "valid" };
571
655
  const res = await request("GET", args.base, "/api/mcp/people/reveal", { key, query });
572
656
  const r = need(res, "reveal");
657
+ const people = (r.results || []).filter((x) => x && typeof x === "object");
658
+ if (args.phones && people.length) r.phones = await addPhones(args, key, people);
659
+ else people.forEach(stripPhones);
573
660
  if (args.json) return console.log(JSON.stringify(r, null, 2));
574
661
  console.log(dim(`${Number(r.total).toLocaleString()} total · revealed ${r.returned}`));
575
662
  for (const p of r.results || []) {
@@ -578,8 +665,16 @@ async function cmdReveal(args) {
578
665
  const name = p.full_name || [p.first_name, p.last_name].filter(Boolean).join(" ") || p.name;
579
666
  const who = [name, p.title].filter(Boolean).join(" · ");
580
667
  console.log(` ${bold(who || "-")}`);
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);
668
+ const bits = [p.email && cyan(p.email), p.linkedin_url, [p.company || p.company_name, p.country].filter(Boolean).join(", ")].filter(Boolean);
582
669
  if (bits.length) console.log(" " + bits.join(dim(" · ")));
670
+ const phone = args.phones ? phoneLine(p) : null;
671
+ if (phone) console.log(" " + phone);
672
+ }
673
+ const charged = Number(r.charged || 0);
674
+ console.log(dim(`${charged} credit${charged === 1 ? "" : "s"} charged for emails`));
675
+ if (r.phones) {
676
+ if (r.phones.included) console.log(dim(r.phones.summary || `${r.phones.credits_charged || 0} credits charged for phone numbers`));
677
+ else warn(r.phones.message || "No phone numbers were added.");
583
678
  }
584
679
  }
585
680
 
@@ -622,16 +717,30 @@ async function cmdEnrich(args) {
622
717
  else if (name) { body.name = name; body.domain = domain; }
623
718
  else body.domain = domain;
624
719
  const personMode = Boolean(body.email || body.name);
720
+ if (args.phones && !personMode) warn("Phone numbers are for people. A company lookup returns the company profile only.");
721
+ const wantPhones = args.phones && personMode;
722
+ const pricing = wantPhones ? await phonePricing(args, key) : null;
723
+ if (pricing && pricing.plan_includes_phones === false) {
724
+ warn("Your plan does not include phone numbers. The email lookup runs as usual; no phone number will be added or charged.");
725
+ }
625
726
  // Company mode is free, so it never asks. Person mode is billed exactly like
626
727
  // a reveal, so it gets the same interactive-only confirmation as `reveal`.
627
728
  if (personMode && !args.yes && !args.json && process.stdin.isTTY) {
628
729
  const ans = await prompt(
629
- `Enrich this contact? A match costs 1 credit; a miss and a non-deliverable address are free. [y/N] `
730
+ `Enrich this contact? A match costs 1 credit; a miss and a non-deliverable address are free.` +
731
+ (wantPhones ? ` ${phonePriceText(pricing)}` : "") + ` [y/N] `
630
732
  );
631
733
  if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
734
+ } else if (wantPhones && !args.json) {
735
+ process.stderr.write(dim(phonePriceText(pricing) + "\n"));
632
736
  }
633
737
  const res = await request("POST", args.base, "/api/v1/enrich", { key, body });
634
738
  const r = need(res, "enrich");
739
+ if (r && r.type === "person" && r.person && typeof r.person === "object") {
740
+ if (!r.person.person_id && r.person_id) r.person.person_id = r.person_id;
741
+ if (wantPhones && r.found) r.phones = await addPhones(args, key, [r.person]);
742
+ else stripPhones(r.person);
743
+ }
635
744
  if (args.json) {
636
745
  console.log(JSON.stringify(r, null, 2));
637
746
  if (!r.found) process.exit(EXIT.ERROR);
@@ -656,11 +765,13 @@ async function cmdEnrich(args) {
656
765
  console.log(` ${bold(who || "-")}`);
657
766
  const bits = [
658
767
  p.email && cyan(p.email),
659
- p.phone,
660
768
  p.linkedin_url,
661
769
  [p.current_company_name, p.current_company_domain, p.country].filter(Boolean).join(", "),
662
770
  ].filter(Boolean);
663
771
  if (bits.length) console.log(" " + bits.join(dim(" · ")));
772
+ const phone = wantPhones ? phoneLine(p) : null;
773
+ if (phone) console.log(" " + phone);
774
+ if (r.phones && !r.phones.included) warn(r.phones.message || "No phone number was added.");
664
775
  if (r.deliverable === false) {
665
776
  console.log(" " + dim(r.message || "This address did not pass live verification, so nothing was charged."));
666
777
  }
@@ -791,9 +902,18 @@ async function cmdExport(args) {
791
902
  const dest = ensureWritable(args.output || "argorant-leads.csv");
792
903
  // Confirmation is interactive-only by design: with --yes, --json, or a
793
904
  // non-TTY stdin (CI, agents, pipes) this spends credits with no prompt.
905
+ const pricing = args.phones ? await phonePricing(args, key) : null;
906
+ if (pricing && pricing.plan_includes_phones === false) {
907
+ die("Your plan does not include phone numbers. Run the export without --phones.", EXIT.UPGRADE);
908
+ }
794
909
  if (!args.yes && !args.json && process.stdin.isTTY) {
795
- const ans = await prompt(`Export up to ${bold(limit)} verified contacts to ${bold(dest)}? Uses quota/credits. [y/N] `);
910
+ const phonesText = args.phones
911
+ ? ` Phone numbers are included. Each number in the file costs ${Number((pricing && pricing.price_per_phone) || 10)} more credits, and people without one cost nothing extra.`
912
+ : "";
913
+ const ans = await prompt(`Export up to ${bold(limit)} verified contacts to ${bold(dest)}? Each person with a working email costs 1 credit.${phonesText} [y/N] `);
796
914
  if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
915
+ } else if (args.phones && !args.json) {
916
+ process.stderr.write(dim(`Phone numbers are included. Each number in the file costs ${Number((pricing && pricing.price_per_phone) || 10)} more credits.\n`));
797
917
  }
798
918
  // Match the MCP/app defaults so the CLI yields the same rows: business email
799
919
  // present by default, and skip rows already exported (override with
@@ -808,7 +928,7 @@ async function cmdExport(args) {
808
928
  const grades = args.grade === "valid-plus-catchall" ? ["valid", "catch_all"] : ["valid"];
809
929
  const create = await request("POST", args.base, "/api/mcp/exports/create", {
810
930
  key,
811
- body: { limit, filters: exportFilters, exclude_previously_exported: !args.includeExported, grades },
931
+ body: { limit, filters: exportFilters, exclude_previously_exported: !args.includeExported, grades, ...(args.phones ? { include_phones: true } : {}) },
812
932
  });
813
933
  const job = need(create, "export");
814
934
  // >EXPORT_MAX_ROWS (50k) → a multi-chunk batch, a different status endpoint
@@ -2386,10 +2506,13 @@ ${bold("COMMANDS")}
2386
2506
  ${cyan("search")} "<query>" -n 10 Preview matches, details redacted ${dim("(0 contact credits)")}
2387
2507
  ${cyan("sample")} <company.com> Build 25 distinct, live-valid company leads ${dim("(free sample)")}
2388
2508
  ${cyan("reveal")} "<query>" -n 25 Reveal full contact details ${dim("(uses credits; live-verified, pay only for deliverable)")}
2509
+ ${cyan("reveal")} ... --phones Also add phone numbers ${dim("(10 credits per number found; none found = free)")}
2389
2510
  ${cyan("enrich")} --email <a@b.com> One address → the full person profile ${dim("(1 credit per match; miss = free)")}
2390
2511
  ${cyan("enrich")} --name "<n>" --domain <d> Find that person + reveal a verified email ${dim("(1 credit per match)")}
2391
2512
  ${cyan("enrich")} --domain <d> Company profile ${dim("(0 contact credits)")}
2513
+ ${cyan("enrich")} ... --phones Also add the person's phone number ${dim("(10 credits if found)")}
2392
2514
  ${cyan("export")} "<query>" -n 1000 -o leads.csv Verified CSV export ${dim("(uses credits)")}
2515
+ ${cyan("export")} ... --phones Include phone numbers in the file ${dim("(10 credits per number in the file)")}
2393
2516
  ${cyan("export status")} <job_id> Status of an existing export ${dim("(free; add --batch for >50k)")}
2394
2517
  ${cyan("export download")} <job_id> -o leads.csv Re-download a finished export ${dim("(free)")}
2395
2518
  ${cyan("list create")} --name "<n>" [filters] Save a reusable list ${dim("(free)")}
@@ -2424,6 +2547,9 @@ ${bold("OPTIONS")}
2424
2547
  --json Raw JSON output -y, --yes Skip confirmations
2425
2548
  --base <url> Override API base (or ARGORANT_API_BASE)
2426
2549
  --batch Treat the id in \`export status/download\` as a batch id
2550
+ --phones Include phone numbers (reveal, enrich, export). Off by default.
2551
+ 10 credits per phone number returned, nothing if none is found;
2552
+ numbers you already unlocked are free. From the Pro plan.
2427
2553
  --max-credits <n> Credit cap for a \`find --file\` job (it pauses at the cap)
2428
2554
  --grade <g> valid (default) or valid-plus-catchall - which deliverable
2429
2555
  grade to include on reveal/export. You only ever pay for
@@ -2451,6 +2577,8 @@ ${bold("EXAMPLES")}
2451
2577
  ${p} enrich --email patrick@stripe.com --json
2452
2578
  ${p} enrich --name "Patrick Collison" --domain stripe.com
2453
2579
  ${p} enrich --domain stripe.com
2580
+ ${p} reveal --title CFO --domain stripe.com -n 5 --phones
2581
+ ${p} export --title CFO --country Germany -n 200 --phones -o cfos.csv
2454
2582
  ${p} verify ceo@stripe.com
2455
2583
  ${p} verify --file my-list.csv -o verified.csv
2456
2584
  ${p} find "Patrick Collison" --domain stripe.com
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argorant",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
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"