argorant 0.8.0 → 0.9.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
@@ -152,6 +152,19 @@ argorant inboxes connect-google --admin admin@yourdomain.com # lists mailb
152
152
  argorant inboxes connect-google --admin admin@yourdomain.com --emails a@x.com,b@x.com # or --all
153
153
  ```
154
154
 
155
+ 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 and the total; the order returns a checkout link that you pay
157
+ yourself. Nothing is bought before that payment, and setup starts once it is confirmed. Warm-up is included
158
+ in every plan; switch it on per mailbox with `argorant inboxes set <mailbox> --warmup`.
159
+
160
+ ```
161
+ argorant inboxes suggest acme.com # free domain names with prices
162
+ 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; prints the checkout link
164
+ argorant inboxes status [<order-id>] # steps, domains, renewal prices, bundles
165
+ argorant inboxes cancel-bundle <order-id> <bundle-id> [--keep-domains] | cancel-domain <order-id> <domain>
166
+ ```
167
+
155
168
  `<campaign>` accepts an id or an unambiguous case-insensitive name prefix. Every address is verified before
156
169
  it is enrolled; only valid ones send unless `--include-catch-all`. Variables: `{{first_name}}`, `{{last_name}}`,
157
170
  `{{company}}`, `{{title}}`; spintax `{a|b}` varies phrasing. `--query "..."` filter enrollment stays available
package/bin/argorant.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // Search, count, reveal, export, and verify B2B contacts from the terminal.
6
6
  // Auth: an Argorant API key (ag_live_*) via `argorant login`, or ARGORANT_API_KEY.
7
7
 
8
+ const crypto = require("crypto");
8
9
  const https = require("https");
9
10
  const http = require("http");
10
11
  const fs = require("fs");
@@ -178,7 +179,7 @@ function parseArgs(argv) {
178
179
  }
179
180
 
180
181
  // ---- HTTP ----
181
- function request(method, base, urlPath, { key, query, body } = {}) {
182
+ function request(method, base, urlPath, { key, query, body, headers: extraHeaders } = {}) {
182
183
  return new Promise((resolve, reject) => {
183
184
  let u;
184
185
  try {
@@ -192,7 +193,7 @@ function request(method, base, urlPath, { key, query, body } = {}) {
192
193
  }
193
194
  }
194
195
  const payload = body ? Buffer.from(JSON.stringify(body)) : null;
195
- const headers = { Accept: "application/json", "User-Agent": `argorant-cli/${VERSION}` };
196
+ const headers = { Accept: "application/json", "User-Agent": `argorant-cli/${VERSION}`, ...(extraHeaders || {}) };
196
197
  if (key) headers["Authorization"] = `Bearer ${key}`;
197
198
  if (payload) {
198
199
  headers["Content-Type"] = "application/json";
@@ -912,27 +913,28 @@ async function cmdVerifyFile(args, key) {
912
913
  const out = ensureWritable(args.output || "argorant-verified.csv");
913
914
  // Interactive-only by design — see reveal/export.
914
915
  if (!args.yes && !args.json && process.stdin.isTTY) {
915
- const ans = await prompt(`Verify ${bold(emails.length.toLocaleString())} emails? You're billed only for fresh checks from your verification-check pool; recent re-checks are free. [y/N] `);
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] `);
916
917
  if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
917
918
  }
918
919
  const all = [];
919
- let charged = 0, cached = 0;
920
+ let charged = 0, credits = 0, cached = 0;
920
921
  for (let i = 0; i < emails.length; i += 500) {
921
922
  const chunk = emails.slice(i, i + 500);
922
923
  const res = await request("POST", args.base, "/api/mcp/email/verify/batch", { key, body: { emails: chunk } });
923
924
  const r = need(res, "verify");
924
925
  charged += r.checks_charged || 0;
926
+ credits += r.credits_charged || 0;
925
927
  cached += r.cached || 0;
926
928
  for (const row of r.results || []) all.push(row);
927
929
  if (!args.json) process.stdout.write(`\r${dim(`verified ${Math.min(i + 500, emails.length).toLocaleString()}/${emails.length.toLocaleString()}`)}`);
928
930
  }
929
931
  if (!args.json) process.stdout.write("\n");
930
- if (args.json) return console.log(JSON.stringify({ ok: true, total: all.length, checks_charged: charged, cached, results: all }, null, 2));
932
+ if (args.json) return console.log(JSON.stringify({ ok: true, total: all.length, checks_charged: charged, credits_charged: credits, cached, results: all }, null, 2));
931
933
  const q = (v) => `"${String(v == null ? "" : v).replace(/"/g, '""')}"`;
932
934
  const csv = ["email,status,deliverable", ...all.map((r) => [r.email, r.status, r.deliverable].map(q).join(","))].join("\n") + "\n";
933
935
  fs.writeFileSync(out, csv);
934
936
  const deliverable = all.filter((r) => r.deliverable).length;
935
- console.log(green("✓") + ` ${bold(all.length.toLocaleString())} verified → ${bold(out)} ${dim(`(${deliverable.toLocaleString()} deliverable · ${charged.toLocaleString()} checks billed · ${cached.toLocaleString()} free)`)}`);
937
+ console.log(green("✓") + ` ${bold(all.length.toLocaleString())} verified → ${bold(out)} ${dim(`(${deliverable.toLocaleString()} deliverable · ${charged.toLocaleString()} fresh checks, ${credits.toLocaleString()} credits · ${cached.toLocaleString()} free)`)}`);
936
938
  }
937
939
 
938
940
  // ---- list: save & inspect reusable lead lists (parity with MCP/app). Creating a
@@ -1565,7 +1567,17 @@ async function cmdInboxThreads(argv) {
1565
1567
  console.log(green("✓") + ` ${r.classification}`);
1566
1568
  return;
1567
1569
  }
1568
- die(`unknown inbox subcommand: ${sub} (list, read, reply, classify)`);
1570
+ if (sub === "forward") {
1571
+ const args = readFlags(argv.slice(1), { "--to": "to", "--note": "note" });
1572
+ const id = args._[0]; if (!id) die("usage: argorant inbox forward <thread-id> --to someone@company.com[,other@company.com] [--note \"...\"]");
1573
+ const to = String(args.to || "").split(",").map((x) => x.trim()).filter(Boolean);
1574
+ if (!to.length) die("give at least one address with --to");
1575
+ const r = need(await request("POST", args.base, `/api/v1/inbox/${encodeURIComponent(id)}/forward`, { key, body: { to, note: args.note || "" } }), "inbox forward");
1576
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1577
+ console.log(green("✓") + ` Forwarded to ${to.join(", ")} from ${r.from || "the mailbox"}`);
1578
+ return;
1579
+ }
1580
+ die(`unknown inbox subcommand: ${sub} (list, read, reply, forward, classify)`);
1569
1581
  }
1570
1582
 
1571
1583
  async function campaignsAnalytics(argv) {
@@ -1642,6 +1654,11 @@ ${bold("USAGE")}
1642
1654
  ${bold("argorant inboxes")} set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--timezone <tz>]
1643
1655
  ${bold("argorant inboxes")} disconnect <mailbox>
1644
1656
  ${bold("argorant inboxes")} connect-google --admin <admin@yourdomain.com> [--emails a,b | --all]
1657
+ ${bold("argorant inboxes")} suggest <website|brand> [-n 20] ${dim("free domain names with prices")}
1658
+ ${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("returns a checkout link; nothing is bought before you pay it")}
1660
+ ${bold("argorant inboxes")} status [<order-id>]
1661
+ ${bold("argorant inboxes")} cancel-bundle <order-id> <bundle-id> [--keep-domains] [-y] | cancel-domain <order-id> <domain> [-y]
1645
1662
  ${bold("argorant blocklist")} list | add <email|domain> | remove <id>
1646
1663
 
1647
1664
  ${bold("argorant inbox")} list [--folder replies|interested|not_interested|out_of_office|bounces|unsubscribes|all] [--campaign <c>] [--q text]
@@ -1763,7 +1780,119 @@ async function cmdInboxes(argv) {
1763
1780
  for (const c of r.connected || []) console.log(dim(` ${c.email} · ${c.health || "ok"}`));
1764
1781
  return;
1765
1782
  }
1766
- die(`unknown inboxes subcommand: ${sub} (list, set, disconnect, connect-google)`);
1783
+ if (sub === "suggest") {
1784
+ const args = readFlags(argv.slice(1), { "-n": "limit", "--limit": "limit" });
1785
+ const company = args._.join(" ").trim();
1786
+ if (!company) die("usage: argorant inboxes suggest <website|brand> [-n 20]");
1787
+ const query = { company, limit: args.limit ? parseLimit(args.limit, "--limit") : 20 };
1788
+ const r = need(await request("GET", args.base, "/api/v1/inboxes/domains/suggest", { key, query }), "inboxes suggest");
1789
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1790
+ for (const d of r.results || []) {
1791
+ const price = d.first_year ? `${money(d.first_year.usd, "usd")} first year, ${money(d.renewal.usd, "usd")}/year after` : "";
1792
+ console.log(`${d.available ? green("✓") : dim("x")} ${d.available ? bold(d.name) : dim(d.name)} ${dim(d.available ? price : "taken")}`);
1793
+ }
1794
+ return;
1795
+ }
1796
+ if (sub === "quote") {
1797
+ const args = readFlags(argv.slice(1), { "--mailboxes": "mailboxes", "--daily-volume": "dailyVolume", "--domains": "domains",
1798
+ "--company": "company", "--currency": "currency", "--billing": "billing" });
1799
+ if (!args.mailboxes && !args.dailyVolume) die("usage: argorant inboxes quote --mailboxes 100 [--company acme.com] [--domains a.com,b.com] [--currency usd|eur] [--billing month|quarter] (or --daily-volume 500)");
1800
+ const body = {
1801
+ mailboxes: args.mailboxes ? parseLimit(args.mailboxes, "--mailboxes") : null,
1802
+ daily_volume: args.dailyVolume ? parseLimit(args.dailyVolume, "--daily-volume") : null,
1803
+ domains: args.domains ? String(args.domains).split(",").map((x) => x.trim()).filter(Boolean) : [],
1804
+ company: args.company || "",
1805
+ currency: (args.currency || "usd").toLowerCase(),
1806
+ billing: (args.billing || "month").toLowerCase(),
1807
+ };
1808
+ const q = need(await request("POST", args.base, "/api/v1/inboxes/quote", { key, body }), "inboxes quote");
1809
+ if (args.json) return console.log(JSON.stringify(q, null, 2));
1810
+ const cur = q.currency;
1811
+ console.log(bold(`${q.plan.mailboxes} mailboxes on ${q.plan.domains} new domains`) + dim(` (${q.plan.daily_capacity} cold emails a day)`));
1812
+ for (const d of q.domains || []) {
1813
+ console.log(` ${d.available ? green("✓") : red("x")} ${d.name} ${money(d.first_year_cents, cur)} first year, renews at ${money(d.renewal_per_year_cents, cur)}/year${d.available ? "" : red(" not available")}${d.suggested ? dim(" suggested") : ""}`);
1814
+ }
1815
+ console.log(` Mailboxes: ${money(q.mailboxes.price_cents, cur)} per ${q.mailboxes.billing}`);
1816
+ 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
+ console.log(dim(q.currency_rule));
1818
+ for (const line of q.included || []) console.log(dim(` · ${line}`));
1819
+ console.log(`\nQuote ${bold(q.quote_id)} ${dim(`(valid until ${String(q.expires_at).slice(0, 16).replace("T", " ")} UTC)`)}`);
1820
+ if (q.can_order) console.log(dim(`Order it: argorant inboxes order ${q.quote_id} --senders "First Last"`));
1821
+ else console.log(yellow(q.next_step));
1822
+ return;
1823
+ }
1824
+ if (sub === "order") {
1825
+ const args = readFlags(argv.slice(1), { "--senders": "senders", "--sender": "senders", "--website": "website" });
1826
+ 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]");
1828
+ const senders = String(args.senders).split(",").map((x) => x.trim()).filter(Boolean).map((full) => {
1829
+ const parts = full.split(/\s+/);
1830
+ if (parts.length < 2) die(`sender "${full}" needs a first and a last name`);
1831
+ return { first_name: parts[0], last_name: parts.slice(1).join(" ") };
1832
+ });
1833
+ if (!args.yes) {
1834
+ 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}? You get a checkout link to pay; nothing is bought before that. [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
1836
+ if (!ok) return console.log(dim("aborted"));
1837
+ }
1838
+ const body = { quote_id: quoteId, senders, website: args.website || "", confirmed: true };
1839
+ // Same command, same key: running it again returns the same order and link instead of a second order.
1840
+ const idem = "cli-" + crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex").slice(0, 40);
1841
+ const r = need(await request("POST", args.base, "/api/v1/inboxes/orders", { key, body, headers: { "Idempotency-Key": idem } }), "inboxes order");
1842
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1843
+ if (!r.checkout_url) return console.log(`Order ${r.order_id}: ${r.status}. ${dim(r.message || "")}`);
1844
+ console.log(green("✓") + ` Order ${bold(r.order_id)}${r.existing || r.replayed ? dim(" (already placed)") : ""}`);
1845
+ console.log(`Pay here: ${cyan(r.checkout_url)}`);
1846
+ console.log(dim("Nothing is bought until you pay it; setup starts after the payment is confirmed."));
1847
+ console.log(dim(`Track it: argorant inboxes status ${r.order_id}`));
1848
+ return;
1849
+ }
1850
+ if (sub === "status" || sub === "orders") {
1851
+ const args = readFlags(argv.slice(1));
1852
+ const orderId = sub === "status" ? args._[0] : undefined;
1853
+ const path_ = orderId ? `/api/v1/inboxes/orders/${encodeURIComponent(orderId)}` : "/api/v1/inboxes/orders";
1854
+ const r = need(await request("GET", args.base, path_, { key }), "inboxes status");
1855
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1856
+ const rows = orderId ? [r] : r.orders || [];
1857
+ if (!rows.length) return console.log(dim("No inbox orders yet. Start with: argorant inboxes quote --mailboxes 100 --company <your website>"));
1858
+ for (const o of rows) {
1859
+ 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}`) : ""}`);
1861
+ for (const d of o.domains || []) {
1862
+ 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
+ }
1864
+ for (const b of (o.billing && o.billing.bundles) || []) {
1865
+ console.log(dim(` bundle ${b.bundle_id}: ${b.mailboxes} mailboxes on ${(b.domains || []).join(", ")} · ${b.status}${b.ends_at ? ` · ends ${String(b.ends_at).slice(0, 10)}` : ""}`));
1866
+ }
1867
+ }
1868
+ return;
1869
+ }
1870
+ if (sub === "cancel-bundle" || sub === "cancel-domain") {
1871
+ const args = readFlags(argv.slice(1), {}, { "--keep-domains": "keepDomains" });
1872
+ const [orderId, target] = args._;
1873
+ if (!orderId || !target) die(sub === "cancel-bundle" ? "usage: argorant inboxes cancel-bundle <order-id> <bundle-id> [--keep-domains] [-y]" : "usage: argorant inboxes cancel-domain <order-id> <domain> [-y]");
1874
+ const question = sub === "cancel-bundle"
1875
+ ? `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] `
1876
+ : `Stop renewing ${target}? It works until its expiry date and is not renewed. [y/N] `;
1877
+ if (!args.yes) {
1878
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1879
+ const ok = await new Promise((r) => rl.question(question, (a) => { rl.close(); r(/^y/i.test(a)); }));
1880
+ if (!ok) return console.log(dim("aborted"));
1881
+ }
1882
+ const base = `/api/v1/inboxes/orders/${encodeURIComponent(orderId)}`;
1883
+ const r = sub === "cancel-bundle"
1884
+ ? need(await request("POST", args.base, `${base}/bundles/${encodeURIComponent(target)}/cancel`, { key, body: { keep_domains: !!args.keepDomains } }), "inboxes cancel-bundle")
1885
+ : need(await request("POST", args.base, `${base}/domains/${encodeURIComponent(target)}/cancel`, { key, body: {} }), "inboxes cancel-domain");
1886
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1887
+ return console.log(green("✓") + (sub === "cancel-bundle" ? ` Bundle ${target} cancelled${r.existing ? dim(" (already)") : ""}` : ` ${target} will not renew`));
1888
+ }
1889
+ die(`unknown inboxes subcommand: ${sub} (list, set, disconnect, connect-google, suggest, quote, order, status, cancel-bundle, cancel-domain)`);
1890
+ }
1891
+
1892
+ function money(cents, currency) {
1893
+ if (cents === null || cents === undefined) return "?";
1894
+ const sign = String(currency || "usd").toLowerCase() === "eur" ? "€" : "$";
1895
+ return sign + (Number(cents) / 100).toFixed(2);
1767
1896
  }
1768
1897
 
1769
1898
  function help() {
@@ -1795,7 +1924,8 @@ ${bold("COMMANDS")}
1795
1924
  ${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
1796
1925
  ${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
1797
1926
  ${cyan("inboxes")} list | connect-google Connected mailboxes; connect a Google Workspace
1798
- ${cyan("inbox")} list | read | reply | classify Replies across campaigns; answer in-thread
1927
+ ${cyan("inboxes")} quote | order | status New mailboxes on new domains ${dim("(you pay each order at checkout)")}
1928
+ ${cyan("inbox")} list | read | reply | forward | classify Replies across campaigns; answer in-thread or forward
1799
1929
  ${cyan("blocklist")} list | add | remove Addresses and domains never contacted
1800
1930
 
1801
1931
  ${bold("FILTERS")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argorant",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Search, count, reveal, export, and verify B2B contacts from the Argorant database — from your terminal, scripts, or coding agent.",
5
5
  "bin": {
6
6
  "argorant": "bin/argorant.js"