argorant 0.7.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/bin/argorant.js +215 -10
  3. package/package.json +1 -1
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,58 @@ 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)`);
1581
+ }
1582
+
1583
+ async function campaignsAnalytics(argv) {
1584
+ const key = requireKey();
1585
+ const args = readFlags(argv, { "--days": "days" });
1586
+ const identifier = args._[0];
1587
+ if (!identifier) die("usage: argorant campaigns analytics <campaign> [--days 30]");
1588
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1589
+ const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}/analytics`, { key, query: { days: args.days || 30 } }), "campaigns analytics");
1590
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1591
+ const t = r.totals || {};
1592
+ console.log(`${bold("last " + r.days + " days")} sent ${Number(t.sent || 0).toLocaleString()} · replies ${Number(t.replies || 0).toLocaleString()} (${t.reply_rate}%) · positive ${Number(t.positive || 0).toLocaleString()} · bounces ${Number(t.bounces || 0).toLocaleString()} (${t.bounce_rate}%) · unsubscribes ${Number(t.unsubscribes || 0).toLocaleString()}`);
1593
+ for (const d of r.series || []) console.log(dim(` ${d.day} sent ${d.sent} replies ${d.replies} positive ${d.positive} bounces ${d.bounces}`));
1594
+ }
1595
+
1596
+ async function cmdBlocklist(argv) {
1597
+ const sub = argv[0] || "list";
1598
+ const key = requireKey();
1599
+ if (sub === "list") {
1600
+ const args = readFlags(argv.slice(1));
1601
+ const r = need(await request("GET", args.base, "/api/v1/blocklist", { key }), "blocklist");
1602
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1603
+ if (!(r.entries || []).length) return console.log(dim("Blocklist is empty."));
1604
+ for (const e of r.entries) console.log(`${e.type === "domain" ? "@" : " "}${e.value} ${dim(e.reason || "")}${e.yours ? "" : dim(" (Argorant-wide)")} ${dim(e.id)}`);
1605
+ return;
1606
+ }
1607
+ if (sub === "add") {
1608
+ const args = readFlags(argv.slice(1), { "--reason": "reason" });
1609
+ const value = args._[0]; if (!value) die("usage: argorant blocklist add <email|domain> [--reason text]");
1610
+ const r = need(await request("POST", args.base, "/api/v1/blocklist", { key, body: { value, reason: args.reason || "manual" } }), "blocklist add");
1611
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1612
+ return console.log(green("✓") + ` Blocked ${r.type} ${r.value}` + (r.leads_suppressed ? dim(` (${r.leads_suppressed} enrolled lead(s) suppressed)`) : ""));
1613
+ }
1614
+ if (sub === "remove") {
1615
+ const args = readFlags(argv.slice(1));
1616
+ const id = args._[0]; if (!id) die("usage: argorant blocklist remove <entry-id>");
1617
+ const r = need(await request("DELETE", args.base, `/api/v1/blocklist/${encodeURIComponent(id)}`, { key }), "blocklist remove");
1618
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1619
+ return console.log(green("✓") + ` Removed ${r.removed}`);
1620
+ }
1621
+ die(`unknown blocklist subcommand: ${sub} (list, add, remove)`);
1569
1622
  }
1570
1623
 
1571
1624
  function campaignsHelp() {
@@ -1590,6 +1643,7 @@ ${bold("USAGE")}
1590
1643
  ${p} senders set <campaign> --emails a@x.com,b@y.com | --all
1591
1644
  ${p} senders remove <campaign> <mailbox>
1592
1645
  ${p} replies <campaign> [-n 50]
1646
+ ${p} analytics <campaign> [--days 30] ${dim("daily sent / replies / positive / bounces")}
1593
1647
  ${p} delete <campaign> [-y]
1594
1648
  ${p} launch <campaign> [-y] ${dim("asks for confirmation; sends real email")}
1595
1649
  ${p} pause <campaign>
@@ -1597,7 +1651,15 @@ ${bold("USAGE")}
1597
1651
  ${p} status <campaign>
1598
1652
 
1599
1653
  ${bold("argorant inboxes")} list
1654
+ ${bold("argorant inboxes")} set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--timezone <tz>]
1655
+ ${bold("argorant inboxes")} disconnect <mailbox>
1600
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]
1662
+ ${bold("argorant blocklist")} list | add <email|domain> | remove <id>
1601
1663
 
1602
1664
  ${bold("argorant inbox")} list [--folder replies|interested|not_interested|out_of_office|bounces|unsubscribes|all] [--campaign <c>] [--q text]
1603
1665
  ${bold("argorant inbox")} read <thread-id>
@@ -1634,6 +1696,7 @@ async function cmdCampaigns(argv) {
1634
1696
  set: campaignsUpdate,
1635
1697
  delete: campaignsDelete,
1636
1698
  replies: campaignsReplies,
1699
+ analytics: campaignsAnalytics,
1637
1700
  emails: campaignsEmails,
1638
1701
  steps: campaignsSteps,
1639
1702
  senders: campaignsSenders,
@@ -1666,6 +1729,33 @@ async function cmdInboxes(argv) {
1666
1729
  }
1667
1730
  return;
1668
1731
  }
1732
+ if (sub === "set") {
1733
+ const args = readFlags(argv.slice(1), { "--daily-limit": "dailyLimit", "--name": "name", "--timezone": "timezone" }, { "--warmup": "warmupOn", "--no-warmup": "warmupOff", "--active": "activeOn", "--inactive": "activeOff" });
1734
+ const email = args._[0]; if (!email) die("usage: argorant inboxes set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--name \"…\"] [--timezone Europe/Berlin]");
1735
+ const body = {};
1736
+ if (args.dailyLimit) body.daily_limit = parseLimit(args.dailyLimit, "--daily-limit");
1737
+ if (args.warmupOn) body.warmup = true; if (args.warmupOff) body.warmup = false;
1738
+ if (args.activeOn) body.active = true; if (args.activeOff) body.active = false;
1739
+ if (args.name !== undefined) body.display_name = args.name;
1740
+ if (args.timezone) body.timezone = args.timezone;
1741
+ if (!Object.keys(body).length) die("nothing to change");
1742
+ const r = need(await request("PATCH", args.base, `/api/v1/inboxes/${encodeURIComponent(email)}`, { key, body }), "inboxes set");
1743
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1744
+ const i = r.inbox || {};
1745
+ return console.log(green("✓") + ` ${i.email}: ${i.daily_limit} a day · warm-up ${i.warmup ? "on" : "off"} · ${i.active ? "active" : "inactive"} · ${i.timezone || ""}`);
1746
+ }
1747
+ if (sub === "disconnect") {
1748
+ const args = readFlags(argv.slice(1));
1749
+ const email = args._[0]; if (!email) die("usage: argorant inboxes disconnect <mailbox> [-y]");
1750
+ if (!args.yes) {
1751
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1752
+ const ok = await new Promise((r) => rl.question(`Disconnect ${email}? It is detached from every campaign. [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
1753
+ if (!ok) return console.log(dim("aborted"));
1754
+ }
1755
+ const r = need(await request("DELETE", args.base, `/api/v1/inboxes/${encodeURIComponent(email)}`, { key }), "inboxes disconnect");
1756
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1757
+ return console.log(green("✓") + ` Disconnected ${r.disconnected}`);
1758
+ }
1669
1759
  if (sub === "connect-google") {
1670
1760
  const args = readFlags(argv.slice(1), { "--admin": "admin", "--emails": "emails" }, { "--all": "all" });
1671
1761
  if (!args.admin) die("usage: argorant inboxes connect-google --admin admin@yourdomain.com [--emails a@x.com,b@x.com | --all]");
@@ -1690,7 +1780,119 @@ async function cmdInboxes(argv) {
1690
1780
  for (const c of r.connected || []) console.log(dim(` ${c.email} · ${c.health || "ok"}`));
1691
1781
  return;
1692
1782
  }
1693
- die(`unknown inboxes subcommand: ${sub} (list, 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);
1694
1896
  }
1695
1897
 
1696
1898
  function help() {
@@ -1722,7 +1924,9 @@ ${bold("COMMANDS")}
1722
1924
  ${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
1723
1925
  ${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
1724
1926
  ${cyan("inboxes")} list | connect-google Connected mailboxes; connect a Google Workspace
1725
- ${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
1929
+ ${cyan("blocklist")} list | add | remove Addresses and domains never contacted
1726
1930
 
1727
1931
  ${bold("FILTERS")}
1728
1932
  --keywords <k> Comma = OR. The widest, most reliable filter - prefer it
@@ -1780,9 +1984,10 @@ async function main() {
1780
1984
  // `campaigns` has its own flag vocabulary (--step, --count, --pool, --csv, ...)
1781
1985
  // handled by readFlags — it never goes through the generic filter parser
1782
1986
  // below, which would reject those flags as unknown.
1783
- if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "mailboxes" || cmd === "inbox" || cmd === "unibox") {
1987
+ if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "mailboxes" || cmd === "inbox" || cmd === "unibox" || cmd === "blocklist") {
1784
1988
  try {
1785
1989
  if (cmd.startsWith("campaign")) await cmdCampaigns(argv.slice(1));
1990
+ else if (cmd === "blocklist") await cmdBlocklist(argv.slice(1));
1786
1991
  else if (cmd === "inbox" || cmd === "unibox") await cmdInboxThreads(argv.slice(1));
1787
1992
  else await cmdInboxes(argv.slice(1));
1788
1993
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argorant",
3
- "version": "0.7.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"