@sesender/cli 1.0.0 → 1.0.1

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.
@@ -1,32 +1,36 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { apiRequest } from "../api.mjs";
3
- import { formatTable, outputResult, success } from "../output.mjs";
3
+ import { formatTable, outputResult, success, info } from "../output.mjs";
4
4
 
5
5
  export async function contactsList(args, jsonMode) {
6
6
  const limit = args["--limit"] || "20";
7
7
  const page = args["--page"] || "1";
8
8
  const group = args["--group"];
9
+ const search = args["--search"];
9
10
 
10
11
  const params = { limit, page };
11
12
  if (group) params.group = group;
13
+ if (search) params.search = search;
12
14
 
13
15
  const result = await apiRequest("GET", "/contacts", null, params);
14
16
 
15
17
  if (jsonMode) {
16
18
  outputResult(result, true);
17
19
  } else {
18
- const contacts = result.contacts || result.data || [];
20
+ const contacts = result.data?.contacts || result.contacts || result.data || [];
19
21
  if (contacts.length === 0) {
20
22
  console.log("No contacts found.");
21
23
  return;
22
24
  }
23
25
  formatTable(
24
- ["Name", "Phone", "Email", "Groups"],
26
+ ["ID", "Name", "Email", "Phone", "Group", "Created"],
25
27
  contacts.map((c) => [
26
- c.name || c.firstName || "-",
28
+ c.id,
29
+ (c.name || c.firstName || "-").slice(0, 15),
30
+ (c.email || "-").slice(0, 25),
27
31
  c.phone || "-",
28
- c.email || "-",
29
- (c.groups || []).join(", ") || "-",
32
+ (c.groups || []).join(", ").slice(0, 12) || c.group || "-",
33
+ c.createdAt ? new Date(c.createdAt).toLocaleDateString() : "-",
30
34
  ])
31
35
  );
32
36
  console.log(`\nShowing ${contacts.length} contacts (page ${page})`);
@@ -37,10 +41,25 @@ export async function contactsList(args, jsonMode) {
37
41
  export async function contactsImport(args, jsonMode) {
38
42
  const file = args["--file"];
39
43
  const group = args["--group"];
44
+ const skipDuplicates = args["--skip-duplicates"] !== undefined;
40
45
 
41
46
  if (!file) {
42
- console.error("Usage: ses-cli contacts import --file contacts.csv [--group GROUP_NAME]");
43
- console.error("\nCSV format: name,phone,email (first row as headers)");
47
+ console.error(`
48
+ Usage: ses-cli contacts import --file FILE [options]
49
+
50
+ Options:
51
+ --file CSV file to import (required)
52
+ --group Assign contacts to this group
53
+ --skip-duplicates Skip duplicate contacts instead of updating
54
+
55
+ CSV Format:
56
+ Headers should include: email, phone, firstName, lastName, custom1, custom2, etc.
57
+ Minimum: email OR phone column required.
58
+
59
+ Examples:
60
+ ses-cli contacts import --file contacts.csv
61
+ ses-cli contacts import --file leads.csv --group "Newsletter" --skip-duplicates
62
+ `);
44
63
  process.exit(1);
45
64
  }
46
65
 
@@ -52,28 +71,34 @@ export async function contactsImport(args, jsonMode) {
52
71
  process.exit(1);
53
72
  }
54
73
 
55
- const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
74
+ const headers = lines[0].split(",").map((h) => h.trim().toLowerCase().replace(/"/g, ""));
56
75
  const contacts = lines.slice(1).map((line) => {
57
- const values = line.split(",").map((v) => v.trim());
76
+ const values = line.split(",").map((v) => v.trim().replace(/"/g, ""));
58
77
  const contact = {};
59
78
  headers.forEach((h, i) => {
60
79
  if (values[i]) contact[h] = values[i];
61
80
  });
62
81
  return contact;
63
- });
82
+ }).filter(c => c.email || c.phone);
83
+
84
+ info(`Importing ${contacts.length} contacts...`);
64
85
 
65
86
  const payload = { contacts };
66
87
  if (group) payload.group = group;
88
+ if (skipDuplicates) payload.skipDuplicates = true;
67
89
 
68
90
  const result = await apiRequest("POST", "/contacts/bulk", payload);
69
91
 
70
92
  if (jsonMode) {
71
93
  outputResult(result, true);
72
94
  } else {
73
- success(`Import complete: ${result.imported || result.created || contacts.length} contacts imported`);
74
- if (result.duplicates) console.log(` Duplicates skipped: ${result.duplicates}`);
75
- if (result.errors) console.log(` Errors: ${result.errors}`);
76
- if (group) console.log(` Assigned to group: ${group}`);
95
+ const data = result.data || result;
96
+ success(`Import complete`);
97
+ console.log(` Imported: ${data.imported || data.created || contacts.length}`);
98
+ if (data.updated) console.log(` Updated: ${data.updated}`);
99
+ if (data.duplicates || data.skipped) console.log(` Skipped: ${data.duplicates || data.skipped}`);
100
+ if (data.errors) console.log(` Errors: ${data.errors}`);
101
+ if (group) console.log(` Group: ${group}`);
77
102
  }
78
103
  }
79
104
 
@@ -83,14 +108,67 @@ export async function contactsGroups(args, jsonMode) {
83
108
  if (jsonMode) {
84
109
  outputResult(result, true);
85
110
  } else {
86
- const groups = result.groups || result.data || [];
111
+ const groups = result.data?.groups || result.groups || result.data || [];
87
112
  if (groups.length === 0) {
88
113
  console.log("No contact groups found.");
89
114
  return;
90
115
  }
91
116
  formatTable(
92
- ["ID", "Name", "Contacts"],
93
- groups.map((g) => [g.id, g.name, g.contactCount || g.count || "-"])
117
+ ["ID", "Name", "Contacts", "Created"],
118
+ groups.map((g) => [
119
+ g.id,
120
+ g.name || "Untitled",
121
+ g.contactCount || g.count || 0,
122
+ g.createdAt ? new Date(g.createdAt).toLocaleDateString() : "-",
123
+ ])
94
124
  );
125
+ console.log(`\n${groups.length} group(s) found`);
126
+ }
127
+ }
128
+
129
+ export async function contactsExport(args, jsonMode) {
130
+ const group = args["--group"];
131
+ const format = args["--format"] || "csv";
132
+
133
+ const params = { format };
134
+ if (group) params.group = group;
135
+
136
+ const result = await apiRequest("GET", "/contacts/export", null, params);
137
+
138
+ if (jsonMode) {
139
+ outputResult(result, true);
140
+ } else {
141
+ const data = result.data || result;
142
+ if (data.downloadUrl) {
143
+ success(`Export ready: ${data.downloadUrl}`);
144
+ } else if (data.csv || typeof data === "string") {
145
+ console.log(data.csv || data);
146
+ } else {
147
+ success("Export initiated. Check your dashboard for the download link.");
148
+ }
149
+ }
150
+ }
151
+
152
+ export async function contactsDelete(args, jsonMode) {
153
+ const id = args["--id"];
154
+ const email = args["--email"];
155
+
156
+ if (!id && !email) {
157
+ console.error(`
158
+ Usage: ses-cli contacts delete --id CONTACT_ID
159
+ ses-cli contacts delete --email EMAIL
160
+
161
+ Permanently deletes a contact. This cannot be undone.
162
+ `);
163
+ process.exit(1);
164
+ }
165
+
166
+ const path = id ? `/contacts/${id}` : `/contacts/by-email/${encodeURIComponent(email)}`;
167
+ const result = await apiRequest("DELETE", path);
168
+
169
+ if (jsonMode) {
170
+ outputResult(result, true);
171
+ } else {
172
+ success(`Contact ${id || email} deleted`);
95
173
  }
96
174
  }
@@ -1,35 +1,64 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { apiRequest } from "../api.mjs";
3
- import { formatTable, outputResult, success } from "../output.mjs";
3
+ import { formatTable, outputResult, success, info } from "../output.mjs";
4
4
 
5
5
  export async function emailSend(args, jsonMode) {
6
6
  const to = args["--to"];
7
7
  const subject = args["--subject"];
8
8
  const body = args["--body"];
9
- const html = args["--html"];
9
+ const htmlFile = args["--html-file"] || args["--html"];
10
10
  const from = args["--from"];
11
-
12
- if (!to || !subject || (!body && !html)) {
13
- console.error("Usage: ses-cli email send --to EMAIL --subject SUBJECT --body TEXT [--html FILE] [--from SENDER]");
11
+ const fromName = args["--from-name"];
12
+ const provider = args["--provider"];
13
+ const schedule = args["--schedule"];
14
+ const templateId = args["--template"];
15
+
16
+ if (!to || (!subject && !templateId)) {
17
+ console.error(`
18
+ Usage: ses-cli email send --to EMAIL --subject SUBJECT [options]
19
+
20
+ Options:
21
+ --to Recipient email address
22
+ --subject Email subject line
23
+ --body Plain text body
24
+ --html-file Path to HTML file for email body
25
+ --from Sender email address
26
+ --from-name Sender display name
27
+ --provider Provider ID to use (see: ses-cli providers list)
28
+ --template Template ID to use (see: ses-cli templates list)
29
+ --schedule Schedule send time (ISO 8601)
30
+
31
+ Examples:
32
+ ses-cli email send --to user@example.com --subject "Hello" --body "Hi there!"
33
+ ses-cli email send --to user@example.com --subject "Newsletter" --html-file ./email.html
34
+ ses-cli email send --to user@example.com --subject "Promo" --template 12 --provider 3
35
+ `);
14
36
  process.exit(1);
15
37
  }
16
38
 
17
- const payload = { to, subject };
18
- if (from) payload.from = from;
19
-
20
- if (html) {
21
- payload.html = readFileSync(html, "utf-8");
22
- } else {
23
- payload.body = body;
39
+ let htmlBody = body || "";
40
+ if (htmlFile) {
41
+ htmlBody = readFileSync(htmlFile, "utf-8");
24
42
  }
25
43
 
44
+ const payload = { to, subject };
45
+ if (htmlFile) payload.htmlBody = htmlBody;
46
+ else if (body) payload.body = body;
47
+ if (from) payload.fromEmail = from;
48
+ if (fromName) payload.fromName = fromName;
49
+ if (provider) payload.providerId = parseInt(provider);
50
+ if (schedule) payload.scheduledAt = schedule;
51
+ if (templateId) payload.templateId = parseInt(templateId);
52
+
26
53
  const result = await apiRequest("POST", "/email/send", payload);
27
54
 
28
55
  if (jsonMode) {
29
56
  outputResult(result, true);
30
57
  } else {
31
- success(`Email sent to ${to}`);
32
- if (result.messageId) console.log(` Message ID: ${result.messageId}`);
58
+ success(`Email ${schedule ? "scheduled" : "sent"} to ${to}`);
59
+ if (result.data?.messageId) console.log(` Message ID: ${result.data.messageId}`);
60
+ if (result.data?.provider) console.log(` Provider: ${result.data.provider}`);
61
+ if (schedule) console.log(` Scheduled: ${schedule}`);
33
62
  }
34
63
  }
35
64
 
@@ -37,42 +66,82 @@ export async function emailBulk(args, jsonMode) {
37
66
  const file = args["--file"];
38
67
  const subject = args["--subject"];
39
68
  const body = args["--body"];
40
- const html = args["--html"];
69
+ const htmlFile = args["--html-file"] || args["--html"];
41
70
  const from = args["--from"];
42
-
43
- if (!file || !subject || (!body && !html)) {
44
- console.error("Usage: ses-cli email bulk --file contacts.csv --subject SUBJECT --body TEXT [--html FILE] [--from SENDER]");
45
- console.error("\nCSV format: email (one email per line, or column header 'email')");
71
+ const fromName = args["--from-name"];
72
+ const provider = args["--provider"];
73
+ const delay = parseInt(args["--delay"] || "0");
74
+
75
+ if (!file || !subject) {
76
+ console.error(`
77
+ Usage: ses-cli email bulk --file FILE --subject SUBJECT [options]
78
+
79
+ Options:
80
+ --file CSV file with email addresses (column: email or first column)
81
+ --subject Email subject. Use [[ColumnName]] for personalization
82
+ --body Plain text body. Use [[ColumnName]] for personalization
83
+ --html-file Path to HTML template file. Use [[ColumnName]] for personalization
84
+ --from Sender email address
85
+ --from-name Sender display name
86
+ --provider Provider ID to use
87
+ --delay Delay between sends in ms (default: 0)
88
+
89
+ Personalization:
90
+ Use [[First]], [[Last]], [[Email]], [[Custom1]], etc. in subject/body.
91
+ Column names are matched from your CSV headers.
92
+
93
+ Examples:
94
+ ses-cli email bulk --file contacts.csv --subject "Hi [[First]]!" --html-file ./promo.html
95
+ ses-cli email bulk --file vip.csv --subject "Exclusive" --body "Hey [[First]]" --provider 5
96
+ `);
46
97
  process.exit(1);
47
98
  }
48
99
 
100
+ let htmlBody = "";
101
+ if (htmlFile) {
102
+ htmlBody = readFileSync(htmlFile, "utf-8");
103
+ }
104
+
49
105
  const csvContent = readFileSync(file, "utf-8");
50
106
  const lines = csvContent.trim().split("\n");
51
- const hasHeader = lines[0].toLowerCase().includes("email");
52
- const emails = (hasHeader ? lines.slice(1) : lines)
53
- .map((l) => l.trim().split(",")[0].trim())
54
- .filter(Boolean);
107
+ const headers = lines[0].toLowerCase().split(",").map(h => h.trim().replace(/"/g, ""));
108
+ const emailIdx = headers.indexOf("email") !== -1 ? headers.indexOf("email") : 0;
109
+ const dataLines = lines.slice(1);
55
110
 
56
- if (emails.length === 0) {
57
- console.error("Error: No email addresses found in CSV file");
111
+ if (dataLines.length === 0) {
112
+ console.error("Error: No data rows found in CSV file");
58
113
  process.exit(1);
59
114
  }
60
115
 
61
- const htmlContent = html ? readFileSync(html, "utf-8") : null;
62
- console.log(`Sending to ${emails.length} recipients...`);
116
+ info(`Sending to ${dataLines.length} recipients...`);
63
117
 
64
118
  let sent = 0;
65
119
  let failed = 0;
66
120
  const errors = [];
67
121
 
68
- for (const email of emails) {
122
+ for (const line of dataLines) {
123
+ const cols = line.split(",").map(c => c.trim().replace(/"/g, ""));
124
+ const email = cols[emailIdx];
125
+ if (!email) continue;
126
+
127
+ let personalizedSubject = subject;
128
+ let personalizedBody = htmlBody || body || "";
129
+ headers.forEach((header, idx) => {
130
+ const regex = new RegExp(`\\[\\[${header}\\]\\]`, "gi");
131
+ personalizedSubject = personalizedSubject.replace(regex, cols[idx] || "");
132
+ personalizedBody = personalizedBody.replace(regex, cols[idx] || "");
133
+ });
134
+
69
135
  try {
70
- const payload = { to: email, subject };
71
- if (from) payload.from = from;
72
- if (htmlContent) payload.html = htmlContent;
73
- else payload.body = body;
136
+ const payload = { to: email, subject: personalizedSubject };
137
+ if (htmlFile) payload.htmlBody = personalizedBody;
138
+ else payload.body = personalizedBody;
139
+ if (from) payload.fromEmail = from;
140
+ if (fromName) payload.fromName = fromName;
141
+ if (provider) payload.providerId = parseInt(provider);
74
142
  await apiRequest("POST", "/email/send", payload);
75
143
  sent++;
144
+ if (delay > 0) await new Promise(r => setTimeout(r, delay));
76
145
  } catch {
77
146
  failed++;
78
147
  errors.push(email);
@@ -80,11 +149,11 @@ export async function emailBulk(args, jsonMode) {
80
149
  }
81
150
 
82
151
  if (jsonMode) {
83
- outputResult({ sent, failed, total: emails.length, errors }, true);
152
+ outputResult({ sent, failed, total: dataLines.length, errors }, true);
84
153
  } else {
85
- success(`Bulk email complete: ${sent} sent, ${failed} failed out of ${emails.length}`);
154
+ success(`Bulk email complete: ${sent} sent, ${failed} failed out of ${dataLines.length}`);
86
155
  if (errors.length > 0) {
87
- console.log(` Failed emails: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? "..." : ""}`);
156
+ console.log(` Failed: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? ` +${errors.length - 5} more` : ""}`);
88
157
  }
89
158
  }
90
159
  }
@@ -92,23 +161,28 @@ export async function emailBulk(args, jsonMode) {
92
161
  export async function emailHistory(args, jsonMode) {
93
162
  const limit = args["--limit"] || "20";
94
163
  const page = args["--page"] || "1";
164
+ const status = args["--status"];
165
+
166
+ const params = { limit, page };
167
+ if (status) params.status = status;
95
168
 
96
- const result = await apiRequest("GET", "/email/history", null, { limit, page });
169
+ const result = await apiRequest("GET", "/email/messages", null, params);
97
170
 
98
171
  if (jsonMode) {
99
172
  outputResult(result, true);
100
173
  } else {
101
- const messages = result.messages || result.data || [];
174
+ const messages = result.data?.messages || result.messages || result.data || [];
102
175
  if (messages.length === 0) {
103
176
  console.log("No email messages found.");
104
177
  return;
105
178
  }
106
179
  formatTable(
107
- ["To", "Subject", "Status", "Date"],
180
+ ["To", "Subject", "Status", "Provider", "Date"],
108
181
  messages.map((m) => [
109
182
  m.to || m.recipient,
110
- (m.subject || "").slice(0, 30),
183
+ (m.subject || "").slice(0, 25),
111
184
  m.status || "sent",
185
+ m.provider || "-",
112
186
  new Date(m.createdAt || m.sentAt).toLocaleString(),
113
187
  ])
114
188
  );
@@ -0,0 +1,50 @@
1
+ import { apiRequest } from "../api.mjs";
2
+ import { formatTable, outputResult, success } from "../output.mjs";
3
+
4
+ export async function providersList(args, jsonMode) {
5
+ const type = args["--type"]; // sms, email, or all
6
+
7
+ const params = {};
8
+ if (type) params.type = type;
9
+
10
+ const result = await apiRequest("GET", "/providers", null, params);
11
+
12
+ if (jsonMode) {
13
+ outputResult(result, true);
14
+ } else {
15
+ const providers = result.data?.providers || result.providers || result.data || [];
16
+ if (providers.length === 0) {
17
+ console.log("No providers configured. Set up providers in your SESender dashboard.");
18
+ return;
19
+ }
20
+ formatTable(
21
+ ["ID", "Name", "Type", "Provider", "Status"],
22
+ providers.map((p) => [
23
+ p.id,
24
+ p.name || p.providerType,
25
+ p.type || p.providerType,
26
+ p.providerType,
27
+ p.isActive ? "Active" : "Inactive",
28
+ ])
29
+ );
30
+ console.log(`\n${providers.length} provider(s) found`);
31
+ }
32
+ }
33
+
34
+ export async function providersBalance(args, jsonMode) {
35
+ const result = await apiRequest("GET", "/providers/balance");
36
+
37
+ if (jsonMode) {
38
+ outputResult(result, true);
39
+ } else {
40
+ const balances = result.data || result;
41
+ if (Array.isArray(balances)) {
42
+ formatTable(
43
+ ["Provider", "Balance", "Currency"],
44
+ balances.map((b) => [b.provider || b.name, b.balance || "N/A", b.currency || "USD"])
45
+ );
46
+ } else {
47
+ console.log(JSON.stringify(balances, null, 2));
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,52 @@
1
+ import { apiRequest } from "../api.mjs";
2
+ import { formatTable, outputResult, success } from "../output.mjs";
3
+
4
+ export async function scheduleList(args, jsonMode) {
5
+ const status = args["--status"];
6
+ const params = {};
7
+ if (status) params.status = status;
8
+
9
+ const result = await apiRequest("GET", "/scheduled", null, params);
10
+
11
+ if (jsonMode) {
12
+ outputResult(result, true);
13
+ } else {
14
+ const items = result.data?.scheduled || result.scheduled || result.data || [];
15
+ if (items.length === 0) {
16
+ console.log("No scheduled messages found.");
17
+ return;
18
+ }
19
+ formatTable(
20
+ ["ID", "Type", "To", "Scheduled For", "Status"],
21
+ items.map((s) => [
22
+ s.id,
23
+ s.type || "email",
24
+ (s.to || s.recipient || "").slice(0, 20),
25
+ s.scheduledAt ? new Date(s.scheduledAt).toLocaleString() : "-",
26
+ s.status || "pending",
27
+ ])
28
+ );
29
+ console.log(`\n${items.length} scheduled message(s)`);
30
+ }
31
+ }
32
+
33
+ export async function scheduleCancel(args, jsonMode) {
34
+ const id = args["--id"];
35
+
36
+ if (!id) {
37
+ console.error(`
38
+ Usage: ses-cli schedule cancel --id SCHEDULE_ID
39
+
40
+ Cancels a scheduled message before it is sent.
41
+ `);
42
+ process.exit(1);
43
+ }
44
+
45
+ const result = await apiRequest("POST", `/scheduled/${id}/cancel`);
46
+
47
+ if (jsonMode) {
48
+ outputResult(result, true);
49
+ } else {
50
+ success(`Scheduled message ${id} cancelled`);
51
+ }
52
+ }
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "fs";
2
+ import { apiRequest } from "../api.mjs";
3
+ import { formatTable, outputResult, success, info } from "../output.mjs";
4
+
5
+ export async function seedCheck(args, jsonMode) {
6
+ const file = args["--file"];
7
+ const emails = args["--emails"];
8
+ const words = args["--words"] || args["--seed-words"];
9
+
10
+ if ((!file && !emails) || !words) {
11
+ console.error(`
12
+ Usage: ses-cli seed-check --emails EMAILS --words WORDS
13
+ ses-cli seed-check --file FILE --words WORDS
14
+
15
+ Options:
16
+ --emails Comma-separated list of emails to check
17
+ --file CSV file with emails (column: email or first column)
18
+ --words Comma-separated seed words to check against
19
+ --seed-words Alias for --words
20
+
21
+ Description:
22
+ Checks emails against your seed word database to identify risky
23
+ addresses that may be spam traps or seed addresses.
24
+
25
+ Examples:
26
+ ses-cli seed-check --emails "user@test.com,info@example.com" --words "trap,seed,monitor"
27
+ ses-cli seed-check --file contacts.csv --words "spamtrap,honeypot,seedlist"
28
+ `);
29
+ process.exit(1);
30
+ }
31
+
32
+ let emailList = [];
33
+ if (file) {
34
+ const csvContent = readFileSync(file, "utf-8");
35
+ const lines = csvContent.trim().split("\n");
36
+ const headers = lines[0].toLowerCase().split(",").map(h => h.trim().replace(/"/g, ""));
37
+ const emailIdx = headers.indexOf("email") !== -1 ? headers.indexOf("email") : 0;
38
+ emailList = lines.slice(1)
39
+ .map(l => l.split(",")[emailIdx]?.trim().replace(/"/g, ""))
40
+ .filter(Boolean);
41
+ } else {
42
+ emailList = emails.split(",").map(e => e.trim()).filter(Boolean);
43
+ }
44
+
45
+ const seedWords = words.split(",").map(w => w.trim()).filter(Boolean);
46
+
47
+ if (emailList.length === 0) {
48
+ console.error("Error: No emails provided");
49
+ process.exit(1);
50
+ }
51
+
52
+ if (emailList.length > 500) {
53
+ console.error("Error: Maximum 500 emails per request");
54
+ process.exit(1);
55
+ }
56
+
57
+ info(`Checking ${emailList.length} emails against ${seedWords.length} seed words...`);
58
+
59
+ const result = await apiRequest("POST", "/seed-check", {
60
+ emails: emailList,
61
+ seedWords: seedWords,
62
+ });
63
+
64
+ if (jsonMode) {
65
+ outputResult(result, true);
66
+ } else {
67
+ const data = result.data || result;
68
+ const results = data.results || [];
69
+ const risky = results.filter(r => r.isRisky || r.riskScore > 50);
70
+ const safe = results.filter(r => !r.isRisky && (r.riskScore || 0) <= 50);
71
+
72
+ success(`Seed check complete: ${safe.length} safe, ${risky.length} risky`);
73
+
74
+ if (risky.length > 0) {
75
+ console.log("\n Risky emails:");
76
+ formatTable(
77
+ ["Email", "Risk Score", "Source"],
78
+ risky.map(r => [r.email, `${r.riskScore || 100}%`, r.source || "database"])
79
+ );
80
+ }
81
+
82
+ if (data.cost) console.log(`\n Cost: €${data.cost.toFixed(2)}`);
83
+ if (data.balance) console.log(` Remaining balance: €${data.balance.toFixed(2)}`);
84
+ }
85
+ }
86
+
87
+ export async function seedBalance(args, jsonMode) {
88
+ const result = await apiRequest("GET", "/seed-check/balance");
89
+
90
+ if (jsonMode) {
91
+ outputResult(result, true);
92
+ } else {
93
+ const data = result.data || result;
94
+ console.log(` Balance: €${(data.balance || 0).toFixed(2)}`);
95
+ console.log(` Rate: €${(data.rate || 0.01).toFixed(4)} per email`);
96
+ }
97
+ }