@btcemail/cli 0.1.0 → 0.3.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,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc9 from "picocolors";
5
+ import pc11 from "picocolors";
6
6
 
7
7
  // src/commands/login.ts
8
8
  import { randomUUID } from "crypto";
@@ -200,14 +200,14 @@ async function logoutCommand() {
200
200
 
201
201
  // src/commands/whoami.ts
202
202
  import pc3 from "picocolors";
203
+ import ora2 from "ora";
203
204
 
204
205
  // src/api.ts
205
206
  async function apiRequest(endpoint, options = {}) {
206
207
  const token = getToken();
207
208
  const baseUrl = getApiBaseUrl();
208
209
  const headers = {
209
- "Content-Type": "application/json",
210
- ...options.headers
210
+ "Content-Type": "application/json"
211
211
  };
212
212
  if (token) {
213
213
  headers.Authorization = `Bearer ${token}`;
@@ -285,15 +285,140 @@ async function sendEmail(options) {
285
285
  });
286
286
  }
287
287
  async function getL402Invoice(options) {
288
- return apiRequest("/l402/invoice", {
288
+ const v1BaseUrl = getApiBaseUrl();
289
+ const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
290
+ const token = getToken();
291
+ const headers = {
292
+ "Content-Type": "application/json"
293
+ };
294
+ if (token) {
295
+ headers.Authorization = `Bearer ${token}`;
296
+ }
297
+ try {
298
+ const response = await fetch(`${rootApiUrl}/l402/invoice`, {
299
+ method: "POST",
300
+ headers,
301
+ body: JSON.stringify({
302
+ sender_email: options.fromEmail,
303
+ recipient_emails: options.toEmails,
304
+ amount_sats: options.amountSats
305
+ })
306
+ });
307
+ const json = await response.json();
308
+ if (!response.ok || json.success === false) {
309
+ return {
310
+ ok: false,
311
+ error: json.error || {
312
+ code: "UNKNOWN_ERROR",
313
+ message: `Request failed with status ${response.status}`
314
+ }
315
+ };
316
+ }
317
+ return { ok: true, data: json.data };
318
+ } catch (error) {
319
+ return {
320
+ ok: false,
321
+ error: {
322
+ code: "NETWORK_ERROR",
323
+ message: error instanceof Error ? error.message : "Network request failed"
324
+ }
325
+ };
326
+ }
327
+ }
328
+ async function getDeliveredEmails(options = {}) {
329
+ const params = new URLSearchParams();
330
+ if (options.limit) params.set("limit", String(options.limit));
331
+ if (options.offset) params.set("offset", String(options.offset));
332
+ return apiRequest(`/inbound/delivered?${params}`);
333
+ }
334
+ async function getCreditTransactions(options = {}) {
335
+ const params = new URLSearchParams();
336
+ if (options.limit) params.set("limit", String(options.limit));
337
+ if (options.offset) params.set("offset", String(options.offset));
338
+ if (options.type) params.set("type", options.type);
339
+ return apiRequest(`/credits/transactions?${params}`);
340
+ }
341
+ async function purchaseCredits(optionIndex) {
342
+ return apiRequest("/credits/purchase", {
289
343
  method: "POST",
290
- body: JSON.stringify({
291
- sender_email: options.fromEmail,
292
- recipient_emails: options.toEmails,
293
- amount_sats: options.amountSats
294
- })
344
+ body: JSON.stringify({ optionIndex })
295
345
  });
296
346
  }
347
+ async function checkPaymentStatus(paymentHash) {
348
+ const v1BaseUrl = getApiBaseUrl();
349
+ const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
350
+ const token = getToken();
351
+ const headers = {
352
+ "Content-Type": "application/json"
353
+ };
354
+ if (token) {
355
+ headers.Authorization = `Bearer ${token}`;
356
+ }
357
+ try {
358
+ const response = await fetch(`${rootApiUrl}/lightning/webhook?payment_hash=${paymentHash}`, {
359
+ headers
360
+ });
361
+ const json = await response.json();
362
+ if (!response.ok || json.success === false) {
363
+ return {
364
+ ok: false,
365
+ error: json.error || {
366
+ code: "UNKNOWN_ERROR",
367
+ message: `Request failed with status ${response.status}`
368
+ }
369
+ };
370
+ }
371
+ return {
372
+ ok: true,
373
+ data: {
374
+ paid: json.paid || false,
375
+ delivered: json.delivered,
376
+ status: json.status || json.state,
377
+ pendingEmailId: json.pendingEmailId
378
+ }
379
+ };
380
+ } catch (error) {
381
+ return {
382
+ ok: false,
383
+ error: {
384
+ code: "NETWORK_ERROR",
385
+ message: error instanceof Error ? error.message : "Network request failed"
386
+ }
387
+ };
388
+ }
389
+ }
390
+ function getCreditPurchaseOptions() {
391
+ return [
392
+ {
393
+ amountSats: 1e3,
394
+ priceSats: 1e3,
395
+ bonusSats: 0,
396
+ label: "1,000 sats",
397
+ description: "Good for ~10 emails"
398
+ },
399
+ {
400
+ amountSats: 5e3,
401
+ priceSats: 4500,
402
+ bonusSats: 500,
403
+ label: "5,000 sats",
404
+ description: "Good for ~50 emails (+10% bonus)"
405
+ },
406
+ {
407
+ amountSats: 1e4,
408
+ priceSats: 8500,
409
+ bonusSats: 1500,
410
+ label: "10,000 sats",
411
+ description: "Good for ~100 emails (+15% bonus)"
412
+ },
413
+ {
414
+ amountSats: 5e4,
415
+ priceSats: 4e4,
416
+ bonusSats: 1e4,
417
+ label: "50,000 sats",
418
+ description: "Good for ~500 emails (+20% bonus)"
419
+ }
420
+ ];
421
+ }
297
422
 
298
423
  // src/commands/whoami.ts
299
424
  async function whoamiCommand() {
@@ -307,14 +432,16 @@ async function whoamiCommand() {
307
432
  console.log(pc3.yellow("Not logged in."));
308
433
  process.exit(1);
309
434
  }
435
+ const spinner = ora2("Fetching account info...").start();
310
436
  const result = await getWhoami();
437
+ spinner.stop();
311
438
  if (result.ok) {
312
439
  console.log();
313
440
  console.log(pc3.bold("Current User"));
314
441
  console.log();
315
442
  console.log(` ${pc3.dim("Email:")} ${result.data.email}`);
316
443
  if (result.data.username) {
317
- console.log(` ${pc3.dim("Username:")} ${result.data.username}`);
444
+ console.log(` ${pc3.dim("Username:")} ${result.data.username}@btc.email`);
318
445
  }
319
446
  console.log(` ${pc3.dim("User ID:")} ${result.data.id}`);
320
447
  const expiryInfo = getTokenExpiryInfo();
@@ -329,7 +456,7 @@ async function whoamiCommand() {
329
456
  }
330
457
  } else {
331
458
  console.log();
332
- console.log(pc3.bold("Current User (cached)"));
459
+ console.log(pc3.bold("Current User") + pc3.dim(" (cached)"));
333
460
  console.log();
334
461
  console.log(` ${pc3.dim("Email:")} ${auth.email}`);
335
462
  console.log(` ${pc3.dim("User ID:")} ${auth.userId}`);
@@ -342,29 +469,78 @@ async function whoamiCommand() {
342
469
 
343
470
  // src/commands/list.ts
344
471
  import pc4 from "picocolors";
472
+ import ora3 from "ora";
473
+
474
+ // src/utils/cache.ts
475
+ var CACHE_KEY = "emailCache";
476
+ var CACHE_TTL_MS = 30 * 60 * 1e3;
477
+ function cacheEmailList(folder, emails) {
478
+ config.set(CACHE_KEY, {
479
+ folder,
480
+ emails,
481
+ timestamp: Date.now()
482
+ });
483
+ }
484
+ function getCachedEmailList(folder) {
485
+ const cache = config.get(CACHE_KEY);
486
+ if (!cache) return null;
487
+ if (Date.now() - cache.timestamp > CACHE_TTL_MS) {
488
+ config.delete(CACHE_KEY);
489
+ return null;
490
+ }
491
+ if (folder && cache.folder !== folder) {
492
+ return null;
493
+ }
494
+ return cache;
495
+ }
496
+ function getEmailIdByIndex(index) {
497
+ const cache = getCachedEmailList();
498
+ if (!cache) return null;
499
+ const idx = index - 1;
500
+ if (idx < 0 || idx >= cache.emails.length) {
501
+ return null;
502
+ }
503
+ return cache.emails[idx].id;
504
+ }
505
+ function isNumericId(id) {
506
+ return /^\d+$/.test(id);
507
+ }
508
+
509
+ // src/commands/list.ts
345
510
  async function listCommand(options) {
346
511
  if (!isAuthenticated()) {
347
512
  console.log(pc4.yellow("Not logged in."));
348
513
  console.log(pc4.dim("Run `btcemail login` to authenticate."));
349
514
  process.exit(1);
350
515
  }
516
+ const folder = options.folder || "inbox";
517
+ const limit = options.limit || 20;
518
+ const page = options.page || 1;
519
+ const offset = (page - 1) * limit;
520
+ const spinner = ora3("Loading emails...").start();
351
521
  const result = await getEmails({
352
- folder: options.folder || "inbox",
353
- limit: options.limit || 20
522
+ folder,
523
+ limit,
524
+ offset
354
525
  });
355
526
  if (!result.ok) {
356
- console.error(pc4.red(`Error: ${result.error.message}`));
527
+ spinner.fail(`Error: ${result.error.message}`);
357
528
  process.exit(1);
358
529
  }
359
530
  const { data: emails, pagination } = result.data;
531
+ spinner.stop();
532
+ const cachedEmails = emails.map((e) => ({
533
+ id: e.id,
534
+ subject: e.subject,
535
+ from: e.from.name || e.from.email
536
+ }));
537
+ cacheEmailList(folder, cachedEmails);
360
538
  if (options.json) {
361
539
  console.log(JSON.stringify({ emails, pagination }, null, 2));
362
540
  return;
363
541
  }
364
542
  console.log();
365
- console.log(
366
- pc4.bold(`${options.folder || "Inbox"} (${pagination.total} emails)`)
367
- );
543
+ console.log(pc4.bold(`${capitalize(folder)} (${pagination.total} emails)`));
368
544
  console.log();
369
545
  if (emails.length === 0) {
370
546
  console.log(pc4.dim(" No emails found."));
@@ -372,26 +548,34 @@ async function listCommand(options) {
372
548
  return;
373
549
  }
374
550
  console.log(
375
- ` ${pc4.dim("ID".padEnd(12))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(40))} ${pc4.dim("Date")}`
551
+ ` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(45))} ${pc4.dim("Date")}`
376
552
  );
377
- console.log(pc4.dim(" " + "-".repeat(90)));
378
- for (const email of emails) {
379
- const unreadMarker = email.unread ? pc4.cyan("*") : " ";
380
- const id = email.id.slice(0, 10).padEnd(12);
553
+ console.log(pc4.dim(" " + "-".repeat(85)));
554
+ emails.forEach((email, index) => {
555
+ const num = pc4.cyan(String(index + 1).padEnd(4));
556
+ const unreadMarker = email.unread ? pc4.cyan("\u25CF") : " ";
381
557
  const from = truncate(email.from.name || email.from.email, 23).padEnd(25);
382
- const subject = truncate(email.subject, 38).padEnd(40);
558
+ const subject = truncate(email.subject, 43).padEnd(45);
383
559
  const date = formatDate(email.date);
384
- console.log(`${unreadMarker} ${id} ${from} ${subject} ${pc4.dim(date)}`);
385
- }
560
+ console.log(`${unreadMarker} ${num}${from} ${subject} ${pc4.dim(date)}`);
561
+ });
386
562
  console.log();
387
- if (pagination.hasMore) {
563
+ const totalPages = Math.ceil(pagination.total / limit);
564
+ const currentPage = page;
565
+ if (totalPages > 1) {
388
566
  console.log(
389
- pc4.dim(
390
- ` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
391
- )
567
+ pc4.dim(` Page ${currentPage} of ${totalPages} (${emails.length} of ${pagination.total})`)
392
568
  );
569
+ if (currentPage < totalPages) {
570
+ console.log(pc4.dim(` Next page: btcemail list --page ${currentPage + 1}`));
571
+ }
393
572
  console.log();
394
573
  }
574
+ console.log(pc4.dim(" Read email: btcemail read <#> or btcemail read <id>"));
575
+ console.log();
576
+ }
577
+ function capitalize(str) {
578
+ return str.charAt(0).toUpperCase() + str.slice(1);
395
579
  }
396
580
  function truncate(str, maxLength) {
397
581
  if (str.length <= maxLength) return str;
@@ -419,21 +603,42 @@ function formatDate(dateString) {
419
603
 
420
604
  // src/commands/read.ts
421
605
  import pc5 from "picocolors";
422
- async function readCommand(id, options) {
606
+ import ora4 from "ora";
607
+ async function readCommand(idOrNumber, options) {
423
608
  if (!isAuthenticated()) {
424
609
  console.log(pc5.yellow("Not logged in."));
425
610
  console.log(pc5.dim("Run `btcemail login` to authenticate."));
426
611
  process.exit(1);
427
612
  }
428
- const result = await getEmail(id);
613
+ let emailId = idOrNumber;
614
+ if (isNumericId(idOrNumber)) {
615
+ const index = parseInt(idOrNumber, 10);
616
+ const cachedId = getEmailIdByIndex(index);
617
+ if (cachedId) {
618
+ emailId = cachedId;
619
+ } else {
620
+ const cache = getCachedEmailList();
621
+ if (cache) {
622
+ console.error(pc5.red(`Email #${index} not found in list.`));
623
+ console.log(pc5.dim(`List has ${cache.emails.length} emails. Run 'btcemail list' to refresh.`));
624
+ } else {
625
+ console.error(pc5.red(`No email list cached.`));
626
+ console.log(pc5.dim(`Run 'btcemail list' first, then use 'btcemail read <#>'.`));
627
+ }
628
+ process.exit(1);
629
+ }
630
+ }
631
+ const spinner = ora4("Loading email...").start();
632
+ const result = await getEmail(emailId);
429
633
  if (!result.ok) {
430
634
  if (result.error.code === "NOT_FOUND") {
431
- console.error(pc5.red(`Email not found: ${id}`));
635
+ spinner.fail(`Email not found: ${emailId}`);
432
636
  } else {
433
- console.error(pc5.red(`Error: ${result.error.message}`));
637
+ spinner.fail(`Error: ${result.error.message}`);
434
638
  }
435
639
  process.exit(1);
436
640
  }
641
+ spinner.stop();
437
642
  const email = result.data;
438
643
  if (options.json) {
439
644
  console.log(JSON.stringify(email, null, 2));
@@ -482,46 +687,349 @@ function formatFullDate(dateString) {
482
687
  });
483
688
  }
484
689
 
485
- // src/commands/credits.ts
690
+ // src/commands/search.ts
486
691
  import pc6 from "picocolors";
487
- async function creditsBalanceCommand() {
692
+ import ora5 from "ora";
693
+ async function searchCommand(query, options) {
488
694
  if (!isAuthenticated()) {
489
695
  console.log(pc6.yellow("Not logged in."));
490
696
  console.log(pc6.dim("Run `btcemail login` to authenticate."));
491
697
  process.exit(1);
492
698
  }
699
+ if (!query || query.trim().length === 0) {
700
+ console.error(pc6.red("Error: Search query is required"));
701
+ process.exit(1);
702
+ }
703
+ const spinner = ora5(`Searching for "${query}"...`).start();
704
+ const result = await getEmails({
705
+ search: query.trim(),
706
+ limit: options.limit || 20
707
+ });
708
+ if (!result.ok) {
709
+ spinner.fail(`Error: ${result.error.message}`);
710
+ process.exit(1);
711
+ }
712
+ const { data: emails, pagination } = result.data;
713
+ spinner.stop();
714
+ const cachedEmails = emails.map((e) => ({
715
+ id: e.id,
716
+ subject: e.subject,
717
+ from: e.from.name || e.from.email
718
+ }));
719
+ cacheEmailList("search", cachedEmails);
720
+ if (options.json) {
721
+ console.log(JSON.stringify({ query, emails, pagination }, null, 2));
722
+ return;
723
+ }
724
+ console.log();
725
+ console.log(pc6.bold(`Search: "${query}" (${pagination.total} found)`));
726
+ console.log();
727
+ if (emails.length === 0) {
728
+ console.log(pc6.dim(" No emails found matching your search."));
729
+ console.log();
730
+ return;
731
+ }
732
+ console.log(
733
+ ` ${pc6.dim("#".padEnd(4))} ${pc6.dim("From".padEnd(25))} ${pc6.dim("Subject".padEnd(45))} ${pc6.dim("Date")}`
734
+ );
735
+ console.log(pc6.dim(" " + "-".repeat(85)));
736
+ emails.forEach((email, index) => {
737
+ const num = pc6.cyan(String(index + 1).padEnd(4));
738
+ const unreadMarker = email.unread ? pc6.cyan("\u25CF") : " ";
739
+ const from = truncate2(email.from.name || email.from.email, 23).padEnd(25);
740
+ const subject = truncate2(email.subject, 43).padEnd(45);
741
+ const date = formatDate2(email.date);
742
+ console.log(`${unreadMarker} ${num}${from} ${subject} ${pc6.dim(date)}`);
743
+ });
744
+ console.log();
745
+ if (pagination.hasMore) {
746
+ console.log(
747
+ pc6.dim(
748
+ ` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
749
+ )
750
+ );
751
+ console.log();
752
+ }
753
+ console.log(pc6.dim(" Read email: btcemail read <#> or btcemail read <id>"));
754
+ console.log();
755
+ }
756
+ function truncate2(str, maxLength) {
757
+ if (str.length <= maxLength) return str;
758
+ return str.slice(0, maxLength - 1) + "\u2026";
759
+ }
760
+ function formatDate2(dateString) {
761
+ const date = new Date(dateString);
762
+ const now = /* @__PURE__ */ new Date();
763
+ const diffMs = now.getTime() - date.getTime();
764
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
765
+ if (diffDays === 0) {
766
+ return date.toLocaleTimeString("en-US", {
767
+ hour: "numeric",
768
+ minute: "2-digit"
769
+ });
770
+ }
771
+ if (diffDays < 7) {
772
+ return date.toLocaleDateString("en-US", { weekday: "short" });
773
+ }
774
+ return date.toLocaleDateString("en-US", {
775
+ month: "short",
776
+ day: "numeric"
777
+ });
778
+ }
779
+
780
+ // src/commands/credits.ts
781
+ import pc8 from "picocolors";
782
+ import ora6 from "ora";
783
+ import qrcode from "qrcode-terminal";
784
+
785
+ // src/utils/payment-polling.ts
786
+ import pc7 from "picocolors";
787
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
788
+ async function pollForPayment(options) {
789
+ const { paymentHash, checkFn, maxAttempts = 120, intervalMs = 2500, onPaid } = options;
790
+ let frame = 0;
791
+ let attempts = 0;
792
+ process.stdout.write(`${pc7.cyan(SPINNER_FRAMES[0])} Waiting for payment...`);
793
+ while (attempts < maxAttempts) {
794
+ try {
795
+ const status = await checkFn(paymentHash);
796
+ if (status.paid) {
797
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
798
+ console.log(pc7.green("\u2713") + " Payment received!");
799
+ if (onPaid) onPaid();
800
+ return status;
801
+ }
802
+ } catch {
803
+ }
804
+ frame = (frame + 1) % SPINNER_FRAMES.length;
805
+ const timeLeft = Math.ceil((maxAttempts - attempts) * intervalMs / 1e3 / 60);
806
+ process.stdout.write(
807
+ `\r${pc7.cyan(SPINNER_FRAMES[frame])} Waiting for payment... (${timeLeft}m remaining)`
808
+ );
809
+ await sleep2(intervalMs);
810
+ attempts++;
811
+ }
812
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
813
+ console.log(pc7.yellow("\u26A0") + " Timed out waiting for payment.");
814
+ return { paid: false };
815
+ }
816
+ function sleep2(ms) {
817
+ return new Promise((resolve) => setTimeout(resolve, ms));
818
+ }
819
+
820
+ // src/commands/credits.ts
821
+ async function creditsBalanceCommand() {
822
+ if (!isAuthenticated()) {
823
+ console.log(pc8.yellow("Not logged in."));
824
+ console.log(pc8.dim("Run `btcemail login` to authenticate."));
825
+ process.exit(1);
826
+ }
827
+ const spinner = ora6("Fetching balance...").start();
493
828
  const result = await getCreditsBalance();
494
829
  if (!result.ok) {
495
- console.error(pc6.red(`Error: ${result.error.message}`));
830
+ spinner.fail(`Error: ${result.error.message}`);
496
831
  process.exit(1);
497
832
  }
498
- const { balanceSats } = result.data;
833
+ spinner.stop();
834
+ const { balanceSats, lifetimePurchasedSats, lifetimeSpentSats } = result.data;
499
835
  console.log();
500
- console.log(pc6.bold("Credit Balance"));
836
+ console.log(pc8.bold("Credit Balance"));
501
837
  console.log();
502
- console.log(` ${pc6.green(formatSats(balanceSats))} sats`);
838
+ console.log(` ${pc8.green(formatSats(balanceSats))} sats`);
503
839
  console.log();
840
+ if (lifetimePurchasedSats > 0 || lifetimeSpentSats > 0) {
841
+ console.log(pc8.dim(` Purchased: ${formatSats(lifetimePurchasedSats)} sats`));
842
+ console.log(pc8.dim(` Spent: ${formatSats(lifetimeSpentSats)} sats`));
843
+ console.log();
844
+ }
504
845
  if (balanceSats === 0) {
505
- console.log(pc6.dim(" No credits available."));
506
- console.log(pc6.dim(" Purchase credits at https://btc.email/payments"));
846
+ console.log(pc8.dim(" No credits available."));
847
+ console.log(pc8.dim(" Run `btcemail credits purchase` to buy credits."));
848
+ console.log();
849
+ }
850
+ }
851
+ async function creditsHistoryCommand(options) {
852
+ if (!isAuthenticated()) {
853
+ console.log(pc8.yellow("Not logged in."));
854
+ console.log(pc8.dim("Run `btcemail login` to authenticate."));
855
+ process.exit(1);
856
+ }
857
+ const spinner = ora6("Loading transaction history...").start();
858
+ const result = await getCreditTransactions({
859
+ limit: options.limit || 20,
860
+ type: options.type
861
+ });
862
+ if (!result.ok) {
863
+ spinner.fail(`Error: ${result.error.message}`);
864
+ process.exit(1);
865
+ }
866
+ const { data: transactions, pagination } = result.data;
867
+ spinner.stop();
868
+ if (options.json) {
869
+ console.log(JSON.stringify({ transactions, pagination }, null, 2));
870
+ return;
871
+ }
872
+ console.log();
873
+ console.log(pc8.bold("Transaction History"));
874
+ console.log();
875
+ if (transactions.length === 0) {
876
+ console.log(pc8.dim(" No transactions found."));
877
+ console.log();
878
+ return;
879
+ }
880
+ console.log(
881
+ ` ${pc8.dim("Date".padEnd(12))} ${pc8.dim("Type".padEnd(15))} ${pc8.dim("Amount".padEnd(12))} ${pc8.dim("Balance")}`
882
+ );
883
+ console.log(pc8.dim(" " + "-".repeat(55)));
884
+ for (const tx of transactions) {
885
+ const date = formatDate3(tx.createdAt);
886
+ const type = formatTransactionType(tx.transactionType).padEnd(15);
887
+ const amount = formatAmount(tx.amountSats).padEnd(12);
888
+ const balance = formatSats(tx.balanceAfter);
889
+ console.log(` ${date.padEnd(12)} ${type} ${amount} ${pc8.dim(balance)}`);
890
+ }
891
+ console.log();
892
+ if (pagination.hasMore) {
893
+ console.log(pc8.dim(` Use --limit to see more transactions.`));
894
+ console.log();
895
+ }
896
+ }
897
+ async function creditsPurchaseCommand(options) {
898
+ if (!isAuthenticated()) {
899
+ console.log(pc8.yellow("Not logged in."));
900
+ console.log(pc8.dim("Run `btcemail login` to authenticate."));
901
+ process.exit(1);
902
+ }
903
+ const purchaseOptions = getCreditPurchaseOptions();
904
+ if (options.option === void 0) {
905
+ console.log();
906
+ console.log(pc8.bold("Purchase Options"));
907
+ console.log();
908
+ purchaseOptions.forEach((opt, index) => {
909
+ console.log(` ${pc8.cyan(`[${index}]`)} ${opt.label}`);
910
+ console.log(` ${pc8.dim(opt.description)}`);
911
+ console.log(` ${pc8.dim(`Price: ${formatSats(opt.priceSats)} sats`)}`);
912
+ if (opt.bonusSats > 0) {
913
+ console.log(` ${pc8.green(`+${formatSats(opt.bonusSats)} bonus sats`)}`);
914
+ }
915
+ console.log();
916
+ });
917
+ console.log(pc8.dim(" Usage: btcemail credits purchase --option <index>"));
918
+ console.log(pc8.dim(" Add --wait to wait for payment confirmation."));
919
+ console.log();
920
+ return;
921
+ }
922
+ if (options.option < 0 || options.option >= purchaseOptions.length) {
923
+ console.error(pc8.red(`Error: Invalid option. Choose 0-${purchaseOptions.length - 1}.`));
924
+ process.exit(1);
925
+ }
926
+ const selectedOption = purchaseOptions[options.option];
927
+ const spinner = ora6("Creating invoice...").start();
928
+ const result = await purchaseCredits(options.option);
929
+ if (!result.ok) {
930
+ spinner.fail(`Error: ${result.error.message}`);
931
+ process.exit(1);
932
+ }
933
+ spinner.stop();
934
+ const purchase = result.data;
935
+ if (options.json) {
936
+ console.log(JSON.stringify(purchase, null, 2));
937
+ return;
938
+ }
939
+ console.log();
940
+ console.log(pc8.bold("Lightning Invoice"));
941
+ console.log();
942
+ console.log(` ${pc8.dim("Amount:")} ${formatSats(purchase.amountSats)} sats`);
943
+ console.log(` ${pc8.dim("Credits:")} ${formatSats(purchase.creditsToReceive)} sats`);
944
+ if (purchase.bonusSats > 0) {
945
+ console.log(` ${pc8.dim("Bonus:")} ${pc8.green(`+${formatSats(purchase.bonusSats)} sats`)}`);
946
+ }
947
+ console.log();
948
+ console.log(pc8.bold("Scan to pay:"));
949
+ console.log();
950
+ qrcode.generate(purchase.invoice, { small: true }, (qr) => {
951
+ const indentedQr = qr.split("\n").map((line) => " " + line).join("\n");
952
+ console.log(indentedQr);
953
+ });
954
+ console.log();
955
+ console.log(pc8.bold("Invoice:"));
956
+ console.log();
957
+ console.log(` ${purchase.invoice}`);
958
+ console.log();
959
+ console.log(pc8.dim(` Payment hash: ${purchase.paymentHash}`));
960
+ console.log(pc8.dim(` Expires: ${new Date(purchase.expiresAt).toLocaleString()}`));
961
+ console.log();
962
+ if (options.wait) {
963
+ console.log();
964
+ const status = await pollForPayment({
965
+ paymentHash: purchase.paymentHash,
966
+ checkFn: async (hash) => {
967
+ const result2 = await checkPaymentStatus(hash);
968
+ if (result2.ok) {
969
+ return { paid: result2.data.paid };
970
+ }
971
+ return { paid: false };
972
+ },
973
+ onPaid: async () => {
974
+ const balanceResult = await getCreditsBalance();
975
+ if (balanceResult.ok) {
976
+ console.log();
977
+ console.log(`${pc8.dim("New balance:")} ${pc8.green(formatSats(balanceResult.data.balanceSats))} sats`);
978
+ }
979
+ }
980
+ });
981
+ if (!status.paid) {
982
+ console.log(pc8.dim(" You can still pay the invoice and credits will be added."));
983
+ }
984
+ console.log();
985
+ } else {
986
+ console.log(pc8.dim(" Scan the QR code or copy the invoice above to pay."));
987
+ console.log(pc8.dim(" Use --wait to wait for payment confirmation."));
507
988
  console.log();
508
989
  }
509
990
  }
510
991
  function formatSats(sats) {
511
992
  return sats.toLocaleString();
512
993
  }
994
+ function formatAmount(sats) {
995
+ if (sats >= 0) {
996
+ return pc8.green(`+${formatSats(sats)}`);
997
+ }
998
+ return pc8.red(formatSats(sats));
999
+ }
1000
+ function formatTransactionType(type) {
1001
+ const types = {
1002
+ topup: "Top-up",
1003
+ email_sent: "Email Sent",
1004
+ email_received: "Email Received",
1005
+ refund: "Refund",
1006
+ bonus: "Bonus"
1007
+ };
1008
+ return types[type] || type;
1009
+ }
1010
+ function formatDate3(dateString) {
1011
+ const date = new Date(dateString);
1012
+ return date.toLocaleDateString("en-US", {
1013
+ month: "short",
1014
+ day: "numeric"
1015
+ });
1016
+ }
513
1017
 
514
1018
  // src/commands/send.ts
515
- import pc7 from "picocolors";
1019
+ import pc9 from "picocolors";
1020
+ import ora7 from "ora";
1021
+ import qrcode2 from "qrcode-terminal";
516
1022
  async function sendCommand(options) {
517
1023
  if (!isAuthenticated()) {
518
- console.log(pc7.yellow("Not logged in."));
519
- console.log(pc7.dim("Run `btcemail login` to authenticate."));
1024
+ console.log(pc9.yellow("Not logged in."));
1025
+ console.log(pc9.dim("Run `btcemail login` to authenticate."));
520
1026
  process.exit(1);
521
1027
  }
522
1028
  let fromEmail = options.fromEmail;
523
1029
  if (!fromEmail) {
1030
+ const spinner2 = ora7("Fetching account info...").start();
524
1031
  const whoamiResult = await getWhoami();
1032
+ spinner2.stop();
525
1033
  if (whoamiResult.ok && whoamiResult.data.username) {
526
1034
  fromEmail = `${whoamiResult.data.username}@btc.email`;
527
1035
  } else {
@@ -532,200 +1040,303 @@ async function sendCommand(options) {
532
1040
  }
533
1041
  }
534
1042
  if (!fromEmail || !fromEmail.endsWith("@btc.email")) {
535
- console.error(pc7.red("No @btc.email address found."));
536
- console.log(pc7.dim("Register a username at https://btc.email"));
1043
+ console.error(pc9.red("No @btc.email address found."));
1044
+ console.log(pc9.dim("Register a username at https://btc.email"));
537
1045
  process.exit(1);
538
1046
  }
539
1047
  if (!options.to) {
540
- console.error(pc7.red("Recipient is required. Use --to <email>"));
1048
+ console.error(pc9.red("Recipient is required. Use --to <email>"));
541
1049
  process.exit(1);
542
1050
  }
543
1051
  if (!options.subject) {
544
- console.error(pc7.red("Subject is required. Use --subject <text>"));
1052
+ console.error(pc9.red("Subject is required. Use --subject <text>"));
545
1053
  process.exit(1);
546
1054
  }
547
1055
  if (!options.body) {
548
- console.error(pc7.red("Body is required. Use --body <text>"));
1056
+ console.error(pc9.red("Body is required. Use --body <text>"));
549
1057
  process.exit(1);
550
1058
  }
551
1059
  const toEmails = options.to.split(",").map((e) => e.trim());
552
1060
  console.log();
553
- console.log(pc7.bold("Sending Email"));
1061
+ console.log(pc9.bold("Sending Email"));
554
1062
  console.log();
555
- console.log(`${pc7.dim("From:")} ${fromEmail}`);
556
- console.log(`${pc7.dim("To:")} ${toEmails.join(", ")}`);
557
- console.log(`${pc7.dim("Subject:")} ${options.subject}`);
1063
+ console.log(`${pc9.dim("From:")} ${fromEmail}`);
1064
+ console.log(`${pc9.dim("To:")} ${toEmails.join(", ")}`);
1065
+ console.log(`${pc9.dim("Subject:")} ${options.subject}`);
558
1066
  console.log();
559
- const invoiceResult = await getL402Invoice({
560
- fromEmail,
561
- toEmails
562
- });
563
- if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
564
- const invoice = invoiceResult.data;
565
- console.log(pc7.yellow(`Payment required: ${invoice.amountSats} sats`));
566
- console.log();
567
- console.log(pc7.dim("Lightning Invoice:"));
568
- console.log(pc7.cyan(invoice.invoice));
569
- console.log();
570
- console.log(pc7.dim("Pay this invoice with your Lightning wallet, then run:"));
571
- console.log(
572
- pc7.cyan(
573
- ` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
574
- )
575
- );
576
- console.log();
577
- return;
1067
+ if (!options.paymentHash) {
1068
+ const spinner2 = ora7("Getting invoice...").start();
1069
+ const invoiceResult = await getL402Invoice({
1070
+ fromEmail,
1071
+ toEmails
1072
+ });
1073
+ spinner2.stop();
1074
+ if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
1075
+ const invoice = invoiceResult.data;
1076
+ console.log(pc9.yellow(`Payment required: ${invoice.amountSats} sats`));
1077
+ console.log();
1078
+ console.log(pc9.bold("Scan to pay:"));
1079
+ console.log();
1080
+ qrcode2.generate(invoice.invoice, { small: true }, (qr) => {
1081
+ const indentedQr = qr.split("\n").map((line) => " " + line).join("\n");
1082
+ console.log(indentedQr);
1083
+ });
1084
+ console.log();
1085
+ console.log(pc9.dim("Lightning Invoice:"));
1086
+ console.log(pc9.cyan(invoice.invoice));
1087
+ console.log();
1088
+ if (options.wait) {
1089
+ const status = await pollForPayment({
1090
+ paymentHash: invoice.paymentHash,
1091
+ checkFn: async (hash) => {
1092
+ const result2 = await checkPaymentStatus(hash);
1093
+ if (result2.ok) {
1094
+ return { paid: result2.data.paid };
1095
+ }
1096
+ return { paid: false };
1097
+ }
1098
+ });
1099
+ if (status.paid) {
1100
+ const sendSpinner = ora7("Sending email...").start();
1101
+ const result2 = await sendEmail({
1102
+ to: toEmails,
1103
+ subject: options.subject,
1104
+ body: options.body,
1105
+ fromEmail,
1106
+ paymentHash: invoice.paymentHash
1107
+ });
1108
+ if (!result2.ok) {
1109
+ sendSpinner.fail(`Error: ${result2.error.message}`);
1110
+ process.exit(1);
1111
+ }
1112
+ sendSpinner.succeed("Email sent!");
1113
+ console.log();
1114
+ console.log(`${pc9.dim("Email ID:")} ${result2.data.emailId}`);
1115
+ if (result2.data.messageId) {
1116
+ console.log(`${pc9.dim("Message ID:")} ${result2.data.messageId}`);
1117
+ }
1118
+ console.log();
1119
+ } else {
1120
+ console.log(pc9.dim("Payment not confirmed. You can still pay and resend with --payment-hash."));
1121
+ }
1122
+ return;
1123
+ }
1124
+ console.log(pc9.dim("Pay this invoice with your Lightning wallet, then run:"));
1125
+ console.log(
1126
+ pc9.cyan(
1127
+ ` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
1128
+ )
1129
+ );
1130
+ console.log();
1131
+ console.log(pc9.dim("Or use --wait to automatically wait for payment."));
1132
+ console.log();
1133
+ return;
1134
+ }
578
1135
  }
1136
+ const spinner = ora7("Sending email...").start();
579
1137
  const result = await sendEmail({
580
1138
  to: toEmails,
581
1139
  subject: options.subject,
582
1140
  body: options.body,
583
- fromEmail
1141
+ fromEmail,
1142
+ paymentHash: options.paymentHash
584
1143
  });
585
1144
  if (!result.ok) {
586
1145
  if (result.error.code === "PAYMENT_REQUIRED") {
587
- console.error(pc7.yellow("Payment required to send this email."));
588
- console.log(pc7.dim("Use the invoice above to pay, then include --payment-hash"));
1146
+ spinner.fail("Payment required to send this email.");
1147
+ console.log(pc9.dim("Use the invoice above to pay, then include --payment-hash"));
589
1148
  } else {
590
- console.error(pc7.red(`Error: ${result.error.message}`));
1149
+ spinner.fail(`Error: ${result.error.message}`);
591
1150
  }
592
1151
  process.exit(1);
593
1152
  }
594
- console.log(pc7.green("Email sent successfully!"));
1153
+ spinner.succeed("Email sent!");
595
1154
  console.log();
596
- console.log(`${pc7.dim("Email ID:")} ${result.data.emailId}`);
1155
+ console.log(`${pc9.dim("Email ID:")} ${result.data.emailId}`);
597
1156
  if (result.data.messageId) {
598
- console.log(`${pc7.dim("Message ID:")} ${result.data.messageId}`);
1157
+ console.log(`${pc9.dim("Message ID:")} ${result.data.messageId}`);
599
1158
  }
600
1159
  console.log();
601
1160
  }
602
1161
 
603
1162
  // src/commands/inbound.ts
604
- import pc8 from "picocolors";
1163
+ import pc10 from "picocolors";
1164
+ import ora8 from "ora";
1165
+ async function inboundDeliveredCommand(options) {
1166
+ if (!isAuthenticated()) {
1167
+ console.log(pc10.yellow("Not logged in."));
1168
+ console.log(pc10.dim("Run `btcemail login` to authenticate."));
1169
+ process.exit(1);
1170
+ }
1171
+ const spinner = ora8("Loading delivered emails...").start();
1172
+ const result = await getDeliveredEmails({
1173
+ limit: options.limit || 20
1174
+ });
1175
+ if (!result.ok) {
1176
+ spinner.fail(`Error: ${result.error.message}`);
1177
+ process.exit(1);
1178
+ }
1179
+ const { data: emails, pagination } = result.data;
1180
+ spinner.stop();
1181
+ if (options.json) {
1182
+ console.log(JSON.stringify({ emails, pagination }, null, 2));
1183
+ return;
1184
+ }
1185
+ console.log();
1186
+ console.log(pc10.bold(`Delivered (${pagination.total} paid emails received)`));
1187
+ console.log();
1188
+ if (emails.length === 0) {
1189
+ console.log(pc10.dim(" No delivered emails yet."));
1190
+ console.log();
1191
+ return;
1192
+ }
1193
+ console.log(
1194
+ ` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(28))} ${pc10.dim("Subject".padEnd(40))} ${pc10.dim("Date")}`
1195
+ );
1196
+ console.log(pc10.dim(" " + "-".repeat(85)));
1197
+ emails.forEach((email, index) => {
1198
+ const num = pc10.cyan(String(index + 1).padEnd(4));
1199
+ const from = truncate3(email.from.name || email.from.email, 26).padEnd(28);
1200
+ const subject = truncate3(email.subject, 38).padEnd(40);
1201
+ const date = formatDate4(email.date);
1202
+ console.log(` ${num}${from} ${subject} ${pc10.dim(date)}`);
1203
+ });
1204
+ console.log();
1205
+ if (pagination.hasMore) {
1206
+ console.log(
1207
+ pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
1208
+ );
1209
+ console.log();
1210
+ }
1211
+ console.log(pc10.dim(" Read email: btcemail read <id>"));
1212
+ console.log();
1213
+ }
605
1214
  async function inboundPendingCommand(options) {
606
1215
  if (!isAuthenticated()) {
607
- console.log(pc8.yellow("Not logged in."));
608
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1216
+ console.log(pc10.yellow("Not logged in."));
1217
+ console.log(pc10.dim("Run `btcemail login` to authenticate."));
609
1218
  process.exit(1);
610
1219
  }
1220
+ const spinner = ora8("Loading pending emails...").start();
611
1221
  const result = await getPendingEmails({
612
1222
  limit: options.limit || 20
613
1223
  });
614
1224
  if (!result.ok) {
615
- console.error(pc8.red(`Error: ${result.error.message}`));
1225
+ spinner.fail(`Error: ${result.error.message}`);
616
1226
  process.exit(1);
617
1227
  }
618
1228
  const { data: emails, pagination } = result.data;
1229
+ spinner.stop();
619
1230
  if (options.json) {
620
1231
  console.log(JSON.stringify({ emails, pagination }, null, 2));
621
1232
  return;
622
1233
  }
623
1234
  console.log();
624
- console.log(pc8.bold(`Pending Emails (${pagination.total} awaiting payment)`));
1235
+ console.log(pc10.bold(`Pending (${pagination.total} awaiting payment)`));
625
1236
  console.log();
626
1237
  if (emails.length === 0) {
627
- console.log(pc8.dim(" No pending emails."));
1238
+ console.log(pc10.dim(" No pending emails."));
628
1239
  console.log();
629
1240
  return;
630
1241
  }
631
1242
  console.log(
632
- ` ${pc8.dim("ID".padEnd(12))} ${pc8.dim("From".padEnd(30))} ${pc8.dim("Subject".padEnd(30))} ${pc8.dim("Sats")}`
1243
+ ` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(28))} ${pc10.dim("Subject".padEnd(35))} ${pc10.dim("Sats")}`
633
1244
  );
634
- console.log(pc8.dim(" " + "-".repeat(85)));
635
- for (const email of emails) {
636
- const id = email.id.slice(0, 10).padEnd(12);
637
- const from = truncate2(email.from.name || email.from.email, 28).padEnd(30);
638
- const subject = truncate2(email.subject, 28).padEnd(30);
639
- const sats = pc8.yellow(String(email.amountSats));
640
- console.log(` ${id} ${from} ${subject} ${sats}`);
641
- }
1245
+ console.log(pc10.dim(" " + "-".repeat(80)));
1246
+ emails.forEach((email, index) => {
1247
+ const num = pc10.cyan(String(index + 1).padEnd(4));
1248
+ const from = truncate3(email.from.name || email.from.email, 26).padEnd(28);
1249
+ const subject = truncate3(email.subject, 33).padEnd(35);
1250
+ const sats = pc10.yellow(String(email.amountSats));
1251
+ console.log(` ${num}${from} ${subject} ${sats}`);
1252
+ });
642
1253
  console.log();
643
1254
  if (pagination.hasMore) {
644
1255
  console.log(
645
- pc8.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
1256
+ pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
646
1257
  );
647
1258
  console.log();
648
1259
  }
649
- console.log(pc8.dim(" Use `btcemail inbound view <id>` to see details and payment invoice."));
1260
+ console.log(pc10.dim(" View details: btcemail inbound view <id>"));
650
1261
  console.log();
651
1262
  }
652
1263
  async function inboundViewCommand(id, options) {
653
1264
  if (!isAuthenticated()) {
654
- console.log(pc8.yellow("Not logged in."));
655
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1265
+ console.log(pc10.yellow("Not logged in."));
1266
+ console.log(pc10.dim("Run `btcemail login` to authenticate."));
656
1267
  process.exit(1);
657
1268
  }
1269
+ const spinner = ora8("Loading email details...").start();
658
1270
  const result = await getPendingEmail(id);
659
1271
  if (!result.ok) {
660
1272
  if (result.error.code === "NOT_FOUND") {
661
- console.error(pc8.red(`Pending email not found: ${id}`));
1273
+ spinner.fail(`Pending email not found: ${id}`);
662
1274
  } else {
663
- console.error(pc8.red(`Error: ${result.error.message}`));
1275
+ spinner.fail(`Error: ${result.error.message}`);
664
1276
  }
665
1277
  process.exit(1);
666
1278
  }
1279
+ spinner.stop();
667
1280
  const email = result.data;
668
1281
  if (options.json) {
669
1282
  console.log(JSON.stringify(email, null, 2));
670
1283
  return;
671
1284
  }
672
1285
  console.log();
673
- console.log(pc8.bold(email.subject || "(No subject)"));
1286
+ console.log(pc10.bold(email.subject || "(No subject)"));
674
1287
  console.log();
675
- console.log(`${pc8.dim("From:")} ${formatAddress2(email.from)}`);
676
- console.log(`${pc8.dim("Date:")} ${formatFullDate2(email.createdAt)}`);
677
- console.log(`${pc8.dim("ID:")} ${email.id}`);
1288
+ console.log(`${pc10.dim("From:")} ${formatAddress2(email.from)}`);
1289
+ console.log(`${pc10.dim("Date:")} ${formatFullDate2(email.createdAt)}`);
1290
+ console.log(`${pc10.dim("ID:")} ${email.id}`);
678
1291
  console.log();
679
- console.log(pc8.yellow(`Payment Required: ${email.amountSats} sats`));
1292
+ console.log(pc10.yellow(`Payment Required: ${email.amountSats} sats`));
680
1293
  console.log();
681
1294
  if (email.paymentHash) {
682
- console.log(pc8.dim("To receive this email, pay the invoice and run:"));
683
- console.log(pc8.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
1295
+ console.log(pc10.dim("To receive this email, pay the invoice and run:"));
1296
+ console.log(pc10.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
684
1297
  } else {
685
- console.log(pc8.dim("Payment information not available."));
1298
+ console.log(pc10.dim("Payment information not available."));
686
1299
  }
687
1300
  console.log();
688
- console.log(pc8.dim("-".repeat(60)));
1301
+ console.log(pc10.dim("-".repeat(60)));
689
1302
  console.log();
690
- console.log(pc8.dim("Preview (full content available after payment):"));
1303
+ console.log(pc10.dim("Preview (full content available after payment):"));
691
1304
  console.log();
692
- console.log(truncate2(email.bodyText || email.body, 200));
1305
+ console.log(truncate3(email.bodyText || email.body, 200));
693
1306
  console.log();
694
1307
  }
695
1308
  async function inboundPayCommand(id, options) {
696
1309
  if (!isAuthenticated()) {
697
- console.log(pc8.yellow("Not logged in."));
698
- console.log(pc8.dim("Run `btcemail login` to authenticate."));
1310
+ console.log(pc10.yellow("Not logged in."));
1311
+ console.log(pc10.dim("Run `btcemail login` to authenticate."));
699
1312
  process.exit(1);
700
1313
  }
701
1314
  if (!options.paymentHash) {
702
- console.error(pc8.red("Payment hash is required. Use --payment-hash <preimage>"));
1315
+ console.error(pc10.red("Payment hash is required. Use --payment-hash <preimage>"));
703
1316
  process.exit(1);
704
1317
  }
705
- console.log();
706
- console.log(pc8.dim("Verifying payment..."));
1318
+ const spinner = ora8("Verifying payment...").start();
707
1319
  const result = await payForEmail(id, options.paymentHash);
708
1320
  if (!result.ok) {
709
1321
  if (result.error.code === "PAYMENT_INVALID") {
710
- console.error(pc8.red("Payment verification failed."));
711
- console.log(pc8.dim("Make sure you paid the correct invoice and provided the preimage."));
1322
+ spinner.fail("Payment verification failed.");
1323
+ console.log(pc10.dim("Make sure you paid the correct invoice and provided the preimage."));
712
1324
  } else if (result.error.code === "NOT_FOUND") {
713
- console.error(pc8.red(`Pending email not found or already delivered: ${id}`));
1325
+ spinner.fail(`Pending email not found or already delivered: ${id}`);
714
1326
  } else {
715
- console.error(pc8.red(`Error: ${result.error.message}`));
1327
+ spinner.fail(`Error: ${result.error.message}`);
716
1328
  }
717
1329
  process.exit(1);
718
1330
  }
1331
+ spinner.succeed("Payment verified! Email delivered.");
719
1332
  console.log();
720
- console.log(pc8.green("Payment verified! Email delivered."));
721
- console.log();
722
- console.log(`${pc8.dim("Email ID:")} ${result.data.emailId}`);
723
- console.log(`${pc8.dim("Status:")} ${result.data.status}`);
1333
+ console.log(`${pc10.dim("Email ID:")} ${result.data.emailId}`);
1334
+ console.log(`${pc10.dim("Status:")} ${result.data.status}`);
724
1335
  console.log();
725
- console.log(pc8.dim("Run `btcemail read " + result.data.emailId + "` to read the email."));
1336
+ console.log(pc10.dim(`Read email: btcemail read ${result.data.emailId}`));
726
1337
  console.log();
727
1338
  }
728
- function truncate2(str, maxLength) {
1339
+ function truncate3(str, maxLength) {
729
1340
  if (str.length <= maxLength) return str;
730
1341
  return str.slice(0, maxLength - 1) + "\u2026";
731
1342
  }
@@ -746,38 +1357,72 @@ function formatFullDate2(dateString) {
746
1357
  minute: "2-digit"
747
1358
  });
748
1359
  }
1360
+ function formatDate4(dateString) {
1361
+ const date = new Date(dateString);
1362
+ const now = /* @__PURE__ */ new Date();
1363
+ const diffMs = now.getTime() - date.getTime();
1364
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1365
+ if (diffDays === 0) {
1366
+ return date.toLocaleTimeString("en-US", {
1367
+ hour: "numeric",
1368
+ minute: "2-digit"
1369
+ });
1370
+ }
1371
+ if (diffDays < 7) {
1372
+ return date.toLocaleDateString("en-US", { weekday: "short" });
1373
+ }
1374
+ return date.toLocaleDateString("en-US", {
1375
+ month: "short",
1376
+ day: "numeric"
1377
+ });
1378
+ }
749
1379
 
750
1380
  // src/index.ts
751
1381
  var program = new Command();
752
- program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.1.0");
1382
+ program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.3.0");
753
1383
  program.command("login").description("Authenticate with btc.email (opens browser)").action(loginCommand);
754
1384
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
755
1385
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
756
- 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) => {
1386
+ 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) => {
757
1387
  await listCommand({
758
1388
  folder: options.folder,
759
1389
  limit: parseInt(options.limit, 10),
1390
+ page: parseInt(options.page, 10),
760
1391
  json: options.json
761
1392
  });
762
1393
  });
763
- program.command("read <id>").description("Read an email by ID").option("--json", "Output as JSON").action(async (id, options) => {
1394
+ program.command("read <id>").alias("r").description("Read an email by # (from list) or ID").option("--json", "Output as JSON").action(async (id, options) => {
764
1395
  await readCommand(id, { json: options.json });
765
1396
  });
766
- 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) => {
1397
+ 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) => {
1398
+ await searchCommand(query, {
1399
+ limit: parseInt(options.limit, 10),
1400
+ json: options.json
1401
+ });
1402
+ });
1403
+ 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) => {
767
1404
  await sendCommand({
768
1405
  to: options.to,
769
1406
  subject: options.subject,
770
1407
  body: options.body,
771
- fromEmail: options.from
1408
+ fromEmail: options.from,
1409
+ paymentHash: options.paymentHash,
1410
+ wait: options.wait
772
1411
  });
773
1412
  });
774
- var inbound = program.command("inbound").description("Manage pending emails requiring payment");
775
- 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) => {
1413
+ var inbound = program.command("inbound").description("Manage inbound emails (pending and delivered)");
1414
+ 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) => {
776
1415
  await inboundPendingCommand({
777
1416
  limit: parseInt(options.limit, 10),
778
1417
  json: options.json
779
1418
  });
780
1419
  });
1420
+ 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) => {
1421
+ await inboundDeliveredCommand({
1422
+ limit: parseInt(options.limit, 10),
1423
+ json: options.json
1424
+ });
1425
+ });
781
1426
  inbound.command("view <id>").description("View pending email details and payment info").option("--json", "Output as JSON").action(async (id, options) => {
782
1427
  await inboundViewCommand(id, { json: options.json });
783
1428
  });
@@ -785,11 +1430,24 @@ inbound.command("pay <id>").description("Pay for a pending email").requiredOptio
785
1430
  await inboundPayCommand(id, { paymentHash: options.paymentHash });
786
1431
  });
787
1432
  var credits = program.command("credits").description("Manage credits");
788
- credits.command("balance").description("Show credit balance").action(creditsBalanceCommand);
1433
+ credits.command("balance").alias("bal").description("Show credit balance").action(creditsBalanceCommand);
1434
+ 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) => {
1435
+ await creditsHistoryCommand({
1436
+ limit: parseInt(options.limit, 10),
1437
+ type: options.type,
1438
+ json: options.json
1439
+ });
1440
+ });
1441
+ 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) => {
1442
+ await creditsPurchaseCommand({
1443
+ option: options.option !== void 0 ? parseInt(options.option, 10) : void 0,
1444
+ json: options.json,
1445
+ wait: options.wait
1446
+ });
1447
+ });
789
1448
  program.action(() => {
790
1449
  console.log();
791
- console.log(pc9.bold("btc.email CLI"));
792
- console.log(pc9.dim("Spam-free email powered by Bitcoin Lightning"));
1450
+ console.log(pc11.bold("btc.email CLI") + pc11.dim(" - Spam-free email powered by Bitcoin Lightning"));
793
1451
  console.log();
794
1452
  program.outputHelp();
795
1453
  });