otpilot 2.3.2__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {otpilot-2.3.2/otpilot.egg-info → otpilot-2.3.4}/PKG-INFO +2 -1
  2. {otpilot-2.3.2 → otpilot-2.3.4}/docs/README.md +1 -0
  3. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/__init__.py +1 -1
  4. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/config.py +1 -0
  5. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/main.py +263 -1
  6. {otpilot-2.3.2 → otpilot-2.3.4/otpilot.egg-info}/PKG-INFO +2 -1
  7. {otpilot-2.3.2 → otpilot-2.3.4}/LICENSE +0 -0
  8. {otpilot-2.3.2 → otpilot-2.3.4}/README.md +0 -0
  9. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/clipboard.py +0 -0
  10. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/gmail_client.py +0 -0
  11. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/history.py +0 -0
  12. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/hotkey_listener.py +0 -0
  13. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/logger.py +0 -0
  14. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/notifier.py +0 -0
  15. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/otp_extractor.py +0 -0
  16. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/setup_wizard.py +0 -0
  17. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/token_store.py +0 -0
  18. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot/tray.py +0 -0
  19. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot.egg-info/SOURCES.txt +0 -0
  20. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot.egg-info/dependency_links.txt +0 -0
  21. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot.egg-info/entry_points.txt +0 -0
  22. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot.egg-info/requires.txt +0 -0
  23. {otpilot-2.3.2 → otpilot-2.3.4}/otpilot.egg-info/top_level.txt +0 -0
  24. {otpilot-2.3.2 → otpilot-2.3.4}/pyproject.toml +0 -0
  25. {otpilot-2.3.2 → otpilot-2.3.4}/setup.cfg +0 -0
  26. {otpilot-2.3.2 → otpilot-2.3.4}/setup.py +0 -0
  27. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_clipboard.py +0 -0
  28. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_config.py +0 -0
  29. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_gmail_client.py +0 -0
  30. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_history.py +0 -0
  31. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_logger.py +0 -0
  32. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_main.py +0 -0
  33. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_notifier.py +0 -0
  34. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_otp_extractor.py +0 -0
  35. {otpilot-2.3.2 → otpilot-2.3.4}/tests/test_token_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otpilot
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: Background CLI utility that copies OTPs from Gmail to clipboard on hotkey trigger
5
5
  Author: Jenil
6
6
  Author-email: Jenil <mail2jenil.pokar19@gmail.com>
@@ -261,6 +261,7 @@ Use this when you prefer no OAuth flow inside OTPilot.
261
261
  | `otpilot history --clear`| Clear OTP history |
262
262
  | `otpilot logs` | Tail the OTPilot log file |
263
263
  | `otpilot status` | Show auth state, hotkey, and config |
264
+ | `otpilot doctor` | Run diagnostics to validate environment |
264
265
  | `otpilot hotkey` | View or reconfigure the global hotkey |
265
266
  | `otpilot logout` | Clear stored auth token |
266
267
  | `otpilot update` | Check PyPI for updates and upgrade |
@@ -192,6 +192,7 @@ Use this when you prefer no OAuth flow inside OTPilot.
192
192
  | `otpilot history --clear`| Clear OTP history |
193
193
  | `otpilot logs` | Tail the OTPilot log file |
194
194
  | `otpilot status` | Show auth state, hotkey, and config |
195
+ | `otpilot doctor` | Run diagnostics to validate environment |
195
196
  | `otpilot hotkey` | View or reconfigure the global hotkey |
196
197
  | `otpilot logout` | Clear stored auth token |
197
198
  | `otpilot update` | Check PyPI for updates and upgrade |
@@ -10,5 +10,5 @@ Key exports:
10
10
  __app_name__: Human-readable application name.
11
11
  """
12
12
 
13
- __version__ = "2.3.2"
13
+ __version__ = "2.3.4"
14
14
  __app_name__ = "OTPilot"
@@ -25,6 +25,7 @@ TOKEN_FILE: Path = CONFIG_DIR / "token.json"
25
25
  # Default configuration values used for first run and missing keys.
26
26
  DEFAULT_CONFIG: Dict[str, Any] = {
27
27
  "auth_mode": "firebase",
28
+ "firebase_web_url": "https://jenil-otpilot.vercel.app/auth",
28
29
  "hotkey": "ctrl+shift+o",
29
30
  "notify_on_copy": True,
30
31
  "otp_max_age_minutes": 10,
@@ -11,9 +11,11 @@ Key exports:
11
11
  main: Console script entry point.
12
12
  """
13
13
 
14
+ import imaplib
14
15
  import os
15
16
  import shutil
16
17
  import signal
18
+ import socket
17
19
  import subprocess
18
20
  import sys
19
21
  import threading
@@ -26,7 +28,7 @@ from rich.console import Console
26
28
 
27
29
  from otpilot import __version__
28
30
  from otpilot.clipboard import ClipboardError, copy_to_clipboard
29
- from otpilot.config import config_exists, get_config, token_exists
31
+ from otpilot.config import CONFIG_FILE, config_exists, get_config, get_value, token_exists
30
32
  from otpilot.gmail_client import GmailAuthError, NotAuthenticatedError, get_fetch_function
31
33
  from otpilot.logger import LOG_FILE, get_logger
32
34
 
@@ -517,6 +519,266 @@ def status() -> None:
517
519
  _print_feedback_prompt()
518
520
 
519
521
 
522
+ @cli.command()
523
+ def doctor() -> None:
524
+ """Run diagnostic checks to validate OTPilot environment health.
525
+
526
+ Returns:
527
+ None: This command does not return a value.
528
+ """
529
+ from otpilot.token_store import load_app_password, load_token
530
+
531
+ console.print("[bold]OTPilot Doctor[/bold]")
532
+ console.print("Running diagnostics...")
533
+
534
+ passed = 0
535
+ warnings = 0
536
+ failed = 0
537
+
538
+ auth_mode: Optional[str] = None
539
+ gmail_reachable: Optional[bool] = None
540
+ imap_reachable: Optional[bool] = None
541
+
542
+ try:
543
+ version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
544
+ if sys.version_info >= (3, 10):
545
+ console.print(f"[green]✓[/green] Python {version} — OK")
546
+ passed += 1
547
+ else:
548
+ console.print(f"[red]✗[/red] Python {version} — OTPilot requires 3.10+")
549
+ failed += 1
550
+ except Exception as exc:
551
+ console.print(f"[red]✗[/red] Python version: unexpected error — {exc}")
552
+ failed += 1
553
+
554
+ try:
555
+ if config_exists():
556
+ console.print(f"[green]✓[/green] Config file found at {CONFIG_FILE}")
557
+ passed += 1
558
+ else:
559
+ console.print("[red]✗[/red] Config file missing — run [bold]otpilot setup[/bold]")
560
+ failed += 1
561
+ except Exception as exc:
562
+ console.print(f"[red]✗[/red] Config file exists: unexpected error — {exc}")
563
+ failed += 1
564
+
565
+ try:
566
+ auth_mode = get_value("auth_mode")
567
+ console.print(f"[green]✓[/green] Auth mode: {auth_mode}")
568
+ passed += 1
569
+ except Exception as exc:
570
+ console.print(f"[red]✗[/red] Auth mode: unexpected error — {exc}")
571
+ failed += 1
572
+
573
+ try:
574
+ if token_exists():
575
+ console.print("[green]✓[/green] Credential stored")
576
+ passed += 1
577
+ else:
578
+ console.print("[red]✗[/red] No credential found — run [bold]otpilot setup[/bold]")
579
+ failed += 1
580
+ except Exception as exc:
581
+ console.print(f"[red]✗[/red] Token stored: unexpected error — {exc}")
582
+ failed += 1
583
+
584
+ if auth_mode in {"firebase", "credentials"}:
585
+ try:
586
+ token_payload = load_token() or {}
587
+ expires_at = token_payload.get("expires_at")
588
+ if not expires_at:
589
+ console.print(
590
+ "[yellow]⚠[/yellow] Token expiry unknown — no expires_at stored"
591
+ )
592
+ warnings += 1
593
+ else:
594
+ now = time.time()
595
+ expires_at_int = int(expires_at)
596
+ delta = expires_at_int - now
597
+ if delta < 0:
598
+ minutes_ago = int((abs(delta) + 59) // 60)
599
+ console.print(
600
+ "[red]✗[/red] Token expired "
601
+ f"{minutes_ago} minutes ago — run [bold]otpilot fetch[/bold] "
602
+ "to trigger silent refresh or [bold]otpilot setup[/bold] to re-authenticate"
603
+ )
604
+ failed += 1
605
+ elif delta <= 5 * 60:
606
+ minutes_left = int((delta + 59) // 60)
607
+ console.print(
608
+ "[yellow]⚠[/yellow] Token expires in "
609
+ f"{minutes_left} minutes — will auto-refresh on next fetch"
610
+ )
611
+ warnings += 1
612
+ else:
613
+ minutes_left = int((delta + 59) // 60)
614
+ console.print(f"[green]✓[/green] Token valid — expires in {minutes_left} minutes")
615
+ passed += 1
616
+ except Exception as exc:
617
+ console.print(f"[red]✗[/red] Token expiry: unexpected error — {exc}")
618
+ failed += 1
619
+
620
+ try:
621
+ token_payload = load_token() or {}
622
+ refresh_token = str(token_payload.get("refresh_token", "")).strip()
623
+ if refresh_token:
624
+ console.print("[green]✓[/green] Refresh token stored — silent renewal enabled")
625
+ passed += 1
626
+ else:
627
+ console.print(
628
+ "[red]✗[/red] No refresh token — token cannot be silently renewed. "
629
+ "Re-run [bold]otpilot setup[/bold] to fix this"
630
+ )
631
+ failed += 1
632
+ except Exception as exc:
633
+ console.print(f"[red]✗[/red] Refresh token present: unexpected error — {exc}")
634
+ failed += 1
635
+
636
+ try:
637
+ requests.get("https://gmail.googleapis.com", timeout=5)
638
+ console.print("[green]✓[/green] Gmail API reachable")
639
+ passed += 1
640
+ gmail_reachable = True
641
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
642
+ console.print("[red]✗[/red] Gmail API unreachable — check your internet connection")
643
+ failed += 1
644
+ gmail_reachable = False
645
+ except Exception as exc:
646
+ console.print(f"[red]✗[/red] Gmail API reachability: unexpected error — {exc}")
647
+ failed += 1
648
+
649
+ if auth_mode == "imap":
650
+ host = ""
651
+ port = 993
652
+ try:
653
+ host = str(get_value("imap_host", ""))
654
+ port = int(get_value("imap_port", 993) or 993)
655
+ _ = get_value("imap_user", "")
656
+ previous_timeout = socket.getdefaulttimeout()
657
+ socket.setdefaulttimeout(5)
658
+ client: Optional[imaplib.IMAP4_SSL] = None
659
+ try:
660
+ client = imaplib.IMAP4_SSL(host, port)
661
+ console.print(f"[green]✓[/green] IMAP server {host}:{port} reachable")
662
+ passed += 1
663
+ imap_reachable = True
664
+ finally:
665
+ if client is not None:
666
+ try:
667
+ client.logout()
668
+ except Exception:
669
+ pass
670
+ socket.setdefaulttimeout(previous_timeout)
671
+ except (OSError, socket.timeout, imaplib.IMAP4.error):
672
+ console.print(
673
+ f"[red]✗[/red] IMAP server {host}:{port} unreachable — check your internet connection or imap settings"
674
+ )
675
+ failed += 1
676
+ imap_reachable = False
677
+ except Exception as exc:
678
+ console.print(f"[red]✗[/red] IMAP connectivity: unexpected error — {exc}")
679
+ failed += 1
680
+
681
+ try:
682
+ app_password = load_app_password()
683
+ if app_password:
684
+ console.print("[green]✓[/green] App password stored")
685
+ passed += 1
686
+ else:
687
+ console.print("[red]✗[/red] App password missing — run [bold]otpilot setup[/bold]")
688
+ failed += 1
689
+ except Exception as exc:
690
+ console.print(f"[red]✗[/red] App password stored: unexpected error — {exc}")
691
+ failed += 1
692
+
693
+ try:
694
+ hotkey = get_value("hotkey", "")
695
+ if hotkey == "":
696
+ console.print("[yellow]⚠[/yellow] Hotkey is empty — run [bold]otpilot hotkey[/bold]")
697
+ warnings += 1
698
+ else:
699
+ console.print(f"[green]✓[/green] Hotkey configured: {hotkey}")
700
+ passed += 1
701
+ except Exception as exc:
702
+ console.print(f"[red]✗[/red] Hotkey configured: unexpected error — {exc}")
703
+ failed += 1
704
+
705
+ try:
706
+ if _HOTKEY_LISTENER_AVAILABLE:
707
+ console.print("[green]✓[/green] Hotkey listener available")
708
+ passed += 1
709
+ else:
710
+ console.print(
711
+ "[yellow]⚠[/yellow] Hotkey listener unavailable on this platform — OTPilot will run without hotkey support"
712
+ )
713
+ warnings += 1
714
+ except Exception as exc:
715
+ console.print(f"[red]✗[/red] Hotkey listener available: unexpected error — {exc}")
716
+ failed += 1
717
+
718
+ try:
719
+ if _TRAY_AVAILABLE:
720
+ console.print("[green]✓[/green] System tray available")
721
+ passed += 1
722
+ else:
723
+ console.print(
724
+ "[yellow]⚠[/yellow] System tray unavailable — OTPilot will run headlessly without a tray icon"
725
+ )
726
+ warnings += 1
727
+ except Exception as exc:
728
+ console.print(f"[red]✗[/red] System tray available: unexpected error — {exc}")
729
+ failed += 1
730
+
731
+ try:
732
+ if not os.path.exists(PID_FILE):
733
+ console.print(
734
+ "[yellow]⚠[/yellow] OTPilot is not running — use [bold]otpilot start[/bold]"
735
+ )
736
+ warnings += 1
737
+ else:
738
+ with open(PID_FILE, "r", encoding="utf-8") as pid_file:
739
+ pid_str = pid_file.read().strip()
740
+ pid = int(pid_str)
741
+ try:
742
+ os.kill(pid, 0)
743
+ console.print(f"[green]✓[/green] OTPilot is running (PID {pid})")
744
+ passed += 1
745
+ except ProcessLookupError:
746
+ console.print(
747
+ "[yellow]⚠[/yellow] Stale PID file found "
748
+ f"(process {pid} is gone) — run [bold]otpilot stop[/bold] to clean up"
749
+ )
750
+ warnings += 1
751
+ except Exception as exc:
752
+ console.print(f"[red]✗[/red] OTPilot process running: unexpected error — {exc}")
753
+ failed += 1
754
+
755
+ try:
756
+ latest = _check_for_update()
757
+ if latest:
758
+ console.print(f"[yellow]⚠[/yellow] Update available: v{latest} — run [bold]otpilot update[/bold]")
759
+ warnings += 1
760
+ else:
761
+ if (auth_mode in {"firebase", "credentials"} and gmail_reachable is False) or (
762
+ auth_mode == "imap" and imap_reachable is False
763
+ ):
764
+ console.print(
765
+ "[dim] Could not check for updates (no internet or PyPI unreachable)[/dim]"
766
+ )
767
+ else:
768
+ console.print(f"[green]✓[/green] OTPilot v{__version__} is up to date")
769
+ passed += 1
770
+ except Exception as exc:
771
+ console.print(f"[red]✗[/red] Update available: unexpected error — {exc}")
772
+ failed += 1
773
+
774
+ console.print(f"[bold]{passed} passed {warnings} warnings {failed} failed[/bold]")
775
+ if failed > 0:
776
+ console.print(
777
+ "[dim]Run [bold]otpilot setup[/bold] to fix authentication issues, or [bold]otpilot logs[/bold] for runtime errors.[/dim]"
778
+ )
779
+ _print_feedback_prompt()
780
+
781
+
520
782
  @cli.command()
521
783
  @click.option("--set", "hotkey_value", default=None, help='Set hotkey directly, e.g. --set "ctrl+shift+o"')
522
784
  def hotkey(hotkey_value: Optional[str]) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otpilot
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: Background CLI utility that copies OTPs from Gmail to clipboard on hotkey trigger
5
5
  Author: Jenil
6
6
  Author-email: Jenil <mail2jenil.pokar19@gmail.com>
@@ -261,6 +261,7 @@ Use this when you prefer no OAuth flow inside OTPilot.
261
261
  | `otpilot history --clear`| Clear OTP history |
262
262
  | `otpilot logs` | Tail the OTPilot log file |
263
263
  | `otpilot status` | Show auth state, hotkey, and config |
264
+ | `otpilot doctor` | Run diagnostics to validate environment |
264
265
  | `otpilot hotkey` | View or reconfigure the global hotkey |
265
266
  | `otpilot logout` | Clear stored auth token |
266
267
  | `otpilot update` | Check PyPI for updates and upgrade |
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes