@btcemail/cli 0.1.1 → 0.4.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/dist/index.js CHANGED
@@ -2,12 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc9 from "picocolors";
5
+ import pc16 from "picocolors";
6
6
 
7
- // src/commands/login.ts
8
- import { randomUUID } from "crypto";
9
- import open from "open";
10
- import ora from "ora";
7
+ // src/commands/blocklist.ts
11
8
  import pc from "picocolors";
12
9
 
13
10
  // src/config.ts
@@ -104,103 +101,6 @@ function getToken() {
104
101
  return auth.token;
105
102
  }
106
103
 
107
- // src/commands/login.ts
108
- var POLL_INTERVAL_MS = 2e3;
109
- var POLL_TIMEOUT_MS = 3e5;
110
- async function loginCommand() {
111
- const sessionId = randomUUID();
112
- const baseUrl = getApiBaseUrl().replace("/api/v1", "");
113
- const authUrl = `${baseUrl}/auth/cli?session_id=${sessionId}`;
114
- const pollUrl = `${baseUrl}/api/auth/cli-session?session_id=${sessionId}`;
115
- console.log();
116
- console.log(pc.bold("btc.email CLI Login"));
117
- console.log();
118
- try {
119
- const createResponse = await fetch(
120
- `${baseUrl}/api/auth/cli-session`,
121
- {
122
- method: "POST",
123
- headers: { "Content-Type": "application/json" },
124
- body: JSON.stringify({ session_id: sessionId })
125
- }
126
- );
127
- if (!createResponse.ok) {
128
- console.error(pc.red("Failed to initialize login session"));
129
- process.exit(1);
130
- }
131
- } catch (error) {
132
- console.error(pc.red("Failed to connect to btc.email server"));
133
- console.error(
134
- pc.dim(error instanceof Error ? error.message : "Unknown error")
135
- );
136
- process.exit(1);
137
- }
138
- console.log(pc.dim("Opening browser for authentication..."));
139
- console.log();
140
- console.log(pc.dim("If the browser doesn't open, visit:"));
141
- console.log(pc.cyan(authUrl));
142
- console.log();
143
- try {
144
- await open(authUrl);
145
- } catch {
146
- }
147
- const spinner = ora("Waiting for authentication...").start();
148
- const startTime = Date.now();
149
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
150
- try {
151
- const response = await fetch(pollUrl);
152
- if (response.status === 202) {
153
- await sleep(POLL_INTERVAL_MS);
154
- continue;
155
- }
156
- if (response.status === 410) {
157
- spinner.fail("Session expired. Please try again.");
158
- process.exit(1);
159
- }
160
- if (response.ok) {
161
- const data = await response.json();
162
- if (data.status === "complete") {
163
- setAuth({
164
- token: data.data.token,
165
- expiresAt: data.data.expiresAt,
166
- userId: data.data.userId,
167
- email: data.data.email
168
- });
169
- spinner.succeed(pc.green("Successfully logged in!"));
170
- console.log();
171
- console.log(` ${pc.dim("Email:")} ${data.data.email}`);
172
- console.log();
173
- console.log(pc.dim("You can now use btcemail commands."));
174
- console.log(pc.dim("Run `btcemail --help` to see available commands."));
175
- return;
176
- }
177
- }
178
- await sleep(POLL_INTERVAL_MS);
179
- } catch (error) {
180
- await sleep(POLL_INTERVAL_MS);
181
- }
182
- }
183
- spinner.fail("Authentication timed out. Please try again.");
184
- process.exit(1);
185
- }
186
- function sleep(ms) {
187
- return new Promise((resolve) => setTimeout(resolve, ms));
188
- }
189
-
190
- // src/commands/logout.ts
191
- import pc2 from "picocolors";
192
- async function logoutCommand() {
193
- if (!isAuthenticated()) {
194
- console.log(pc2.yellow("You are not logged in."));
195
- return;
196
- }
197
- clearAuth();
198
- console.log(pc2.green("Successfully logged out."));
199
- }
200
-
201
- // src/commands/whoami.ts
202
- import pc3 from "picocolors";
203
-
204
104
  // src/api.ts
205
105
  async function apiRequest(endpoint, options = {}) {
206
106
  const token = getToken();
@@ -258,9 +158,7 @@ async function getCreditsBalance() {
258
158
  return apiRequest("/credits/balance");
259
159
  }
260
160
  async function getWhoami() {
261
- return apiRequest(
262
- "/auth/whoami"
263
- );
161
+ return apiRequest("/auth/whoami");
264
162
  }
265
163
  async function getPendingEmails(options = {}) {
266
164
  const params = new URLSearchParams();
@@ -324,175 +222,654 @@ async function getL402Invoice(options) {
324
222
  };
325
223
  }
326
224
  }
327
-
328
- // src/commands/whoami.ts
329
- async function whoamiCommand() {
330
- if (!isAuthenticated()) {
331
- console.log(pc3.yellow("Not logged in."));
332
- console.log(pc3.dim("Run `btcemail login` to authenticate."));
333
- process.exit(1);
334
- }
335
- const auth = getAuth();
336
- if (!auth) {
337
- console.log(pc3.yellow("Not logged in."));
338
- process.exit(1);
225
+ async function getDeliveredEmails(options = {}) {
226
+ const params = new URLSearchParams();
227
+ if (options.limit) params.set("limit", String(options.limit));
228
+ if (options.offset) params.set("offset", String(options.offset));
229
+ return apiRequest(`/inbound/delivered?${params}`);
230
+ }
231
+ async function getCreditTransactions(options = {}) {
232
+ const params = new URLSearchParams();
233
+ if (options.limit) params.set("limit", String(options.limit));
234
+ if (options.offset) params.set("offset", String(options.offset));
235
+ if (options.type) params.set("type", options.type);
236
+ return apiRequest(`/credits/transactions?${params}`);
237
+ }
238
+ async function purchaseCredits(optionIndex) {
239
+ return apiRequest("/credits/purchase", {
240
+ method: "POST",
241
+ body: JSON.stringify({ optionIndex })
242
+ });
243
+ }
244
+ async function checkPaymentStatus(paymentHash) {
245
+ const v1BaseUrl = getApiBaseUrl();
246
+ const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
247
+ const token = getToken();
248
+ const headers = {
249
+ "Content-Type": "application/json"
250
+ };
251
+ if (token) {
252
+ headers.Authorization = `Bearer ${token}`;
339
253
  }
340
- const result = await getWhoami();
341
- if (result.ok) {
342
- console.log();
343
- console.log(pc3.bold("Current User"));
344
- console.log();
345
- console.log(` ${pc3.dim("Email:")} ${result.data.email}`);
346
- if (result.data.username) {
347
- console.log(` ${pc3.dim("Username:")} ${result.data.username}`);
348
- }
349
- console.log(` ${pc3.dim("User ID:")} ${result.data.id}`);
350
- const expiryInfo = getTokenExpiryInfo();
351
- if (expiryInfo.expiresIn) {
352
- const expiryColor = isTokenExpiringSoon() ? pc3.yellow : pc3.dim;
353
- console.log(` ${pc3.dim("Session:")} ${expiryColor(`expires in ${expiryInfo.expiresIn}`)}`);
254
+ try {
255
+ const response = await fetch(`${rootApiUrl}/lightning/webhook?payment_hash=${paymentHash}`, {
256
+ headers
257
+ });
258
+ const json = await response.json();
259
+ if (!response.ok || json.success === false) {
260
+ return {
261
+ ok: false,
262
+ error: json.error || {
263
+ code: "UNKNOWN_ERROR",
264
+ message: `Request failed with status ${response.status}`
265
+ }
266
+ };
354
267
  }
355
- console.log();
356
- if (isTokenExpiringSoon()) {
357
- console.log(pc3.yellow("Session expiring soon. Run `btcemail login` to refresh."));
358
- console.log();
268
+ return {
269
+ ok: true,
270
+ data: {
271
+ paid: json.paid || false,
272
+ delivered: json.delivered,
273
+ status: json.status || json.state,
274
+ pendingEmailId: json.pendingEmailId
275
+ }
276
+ };
277
+ } catch (error) {
278
+ return {
279
+ ok: false,
280
+ error: {
281
+ code: "NETWORK_ERROR",
282
+ message: error instanceof Error ? error.message : "Network request failed"
283
+ }
284
+ };
285
+ }
286
+ }
287
+ async function getSettings(username) {
288
+ const params = username ? `?username=${username}` : "";
289
+ return apiRequest(`/settings${params}`);
290
+ }
291
+ async function updateSettings(settings2, username) {
292
+ const params = username ? `?username=${username}` : "";
293
+ return apiRequest(
294
+ `/settings${params}`,
295
+ {
296
+ method: "POST",
297
+ body: JSON.stringify(settings2)
359
298
  }
360
- } else {
361
- console.log();
362
- console.log(pc3.bold("Current User (cached)"));
363
- console.log();
364
- console.log(` ${pc3.dim("Email:")} ${auth.email}`);
365
- console.log(` ${pc3.dim("User ID:")} ${auth.userId}`);
366
- console.log();
367
- if (result.error.code === "UNAUTHENTICATED" || result.error.code === "UNAUTHORIZED") {
368
- console.log(pc3.yellow("Session expired. Run `btcemail login` to re-authenticate."));
299
+ );
300
+ }
301
+ async function getWhitelist(username) {
302
+ const params = username ? `?username=${username}` : "";
303
+ return apiRequest(`/whitelist${params}`);
304
+ }
305
+ async function addToWhitelist(entry, username) {
306
+ const params = username ? `?username=${username}` : "";
307
+ return apiRequest(`/whitelist${params}`, {
308
+ method: "POST",
309
+ body: JSON.stringify(entry)
310
+ });
311
+ }
312
+ async function removeFromWhitelist(id) {
313
+ return apiRequest(`/whitelist/${id}`, {
314
+ method: "DELETE"
315
+ });
316
+ }
317
+ async function getBlocklist(username) {
318
+ const params = username ? `?username=${username}` : "";
319
+ return apiRequest(`/blocklist${params}`);
320
+ }
321
+ async function addToBlocklist(entry, username) {
322
+ const params = username ? `?username=${username}` : "";
323
+ return apiRequest(`/blocklist${params}`, {
324
+ method: "POST",
325
+ body: JSON.stringify(entry)
326
+ });
327
+ }
328
+ async function removeFromBlocklist(id) {
329
+ return apiRequest(`/blocklist/${id}`, {
330
+ method: "DELETE"
331
+ });
332
+ }
333
+ async function getNetwork() {
334
+ return apiRequest("/network");
335
+ }
336
+ async function setNetwork(network2) {
337
+ return apiRequest("/network", {
338
+ method: "POST",
339
+ body: JSON.stringify({ network: network2 })
340
+ });
341
+ }
342
+ async function getStats() {
343
+ return apiRequest("/stats");
344
+ }
345
+ function getCreditPurchaseOptions() {
346
+ return [
347
+ {
348
+ amountSats: 1e3,
349
+ priceSats: 1e3,
350
+ bonusSats: 0,
351
+ label: "1,000 sats",
352
+ description: "Good for ~10 emails"
353
+ },
354
+ {
355
+ amountSats: 5e3,
356
+ priceSats: 4500,
357
+ bonusSats: 500,
358
+ label: "5,000 sats",
359
+ description: "Good for ~50 emails (+10% bonus)"
360
+ },
361
+ {
362
+ amountSats: 1e4,
363
+ priceSats: 8500,
364
+ bonusSats: 1500,
365
+ label: "10,000 sats",
366
+ description: "Good for ~100 emails (+15% bonus)"
367
+ },
368
+ {
369
+ amountSats: 5e4,
370
+ priceSats: 4e4,
371
+ bonusSats: 1e4,
372
+ label: "50,000 sats",
373
+ description: "Good for ~500 emails (+20% bonus)"
369
374
  }
370
- }
375
+ ];
371
376
  }
372
377
 
373
- // src/commands/list.ts
374
- import pc4 from "picocolors";
375
- async function listCommand(options) {
376
- if (!isAuthenticated()) {
377
- console.log(pc4.yellow("Not logged in."));
378
- console.log(pc4.dim("Run `btcemail login` to authenticate."));
378
+ // src/commands/blocklist.ts
379
+ async function blocklistListCommand(options = {}) {
380
+ const token = getToken();
381
+ if (!token) {
382
+ console.error(pc.red("Not logged in. Run 'btcemail login' first."));
379
383
  process.exit(1);
380
384
  }
381
- const result = await getEmails({
382
- folder: options.folder || "inbox",
383
- limit: options.limit || 20
384
- });
385
+ const result = await getBlocklist(options.username);
385
386
  if (!result.ok) {
386
- console.error(pc4.red(`Error: ${result.error.message}`));
387
+ console.error(pc.red(`Error: ${result.error.message}`));
387
388
  process.exit(1);
388
389
  }
389
- const { data: emails, pagination } = result.data;
390
+ const { username, entries } = result.data;
390
391
  if (options.json) {
391
- console.log(JSON.stringify({ emails, pagination }, null, 2));
392
+ console.log(JSON.stringify({ username, entries }, null, 2));
392
393
  return;
393
394
  }
394
395
  console.log();
395
- console.log(
396
- pc4.bold(`${options.folder || "Inbox"} (${pagination.total} emails)`)
397
- );
396
+ console.log(pc.bold("Blocklist for ") + pc.cyan(`${username}@btc.email`));
398
397
  console.log();
399
- if (emails.length === 0) {
400
- console.log(pc4.dim(" No emails found."));
398
+ if (entries.length === 0) {
399
+ console.log(pc.dim(" No entries in blocklist."));
400
+ console.log();
401
+ console.log(pc.dim(" Add entries with: btcemail blocklist add <email>"));
402
+ console.log(pc.dim(" Or for domains: btcemail blocklist add @domain.com"));
401
403
  console.log();
402
404
  return;
403
405
  }
404
- console.log(
405
- ` ${pc4.dim("ID".padEnd(12))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(40))} ${pc4.dim("Date")}`
406
- );
407
- console.log(pc4.dim(" " + "-".repeat(90)));
408
- for (const email of emails) {
409
- const unreadMarker = email.unread ? pc4.cyan("*") : " ";
410
- const id = email.id.slice(0, 10).padEnd(12);
411
- const from = truncate(email.from.name || email.from.email, 23).padEnd(25);
412
- const subject = truncate(email.subject, 38).padEnd(40);
413
- const date = formatDate(email.date);
414
- console.log(`${unreadMarker} ${id} ${from} ${subject} ${pc4.dim(date)}`);
415
- }
406
+ console.log(pc.dim(` ${entries.length} ${entries.length === 1 ? "entry" : "entries"}:`));
416
407
  console.log();
417
- if (pagination.hasMore) {
418
- console.log(
419
- pc4.dim(
420
- ` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
421
- )
422
- );
423
- console.log();
408
+ for (const entry of entries) {
409
+ const value = entry.sender_email || `@${entry.sender_domain}`;
410
+ const reason = entry.reason ? pc.dim(` - ${entry.reason}`) : "";
411
+ console.log(` ${pc.red(value)}${reason}`);
412
+ console.log(pc.dim(` ID: ${entry.id}`));
424
413
  }
414
+ console.log();
425
415
  }
426
- function truncate(str, maxLength) {
427
- if (str.length <= maxLength) return str;
428
- return str.slice(0, maxLength - 1) + "\u2026";
429
- }
430
- function formatDate(dateString) {
431
- const date = new Date(dateString);
432
- const now = /* @__PURE__ */ new Date();
433
- const diffMs = now.getTime() - date.getTime();
434
- const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
435
- if (diffDays === 0) {
436
- return date.toLocaleTimeString("en-US", {
437
- hour: "numeric",
438
- minute: "2-digit"
439
- });
416
+ async function blocklistAddCommand(entry, options = {}) {
417
+ const token = getToken();
418
+ if (!token) {
419
+ console.error(pc.red("Not logged in. Run 'btcemail login' first."));
420
+ process.exit(1);
440
421
  }
441
- if (diffDays < 7) {
442
- return date.toLocaleDateString("en-US", { weekday: "short" });
422
+ const isDomain = entry.startsWith("@");
423
+ const payload = isDomain ? { domain: entry.slice(1), reason: options.reason } : { email: entry, reason: options.reason };
424
+ const result = await addToBlocklist(payload, options.username);
425
+ if (!result.ok) {
426
+ console.error(pc.red(`Error: ${result.error.message}`));
427
+ process.exit(1);
443
428
  }
444
- return date.toLocaleDateString("en-US", {
445
- month: "short",
446
- day: "numeric"
447
- });
429
+ if (options.json) {
430
+ console.log(JSON.stringify({ success: true, entry: result.data.entry }, null, 2));
431
+ return;
432
+ }
433
+ console.log(pc.green(`Added to blocklist: ${pc.red(entry)}`));
448
434
  }
449
-
450
- // src/commands/read.ts
451
- import pc5 from "picocolors";
452
- async function readCommand(id, options) {
453
- if (!isAuthenticated()) {
454
- console.log(pc5.yellow("Not logged in."));
455
- console.log(pc5.dim("Run `btcemail login` to authenticate."));
435
+ async function blocklistRemoveCommand(id, options = {}) {
436
+ const token = getToken();
437
+ if (!token) {
438
+ console.error(pc.red("Not logged in. Run 'btcemail login' first."));
456
439
  process.exit(1);
457
440
  }
458
- const result = await getEmail(id);
441
+ const result = await removeFromBlocklist(id);
459
442
  if (!result.ok) {
460
- if (result.error.code === "NOT_FOUND") {
461
- console.error(pc5.red(`Email not found: ${id}`));
462
- } else {
463
- console.error(pc5.red(`Error: ${result.error.message}`));
464
- }
443
+ console.error(pc.red(`Error: ${result.error.message}`));
465
444
  process.exit(1);
466
445
  }
467
- const email = result.data;
468
446
  if (options.json) {
469
- console.log(JSON.stringify(email, null, 2));
447
+ console.log(JSON.stringify({ success: true }, null, 2));
470
448
  return;
471
449
  }
472
- console.log();
473
- console.log(pc5.bold(email.subject || "(No subject)"));
474
- console.log();
475
- console.log(`${pc5.dim("From:")} ${formatAddress(email.from)}`);
476
- console.log(`${pc5.dim("To:")} ${email.to.map(formatAddress).join(", ")}`);
477
- console.log(`${pc5.dim("Date:")} ${formatFullDate(email.date)}`);
478
- console.log(`${pc5.dim("ID:")} ${email.id}`);
479
- if (email.labels.length > 0) {
480
- console.log(`${pc5.dim("Labels:")} ${email.labels.join(", ")}`);
481
- }
482
- console.log();
483
- console.log(pc5.dim("-".repeat(60)));
484
- console.log();
485
- const content = email.bodyText || email.body || email.snippet;
486
- if (content) {
487
- const displayContent = email.bodyText || stripHtml(email.body || email.snippet);
488
- console.log(displayContent);
489
- } else {
490
- console.log(pc5.dim("(No content)"));
491
- }
492
- console.log();
493
- }
494
- function stripHtml(html) {
495
- return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').trim();
450
+ console.log(pc.green("Removed from blocklist"));
451
+ }
452
+
453
+ // src/commands/credits.ts
454
+ import ora from "ora";
455
+ import pc3 from "picocolors";
456
+ import qrcode from "qrcode-terminal";
457
+
458
+ // src/utils/payment-polling.ts
459
+ import pc2 from "picocolors";
460
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
461
+ async function pollForPayment(options) {
462
+ const { paymentHash, checkFn, maxAttempts = 120, intervalMs = 2500, onPaid } = options;
463
+ let frame = 0;
464
+ let attempts = 0;
465
+ process.stdout.write(`${pc2.cyan(SPINNER_FRAMES[0])} Waiting for payment...`);
466
+ while (attempts < maxAttempts) {
467
+ try {
468
+ const status = await checkFn(paymentHash);
469
+ if (status.paid) {
470
+ process.stdout.write(`\r${" ".repeat(50)}\r`);
471
+ console.log(`${pc2.green("\u2713")} Payment received!`);
472
+ if (onPaid) onPaid();
473
+ return status;
474
+ }
475
+ } catch {
476
+ }
477
+ frame = (frame + 1) % SPINNER_FRAMES.length;
478
+ const timeLeft = Math.ceil((maxAttempts - attempts) * intervalMs / 1e3 / 60);
479
+ process.stdout.write(
480
+ `\r${pc2.cyan(SPINNER_FRAMES[frame])} Waiting for payment... (${timeLeft}m remaining)`
481
+ );
482
+ await sleep(intervalMs);
483
+ attempts++;
484
+ }
485
+ process.stdout.write(`\r${" ".repeat(60)}\r`);
486
+ console.log(`${pc2.yellow("\u26A0")} Timed out waiting for payment.`);
487
+ return { paid: false };
488
+ }
489
+ function sleep(ms) {
490
+ return new Promise((resolve) => setTimeout(resolve, ms));
491
+ }
492
+
493
+ // src/commands/credits.ts
494
+ async function creditsBalanceCommand() {
495
+ if (!isAuthenticated()) {
496
+ console.log(pc3.yellow("Not logged in."));
497
+ console.log(pc3.dim("Run `btcemail login` to authenticate."));
498
+ process.exit(1);
499
+ }
500
+ const spinner = ora("Fetching balance...").start();
501
+ const result = await getCreditsBalance();
502
+ if (!result.ok) {
503
+ spinner.fail(`Error: ${result.error.message}`);
504
+ process.exit(1);
505
+ }
506
+ spinner.stop();
507
+ const { balanceSats, lifetimePurchasedSats, lifetimeSpentSats } = result.data;
508
+ console.log();
509
+ console.log(pc3.bold("Credit Balance"));
510
+ console.log();
511
+ console.log(` ${pc3.green(formatSats(balanceSats))} sats`);
512
+ console.log();
513
+ if (lifetimePurchasedSats > 0 || lifetimeSpentSats > 0) {
514
+ console.log(pc3.dim(` Purchased: ${formatSats(lifetimePurchasedSats)} sats`));
515
+ console.log(pc3.dim(` Spent: ${formatSats(lifetimeSpentSats)} sats`));
516
+ console.log();
517
+ }
518
+ if (balanceSats === 0) {
519
+ console.log(pc3.dim(" No credits available."));
520
+ console.log(pc3.dim(" Run `btcemail credits purchase` to buy credits."));
521
+ console.log();
522
+ }
523
+ }
524
+ async function creditsHistoryCommand(options) {
525
+ if (!isAuthenticated()) {
526
+ console.log(pc3.yellow("Not logged in."));
527
+ console.log(pc3.dim("Run `btcemail login` to authenticate."));
528
+ process.exit(1);
529
+ }
530
+ const spinner = ora("Loading transaction history...").start();
531
+ const result = await getCreditTransactions({
532
+ limit: options.limit || 20,
533
+ type: options.type
534
+ });
535
+ if (!result.ok) {
536
+ spinner.fail(`Error: ${result.error.message}`);
537
+ process.exit(1);
538
+ }
539
+ const { data: transactions, pagination } = result.data;
540
+ spinner.stop();
541
+ if (options.json) {
542
+ console.log(JSON.stringify({ transactions, pagination }, null, 2));
543
+ return;
544
+ }
545
+ console.log();
546
+ console.log(pc3.bold("Transaction History"));
547
+ console.log();
548
+ if (transactions.length === 0) {
549
+ console.log(pc3.dim(" No transactions found."));
550
+ console.log();
551
+ return;
552
+ }
553
+ console.log(
554
+ ` ${pc3.dim("Date".padEnd(12))} ${pc3.dim("Type".padEnd(15))} ${pc3.dim("Amount".padEnd(12))} ${pc3.dim("Balance")}`
555
+ );
556
+ console.log(pc3.dim(` ${"-".repeat(55)}`));
557
+ for (const tx of transactions) {
558
+ const date = formatDate(tx.createdAt);
559
+ const type = formatTransactionType(tx.transactionType).padEnd(15);
560
+ const amount = formatAmount(tx.amountSats).padEnd(12);
561
+ const balance = formatSats(tx.balanceAfter);
562
+ console.log(` ${date.padEnd(12)} ${type} ${amount} ${pc3.dim(balance)}`);
563
+ }
564
+ console.log();
565
+ if (pagination.hasMore) {
566
+ console.log(pc3.dim(` Use --limit to see more transactions.`));
567
+ console.log();
568
+ }
569
+ }
570
+ async function creditsPurchaseCommand(options) {
571
+ if (!isAuthenticated()) {
572
+ console.log(pc3.yellow("Not logged in."));
573
+ console.log(pc3.dim("Run `btcemail login` to authenticate."));
574
+ process.exit(1);
575
+ }
576
+ const purchaseOptions = getCreditPurchaseOptions();
577
+ if (options.option === void 0) {
578
+ console.log();
579
+ console.log(pc3.bold("Purchase Options"));
580
+ console.log();
581
+ purchaseOptions.forEach((opt, index) => {
582
+ console.log(` ${pc3.cyan(`[${index}]`)} ${opt.label}`);
583
+ console.log(` ${pc3.dim(opt.description)}`);
584
+ console.log(` ${pc3.dim(`Price: ${formatSats(opt.priceSats)} sats`)}`);
585
+ if (opt.bonusSats > 0) {
586
+ console.log(` ${pc3.green(`+${formatSats(opt.bonusSats)} bonus sats`)}`);
587
+ }
588
+ console.log();
589
+ });
590
+ console.log(pc3.dim(" Usage: btcemail credits purchase --option <index>"));
591
+ console.log(pc3.dim(" Add --wait to wait for payment confirmation."));
592
+ console.log();
593
+ return;
594
+ }
595
+ if (options.option < 0 || options.option >= purchaseOptions.length) {
596
+ console.error(pc3.red(`Error: Invalid option. Choose 0-${purchaseOptions.length - 1}.`));
597
+ process.exit(1);
598
+ }
599
+ const _selectedOption = purchaseOptions[options.option];
600
+ const spinner = ora("Creating invoice...").start();
601
+ const result = await purchaseCredits(options.option);
602
+ if (!result.ok) {
603
+ spinner.fail(`Error: ${result.error.message}`);
604
+ process.exit(1);
605
+ }
606
+ spinner.stop();
607
+ const purchase = result.data;
608
+ if (options.json) {
609
+ console.log(JSON.stringify(purchase, null, 2));
610
+ return;
611
+ }
612
+ console.log();
613
+ console.log(pc3.bold("Lightning Invoice"));
614
+ console.log();
615
+ console.log(` ${pc3.dim("Amount:")} ${formatSats(purchase.amountSats)} sats`);
616
+ console.log(` ${pc3.dim("Credits:")} ${formatSats(purchase.creditsToReceive)} sats`);
617
+ if (purchase.bonusSats > 0) {
618
+ console.log(` ${pc3.dim("Bonus:")} ${pc3.green(`+${formatSats(purchase.bonusSats)} sats`)}`);
619
+ }
620
+ console.log();
621
+ console.log(pc3.bold("Scan to pay:"));
622
+ console.log();
623
+ qrcode.generate(purchase.invoice, { small: true }, (qr) => {
624
+ const indentedQr = qr.split("\n").map((line) => ` ${line}`).join("\n");
625
+ console.log(indentedQr);
626
+ });
627
+ console.log();
628
+ console.log(pc3.bold("Invoice:"));
629
+ console.log();
630
+ console.log(` ${purchase.invoice}`);
631
+ console.log();
632
+ console.log(pc3.dim(` Payment hash: ${purchase.paymentHash}`));
633
+ console.log(pc3.dim(` Expires: ${new Date(purchase.expiresAt).toLocaleString()}`));
634
+ console.log();
635
+ if (options.wait) {
636
+ console.log();
637
+ const status = await pollForPayment({
638
+ paymentHash: purchase.paymentHash,
639
+ checkFn: async (hash) => {
640
+ const result2 = await checkPaymentStatus(hash);
641
+ if (result2.ok) {
642
+ return { paid: result2.data.paid };
643
+ }
644
+ return { paid: false };
645
+ },
646
+ onPaid: async () => {
647
+ const balanceResult = await getCreditsBalance();
648
+ if (balanceResult.ok) {
649
+ console.log();
650
+ console.log(
651
+ `${pc3.dim("New balance:")} ${pc3.green(formatSats(balanceResult.data.balanceSats))} sats`
652
+ );
653
+ }
654
+ }
655
+ });
656
+ if (!status.paid) {
657
+ console.log(pc3.dim(" You can still pay the invoice and credits will be added."));
658
+ }
659
+ console.log();
660
+ } else {
661
+ console.log(pc3.dim(" Scan the QR code or copy the invoice above to pay."));
662
+ console.log(pc3.dim(" Use --wait to wait for payment confirmation."));
663
+ console.log();
664
+ }
665
+ }
666
+ function formatSats(sats) {
667
+ return sats.toLocaleString();
668
+ }
669
+ function formatAmount(sats) {
670
+ if (sats >= 0) {
671
+ return pc3.green(`+${formatSats(sats)}`);
672
+ }
673
+ return pc3.red(formatSats(sats));
674
+ }
675
+ function formatTransactionType(type) {
676
+ const types = {
677
+ topup: "Top-up",
678
+ email_sent: "Email Sent",
679
+ email_received: "Email Received",
680
+ refund: "Refund",
681
+ bonus: "Bonus"
682
+ };
683
+ return types[type] || type;
684
+ }
685
+ function formatDate(dateString) {
686
+ const date = new Date(dateString);
687
+ return date.toLocaleDateString("en-US", {
688
+ month: "short",
689
+ day: "numeric"
690
+ });
691
+ }
692
+
693
+ // src/commands/inbound.ts
694
+ import ora2 from "ora";
695
+ import pc4 from "picocolors";
696
+ async function inboundDeliveredCommand(options) {
697
+ if (!isAuthenticated()) {
698
+ console.log(pc4.yellow("Not logged in."));
699
+ console.log(pc4.dim("Run `btcemail login` to authenticate."));
700
+ process.exit(1);
701
+ }
702
+ const spinner = ora2("Loading delivered emails...").start();
703
+ const result = await getDeliveredEmails({
704
+ limit: options.limit || 20
705
+ });
706
+ if (!result.ok) {
707
+ spinner.fail(`Error: ${result.error.message}`);
708
+ process.exit(1);
709
+ }
710
+ const { data: emails, pagination } = result.data;
711
+ spinner.stop();
712
+ if (options.json) {
713
+ console.log(JSON.stringify({ emails, pagination }, null, 2));
714
+ return;
715
+ }
716
+ console.log();
717
+ console.log(pc4.bold(`Delivered (${pagination.total} paid emails received)`));
718
+ console.log();
719
+ if (emails.length === 0) {
720
+ console.log(pc4.dim(" No delivered emails yet."));
721
+ console.log();
722
+ return;
723
+ }
724
+ console.log(
725
+ ` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(28))} ${pc4.dim("Subject".padEnd(40))} ${pc4.dim("Date")}`
726
+ );
727
+ console.log(pc4.dim(` ${"-".repeat(85)}`));
728
+ emails.forEach((email, index) => {
729
+ const num = pc4.cyan(String(index + 1).padEnd(4));
730
+ const from = truncate(email.from.name || email.from.email, 26).padEnd(28);
731
+ const subject = truncate(email.subject, 38).padEnd(40);
732
+ const date = formatDate2(email.date);
733
+ console.log(` ${num}${from} ${subject} ${pc4.dim(date)}`);
734
+ });
735
+ console.log();
736
+ if (pagination.hasMore) {
737
+ console.log(
738
+ pc4.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
739
+ );
740
+ console.log();
741
+ }
742
+ console.log(pc4.dim(" Read email: btcemail read <id>"));
743
+ console.log();
744
+ }
745
+ async function inboundPendingCommand(options) {
746
+ if (!isAuthenticated()) {
747
+ console.log(pc4.yellow("Not logged in."));
748
+ console.log(pc4.dim("Run `btcemail login` to authenticate."));
749
+ process.exit(1);
750
+ }
751
+ const spinner = ora2("Loading pending emails...").start();
752
+ const result = await getPendingEmails({
753
+ limit: options.limit || 20
754
+ });
755
+ if (!result.ok) {
756
+ spinner.fail(`Error: ${result.error.message}`);
757
+ process.exit(1);
758
+ }
759
+ const { data: emails, pagination } = result.data;
760
+ spinner.stop();
761
+ if (options.json) {
762
+ console.log(JSON.stringify({ emails, pagination }, null, 2));
763
+ return;
764
+ }
765
+ console.log();
766
+ console.log(pc4.bold(`Pending (${pagination.total} awaiting payment)`));
767
+ console.log();
768
+ if (emails.length === 0) {
769
+ console.log(pc4.dim(" No pending emails."));
770
+ console.log();
771
+ return;
772
+ }
773
+ console.log(
774
+ ` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(28))} ${pc4.dim("Subject".padEnd(35))} ${pc4.dim("Sats")}`
775
+ );
776
+ console.log(pc4.dim(` ${"-".repeat(80)}`));
777
+ emails.forEach((email, index) => {
778
+ const num = pc4.cyan(String(index + 1).padEnd(4));
779
+ const from = truncate(email.from.name || email.from.email, 26).padEnd(28);
780
+ const subject = truncate(email.subject, 33).padEnd(35);
781
+ const sats = pc4.yellow(String(email.amountSats));
782
+ console.log(` ${num}${from} ${subject} ${sats}`);
783
+ });
784
+ console.log();
785
+ if (pagination.hasMore) {
786
+ console.log(
787
+ pc4.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
788
+ );
789
+ console.log();
790
+ }
791
+ console.log(pc4.dim(" View details: btcemail inbound view <id>"));
792
+ console.log();
793
+ }
794
+ async function inboundViewCommand(id, options) {
795
+ if (!isAuthenticated()) {
796
+ console.log(pc4.yellow("Not logged in."));
797
+ console.log(pc4.dim("Run `btcemail login` to authenticate."));
798
+ process.exit(1);
799
+ }
800
+ const spinner = ora2("Loading email details...").start();
801
+ const result = await getPendingEmail(id);
802
+ if (!result.ok) {
803
+ if (result.error.code === "NOT_FOUND") {
804
+ spinner.fail(`Pending email not found: ${id}`);
805
+ } else {
806
+ spinner.fail(`Error: ${result.error.message}`);
807
+ }
808
+ process.exit(1);
809
+ }
810
+ spinner.stop();
811
+ const email = result.data;
812
+ if (options.json) {
813
+ console.log(JSON.stringify(email, null, 2));
814
+ return;
815
+ }
816
+ console.log();
817
+ console.log(pc4.bold(email.subject || "(No subject)"));
818
+ console.log();
819
+ console.log(`${pc4.dim("From:")} ${formatAddress(email.from)}`);
820
+ console.log(`${pc4.dim("Date:")} ${formatFullDate(email.createdAt)}`);
821
+ console.log(`${pc4.dim("ID:")} ${email.id}`);
822
+ console.log();
823
+ console.log(pc4.yellow(`Payment Required: ${email.amountSats} sats`));
824
+ console.log();
825
+ if (email.paymentHash) {
826
+ console.log(pc4.dim("To receive this email, pay the invoice and run:"));
827
+ console.log(pc4.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
828
+ } else {
829
+ console.log(pc4.dim("Payment information not available."));
830
+ }
831
+ console.log();
832
+ console.log(pc4.dim("-".repeat(60)));
833
+ console.log();
834
+ console.log(pc4.dim("Preview (full content available after payment):"));
835
+ console.log();
836
+ console.log(truncate(email.bodyText || email.body, 200));
837
+ console.log();
838
+ }
839
+ async function inboundPayCommand(id, options) {
840
+ if (!isAuthenticated()) {
841
+ console.log(pc4.yellow("Not logged in."));
842
+ console.log(pc4.dim("Run `btcemail login` to authenticate."));
843
+ process.exit(1);
844
+ }
845
+ if (!options.paymentHash) {
846
+ console.error(pc4.red("Payment hash is required. Use --payment-hash <preimage>"));
847
+ process.exit(1);
848
+ }
849
+ const spinner = ora2("Verifying payment...").start();
850
+ const result = await payForEmail(id, options.paymentHash);
851
+ if (!result.ok) {
852
+ if (result.error.code === "PAYMENT_INVALID") {
853
+ spinner.fail("Payment verification failed.");
854
+ console.log(pc4.dim("Make sure you paid the correct invoice and provided the preimage."));
855
+ } else if (result.error.code === "NOT_FOUND") {
856
+ spinner.fail(`Pending email not found or already delivered: ${id}`);
857
+ } else {
858
+ spinner.fail(`Error: ${result.error.message}`);
859
+ }
860
+ process.exit(1);
861
+ }
862
+ spinner.succeed("Payment verified! Email delivered.");
863
+ console.log();
864
+ console.log(`${pc4.dim("Email ID:")} ${result.data.emailId}`);
865
+ console.log(`${pc4.dim("Status:")} ${result.data.status}`);
866
+ console.log();
867
+ console.log(pc4.dim(`Read email: btcemail read ${result.data.emailId}`));
868
+ console.log();
869
+ }
870
+ function truncate(str, maxLength) {
871
+ if (str.length <= maxLength) return str;
872
+ return `${str.slice(0, maxLength - 1)}\u2026`;
496
873
  }
497
874
  function formatAddress(addr) {
498
875
  if (addr.name) {
@@ -511,47 +888,511 @@ function formatFullDate(dateString) {
511
888
  minute: "2-digit"
512
889
  });
513
890
  }
891
+ function formatDate2(dateString) {
892
+ const date = new Date(dateString);
893
+ const now = /* @__PURE__ */ new Date();
894
+ const diffMs = now.getTime() - date.getTime();
895
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
896
+ if (diffDays === 0) {
897
+ return date.toLocaleTimeString("en-US", {
898
+ hour: "numeric",
899
+ minute: "2-digit"
900
+ });
901
+ }
902
+ if (diffDays < 7) {
903
+ return date.toLocaleDateString("en-US", { weekday: "short" });
904
+ }
905
+ return date.toLocaleDateString("en-US", {
906
+ month: "short",
907
+ day: "numeric"
908
+ });
909
+ }
514
910
 
515
- // src/commands/credits.ts
911
+ // src/commands/list.ts
912
+ import ora3 from "ora";
913
+ import pc5 from "picocolors";
914
+
915
+ // src/utils/cache.ts
916
+ var CACHE_KEY = "emailCache";
917
+ var CACHE_TTL_MS = 30 * 60 * 1e3;
918
+ function cacheEmailList(folder, emails) {
919
+ config.set(CACHE_KEY, {
920
+ folder,
921
+ emails,
922
+ timestamp: Date.now()
923
+ });
924
+ }
925
+ function getCachedEmailList(folder) {
926
+ const cache = config.get(CACHE_KEY);
927
+ if (!cache) return null;
928
+ if (Date.now() - cache.timestamp > CACHE_TTL_MS) {
929
+ config.delete(CACHE_KEY);
930
+ return null;
931
+ }
932
+ if (folder && cache.folder !== folder) {
933
+ return null;
934
+ }
935
+ return cache;
936
+ }
937
+ function getEmailIdByIndex(index) {
938
+ const cache = getCachedEmailList();
939
+ if (!cache) return null;
940
+ const idx = index - 1;
941
+ if (idx < 0 || idx >= cache.emails.length) {
942
+ return null;
943
+ }
944
+ return cache.emails[idx].id;
945
+ }
946
+ function isNumericId(id) {
947
+ return /^\d+$/.test(id);
948
+ }
949
+
950
+ // src/commands/list.ts
951
+ async function listCommand(options) {
952
+ if (!isAuthenticated()) {
953
+ console.log(pc5.yellow("Not logged in."));
954
+ console.log(pc5.dim("Run `btcemail login` to authenticate."));
955
+ process.exit(1);
956
+ }
957
+ const folder = options.folder || "inbox";
958
+ const limit = options.limit || 20;
959
+ const page = options.page || 1;
960
+ const offset = (page - 1) * limit;
961
+ const spinner = ora3("Loading emails...").start();
962
+ const result = await getEmails({
963
+ folder,
964
+ limit,
965
+ offset
966
+ });
967
+ if (!result.ok) {
968
+ spinner.fail(`Error: ${result.error.message}`);
969
+ process.exit(1);
970
+ }
971
+ const { data: emails, pagination } = result.data;
972
+ spinner.stop();
973
+ const cachedEmails = emails.map((e) => ({
974
+ id: e.id,
975
+ subject: e.subject,
976
+ from: e.from.name || e.from.email
977
+ }));
978
+ cacheEmailList(folder, cachedEmails);
979
+ if (options.json) {
980
+ console.log(JSON.stringify({ emails, pagination }, null, 2));
981
+ return;
982
+ }
983
+ console.log();
984
+ console.log(pc5.bold(`${capitalize(folder)} (${pagination.total} emails)`));
985
+ console.log();
986
+ if (emails.length === 0) {
987
+ console.log(pc5.dim(" No emails found."));
988
+ console.log();
989
+ return;
990
+ }
991
+ console.log(
992
+ ` ${pc5.dim("#".padEnd(4))} ${pc5.dim("From".padEnd(25))} ${pc5.dim("Subject".padEnd(45))} ${pc5.dim("Date")}`
993
+ );
994
+ console.log(pc5.dim(` ${"-".repeat(85)}`));
995
+ emails.forEach((email, index) => {
996
+ const num = pc5.cyan(String(index + 1).padEnd(4));
997
+ const unreadMarker = email.unread ? pc5.cyan("\u25CF") : " ";
998
+ const from = truncate2(email.from.name || email.from.email, 23).padEnd(25);
999
+ const subject = truncate2(email.subject, 43).padEnd(45);
1000
+ const date = formatDate3(email.date);
1001
+ console.log(`${unreadMarker} ${num}${from} ${subject} ${pc5.dim(date)}`);
1002
+ });
1003
+ console.log();
1004
+ const totalPages = Math.ceil(pagination.total / limit);
1005
+ const currentPage = page;
1006
+ if (totalPages > 1) {
1007
+ console.log(
1008
+ pc5.dim(` Page ${currentPage} of ${totalPages} (${emails.length} of ${pagination.total})`)
1009
+ );
1010
+ if (currentPage < totalPages) {
1011
+ console.log(pc5.dim(` Next page: btcemail list --page ${currentPage + 1}`));
1012
+ }
1013
+ console.log();
1014
+ }
1015
+ console.log(pc5.dim(" Read email: btcemail read <#> or btcemail read <id>"));
1016
+ console.log();
1017
+ }
1018
+ function capitalize(str) {
1019
+ return str.charAt(0).toUpperCase() + str.slice(1);
1020
+ }
1021
+ function truncate2(str, maxLength) {
1022
+ if (str.length <= maxLength) return str;
1023
+ return `${str.slice(0, maxLength - 1)}\u2026`;
1024
+ }
1025
+ function formatDate3(dateString) {
1026
+ const date = new Date(dateString);
1027
+ const now = /* @__PURE__ */ new Date();
1028
+ const diffMs = now.getTime() - date.getTime();
1029
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1030
+ if (diffDays === 0) {
1031
+ return date.toLocaleTimeString("en-US", {
1032
+ hour: "numeric",
1033
+ minute: "2-digit"
1034
+ });
1035
+ }
1036
+ if (diffDays < 7) {
1037
+ return date.toLocaleDateString("en-US", { weekday: "short" });
1038
+ }
1039
+ return date.toLocaleDateString("en-US", {
1040
+ month: "short",
1041
+ day: "numeric"
1042
+ });
1043
+ }
1044
+
1045
+ // src/commands/login.ts
1046
+ import { randomUUID } from "crypto";
1047
+ import open from "open";
1048
+ import ora4 from "ora";
516
1049
  import pc6 from "picocolors";
517
- async function creditsBalanceCommand() {
1050
+ var POLL_INTERVAL_MS = 2e3;
1051
+ var POLL_TIMEOUT_MS = 3e5;
1052
+ async function loginCommand() {
1053
+ const sessionId = randomUUID();
1054
+ const baseUrl = getApiBaseUrl().replace("/api/v1", "");
1055
+ const authUrl = `${baseUrl}/auth/cli?session_id=${sessionId}`;
1056
+ const pollUrl = `${baseUrl}/api/auth/cli-session?session_id=${sessionId}`;
1057
+ console.log();
1058
+ console.log(pc6.bold("btc.email CLI Login"));
1059
+ console.log();
1060
+ try {
1061
+ const createResponse = await fetch(`${baseUrl}/api/auth/cli-session`, {
1062
+ method: "POST",
1063
+ headers: { "Content-Type": "application/json" },
1064
+ body: JSON.stringify({ session_id: sessionId })
1065
+ });
1066
+ if (!createResponse.ok) {
1067
+ console.error(pc6.red("Failed to initialize login session"));
1068
+ process.exit(1);
1069
+ }
1070
+ } catch (error) {
1071
+ console.error(pc6.red("Failed to connect to btc.email server"));
1072
+ console.error(pc6.dim(error instanceof Error ? error.message : "Unknown error"));
1073
+ process.exit(1);
1074
+ }
1075
+ console.log(pc6.dim("Opening browser for authentication..."));
1076
+ console.log();
1077
+ console.log(pc6.dim("If the browser doesn't open, visit:"));
1078
+ console.log(pc6.cyan(authUrl));
1079
+ console.log();
1080
+ try {
1081
+ await open(authUrl);
1082
+ } catch {
1083
+ }
1084
+ const spinner = ora4("Waiting for authentication...").start();
1085
+ const startTime = Date.now();
1086
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1087
+ try {
1088
+ const response = await fetch(pollUrl);
1089
+ if (response.status === 202) {
1090
+ await sleep2(POLL_INTERVAL_MS);
1091
+ continue;
1092
+ }
1093
+ if (response.status === 410) {
1094
+ spinner.fail("Session expired. Please try again.");
1095
+ process.exit(1);
1096
+ }
1097
+ if (response.ok) {
1098
+ const data = await response.json();
1099
+ if (data.status === "complete") {
1100
+ setAuth({
1101
+ token: data.data.token,
1102
+ expiresAt: data.data.expiresAt,
1103
+ userId: data.data.userId,
1104
+ email: data.data.email
1105
+ });
1106
+ spinner.succeed(pc6.green("Successfully logged in!"));
1107
+ console.log();
1108
+ console.log(` ${pc6.dim("Email:")} ${data.data.email}`);
1109
+ console.log();
1110
+ console.log(pc6.dim("You can now use btcemail commands."));
1111
+ console.log(pc6.dim("Run `btcemail --help` to see available commands."));
1112
+ return;
1113
+ }
1114
+ }
1115
+ await sleep2(POLL_INTERVAL_MS);
1116
+ } catch (_error) {
1117
+ await sleep2(POLL_INTERVAL_MS);
1118
+ }
1119
+ }
1120
+ spinner.fail("Authentication timed out. Please try again.");
1121
+ process.exit(1);
1122
+ }
1123
+ function sleep2(ms) {
1124
+ return new Promise((resolve) => setTimeout(resolve, ms));
1125
+ }
1126
+
1127
+ // src/commands/logout.ts
1128
+ import pc7 from "picocolors";
1129
+ async function logoutCommand() {
518
1130
  if (!isAuthenticated()) {
519
- console.log(pc6.yellow("Not logged in."));
520
- console.log(pc6.dim("Run `btcemail login` to authenticate."));
1131
+ console.log(pc7.yellow("You are not logged in."));
1132
+ return;
1133
+ }
1134
+ clearAuth();
1135
+ console.log(pc7.green("Successfully logged out."));
1136
+ }
1137
+
1138
+ // src/commands/network.ts
1139
+ import pc8 from "picocolors";
1140
+ async function networkShowCommand(options = {}) {
1141
+ const token = getToken();
1142
+ if (!token) {
1143
+ console.error(pc8.red("Not logged in. Run 'btcemail login' first."));
1144
+ process.exit(1);
1145
+ }
1146
+ const result = await getNetwork();
1147
+ if (!result.ok) {
1148
+ console.error(pc8.red(`Error: ${result.error.message}`));
1149
+ process.exit(1);
1150
+ }
1151
+ const { network_mode, networks } = result.data;
1152
+ if (options.json) {
1153
+ console.log(JSON.stringify({ network_mode, networks }, null, 2));
1154
+ return;
1155
+ }
1156
+ console.log();
1157
+ console.log(pc8.bold("Network Mode"));
1158
+ console.log();
1159
+ const isTestnet = network_mode === "testnet";
1160
+ const currentLabel = isTestnet ? pc8.blue("testnet") + pc8.dim(" (fake Bitcoin)") : pc8.red("mainnet") + pc8.dim(" (real Bitcoin)");
1161
+ console.log(` Current: ${currentLabel}`);
1162
+ console.log();
1163
+ console.log(pc8.dim(" Available networks:"));
1164
+ console.log(
1165
+ ` testnet: ${networks.testnet.available ? pc8.green("available") : pc8.red("unavailable")}`
1166
+ );
1167
+ console.log(
1168
+ ` mainnet: ${networks.mainnet.available ? pc8.green("available") : pc8.red("unavailable")}`
1169
+ );
1170
+ console.log();
1171
+ if (!isTestnet) {
1172
+ console.log(pc8.yellow(" Warning: You are on mainnet. Payments use real Bitcoin!"));
1173
+ console.log();
1174
+ }
1175
+ }
1176
+ async function networkSwitchCommand(network2, options = {}) {
1177
+ const token = getToken();
1178
+ if (!token) {
1179
+ console.error(pc8.red("Not logged in. Run 'btcemail login' first."));
1180
+ process.exit(1);
1181
+ }
1182
+ if (network2 !== "testnet" && network2 !== "mainnet") {
1183
+ console.error(pc8.red("Invalid network. Use 'testnet' or 'mainnet'."));
1184
+ process.exit(1);
1185
+ }
1186
+ if (network2 === "mainnet") {
1187
+ console.log();
1188
+ console.log(pc8.yellow("Warning: You are switching to mainnet."));
1189
+ console.log(pc8.yellow("All payments will use real Bitcoin!"));
1190
+ console.log();
1191
+ }
1192
+ const result = await setNetwork(network2);
1193
+ if (!result.ok) {
1194
+ console.error(pc8.red(`Error: ${result.error.message}`));
1195
+ process.exit(1);
1196
+ }
1197
+ if (options.json) {
1198
+ console.log(JSON.stringify({ success: true, network_mode: result.data.network_mode }, null, 2));
1199
+ return;
1200
+ }
1201
+ const label = network2 === "testnet" ? pc8.blue("testnet") : pc8.red("mainnet");
1202
+ console.log(pc8.green(`Switched to ${label}`));
1203
+ }
1204
+
1205
+ // src/commands/read.ts
1206
+ import ora5 from "ora";
1207
+ import pc9 from "picocolors";
1208
+ async function readCommand(idOrNumber, options) {
1209
+ if (!isAuthenticated()) {
1210
+ console.log(pc9.yellow("Not logged in."));
1211
+ console.log(pc9.dim("Run `btcemail login` to authenticate."));
1212
+ process.exit(1);
1213
+ }
1214
+ let emailId = idOrNumber;
1215
+ if (isNumericId(idOrNumber)) {
1216
+ const index = parseInt(idOrNumber, 10);
1217
+ const cachedId = getEmailIdByIndex(index);
1218
+ if (cachedId) {
1219
+ emailId = cachedId;
1220
+ } else {
1221
+ const cache = getCachedEmailList();
1222
+ if (cache) {
1223
+ console.error(pc9.red(`Email #${index} not found in list.`));
1224
+ console.log(
1225
+ pc9.dim(`List has ${cache.emails.length} emails. Run 'btcemail list' to refresh.`)
1226
+ );
1227
+ } else {
1228
+ console.error(pc9.red(`No email list cached.`));
1229
+ console.log(pc9.dim(`Run 'btcemail list' first, then use 'btcemail read <#>'.`));
1230
+ }
1231
+ process.exit(1);
1232
+ }
1233
+ }
1234
+ const spinner = ora5("Loading email...").start();
1235
+ const result = await getEmail(emailId);
1236
+ if (!result.ok) {
1237
+ if (result.error.code === "NOT_FOUND") {
1238
+ spinner.fail(`Email not found: ${emailId}`);
1239
+ } else {
1240
+ spinner.fail(`Error: ${result.error.message}`);
1241
+ }
1242
+ process.exit(1);
1243
+ }
1244
+ spinner.stop();
1245
+ const email = result.data;
1246
+ if (options.json) {
1247
+ console.log(JSON.stringify(email, null, 2));
1248
+ return;
1249
+ }
1250
+ console.log();
1251
+ console.log(pc9.bold(email.subject || "(No subject)"));
1252
+ console.log();
1253
+ console.log(`${pc9.dim("From:")} ${formatAddress2(email.from)}`);
1254
+ console.log(`${pc9.dim("To:")} ${email.to.map(formatAddress2).join(", ")}`);
1255
+ console.log(`${pc9.dim("Date:")} ${formatFullDate2(email.date)}`);
1256
+ console.log(`${pc9.dim("ID:")} ${email.id}`);
1257
+ if (email.labels.length > 0) {
1258
+ console.log(`${pc9.dim("Labels:")} ${email.labels.join(", ")}`);
1259
+ }
1260
+ console.log();
1261
+ console.log(pc9.dim("-".repeat(60)));
1262
+ console.log();
1263
+ const content = email.bodyText || email.body || email.snippet;
1264
+ if (content) {
1265
+ const displayContent = email.bodyText || stripHtml(email.body || email.snippet);
1266
+ console.log(displayContent);
1267
+ } else {
1268
+ console.log(pc9.dim("(No content)"));
1269
+ }
1270
+ console.log();
1271
+ }
1272
+ function stripHtml(html) {
1273
+ return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').trim();
1274
+ }
1275
+ function formatAddress2(addr) {
1276
+ if (addr.name) {
1277
+ return `${addr.name} <${addr.email}>`;
1278
+ }
1279
+ return addr.email;
1280
+ }
1281
+ function formatFullDate2(dateString) {
1282
+ const date = new Date(dateString);
1283
+ return date.toLocaleString("en-US", {
1284
+ weekday: "short",
1285
+ year: "numeric",
1286
+ month: "short",
1287
+ day: "numeric",
1288
+ hour: "numeric",
1289
+ minute: "2-digit"
1290
+ });
1291
+ }
1292
+
1293
+ // src/commands/search.ts
1294
+ import ora6 from "ora";
1295
+ import pc10 from "picocolors";
1296
+ async function searchCommand(query, options) {
1297
+ if (!isAuthenticated()) {
1298
+ console.log(pc10.yellow("Not logged in."));
1299
+ console.log(pc10.dim("Run `btcemail login` to authenticate."));
1300
+ process.exit(1);
1301
+ }
1302
+ if (!query || query.trim().length === 0) {
1303
+ console.error(pc10.red("Error: Search query is required"));
521
1304
  process.exit(1);
522
1305
  }
523
- const result = await getCreditsBalance();
1306
+ const spinner = ora6(`Searching for "${query}"...`).start();
1307
+ const result = await getEmails({
1308
+ search: query.trim(),
1309
+ limit: options.limit || 20
1310
+ });
524
1311
  if (!result.ok) {
525
- console.error(pc6.red(`Error: ${result.error.message}`));
1312
+ spinner.fail(`Error: ${result.error.message}`);
526
1313
  process.exit(1);
527
1314
  }
528
- const { balanceSats } = result.data;
1315
+ const { data: emails, pagination } = result.data;
1316
+ spinner.stop();
1317
+ const cachedEmails = emails.map((e) => ({
1318
+ id: e.id,
1319
+ subject: e.subject,
1320
+ from: e.from.name || e.from.email
1321
+ }));
1322
+ cacheEmailList("search", cachedEmails);
1323
+ if (options.json) {
1324
+ console.log(JSON.stringify({ query, emails, pagination }, null, 2));
1325
+ return;
1326
+ }
529
1327
  console.log();
530
- console.log(pc6.bold("Credit Balance"));
1328
+ console.log(pc10.bold(`Search: "${query}" (${pagination.total} found)`));
531
1329
  console.log();
532
- console.log(` ${pc6.green(formatSats(balanceSats))} sats`);
1330
+ if (emails.length === 0) {
1331
+ console.log(pc10.dim(" No emails found matching your search."));
1332
+ console.log();
1333
+ return;
1334
+ }
1335
+ console.log(
1336
+ ` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(25))} ${pc10.dim("Subject".padEnd(45))} ${pc10.dim("Date")}`
1337
+ );
1338
+ console.log(pc10.dim(` ${"-".repeat(85)}`));
1339
+ emails.forEach((email, index) => {
1340
+ const num = pc10.cyan(String(index + 1).padEnd(4));
1341
+ const unreadMarker = email.unread ? pc10.cyan("\u25CF") : " ";
1342
+ const from = truncate3(email.from.name || email.from.email, 23).padEnd(25);
1343
+ const subject = truncate3(email.subject, 43).padEnd(45);
1344
+ const date = formatDate4(email.date);
1345
+ console.log(`${unreadMarker} ${num}${from} ${subject} ${pc10.dim(date)}`);
1346
+ });
533
1347
  console.log();
534
- if (balanceSats === 0) {
535
- console.log(pc6.dim(" No credits available."));
536
- console.log(pc6.dim(" Purchase credits at https://btc.email/payments"));
1348
+ if (pagination.hasMore) {
1349
+ console.log(
1350
+ pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
1351
+ );
537
1352
  console.log();
538
1353
  }
1354
+ console.log(pc10.dim(" Read email: btcemail read <#> or btcemail read <id>"));
1355
+ console.log();
539
1356
  }
540
- function formatSats(sats) {
541
- return sats.toLocaleString();
1357
+ function truncate3(str, maxLength) {
1358
+ if (str.length <= maxLength) return str;
1359
+ return `${str.slice(0, maxLength - 1)}\u2026`;
1360
+ }
1361
+ function formatDate4(dateString) {
1362
+ const date = new Date(dateString);
1363
+ const now = /* @__PURE__ */ new Date();
1364
+ const diffMs = now.getTime() - date.getTime();
1365
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1366
+ if (diffDays === 0) {
1367
+ return date.toLocaleTimeString("en-US", {
1368
+ hour: "numeric",
1369
+ minute: "2-digit"
1370
+ });
1371
+ }
1372
+ if (diffDays < 7) {
1373
+ return date.toLocaleDateString("en-US", { weekday: "short" });
1374
+ }
1375
+ return date.toLocaleDateString("en-US", {
1376
+ month: "short",
1377
+ day: "numeric"
1378
+ });
542
1379
  }
543
1380
 
544
1381
  // src/commands/send.ts
545
- import pc7 from "picocolors";
1382
+ import ora7 from "ora";
1383
+ import pc11 from "picocolors";
1384
+ import qrcode2 from "qrcode-terminal";
546
1385
  async function sendCommand(options) {
547
1386
  if (!isAuthenticated()) {
548
- console.log(pc7.yellow("Not logged in."));
549
- console.log(pc7.dim("Run `btcemail login` to authenticate."));
1387
+ console.log(pc11.yellow("Not logged in."));
1388
+ console.log(pc11.dim("Run `btcemail login` to authenticate."));
550
1389
  process.exit(1);
551
1390
  }
552
1391
  let fromEmail = options.fromEmail;
553
1392
  if (!fromEmail) {
1393
+ const spinner2 = ora7("Fetching account info...").start();
554
1394
  const whoamiResult = await getWhoami();
1395
+ spinner2.stop();
555
1396
  if (whoamiResult.ok && whoamiResult.data.username) {
556
1397
  fromEmail = `${whoamiResult.data.username}@btc.email`;
557
1398
  } else {
@@ -562,52 +1403,102 @@ async function sendCommand(options) {
562
1403
  }
563
1404
  }
564
1405
  if (!fromEmail || !fromEmail.endsWith("@btc.email")) {
565
- console.error(pc7.red("No @btc.email address found."));
566
- console.log(pc7.dim("Register a username at https://btc.email"));
1406
+ console.error(pc11.red("No @btc.email address found."));
1407
+ console.log(pc11.dim("Register a username at https://btc.email"));
567
1408
  process.exit(1);
568
1409
  }
569
1410
  if (!options.to) {
570
- console.error(pc7.red("Recipient is required. Use --to <email>"));
1411
+ console.error(pc11.red("Recipient is required. Use --to <email>"));
571
1412
  process.exit(1);
572
1413
  }
573
1414
  if (!options.subject) {
574
- console.error(pc7.red("Subject is required. Use --subject <text>"));
1415
+ console.error(pc11.red("Subject is required. Use --subject <text>"));
575
1416
  process.exit(1);
576
1417
  }
577
1418
  if (!options.body) {
578
- console.error(pc7.red("Body is required. Use --body <text>"));
1419
+ console.error(pc11.red("Body is required. Use --body <text>"));
579
1420
  process.exit(1);
580
1421
  }
581
1422
  const toEmails = options.to.split(",").map((e) => e.trim());
582
1423
  console.log();
583
- console.log(pc7.bold("Sending Email"));
1424
+ console.log(pc11.bold("Sending Email"));
584
1425
  console.log();
585
- console.log(`${pc7.dim("From:")} ${fromEmail}`);
586
- console.log(`${pc7.dim("To:")} ${toEmails.join(", ")}`);
587
- console.log(`${pc7.dim("Subject:")} ${options.subject}`);
1426
+ console.log(`${pc11.dim("From:")} ${fromEmail}`);
1427
+ console.log(`${pc11.dim("To:")} ${toEmails.join(", ")}`);
1428
+ console.log(`${pc11.dim("Subject:")} ${options.subject}`);
588
1429
  console.log();
589
1430
  if (!options.paymentHash) {
1431
+ const spinner2 = ora7("Getting invoice...").start();
590
1432
  const invoiceResult = await getL402Invoice({
591
1433
  fromEmail,
592
1434
  toEmails
593
1435
  });
1436
+ spinner2.stop();
594
1437
  if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
595
1438
  const invoice = invoiceResult.data;
596
- console.log(pc7.yellow(`Payment required: ${invoice.amountSats} sats`));
1439
+ console.log(pc11.yellow(`Payment required: ${invoice.amountSats} sats`));
1440
+ console.log();
1441
+ console.log(pc11.bold("Scan to pay:"));
597
1442
  console.log();
598
- console.log(pc7.dim("Lightning Invoice:"));
599
- console.log(pc7.cyan(invoice.invoice));
1443
+ qrcode2.generate(invoice.invoice, { small: true }, (qr) => {
1444
+ const indentedQr = qr.split("\n").map((line) => ` ${line}`).join("\n");
1445
+ console.log(indentedQr);
1446
+ });
600
1447
  console.log();
601
- console.log(pc7.dim("Pay this invoice with your Lightning wallet, then run:"));
1448
+ console.log(pc11.dim("Lightning Invoice:"));
1449
+ console.log(pc11.cyan(invoice.invoice));
1450
+ console.log();
1451
+ if (options.wait) {
1452
+ const status = await pollForPayment({
1453
+ paymentHash: invoice.paymentHash,
1454
+ checkFn: async (hash) => {
1455
+ const result2 = await checkPaymentStatus(hash);
1456
+ if (result2.ok) {
1457
+ return { paid: result2.data.paid };
1458
+ }
1459
+ return { paid: false };
1460
+ }
1461
+ });
1462
+ if (status.paid) {
1463
+ const sendSpinner = ora7("Sending email...").start();
1464
+ const result2 = await sendEmail({
1465
+ to: toEmails,
1466
+ subject: options.subject,
1467
+ body: options.body,
1468
+ fromEmail,
1469
+ paymentHash: invoice.paymentHash
1470
+ });
1471
+ if (!result2.ok) {
1472
+ sendSpinner.fail(`Error: ${result2.error.message}`);
1473
+ process.exit(1);
1474
+ }
1475
+ sendSpinner.succeed("Email sent!");
1476
+ console.log();
1477
+ console.log(`${pc11.dim("Email ID:")} ${result2.data.emailId}`);
1478
+ if (result2.data.messageId) {
1479
+ console.log(`${pc11.dim("Message ID:")} ${result2.data.messageId}`);
1480
+ }
1481
+ console.log();
1482
+ } else {
1483
+ console.log(
1484
+ pc11.dim("Payment not confirmed. You can still pay and resend with --payment-hash.")
1485
+ );
1486
+ }
1487
+ return;
1488
+ }
1489
+ console.log(pc11.dim("Pay this invoice with your Lightning wallet, then run:"));
602
1490
  console.log(
603
- pc7.cyan(
1491
+ pc11.cyan(
604
1492
  ` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
605
1493
  )
606
1494
  );
607
1495
  console.log();
1496
+ console.log(pc11.dim("Or use --wait to automatically wait for payment."));
1497
+ console.log();
608
1498
  return;
609
1499
  }
610
1500
  }
1501
+ const spinner = ora7("Sending email...").start();
611
1502
  const result = await sendEmail({
612
1503
  to: toEmails,
613
1504
  subject: options.subject,
@@ -617,201 +1508,357 @@ async function sendCommand(options) {
617
1508
  });
618
1509
  if (!result.ok) {
619
1510
  if (result.error.code === "PAYMENT_REQUIRED") {
620
- console.error(pc7.yellow("Payment required to send this email."));
621
- console.log(pc7.dim("Use the invoice above to pay, then include --payment-hash"));
1511
+ spinner.fail("Payment required to send this email.");
1512
+ console.log(pc11.dim("Use the invoice above to pay, then include --payment-hash"));
622
1513
  } else {
623
- console.error(pc7.red(`Error: ${result.error.message}`));
1514
+ spinner.fail(`Error: ${result.error.message}`);
624
1515
  }
625
1516
  process.exit(1);
626
1517
  }
627
- console.log(pc7.green("Email sent successfully!"));
1518
+ spinner.succeed("Email sent!");
628
1519
  console.log();
629
- console.log(`${pc7.dim("Email ID:")} ${result.data.emailId}`);
1520
+ console.log(`${pc11.dim("Email ID:")} ${result.data.emailId}`);
630
1521
  if (result.data.messageId) {
631
- console.log(`${pc7.dim("Message ID:")} ${result.data.messageId}`);
1522
+ console.log(`${pc11.dim("Message ID:")} ${result.data.messageId}`);
632
1523
  }
633
1524
  console.log();
634
1525
  }
635
1526
 
636
- // src/commands/inbound.ts
637
- import pc8 from "picocolors";
638
- async function inboundPendingCommand(options) {
639
- if (!isAuthenticated()) {
640
- console.log(pc8.yellow("Not logged in."));
641
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1527
+ // src/commands/settings.ts
1528
+ import pc12 from "picocolors";
1529
+ async function settingsShowCommand(options = {}) {
1530
+ const token = getToken();
1531
+ if (!token) {
1532
+ console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
642
1533
  process.exit(1);
643
1534
  }
644
- const result = await getPendingEmails({
645
- limit: options.limit || 20
646
- });
1535
+ const result = await getSettings(options.username);
647
1536
  if (!result.ok) {
648
- console.error(pc8.red(`Error: ${result.error.message}`));
1537
+ console.error(pc12.red(`Error: ${result.error.message}`));
649
1538
  process.exit(1);
650
1539
  }
651
- const { data: emails, pagination } = result.data;
1540
+ const { username, usernames, settings: settings2 } = result.data;
652
1541
  if (options.json) {
653
- console.log(JSON.stringify({ emails, pagination }, null, 2));
1542
+ console.log(JSON.stringify({ username, usernames, settings: settings2 }, null, 2));
654
1543
  return;
655
1544
  }
656
1545
  console.log();
657
- console.log(pc8.bold(`Pending Emails (${pagination.total} awaiting payment)`));
658
- console.log();
659
- if (emails.length === 0) {
660
- console.log(pc8.dim(" No pending emails."));
661
- console.log();
662
- return;
1546
+ console.log(pc12.bold("Settings for ") + pc12.cyan(`${username}@btc.email`));
1547
+ if (usernames.length > 1) {
1548
+ console.log(pc12.dim(`(You have ${usernames.length} addresses: ${usernames.join(", ")})`));
663
1549
  }
1550
+ console.log();
1551
+ console.log(pc12.dim("Pricing:"));
1552
+ console.log(` Email price: ${pc12.yellow(settings2.default_price_sats.toLocaleString())} sats`);
1553
+ console.log();
1554
+ console.log(pc12.dim("Toggles:"));
664
1555
  console.log(
665
- ` ${pc8.dim("ID".padEnd(12))} ${pc8.dim("From".padEnd(30))} ${pc8.dim("Subject".padEnd(30))} ${pc8.dim("Sats")}`
1556
+ ` Require payment: ${settings2.require_payment_from_unknown ? pc12.green("on") : pc12.red("off")}`
1557
+ );
1558
+ console.log(
1559
+ ` Auto-whitelist: ${settings2.auto_whitelist_on_payment ? pc12.green("on") : pc12.red("off")}`
666
1560
  );
667
- console.log(pc8.dim(" " + "-".repeat(85)));
668
- for (const email of emails) {
669
- const id = email.id.slice(0, 10).padEnd(12);
670
- const from = truncate2(email.from.name || email.from.email, 28).padEnd(30);
671
- const subject = truncate2(email.subject, 28).padEnd(30);
672
- const sats = pc8.yellow(String(email.amountSats));
673
- console.log(` ${id} ${from} ${subject} ${sats}`);
674
- }
675
1561
  console.log();
676
- if (pagination.hasMore) {
677
- console.log(
678
- pc8.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
679
- );
680
- console.log();
1562
+ console.log(pc12.dim("Auto-reply:"));
1563
+ if (settings2.custom_auto_reply_body) {
1564
+ console.log(` ${pc12.italic(settings2.custom_auto_reply_body)}`);
1565
+ } else {
1566
+ console.log(` ${pc12.dim("(default message)")}`);
681
1567
  }
682
- console.log(pc8.dim(" Use `btcemail inbound view <id>` to see details and payment invoice."));
683
1568
  console.log();
684
1569
  }
685
- async function inboundViewCommand(id, options) {
686
- if (!isAuthenticated()) {
687
- console.log(pc8.yellow("Not logged in."));
688
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1570
+ async function settingsPricingCommand(sats, options = {}) {
1571
+ const token = getToken();
1572
+ if (!token) {
1573
+ console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
689
1574
  process.exit(1);
690
1575
  }
691
- const result = await getPendingEmail(id);
1576
+ const price = parseInt(sats, 10);
1577
+ if (Number.isNaN(price) || price < 0) {
1578
+ console.error(pc12.red("Invalid price. Must be a non-negative number."));
1579
+ process.exit(1);
1580
+ }
1581
+ const result = await updateSettings({ default_price_sats: price }, options.username);
692
1582
  if (!result.ok) {
693
- if (result.error.code === "NOT_FOUND") {
694
- console.error(pc8.red(`Pending email not found: ${id}`));
695
- } else {
696
- console.error(pc8.red(`Error: ${result.error.message}`));
697
- }
1583
+ console.error(pc12.red(`Error: ${result.error.message}`));
698
1584
  process.exit(1);
699
1585
  }
700
- const email = result.data;
701
1586
  if (options.json) {
702
- console.log(JSON.stringify(email, null, 2));
1587
+ console.log(JSON.stringify({ success: true, price }, null, 2));
703
1588
  return;
704
1589
  }
705
- console.log();
706
- console.log(pc8.bold(email.subject || "(No subject)"));
707
- console.log();
708
- console.log(`${pc8.dim("From:")} ${formatAddress2(email.from)}`);
709
- console.log(`${pc8.dim("Date:")} ${formatFullDate2(email.createdAt)}`);
710
- console.log(`${pc8.dim("ID:")} ${email.id}`);
711
- console.log();
712
- console.log(pc8.yellow(`Payment Required: ${email.amountSats} sats`));
713
- console.log();
714
- if (email.paymentHash) {
715
- console.log(pc8.dim("To receive this email, pay the invoice and run:"));
716
- console.log(pc8.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
1590
+ console.log(pc12.green(`Email price set to ${pc12.yellow(price.toLocaleString())} sats`));
1591
+ }
1592
+ async function settingsRequirePaymentCommand(value, options = {}) {
1593
+ const token = getToken();
1594
+ if (!token) {
1595
+ console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
1596
+ process.exit(1);
1597
+ }
1598
+ const enabled = value.toLowerCase() === "on" || value === "true" || value === "1";
1599
+ const disabled = value.toLowerCase() === "off" || value === "false" || value === "0";
1600
+ if (!enabled && !disabled) {
1601
+ console.error(pc12.red("Invalid value. Use 'on' or 'off'."));
1602
+ process.exit(1);
1603
+ }
1604
+ const result = await updateSettings({ require_payment_from_unknown: enabled }, options.username);
1605
+ if (!result.ok) {
1606
+ console.error(pc12.red(`Error: ${result.error.message}`));
1607
+ process.exit(1);
1608
+ }
1609
+ console.log(pc12.green(`Require payment: ${enabled ? pc12.green("on") : pc12.red("off")}`));
1610
+ }
1611
+ async function settingsAutoWhitelistCommand(value, options = {}) {
1612
+ const token = getToken();
1613
+ if (!token) {
1614
+ console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
1615
+ process.exit(1);
1616
+ }
1617
+ const enabled = value.toLowerCase() === "on" || value === "true" || value === "1";
1618
+ const disabled = value.toLowerCase() === "off" || value === "false" || value === "0";
1619
+ if (!enabled && !disabled) {
1620
+ console.error(pc12.red("Invalid value. Use 'on' or 'off'."));
1621
+ process.exit(1);
1622
+ }
1623
+ const result = await updateSettings({ auto_whitelist_on_payment: enabled }, options.username);
1624
+ if (!result.ok) {
1625
+ console.error(pc12.red(`Error: ${result.error.message}`));
1626
+ process.exit(1);
1627
+ }
1628
+ console.log(pc12.green(`Auto-whitelist: ${enabled ? pc12.green("on") : pc12.red("off")}`));
1629
+ }
1630
+ async function settingsAutoReplyCommand(message, options = {}) {
1631
+ const token = getToken();
1632
+ if (!token) {
1633
+ console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
1634
+ process.exit(1);
1635
+ }
1636
+ const newMessage = options.clear ? null : message || null;
1637
+ const result = await updateSettings({ custom_auto_reply_body: newMessage }, options.username);
1638
+ if (!result.ok) {
1639
+ console.error(pc12.red(`Error: ${result.error.message}`));
1640
+ process.exit(1);
1641
+ }
1642
+ if (options.clear) {
1643
+ console.log(pc12.green("Auto-reply reset to default message"));
1644
+ } else if (newMessage) {
1645
+ console.log(pc12.green("Auto-reply message updated"));
717
1646
  } else {
718
- console.log(pc8.dim("Payment information not available."));
1647
+ console.log(pc12.yellow("No message provided. Use --clear to reset to default."));
719
1648
  }
720
- console.log();
721
- console.log(pc8.dim("-".repeat(60)));
722
- console.log();
723
- console.log(pc8.dim("Preview (full content available after payment):"));
724
- console.log();
725
- console.log(truncate2(email.bodyText || email.body, 200));
726
- console.log();
727
1649
  }
728
- async function inboundPayCommand(id, options) {
729
- if (!isAuthenticated()) {
730
- console.log(pc8.yellow("Not logged in."));
731
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1650
+
1651
+ // src/commands/stats.ts
1652
+ import pc13 from "picocolors";
1653
+ async function statsCommand(options = {}) {
1654
+ const token = getToken();
1655
+ if (!token) {
1656
+ console.error(pc13.red("Not logged in. Run 'btcemail login' first."));
732
1657
  process.exit(1);
733
1658
  }
734
- if (!options.paymentHash) {
735
- console.error(pc8.red("Payment hash is required. Use --payment-hash <preimage>"));
1659
+ const result = await getStats();
1660
+ if (!result.ok) {
1661
+ console.error(pc13.red(`Error: ${result.error.message}`));
736
1662
  process.exit(1);
737
1663
  }
1664
+ const { totalReceived, totalSent, pendingCount, addressCount, usernames } = result.data;
1665
+ if (options.json) {
1666
+ console.log(JSON.stringify(result.data, null, 2));
1667
+ return;
1668
+ }
738
1669
  console.log();
739
- console.log(pc8.dim("Verifying payment..."));
740
- const result = await payForEmail(id, options.paymentHash);
741
- if (!result.ok) {
742
- if (result.error.code === "PAYMENT_INVALID") {
743
- console.error(pc8.red("Payment verification failed."));
744
- console.log(pc8.dim("Make sure you paid the correct invoice and provided the preimage."));
745
- } else if (result.error.code === "NOT_FOUND") {
746
- console.error(pc8.red(`Pending email not found or already delivered: ${id}`));
747
- } else {
748
- console.error(pc8.red(`Error: ${result.error.message}`));
1670
+ console.log(pc13.bold("Dashboard Statistics"));
1671
+ console.log();
1672
+ console.log(` Total Received: ${pc13.green(`+${totalReceived.toLocaleString()}`)} sats`);
1673
+ console.log(` Total Sent: ${pc13.yellow(`-${totalSent.toLocaleString()}`)} sats`);
1674
+ console.log(` Pending Emails: ${pendingCount > 0 ? pc13.yellow(pendingCount) : pc13.dim("0")}`);
1675
+ console.log(` Active Addresses: ${addressCount}`);
1676
+ console.log();
1677
+ if (usernames.length > 0) {
1678
+ console.log(pc13.dim(" Your addresses:"));
1679
+ for (const username of usernames) {
1680
+ console.log(` ${pc13.cyan(username)}@btc.email`);
749
1681
  }
1682
+ console.log();
1683
+ }
1684
+ if (pendingCount > 0) {
1685
+ console.log(pc13.dim(" View pending emails: btcemail inbound pending"));
1686
+ console.log();
1687
+ }
1688
+ }
1689
+
1690
+ // src/commands/whitelist.ts
1691
+ import pc14 from "picocolors";
1692
+ async function whitelistListCommand(options = {}) {
1693
+ const token = getToken();
1694
+ if (!token) {
1695
+ console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
750
1696
  process.exit(1);
751
1697
  }
1698
+ const result = await getWhitelist(options.username);
1699
+ if (!result.ok) {
1700
+ console.error(pc14.red(`Error: ${result.error.message}`));
1701
+ process.exit(1);
1702
+ }
1703
+ const { username, entries } = result.data;
1704
+ if (options.json) {
1705
+ console.log(JSON.stringify({ username, entries }, null, 2));
1706
+ return;
1707
+ }
752
1708
  console.log();
753
- console.log(pc8.green("Payment verified! Email delivered."));
1709
+ console.log(pc14.bold("Whitelist for ") + pc14.cyan(`${username}@btc.email`));
754
1710
  console.log();
755
- console.log(`${pc8.dim("Email ID:")} ${result.data.emailId}`);
756
- console.log(`${pc8.dim("Status:")} ${result.data.status}`);
1711
+ if (entries.length === 0) {
1712
+ console.log(pc14.dim(" No entries in whitelist."));
1713
+ console.log();
1714
+ console.log(pc14.dim(" Add entries with: btcemail whitelist add <email>"));
1715
+ console.log(pc14.dim(" Or for domains: btcemail whitelist add @domain.com"));
1716
+ console.log();
1717
+ return;
1718
+ }
1719
+ console.log(pc14.dim(` ${entries.length} ${entries.length === 1 ? "entry" : "entries"}:`));
757
1720
  console.log();
758
- console.log(pc8.dim("Run `btcemail read " + result.data.emailId + "` to read the email."));
1721
+ for (const entry of entries) {
1722
+ const value = entry.sender_email || `@${entry.sender_domain}`;
1723
+ const via = entry.added_via !== "manual" ? pc14.dim(` (via ${entry.added_via})`) : "";
1724
+ const note = entry.note ? pc14.dim(` - ${entry.note}`) : "";
1725
+ console.log(` ${pc14.green(value)}${via}${note}`);
1726
+ console.log(pc14.dim(` ID: ${entry.id}`));
1727
+ }
759
1728
  console.log();
760
1729
  }
761
- function truncate2(str, maxLength) {
762
- if (str.length <= maxLength) return str;
763
- return str.slice(0, maxLength - 1) + "\u2026";
1730
+ async function whitelistAddCommand(entry, options = {}) {
1731
+ const token = getToken();
1732
+ if (!token) {
1733
+ console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
1734
+ process.exit(1);
1735
+ }
1736
+ const isDomain = entry.startsWith("@");
1737
+ const payload = isDomain ? { domain: entry.slice(1), note: options.note } : { email: entry, note: options.note };
1738
+ const result = await addToWhitelist(payload, options.username);
1739
+ if (!result.ok) {
1740
+ console.error(pc14.red(`Error: ${result.error.message}`));
1741
+ process.exit(1);
1742
+ }
1743
+ if (options.json) {
1744
+ console.log(JSON.stringify({ success: true, entry: result.data.entry }, null, 2));
1745
+ return;
1746
+ }
1747
+ console.log(pc14.green(`Added to whitelist: ${pc14.cyan(entry)}`));
764
1748
  }
765
- function formatAddress2(addr) {
766
- if (addr.name) {
767
- return `${addr.name} <${addr.email}>`;
1749
+ async function whitelistRemoveCommand(id, options = {}) {
1750
+ const token = getToken();
1751
+ if (!token) {
1752
+ console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
1753
+ process.exit(1);
768
1754
  }
769
- return addr.email;
1755
+ const result = await removeFromWhitelist(id);
1756
+ if (!result.ok) {
1757
+ console.error(pc14.red(`Error: ${result.error.message}`));
1758
+ process.exit(1);
1759
+ }
1760
+ if (options.json) {
1761
+ console.log(JSON.stringify({ success: true }, null, 2));
1762
+ return;
1763
+ }
1764
+ console.log(pc14.green("Removed from whitelist"));
770
1765
  }
771
- function formatFullDate2(dateString) {
772
- const date = new Date(dateString);
773
- return date.toLocaleString("en-US", {
774
- weekday: "short",
775
- year: "numeric",
776
- month: "short",
777
- day: "numeric",
778
- hour: "numeric",
779
- minute: "2-digit"
780
- });
1766
+
1767
+ // src/commands/whoami.ts
1768
+ import ora8 from "ora";
1769
+ import pc15 from "picocolors";
1770
+ async function whoamiCommand() {
1771
+ if (!isAuthenticated()) {
1772
+ console.log(pc15.yellow("Not logged in."));
1773
+ console.log(pc15.dim("Run `btcemail login` to authenticate."));
1774
+ process.exit(1);
1775
+ }
1776
+ const auth = getAuth();
1777
+ if (!auth) {
1778
+ console.log(pc15.yellow("Not logged in."));
1779
+ process.exit(1);
1780
+ }
1781
+ const spinner = ora8("Fetching account info...").start();
1782
+ const result = await getWhoami();
1783
+ spinner.stop();
1784
+ if (result.ok) {
1785
+ console.log();
1786
+ console.log(pc15.bold("Current User"));
1787
+ console.log();
1788
+ console.log(` ${pc15.dim("Email:")} ${result.data.email}`);
1789
+ if (result.data.username) {
1790
+ console.log(` ${pc15.dim("Username:")} ${result.data.username}@btc.email`);
1791
+ }
1792
+ console.log(` ${pc15.dim("User ID:")} ${result.data.id}`);
1793
+ const expiryInfo = getTokenExpiryInfo();
1794
+ if (expiryInfo.expiresIn) {
1795
+ const expiryColor = isTokenExpiringSoon() ? pc15.yellow : pc15.dim;
1796
+ console.log(` ${pc15.dim("Session:")} ${expiryColor(`expires in ${expiryInfo.expiresIn}`)}`);
1797
+ }
1798
+ console.log();
1799
+ if (isTokenExpiringSoon()) {
1800
+ console.log(pc15.yellow("Session expiring soon. Run `btcemail login` to refresh."));
1801
+ console.log();
1802
+ }
1803
+ } else {
1804
+ console.log();
1805
+ console.log(pc15.bold("Current User") + pc15.dim(" (cached)"));
1806
+ console.log();
1807
+ console.log(` ${pc15.dim("Email:")} ${auth.email}`);
1808
+ console.log(` ${pc15.dim("User ID:")} ${auth.userId}`);
1809
+ console.log();
1810
+ if (result.error.code === "UNAUTHENTICATED" || result.error.code === "UNAUTHORIZED") {
1811
+ console.log(pc15.yellow("Session expired. Run `btcemail login` to re-authenticate."));
1812
+ }
1813
+ }
781
1814
  }
782
1815
 
783
1816
  // src/index.ts
784
1817
  var program = new Command();
785
- program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.1.0");
1818
+ program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.4.0");
786
1819
  program.command("login").description("Authenticate with btc.email (opens browser)").action(loginCommand);
787
1820
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
788
1821
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
789
- program.command("list").description("List emails").option("-f, --folder <folder>", "Folder to list (inbox, sent, drafts)", "inbox").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
1822
+ program.command("list").alias("ls").description("List emails (numbered for quick access)").option("-f, --folder <folder>", "Folder to list (inbox, sent, drafts)", "inbox").option("-l, --limit <number>", "Number of emails to show", "20").option("-p, --page <number>", "Page number", "1").option("--json", "Output as JSON").action(async (options) => {
790
1823
  await listCommand({
791
1824
  folder: options.folder,
792
1825
  limit: parseInt(options.limit, 10),
1826
+ page: parseInt(options.page, 10),
793
1827
  json: options.json
794
1828
  });
795
1829
  });
796
- program.command("read <id>").description("Read an email by ID").option("--json", "Output as JSON").action(async (id, options) => {
1830
+ program.command("read <id>").alias("r").description("Read an email by # (from list) or ID").option("--json", "Output as JSON").action(async (id, options) => {
797
1831
  await readCommand(id, { json: options.json });
798
1832
  });
799
- program.command("send").description("Send an email").option("-t, --to <emails>", "Recipient email(s), comma-separated").option("-s, --subject <subject>", "Email subject").option("-b, --body <body>", "Email body").option("-f, --from <email>", "From email (@btc.email address)").option("--payment-hash <hash>", "Payment hash/preimage for L402").action(async (options) => {
1833
+ program.command("search <query>").alias("s").description("Search emails by subject, body, or sender").option("-l, --limit <number>", "Number of results to show", "20").option("--json", "Output as JSON").action(async (query, options) => {
1834
+ await searchCommand(query, {
1835
+ limit: parseInt(options.limit, 10),
1836
+ json: options.json
1837
+ });
1838
+ });
1839
+ program.command("send").description("Send an email (with Lightning payment)").option("-t, --to <emails>", "Recipient email(s), comma-separated").option("-s, --subject <subject>", "Email subject").option("-b, --body <body>", "Email body").option("-f, --from <email>", "From email (@btc.email address)").option("--payment-hash <hash>", "Payment preimage for L402").option("-w, --wait", "Wait for payment confirmation").action(async (options) => {
800
1840
  await sendCommand({
801
1841
  to: options.to,
802
1842
  subject: options.subject,
803
1843
  body: options.body,
804
1844
  fromEmail: options.from,
805
- paymentHash: options.paymentHash
1845
+ paymentHash: options.paymentHash,
1846
+ wait: options.wait
806
1847
  });
807
1848
  });
808
- var inbound = program.command("inbound").description("Manage pending emails requiring payment");
809
- inbound.command("pending").description("List pending emails awaiting payment").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
1849
+ var inbound = program.command("inbound").description("Manage inbound emails (pending and delivered)");
1850
+ inbound.command("pending").alias("p").description("List pending emails awaiting payment").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
810
1851
  await inboundPendingCommand({
811
1852
  limit: parseInt(options.limit, 10),
812
1853
  json: options.json
813
1854
  });
814
1855
  });
1856
+ inbound.command("delivered").alias("d").description("List delivered emails (paid by senders)").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
1857
+ await inboundDeliveredCommand({
1858
+ limit: parseInt(options.limit, 10),
1859
+ json: options.json
1860
+ });
1861
+ });
815
1862
  inbound.command("view <id>").description("View pending email details and payment info").option("--json", "Output as JSON").action(async (id, options) => {
816
1863
  await inboundViewCommand(id, { json: options.json });
817
1864
  });
@@ -819,11 +1866,93 @@ inbound.command("pay <id>").description("Pay for a pending email").requiredOptio
819
1866
  await inboundPayCommand(id, { paymentHash: options.paymentHash });
820
1867
  });
821
1868
  var credits = program.command("credits").description("Manage credits");
822
- credits.command("balance").description("Show credit balance").action(creditsBalanceCommand);
1869
+ credits.command("balance").alias("bal").description("Show credit balance").action(creditsBalanceCommand);
1870
+ credits.command("history").description("Show transaction history").option("-l, --limit <number>", "Number of transactions to show", "20").option("-t, --type <type>", "Filter by type (topup, email_sent, email_received, refund, bonus)").option("--json", "Output as JSON").action(async (options) => {
1871
+ await creditsHistoryCommand({
1872
+ limit: parseInt(options.limit, 10),
1873
+ type: options.type,
1874
+ json: options.json
1875
+ });
1876
+ });
1877
+ credits.command("purchase").alias("buy").description("Purchase credits with Lightning").option("-o, --option <index>", "Purchase option index (0-3)").option("-w, --wait", "Wait for payment confirmation").option("--json", "Output as JSON").action(async (options) => {
1878
+ await creditsPurchaseCommand({
1879
+ option: options.option !== void 0 ? parseInt(options.option, 10) : void 0,
1880
+ json: options.json,
1881
+ wait: options.wait
1882
+ });
1883
+ });
1884
+ var settings = program.command("settings").description("Manage email pricing and preferences");
1885
+ settings.command("show").description("Show current settings").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
1886
+ await settingsShowCommand({ username: options.username, json: options.json });
1887
+ });
1888
+ settings.command("pricing <sats>").description("Set email price in sats").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (sats, options) => {
1889
+ await settingsPricingCommand(sats, { username: options.username, json: options.json });
1890
+ });
1891
+ settings.command("require-payment <on|off>").description("Toggle payment requirement for unknown senders").option("-u, --username <name>", "Specific @btc.email address").action(async (value, options) => {
1892
+ await settingsRequirePaymentCommand(value, { username: options.username });
1893
+ });
1894
+ settings.command("auto-whitelist <on|off>").description("Toggle auto-whitelist after payment").option("-u, --username <name>", "Specific @btc.email address").action(async (value, options) => {
1895
+ await settingsAutoWhitelistCommand(value, { username: options.username });
1896
+ });
1897
+ settings.command("auto-reply [message]").description("Set custom auto-reply message").option("-u, --username <name>", "Specific @btc.email address").option("--clear", "Reset to default message").action(async (message, options) => {
1898
+ await settingsAutoReplyCommand(message, { username: options.username, clear: options.clear });
1899
+ });
1900
+ settings.action(async () => {
1901
+ await settingsShowCommand({});
1902
+ });
1903
+ var whitelist = program.command("whitelist").alias("wl").description("Manage senders who can email you for free");
1904
+ whitelist.command("list").description("List whitelist entries").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
1905
+ await whitelistListCommand({ username: options.username, json: options.json });
1906
+ });
1907
+ whitelist.command("add <entry>").description("Add email or @domain to whitelist").option("-u, --username <name>", "Specific @btc.email address").option("-n, --note <note>", "Optional note").option("--json", "Output as JSON").action(async (entry, options) => {
1908
+ await whitelistAddCommand(entry, {
1909
+ username: options.username,
1910
+ note: options.note,
1911
+ json: options.json
1912
+ });
1913
+ });
1914
+ whitelist.command("remove <id>").description("Remove entry from whitelist by ID").option("--json", "Output as JSON").action(async (id, options) => {
1915
+ await whitelistRemoveCommand(id, { json: options.json });
1916
+ });
1917
+ whitelist.action(async () => {
1918
+ await whitelistListCommand({});
1919
+ });
1920
+ var blocklist = program.command("blocklist").alias("bl").description("Manage blocked senders");
1921
+ blocklist.command("list").description("List blocklist entries").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
1922
+ await blocklistListCommand({ username: options.username, json: options.json });
1923
+ });
1924
+ blocklist.command("add <entry>").description("Add email or @domain to blocklist").option("-u, --username <name>", "Specific @btc.email address").option("-r, --reason <reason>", "Optional reason").option("--json", "Output as JSON").action(async (entry, options) => {
1925
+ await blocklistAddCommand(entry, {
1926
+ username: options.username,
1927
+ reason: options.reason,
1928
+ json: options.json
1929
+ });
1930
+ });
1931
+ blocklist.command("remove <id>").description("Remove entry from blocklist by ID").option("--json", "Output as JSON").action(async (id, options) => {
1932
+ await blocklistRemoveCommand(id, { json: options.json });
1933
+ });
1934
+ blocklist.action(async () => {
1935
+ await blocklistListCommand({});
1936
+ });
1937
+ var network = program.command("network").description("Manage Lightning network mode");
1938
+ network.command("show").description("Show current network mode").option("--json", "Output as JSON").action(async (options) => {
1939
+ await networkShowCommand({ json: options.json });
1940
+ });
1941
+ network.command("testnet").description("Switch to testnet (fake Bitcoin)").option("--json", "Output as JSON").action(async (options) => {
1942
+ await networkSwitchCommand("testnet", { json: options.json });
1943
+ });
1944
+ network.command("mainnet").description("Switch to mainnet (real Bitcoin)").option("--json", "Output as JSON").action(async (options) => {
1945
+ await networkSwitchCommand("mainnet", { json: options.json });
1946
+ });
1947
+ network.action(async () => {
1948
+ await networkShowCommand({});
1949
+ });
1950
+ program.command("stats").description("Show dashboard statistics").option("--json", "Output as JSON").action(async (options) => {
1951
+ await statsCommand({ json: options.json });
1952
+ });
823
1953
  program.action(() => {
824
1954
  console.log();
825
- console.log(pc9.bold("btc.email CLI"));
826
- console.log(pc9.dim("Spam-free email powered by Bitcoin Lightning"));
1955
+ console.log(pc16.bold("btc.email CLI") + pc16.dim(" - Spam-free email powered by Bitcoin Lightning"));
827
1956
  console.log();
828
1957
  program.outputHelp();
829
1958
  });