argorant 0.6.0 → 0.8.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 +10 -1
  2. package/bin/argorant.js +275 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -136,7 +136,16 @@ argorant campaigns leads add <campaign> --list <list-id> # a saved lis
136
136
  argorant campaigns leads add <campaign> --csv leads.csv # email, first_name, last_name, company, title
137
137
  argorant campaigns senders set <campaign> --emails a@x.com,b@y.com # or --all
138
138
  argorant campaigns launch <campaign> [-y] # asks for confirmation
139
- argorant campaigns pause | stop | status <campaign>
139
+ argorant campaigns update <campaign> --daily-limit 40 --window 08:00-17:00 --timezone Europe/Berlin --timezone-mode lead --gap 60-120 --tags q4
140
+ argorant campaigns leads list <campaign> [--status replied] | leads remove <campaign> <email>
141
+ argorant campaigns senders remove <campaign> <mailbox>
142
+ argorant campaigns replies <campaign>
143
+ argorant campaigns pause | stop | status | delete <campaign>
144
+
145
+ argorant inbox list [--folder replies|interested|not_interested|out_of_office|bounces|unsubscribes] [--campaign <c>] [--q text]
146
+ argorant inbox read <thread-id>
147
+ argorant inbox reply <thread-id> --body "..." [-y]
148
+ argorant inbox classify <thread-id> meeting_booked
140
149
 
141
150
  argorant inboxes list
142
151
  argorant inboxes connect-google --admin admin@yourdomain.com # lists mailboxes (or the delegation setup to do first)
package/bin/argorant.js CHANGED
@@ -1119,35 +1119,19 @@ async function campaignsList(argv) {
1119
1119
 
1120
1120
  async function campaignsCreate(argv) {
1121
1121
  const key = requireKey();
1122
- const args = readFlags(
1123
- argv,
1124
- { "--name": "name", "--timezone": "timezone", "--daily-limit": "dailyLimit", "--gap": "gap", "--start": "start" },
1125
- { "--html": "html" }
1126
- );
1122
+ const args = readFlags(argv, CAMPAIGN_SETTING_FLAGS, CAMPAIGN_SETTING_BOOLS);
1127
1123
  const name = (args.name || "").trim();
1128
1124
  if (!name) {
1129
- die('usage: argorant campaigns create --name "<name>" [--daily-limit 100] [--timezone America/New_York] [--gap 60-120] [--start YYYY-MM-DD] [--html]');
1130
- }
1131
- const body = { name };
1132
- if (args.timezone) body.timezone = args.timezone;
1133
- if (args.dailyLimit) body.daily_limit = parseLimit(args.dailyLimit, "--daily-limit");
1134
- if (args.gap) {
1135
- const m = /^(\d+)(?:-(\d+))?$/.exec(args.gap);
1136
- if (!m) die("--gap must look like 60-120 (minutes between two sends from one mailbox)");
1137
- body.gap_minutes_min = Number(m[1]);
1138
- body.gap_minutes_max = Number(m[2] || m[1]);
1139
- }
1140
- if (args.start) {
1141
- if (!/^\d{4}-\d{2}-\d{2}$/.test(args.start)) die("--start must be YYYY-MM-DD");
1142
- body.planned_start_date = args.start;
1125
+ die('usage: argorant campaigns create --name "<name>" [--daily-limit 100] [--per-mailbox-limit 20] [--timezone America/New_York] [--timezone-mode fixed|lead] [--window 08:00-17:00] [--no-skip-weekends] [--gap 60-120] [--no-unsubscribe] [--start YYYY-MM-DD] [--tags a,b]');
1143
1126
  }
1144
- if (args.html) body.plain_text = false;
1127
+ const body = settingsFromFlags(args);
1128
+ body.name = name;
1145
1129
  const res = await request("POST", args.base, "/api/v1/campaigns", { key, body });
1146
1130
  const r = need(res, "campaigns create");
1147
1131
  const c = r.campaign || r;
1148
1132
  if (args.json) return console.log(JSON.stringify(r, null, 2));
1149
1133
  console.log(green("✓") + ` Created campaign ${bold(c.name || name)} ${dim(c.id)}`);
1150
- console.log(dim(` ${c.status || "draft"} · ${c.default_timezone || body.timezone || "America/New_York"} · ${c.daily_limit || body.daily_limit || 100} a day`));
1134
+ printSettings(c);
1151
1135
  console.log(dim(`Next: argorant campaigns emails set ${c.id} --step 1 --subject "..." --body-file ./copy.txt`));
1152
1136
  }
1153
1137
 
@@ -1205,8 +1189,17 @@ async function campaignsEmails(argv) {
1205
1189
  // senders: which connected mailboxes send this campaign.
1206
1190
  async function campaignsSenders(argv) {
1207
1191
  const sub = argv[0];
1208
- if (sub !== "set") die("usage: argorant campaigns senders set <campaign> --emails a@x.com,b@y.com | --all");
1209
1192
  const key = requireKey();
1193
+ if (sub === "remove") {
1194
+ const args = readFlags(argv.slice(1));
1195
+ const [identifier, email] = args._;
1196
+ if (!identifier || !email) die("usage: argorant campaigns senders remove <campaign> <mailbox@domain>");
1197
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1198
+ const r = need(await request("DELETE", args.base, `/api/v1/campaigns/${campaignId}/senders/${encodeURIComponent(email)}`, { key }), "campaigns senders remove");
1199
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1200
+ return console.log(green("✓") + ` Detached ${r.detached}`);
1201
+ }
1202
+ if (sub !== "set") die("usage: argorant campaigns senders set <campaign> --emails a@x.com,b@y.com | --all\n or: argorant campaigns senders remove <campaign> <mailbox>");
1210
1203
  const args = readFlags(argv.slice(1), { "--emails": "emails" }, { "--all": "all" });
1211
1204
  const identifier = args._[0];
1212
1205
  if (!identifier) die("usage: argorant campaigns senders set <campaign> --emails a@x.com,b@y.com | --all");
@@ -1330,8 +1323,14 @@ async function campaignsLeads(argv) {
1330
1323
  'usage: argorant campaigns leads add <campaign> --list <list-id>\n' +
1331
1324
  ' or: argorant campaigns leads add <campaign> --csv <file> (columns: email, first_name, last_name, company, title)\n' +
1332
1325
  ' or: argorant campaigns leads add <campaign> --query "..." [filters] -n <n> (operator keys)';
1333
- if (sub !== "add") die(usage);
1334
1326
  const key = requireKey();
1327
+ if (sub === "list" || sub === "remove") {
1328
+ const args = readFlags(argv.slice(1), { "--status": "status", "--q": "q", "-n": "limit", "--limit": "limit", "--offset": "offset" });
1329
+ if (!args._[0]) die(`usage: argorant campaigns leads ${sub} <campaign>${sub === "remove" ? " <email>" : " [--status queued|replied|bounced] [--q text] [-n 50]"}`);
1330
+ const campaignId = await resolveCampaign(args.base, key, args._[0]);
1331
+ return sub === "list" ? campaignsLeadsList(args, campaignId, key) : campaignsLeadsRemove(args, campaignId, key);
1332
+ }
1333
+ if (sub !== "add") die(usage);
1335
1334
  const valueFlags = { ...CAMPAIGN_FILTER_VALUE_FLAGS, "--csv": "csv", "--file": "csv", "--list": "list", "-n": "limit", "--limit": "limit" };
1336
1335
  const args = readFlags(argv.slice(1), valueFlags, { ...CAMPAIGN_FILTER_BOOL_FLAGS, "--include-catch-all": "includeCatchAll" });
1337
1336
  const identifier = args._[0];
@@ -1406,7 +1405,8 @@ async function campaignsStatus(argv) {
1406
1405
  const c = r.campaign || {};
1407
1406
  console.log(`${bold(c.name || "—")} ${dim(c.id)}`);
1408
1407
  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()}`);
1409
- console.log(` emails ${(r.emails || []).length} · senders ${(r.senders || []).length} · ${c.daily_limit || "?"} a day · ${c.default_timezone || ""}`);
1408
+ console.log(` emails ${(r.emails || []).length} · senders ${(r.senders || []).length}`);
1409
+ printSettings({ settings: r.settings });
1410
1410
  for (const e of r.emails || []) console.log(dim(` ${e.step}. ${e.subject || "(same thread)"}${e.step > 1 ? ` · +${e.delay_days || 0} days` : ""}`));
1411
1411
  if (r.launch_blockers && r.launch_blockers.length) {
1412
1412
  console.log(yellow(" before launch:"));
@@ -1415,6 +1415,200 @@ async function campaignsStatus(argv) {
1415
1415
  if (c.url) console.log(dim(` ${c.url}`));
1416
1416
  }
1417
1417
 
1418
+ const CAMPAIGN_SETTING_FLAGS = {
1419
+ "--name": "name", "--daily-limit": "daily_limit", "--per-mailbox-limit": "per_mailbox_daily_limit", "--max-mailboxes": "max_active_mailboxes",
1420
+ "--timezone": "timezone", "--timezone-mode": "timezone_mode", "--window": "window", "--gap": "gap", "--start": "planned_start_date",
1421
+ "--days-ahead": "schedule_days_ahead", "--tags": "tags",
1422
+ };
1423
+ const CAMPAIGN_SETTING_BOOLS = { "--skip-weekends": "skip_weekends", "--no-skip-weekends": "no_skip_weekends", "--unsubscribe": "include_unsubscribe", "--no-unsubscribe": "no_unsubscribe" };
1424
+
1425
+ function settingsFromFlags(args) {
1426
+ const body = {};
1427
+ for (const key of ["name", "timezone", "timezone_mode", "planned_start_date"]) if (args[key] !== undefined) body[key] = args[key];
1428
+ for (const key of ["daily_limit", "per_mailbox_daily_limit", "max_active_mailboxes", "schedule_days_ahead"]) if (args[key] !== undefined) body[key] = parseLimit(args[key], key);
1429
+ if (args.window) {
1430
+ const m = /^(\d{1,2}:\d{2})-(\d{1,2}:\d{2})$/.exec(args.window);
1431
+ if (!m) die("--window must look like 08:00-17:00");
1432
+ body.window_start = m[1].padStart(5, "0"); body.window_end = m[2].padStart(5, "0");
1433
+ }
1434
+ if (args.gap) {
1435
+ const m = /^(\d+)(?:-(\d+))?$/.exec(args.gap);
1436
+ if (!m) die("--gap must look like 60-120 (minutes between two sends from one mailbox)");
1437
+ body.gap_minutes_min = Number(m[1]); body.gap_minutes_max = Number(m[2] || m[1]);
1438
+ }
1439
+ if (args.tags !== undefined) body.tags = String(args.tags).split(",").map((t) => t.trim()).filter(Boolean);
1440
+ if (args.skip_weekends) body.skip_weekends = true;
1441
+ if (args.no_skip_weekends) body.skip_weekends = false;
1442
+ if (args.include_unsubscribe) body.include_unsubscribe = true;
1443
+ if (args.no_unsubscribe) body.include_unsubscribe = false;
1444
+ return body;
1445
+ }
1446
+
1447
+ function printSettings(c) {
1448
+ const s = c.settings || {};
1449
+ console.log(dim(` ${s.daily_limit || "?"} a day` + (s.per_mailbox_daily_limit ? ` · ${s.per_mailbox_daily_limit} per mailbox` : "") + ` · ${s.window_start || "?"}-${s.window_end || "?"} ${s.timezone || ""}${s.timezone_mode === "lead" ? " (recipient local time)" : ""}` +
1450
+ ` · ${s.skip_weekends ? "weekdays" : "every day"} · gap ${s.gap_minutes_min || "?"}-${s.gap_minutes_max || "?"} min` + (s.planned_start_date ? ` · starts ${s.planned_start_date}` : "") + (s.include_unsubscribe === false ? " · no opt-out line" : "") + (s.tags && s.tags.length ? ` · ${s.tags.join(", ")}` : "")));
1451
+ }
1452
+
1453
+ async function campaignsUpdate(argv) {
1454
+ const key = requireKey();
1455
+ const args = readFlags(argv, CAMPAIGN_SETTING_FLAGS, CAMPAIGN_SETTING_BOOLS);
1456
+ const identifier = args._[0];
1457
+ if (!identifier) die('usage: argorant campaigns update <campaign> [--name "<n>"] [--daily-limit 40] [--per-mailbox-limit 20] [--max-mailboxes 10] [--timezone Europe/Berlin] [--timezone-mode fixed|lead] [--window 08:00-17:00] [--skip-weekends|--no-skip-weekends] [--gap 60-120] [--unsubscribe|--no-unsubscribe] [--start YYYY-MM-DD] [--tags a,b]');
1458
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1459
+ const body = settingsFromFlags(args);
1460
+ if (!Object.keys(body).length) die("nothing to change (see: argorant campaigns help)");
1461
+ const r = need(await request("PATCH", args.base, `/api/v1/campaigns/${campaignId}`, { key, body }), "campaigns update");
1462
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1463
+ console.log(green("✓") + ` Updated ${(r.changed || []).join(", ")}`);
1464
+ printSettings(r.campaign || {});
1465
+ }
1466
+
1467
+ async function campaignsDelete(argv) {
1468
+ const key = requireKey();
1469
+ const args = readFlags(argv);
1470
+ const identifier = args._[0];
1471
+ if (!identifier) die("usage: argorant campaigns delete <campaign> [-y]");
1472
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1473
+ if (!args.yes) {
1474
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1475
+ const ok = await new Promise((r) => rl.question(`Delete campaign ${campaignId} with all its leads and results? [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
1476
+ if (!ok) return console.log(dim("aborted"));
1477
+ }
1478
+ const r = need(await request("DELETE", args.base, `/api/v1/campaigns/${campaignId}`, { key }), "campaigns delete");
1479
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1480
+ console.log(green("✓") + ` Deleted ${campaignId}`);
1481
+ }
1482
+
1483
+ async function campaignsLeadsList(args, campaignId, key) {
1484
+ const query = { limit: args.limit || 50, offset: args.offset || 0 };
1485
+ if (args.status) query.status = args.status;
1486
+ if (args.q) query.q = args.q;
1487
+ const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}/leads`, { key, query }), "campaigns leads");
1488
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1489
+ console.log(dim(`${r.total} lead(s)`));
1490
+ for (const l of r.leads || []) console.log(`${l.email} ${dim([l.first_name, l.last_name].filter(Boolean).join(" ") || "")} ${l.status}${l.current_step ? dim(` step ${l.current_step}`) : ""}${l.replied_at ? green(" replied") : ""}${l.bounced_at ? red(" bounced") : ""}`);
1491
+ }
1492
+
1493
+ async function campaignsLeadsRemove(args, campaignId, key) {
1494
+ const email = args._[1];
1495
+ if (!email) die("usage: argorant campaigns leads remove <campaign> <email>");
1496
+ const r = need(await request("DELETE", args.base, `/api/v1/campaigns/${campaignId}/leads/${encodeURIComponent(email)}`, { key }), "campaigns leads remove");
1497
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1498
+ console.log(green("✓") + ` Removed ${r.removed}`);
1499
+ }
1500
+
1501
+ async function campaignsReplies(argv) {
1502
+ const key = requireKey();
1503
+ const args = readFlags(argv, { "-n": "limit", "--limit": "limit" });
1504
+ const identifier = args._[0];
1505
+ if (!identifier) die("usage: argorant campaigns replies <campaign> [-n 50]");
1506
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1507
+ const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}/replies`, { key, query: { limit: args.limit || 50 } }), "campaigns replies");
1508
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1509
+ if (!(r.replies || []).length) return console.log(dim("No replies yet."));
1510
+ for (const e of r.replies) {
1511
+ console.log(`${bold(e.from_email || "")} ${dim([e.first_name, e.last_name].filter(Boolean).join(" "))} ${e.classification || ""} ${dim(e.created_at || "")}`);
1512
+ console.log(dim(` ${(e.subject || "").slice(0, 80)} — ${(e.body || "").replace(/\s+/g, " ").slice(0, 140)}`));
1513
+ }
1514
+ }
1515
+
1516
+ async function cmdInboxThreads(argv) {
1517
+ const sub = argv[0] || "list";
1518
+ const key = requireKey();
1519
+ if (sub === "list") {
1520
+ const args = readFlags(argv.slice(1), { "--folder": "folder", "--campaign": "campaign", "--classification": "classification", "--q": "q", "-n": "limit", "--limit": "limit" });
1521
+ const query = { folder: args.folder || "replies", limit: args.limit || 50 };
1522
+ if (args.classification) query.classification = args.classification;
1523
+ if (args.q) query.q = args.q;
1524
+ if (args.campaign) query.campaign_id = await resolveCampaign(args.base, key, args.campaign);
1525
+ const r = need(await request("GET", args.base, "/api/v1/inbox", { key, query }), "inbox");
1526
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1527
+ if (!(r.events || []).length) return console.log(dim(`No conversations in ${query.folder}.`));
1528
+ for (const e of r.events) {
1529
+ console.log(`${dim(e.id)}`);
1530
+ console.log(` ${bold(e.from_email || e.lead_email || "")} ${e.classification || ""} ${dim(e.campaign_name || "")} ${dim(e.created_at || "")}`);
1531
+ console.log(dim(` ${(e.subject || "").slice(0, 80)} — ${(e.body || e.snippet || "").replace(/\s+/g, " ").slice(0, 140)}`));
1532
+ }
1533
+ return;
1534
+ }
1535
+ if (sub === "read") {
1536
+ const args = readFlags(argv.slice(1));
1537
+ const id = args._[0]; if (!id) die("usage: argorant inbox read <thread-id>");
1538
+ const r = need(await request("GET", args.base, `/api/v1/inbox/${encodeURIComponent(id)}`, { key }), "inbox read");
1539
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1540
+ const e = r.event || {};
1541
+ console.log(`${bold(e.from_email || e.lead_email || "")} ${e.classification || ""} ${dim(e.campaign_name || "")}\n${dim(e.subject || "")}\n\n${e.body || ""}`);
1542
+ return;
1543
+ }
1544
+ if (sub === "reply") {
1545
+ const args = readFlags(argv.slice(1), { "--body": "body", "--body-file": "bodyFile" });
1546
+ const id = args._[0]; if (!id) die("usage: argorant inbox reply <thread-id> --body \"...\" | --body-file reply.txt [-y]");
1547
+ let body = args.body;
1548
+ if (args.bodyFile) { try { body = fs.readFileSync(args.bodyFile, "utf8"); } catch { die(`cannot read file: ${args.bodyFile}`); } }
1549
+ if (!body || !body.trim()) die("give the reply text with --body or --body-file");
1550
+ if (!args.yes) {
1551
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1552
+ const ok = await new Promise((r) => rl.question(`Send this reply now? [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
1553
+ if (!ok) return console.log(dim("aborted"));
1554
+ }
1555
+ const r = need(await request("POST", args.base, `/api/v1/inbox/${encodeURIComponent(id)}/reply`, { key, body: { body } }), "inbox reply");
1556
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1557
+ console.log(green("✓") + " Reply sent");
1558
+ return;
1559
+ }
1560
+ if (sub === "classify") {
1561
+ const args = readFlags(argv.slice(1));
1562
+ const [id, cl] = args._; if (!id || !cl) die("usage: argorant inbox classify <thread-id> interested|not_interested|out_of_office|meeting_booked|referral|other");
1563
+ const r = need(await request("POST", args.base, `/api/v1/inbox/${encodeURIComponent(id)}/classify`, { key, body: { classification: cl } }), "inbox classify");
1564
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1565
+ console.log(green("✓") + ` ${r.classification}`);
1566
+ return;
1567
+ }
1568
+ die(`unknown inbox subcommand: ${sub} (list, read, reply, classify)`);
1569
+ }
1570
+
1571
+ async function campaignsAnalytics(argv) {
1572
+ const key = requireKey();
1573
+ const args = readFlags(argv, { "--days": "days" });
1574
+ const identifier = args._[0];
1575
+ if (!identifier) die("usage: argorant campaigns analytics <campaign> [--days 30]");
1576
+ const campaignId = await resolveCampaign(args.base, key, identifier);
1577
+ const r = need(await request("GET", args.base, `/api/v1/campaigns/${campaignId}/analytics`, { key, query: { days: args.days || 30 } }), "campaigns analytics");
1578
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1579
+ const t = r.totals || {};
1580
+ 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()}`);
1581
+ for (const d of r.series || []) console.log(dim(` ${d.day} sent ${d.sent} replies ${d.replies} positive ${d.positive} bounces ${d.bounces}`));
1582
+ }
1583
+
1584
+ async function cmdBlocklist(argv) {
1585
+ const sub = argv[0] || "list";
1586
+ const key = requireKey();
1587
+ if (sub === "list") {
1588
+ const args = readFlags(argv.slice(1));
1589
+ const r = need(await request("GET", args.base, "/api/v1/blocklist", { key }), "blocklist");
1590
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1591
+ if (!(r.entries || []).length) return console.log(dim("Blocklist is empty."));
1592
+ for (const e of r.entries) console.log(`${e.type === "domain" ? "@" : " "}${e.value} ${dim(e.reason || "")}${e.yours ? "" : dim(" (Argorant-wide)")} ${dim(e.id)}`);
1593
+ return;
1594
+ }
1595
+ if (sub === "add") {
1596
+ const args = readFlags(argv.slice(1), { "--reason": "reason" });
1597
+ const value = args._[0]; if (!value) die("usage: argorant blocklist add <email|domain> [--reason text]");
1598
+ const r = need(await request("POST", args.base, "/api/v1/blocklist", { key, body: { value, reason: args.reason || "manual" } }), "blocklist add");
1599
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1600
+ return console.log(green("✓") + ` Blocked ${r.type} ${r.value}` + (r.leads_suppressed ? dim(` (${r.leads_suppressed} enrolled lead(s) suppressed)`) : ""));
1601
+ }
1602
+ if (sub === "remove") {
1603
+ const args = readFlags(argv.slice(1));
1604
+ const id = args._[0]; if (!id) die("usage: argorant blocklist remove <entry-id>");
1605
+ const r = need(await request("DELETE", args.base, `/api/v1/blocklist/${encodeURIComponent(id)}`, { key }), "blocklist remove");
1606
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1607
+ return console.log(green("✓") + ` Removed ${r.removed}`);
1608
+ }
1609
+ die(`unknown blocklist subcommand: ${sub} (list, add, remove)`);
1610
+ }
1611
+
1418
1612
  function campaignsHelp() {
1419
1613
  const p = bold("argorant campaigns");
1420
1614
  console.log(`
@@ -1424,18 +1618,36 @@ Works with any Argorant API key. Nothing is sent until you launch.
1424
1618
 
1425
1619
  ${bold("USAGE")}
1426
1620
  ${p} list
1427
- ${p} create --name "<name>" [--daily-limit 100] [--timezone <tz>] [--gap 60-120] [--start YYYY-MM-DD]
1621
+ ${p} create --name "<name>" [settings]
1622
+ ${p} update <campaign> [settings] ${dim("only the flags you pass change")}
1623
+ settings: --daily-limit 40 --per-mailbox-limit 20 --max-mailboxes 10 --timezone Europe/Berlin
1624
+ --timezone-mode fixed|lead --window 08:00-17:00 --skip-weekends|--no-skip-weekends
1625
+ --gap 60-120 --unsubscribe|--no-unsubscribe --start YYYY-MM-DD --tags a,b --name "<n>"
1428
1626
  ${p} emails set <campaign> --file emails.json
1429
1627
  ${p} emails set <campaign> --step <n> --subject "..." (--body-file <path> | --body <text|->) [--delay-days <d>]
1430
1628
  ${p} leads add <campaign> --list <list-id> | --csv <file> [--include-catch-all]
1629
+ ${p} leads list <campaign> [--status replied] [--q text] [-n 50]
1630
+ ${p} leads remove <campaign> <email>
1431
1631
  ${p} senders set <campaign> --emails a@x.com,b@y.com | --all
1632
+ ${p} senders remove <campaign> <mailbox>
1633
+ ${p} replies <campaign> [-n 50]
1634
+ ${p} analytics <campaign> [--days 30] ${dim("daily sent / replies / positive / bounces")}
1635
+ ${p} delete <campaign> [-y]
1432
1636
  ${p} launch <campaign> [-y] ${dim("asks for confirmation; sends real email")}
1433
1637
  ${p} pause <campaign>
1434
1638
  ${p} stop <campaign>
1435
1639
  ${p} status <campaign>
1436
1640
 
1437
1641
  ${bold("argorant inboxes")} list
1642
+ ${bold("argorant inboxes")} set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--timezone <tz>]
1643
+ ${bold("argorant inboxes")} disconnect <mailbox>
1438
1644
  ${bold("argorant inboxes")} connect-google --admin <admin@yourdomain.com> [--emails a,b | --all]
1645
+ ${bold("argorant blocklist")} list | add <email|domain> | remove <id>
1646
+
1647
+ ${bold("argorant inbox")} list [--folder replies|interested|not_interested|out_of_office|bounces|unsubscribes|all] [--campaign <c>] [--q text]
1648
+ ${bold("argorant inbox")} read <thread-id>
1649
+ ${bold("argorant inbox")} reply <thread-id> --body "..." | --body-file reply.txt [-y]
1650
+ ${bold("argorant inbox")} classify <thread-id> interested|not_interested|out_of_office|meeting_booked|referral|other
1439
1651
 
1440
1652
  ${dim("<campaign> accepts an id or an unambiguous case-insensitive name prefix.")}
1441
1653
  ${dim("Variables in emails: {{first_name}} {{last_name}} {{company}} {{title}}. Spintax {a|b} varies phrasing.")}
@@ -1463,6 +1675,11 @@ async function cmdCampaigns(argv) {
1463
1675
  const table = {
1464
1676
  list: campaignsList,
1465
1677
  create: campaignsCreate,
1678
+ update: campaignsUpdate,
1679
+ set: campaignsUpdate,
1680
+ delete: campaignsDelete,
1681
+ replies: campaignsReplies,
1682
+ analytics: campaignsAnalytics,
1466
1683
  emails: campaignsEmails,
1467
1684
  steps: campaignsSteps,
1468
1685
  senders: campaignsSenders,
@@ -1495,6 +1712,33 @@ async function cmdInboxes(argv) {
1495
1712
  }
1496
1713
  return;
1497
1714
  }
1715
+ if (sub === "set") {
1716
+ const args = readFlags(argv.slice(1), { "--daily-limit": "dailyLimit", "--name": "name", "--timezone": "timezone" }, { "--warmup": "warmupOn", "--no-warmup": "warmupOff", "--active": "activeOn", "--inactive": "activeOff" });
1717
+ const email = args._[0]; if (!email) die("usage: argorant inboxes set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--name \"…\"] [--timezone Europe/Berlin]");
1718
+ const body = {};
1719
+ if (args.dailyLimit) body.daily_limit = parseLimit(args.dailyLimit, "--daily-limit");
1720
+ if (args.warmupOn) body.warmup = true; if (args.warmupOff) body.warmup = false;
1721
+ if (args.activeOn) body.active = true; if (args.activeOff) body.active = false;
1722
+ if (args.name !== undefined) body.display_name = args.name;
1723
+ if (args.timezone) body.timezone = args.timezone;
1724
+ if (!Object.keys(body).length) die("nothing to change");
1725
+ const r = need(await request("PATCH", args.base, `/api/v1/inboxes/${encodeURIComponent(email)}`, { key, body }), "inboxes set");
1726
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1727
+ const i = r.inbox || {};
1728
+ return console.log(green("✓") + ` ${i.email}: ${i.daily_limit} a day · warm-up ${i.warmup ? "on" : "off"} · ${i.active ? "active" : "inactive"} · ${i.timezone || ""}`);
1729
+ }
1730
+ if (sub === "disconnect") {
1731
+ const args = readFlags(argv.slice(1));
1732
+ const email = args._[0]; if (!email) die("usage: argorant inboxes disconnect <mailbox> [-y]");
1733
+ if (!args.yes) {
1734
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1735
+ 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)); }));
1736
+ if (!ok) return console.log(dim("aborted"));
1737
+ }
1738
+ const r = need(await request("DELETE", args.base, `/api/v1/inboxes/${encodeURIComponent(email)}`, { key }), "inboxes disconnect");
1739
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
1740
+ return console.log(green("✓") + ` Disconnected ${r.disconnected}`);
1741
+ }
1498
1742
  if (sub === "connect-google") {
1499
1743
  const args = readFlags(argv.slice(1), { "--admin": "admin", "--emails": "emails" }, { "--all": "all" });
1500
1744
  if (!args.admin) die("usage: argorant inboxes connect-google --admin admin@yourdomain.com [--emails a@x.com,b@x.com | --all]");
@@ -1519,7 +1763,7 @@ async function cmdInboxes(argv) {
1519
1763
  for (const c of r.connected || []) console.log(dim(` ${c.email} · ${c.health || "ok"}`));
1520
1764
  return;
1521
1765
  }
1522
- die(`unknown inboxes subcommand: ${sub} (list, connect-google)`);
1766
+ die(`unknown inboxes subcommand: ${sub} (list, set, disconnect, connect-google)`);
1523
1767
  }
1524
1768
 
1525
1769
  function help() {
@@ -1551,6 +1795,8 @@ ${bold("COMMANDS")}
1551
1795
  ${cyan("verify")} --file emails.csv -o out.csv Bulk-verify your own list ${dim("(recent re-checks free)")}
1552
1796
  ${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
1553
1797
  ${cyan("inboxes")} list | connect-google Connected mailboxes; connect a Google Workspace
1798
+ ${cyan("inbox")} list | read | reply | classify Replies across campaigns; answer in-thread
1799
+ ${cyan("blocklist")} list | add | remove Addresses and domains never contacted
1554
1800
 
1555
1801
  ${bold("FILTERS")}
1556
1802
  --keywords <k> Comma = OR. The widest, most reliable filter - prefer it
@@ -1608,9 +1854,11 @@ async function main() {
1608
1854
  // `campaigns` has its own flag vocabulary (--step, --count, --pool, --csv, ...)
1609
1855
  // handled by readFlags — it never goes through the generic filter parser
1610
1856
  // below, which would reject those flags as unknown.
1611
- if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "inbox") {
1857
+ if (cmd === "campaigns" || cmd === "campaign" || cmd === "inboxes" || cmd === "mailboxes" || cmd === "inbox" || cmd === "unibox" || cmd === "blocklist") {
1612
1858
  try {
1613
1859
  if (cmd.startsWith("campaign")) await cmdCampaigns(argv.slice(1));
1860
+ else if (cmd === "blocklist") await cmdBlocklist(argv.slice(1));
1861
+ else if (cmd === "inbox" || cmd === "unibox") await cmdInboxThreads(argv.slice(1));
1614
1862
  else await cmdInboxes(argv.slice(1));
1615
1863
  } catch (e) {
1616
1864
  die(e && e.message ? e.message : String(e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argorant",
3
- "version": "0.6.0",
3
+ "version": "0.8.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"