usage-lens 0.0.1__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 (42) hide show
  1. usage_lens-0.0.1/PKG-INFO +13 -0
  2. usage_lens-0.0.1/README.md +0 -0
  3. usage_lens-0.0.1/pyproject.toml +26 -0
  4. usage_lens-0.0.1/setup.cfg +4 -0
  5. usage_lens-0.0.1/src/ai_tokenkeeper/__init__.py +4 -0
  6. usage_lens-0.0.1/src/ai_tokenkeeper/__main__.py +4 -0
  7. usage_lens-0.0.1/src/ai_tokenkeeper/cli.py +84 -0
  8. usage_lens-0.0.1/src/ai_tokenkeeper/config/__init__.py +4 -0
  9. usage_lens-0.0.1/src/ai_tokenkeeper/config/settings.py +50 -0
  10. usage_lens-0.0.1/src/ai_tokenkeeper/config.py +46 -0
  11. usage_lens-0.0.1/src/ai_tokenkeeper/core/__init__.py +0 -0
  12. usage_lens-0.0.1/src/ai_tokenkeeper/core/engine.py +102 -0
  13. usage_lens-0.0.1/src/ai_tokenkeeper/core/plugin_loader.py +50 -0
  14. usage_lens-0.0.1/src/ai_tokenkeeper/database/__init__.py +0 -0
  15. usage_lens-0.0.1/src/ai_tokenkeeper/database/history.py +54 -0
  16. usage_lens-0.0.1/src/ai_tokenkeeper/platform/__init__.py +0 -0
  17. usage_lens-0.0.1/src/ai_tokenkeeper/platform/autostart.py +108 -0
  18. usage_lens-0.0.1/src/ai_tokenkeeper/platform/browser_detector.py +45 -0
  19. usage_lens-0.0.1/src/ai_tokenkeeper/platform/tray.py +49 -0
  20. usage_lens-0.0.1/src/ai_tokenkeeper/providers/__init__.py +0 -0
  21. usage_lens-0.0.1/src/ai_tokenkeeper/providers/base.py +42 -0
  22. usage_lens-0.0.1/src/ai_tokenkeeper/providers/claude.py +263 -0
  23. usage_lens-0.0.1/src/ai_tokenkeeper/providers/gemini.py +0 -0
  24. usage_lens-0.0.1/src/ai_tokenkeeper/providers/grok.py +0 -0
  25. usage_lens-0.0.1/src/ai_tokenkeeper/providers/openai.py +0 -0
  26. usage_lens-0.0.1/src/ai_tokenkeeper/secure_storage.py +46 -0
  27. usage_lens-0.0.1/src/ai_tokenkeeper/ui/__init__.py +0 -0
  28. usage_lens-0.0.1/src/ai_tokenkeeper/ui/components/card.py +0 -0
  29. usage_lens-0.0.1/src/ai_tokenkeeper/ui/components/charts.py +0 -0
  30. usage_lens-0.0.1/src/ai_tokenkeeper/ui/components/progress.py +31 -0
  31. usage_lens-0.0.1/src/ai_tokenkeeper/ui/core_window.py +66 -0
  32. usage_lens-0.0.1/src/ai_tokenkeeper/ui/dashboard.py +63 -0
  33. usage_lens-0.0.1/src/ai_tokenkeeper/ui/dashboard_view.py +162 -0
  34. usage_lens-0.0.1/src/ai_tokenkeeper/ui/minibar_view.py +75 -0
  35. usage_lens-0.0.1/src/ai_tokenkeeper/ui/styles.py +52 -0
  36. usage_lens-0.0.1/src/ai_tokenkeeper/ui/widget_view.py +94 -0
  37. usage_lens-0.0.1/src/usage_lens.egg-info/PKG-INFO +13 -0
  38. usage_lens-0.0.1/src/usage_lens.egg-info/SOURCES.txt +40 -0
  39. usage_lens-0.0.1/src/usage_lens.egg-info/dependency_links.txt +1 -0
  40. usage_lens-0.0.1/src/usage_lens.egg-info/entry_points.txt +2 -0
  41. usage_lens-0.0.1/src/usage_lens.egg-info/requires.txt +7 -0
  42. usage_lens-0.0.1/src/usage_lens.egg-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: usage-lens
3
+ Version: 0.0.1
4
+ Summary: AI usage tracker.
5
+ Author: Karthikeya
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: requests
8
+ Requires-Dist: websocket-client
9
+ Requires-Dist: keyring
10
+ Requires-Dist: pywin32
11
+ Requires-Dist: pystray
12
+ Requires-Dist: Pillow
13
+ Requires-Dist: pyyaml
File without changes
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "usage-lens"
7
+ version = "0.0.1"
8
+ description = "AI usage tracker."
9
+ authors = [{name = "Karthikeya"}]
10
+ requires-python = ">=3.9"
11
+
12
+ dependencies = [
13
+ "requests",
14
+ "websocket-client",
15
+ "keyring",
16
+ "pywin32",
17
+ "pystray",
18
+ "Pillow",
19
+ "pyyaml",
20
+ ]
21
+
22
+ [project.scripts]
23
+ usage-lens = "ai_tokenkeeper.cli:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ # src/ai_tokenkeeper/__init__.py
2
+ from .core.engine import TokenKeeperEngine
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from ai_tokenkeeper.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,84 @@
1
+ import sys
2
+ import argparse
3
+ import tkinter as tk
4
+ from ai_tokenkeeper.core.engine import TokenKeeperEngine
5
+ from ai_tokenkeeper.ui.dashboard import TokenKeeperDashboard
6
+ from ai_tokenkeeper.platform.tray import SystemTrayManager
7
+ from ai_tokenkeeper.platform.autostart import AutoStartManager
8
+
9
+ def main():
10
+ """Main terminal entry point for managing the TokenKeeper application."""
11
+ parser = argparse.ArgumentParser(description="AI TokenKeeper: Cross-platform LLM session recovery and monitoring utility.")
12
+ subparsers = parser.add_subparsers(dest="command", help="Available utility operations")
13
+
14
+ # Command: start
15
+ start_parser = subparsers.add_parser("start", help="Launch the primary monitoring daemon and desktop dashboard")
16
+ start_parser.add_argument("--headless", action="store_true", help="Launch directly into the system tray without showing the UI window")
17
+
18
+ # Command: autostart
19
+ autostart_parser = subparsers.add_parser("autostart", help="Configure system boot integrations")
20
+ autostart_parser.add_argument("action", choices=["enable", "disable"], help="Enable or disable launch on system startup")
21
+
22
+ args = parser.parse_args()
23
+
24
+ # Default to 'start' action if no argument is provided
25
+ if not args.command:
26
+ args.command = "start"
27
+ args.headless = False
28
+
29
+ if args.command == "autostart":
30
+ if args.action == "enable":
31
+ success = AutoStartManager.enable()
32
+ print("[CLI] System startup integration successfully enabled.") if success else print("[CLI] Failed to write startup configuration.")
33
+ elif args.action == "disable":
34
+ success = AutoStartManager.disable()
35
+ print("[CLI] System startup integration disabled.") if success else print("[CLI] Failed to clear startup configuration.")
36
+ sys.exit(0)
37
+
38
+ if args.command == "start":
39
+ # Initialize the underlying orchestration logic engine
40
+ engine = TokenKeeperEngine()
41
+ engine.start_background_loop()
42
+
43
+ root = tk.Tk()
44
+ app = TokenKeeperDashboard(root, engine)
45
+
46
+ def open_dashboard_from_tray(icon, item):
47
+ root.after(0, lambda: root.deiconify())
48
+
49
+ def force_refresh_from_tray(icon, item):
50
+ for provider_id in engine.providers:
51
+ engine.update_provider_state(provider_id)
52
+
53
+ def complete_shutdown():
54
+ engine.stop_background_loop()
55
+ if tray_manager.icon:
56
+ tray_manager.icon.stop()
57
+ root.destroy()
58
+ sys.exit(0)
59
+
60
+ # Wire up the cross-platform system tray hooks
61
+ tray_manager = SystemTrayManager(
62
+ on_open_dashboard=open_dashboard_from_tray,
63
+ on_force_refresh=force_refresh_from_tray,
64
+ on_exit=complete_shutdown
65
+ )
66
+
67
+ # Override default window exit protocol to intercept and minimize cleanly to the system tray
68
+ def hide_window_to_tray():
69
+ root.withdraw()
70
+
71
+ root.protocol("WM_DELETE_WINDOW", hide_window_to_tray)
72
+
73
+ import threading
74
+ tray_thread = threading.Thread(target=tray_manager.run_tray_icon, daemon=True)
75
+ tray_thread.start()
76
+
77
+ if args.headless:
78
+ root.withdraw()
79
+
80
+ # Run the primary thread lifecycle loop
81
+ root.mainloop()
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -0,0 +1,4 @@
1
+ from ai_tokenkeeper.config.settings import AppConfig
2
+
3
+ # Bridge the new premium AppConfig to the class name expected by the core engine
4
+ ConfigManager = AppConfig
@@ -0,0 +1,50 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+
6
+ class AppConfig:
7
+ """
8
+ Handles local JSON configuration persistence for premium widget modes,
9
+ provider selections, and pinning choices.
10
+ """
11
+ def __init__(self):
12
+ self.config_dir = Path(os.path.expanduser("~/.config/ai_tokenkeeper"))
13
+ self.config_file = self.config_dir / "config.json"
14
+
15
+ # Default application states
16
+ self.defaults = {
17
+ "selected_provider": "claude",
18
+ "ui_mode": "dashboard", # Options: dashboard, widget, minibar
19
+ "always_on_top": True,
20
+ "threshold_alert_pct": 80
21
+ }
22
+ self.data = self._load_config()
23
+
24
+ def _load_config(self) -> Dict[str, Any]:
25
+ try:
26
+ self.config_dir.mkdir(parents=True, exist_ok=True)
27
+ if self.config_file.exists():
28
+ with open(self.config_file, "r") as f:
29
+ user_data = json.load(f)
30
+ # Merge user flags over defaults to maintain compatibility
31
+ return {**self.defaults, **user_data}
32
+ except Exception:
33
+ pass
34
+ return self.defaults.copy()
35
+
36
+ def save(self) -> None:
37
+ """Saves current memory runtime states safely to disk."""
38
+ try:
39
+ with open(self.config_file, "w") as f:
40
+ json.dump(self.data, f, indent=4)
41
+ except Exception as e:
42
+ print(f"[Config Engine] Save error: {e}")
43
+
44
+ def get(self, key: str, default: Any = None) -> Any:
45
+ """Retrieves a configuration metric or returns the default fallback value."""
46
+ return self.data.get(key, default if default is not None else self.defaults.get(key))
47
+
48
+ def set(self, key: str, value: Any) -> None:
49
+ self.data[key] = value
50
+ self.save()
@@ -0,0 +1,46 @@
1
+ import os
2
+ import json
3
+ from typing import Dict, Any
4
+
5
+ class ConfigManager:
6
+ """
7
+ Manages non-sensitive application configurations and state caching.
8
+ Saves metadata to the user's local application data directory.
9
+ """
10
+ def __init__(self):
11
+ self.app_data_dir = os.path.join(
12
+ os.environ.get("LOCALAPPDATA", os.path.expanduser("~")),
13
+ "AITokenKeeper"
14
+ )
15
+ os.makedirs(self.app_data_dir, exist_ok=True)
16
+ self.config_path = os.path.join(self.app_data_dir, "config.json")
17
+ self.defaults = {
18
+ "polling_interval_seconds": 60,
19
+ "auto_start": False,
20
+ "enabled_providers": ["claude"]
21
+ }
22
+ self.settings = self._load_config()
23
+
24
+ def _load_config(self) -> Dict[str, Any]:
25
+ if os.path.exists(self.config_path):
26
+ try:
27
+ with open(self.config_path, "r") as f:
28
+ loaded = json.load(f)
29
+ # Merge defaults for any missing keys
30
+ return {**self.defaults, **loaded}
31
+ except Exception as e:
32
+ print(f"[Config] Error reading configuration file: {e}")
33
+ return self.defaults.copy()
34
+
35
+ def get(self, key: str, default: Any = None) -> Any:
36
+ """Retrieves a configuration metric or returns default value."""
37
+ return self.settings.get(key, default if default is not None else self.defaults.get(key))
38
+
39
+ def set(self, key: str, value: Any) -> None:
40
+ """Updates a configuration metric and commits the change to disk."""
41
+ self.settings[key] = value
42
+ try:
43
+ with open(self.config_path, "w") as f:
44
+ json.dump(self.settings, f, indent=4)
45
+ except Exception as e:
46
+ print(f"[Config] Failed to persist configuration settings: {e}")
File without changes
@@ -0,0 +1,102 @@
1
+ import threading
2
+ import time
3
+ from typing import Dict, Any, Optional
4
+ from ai_tokenkeeper.config import ConfigManager
5
+ from ai_tokenkeeper.secure_storage import SecureStorage
6
+ from ai_tokenkeeper.core.plugin_loader import PluginLoader
7
+
8
+ class TokenKeeperEngine:
9
+ """
10
+ Central orchestration runner that maintains background execution states,
11
+ coordinates tracking cycles, and provides isolated data streams to the view layer.
12
+ """
13
+ def __init__(self):
14
+ self.config = ConfigManager()
15
+ self.storage = SecureStorage()
16
+ self.providers: Dict[str, Any] = {}
17
+ self.shared_state: Dict[str, Any] = {}
18
+ self.is_running = False
19
+ self._loop_thread: Optional[threading.Thread] = None
20
+
21
+ self._initialize_plugins()
22
+
23
+ def _initialize_plugins(self) -> None:
24
+ """Discovers internal/external provider components and boots them."""
25
+ discovered_classes = PluginLoader.discover_providers()
26
+
27
+ for p_id, provider_class in discovered_classes.items():
28
+ # Instantiate each discovered provider with core resources
29
+ instance = provider_class(config_manager=self.config, secure_store=self.storage)
30
+ self.providers[p_id] = instance
31
+
32
+ # Populate initial safe data mapping state
33
+ self.shared_state[p_id] = {
34
+ "display_name": instance.display_name,
35
+ "is_authenticated": False,
36
+ "metrics": {"status": "Initializing", "input_tokens": 0, "output_tokens": 0}
37
+ }
38
+
39
+ # Run quick non-blocking validation sweep to catch existing cookies
40
+ instance.refresh_session()
41
+
42
+ def start_background_loop(self) -> None:
43
+ """Fires up the non-blocking background orchestration interval worker."""
44
+ if self.is_running:
45
+ return
46
+ self.is_running = True
47
+ self._loop_thread = threading.Thread(target=self._execution_loop, daemon=True)
48
+ self._loop_thread.start()
49
+
50
+ def stop_background_loop(self) -> None:
51
+ """Triggers clean teardown flags for the background operations thread."""
52
+ self.is_running = False
53
+
54
+ def trigger_provider_auth(self, provider_id: str) -> bool:
55
+ """Executes targeted runtime interaction sequence for a specific engine."""
56
+ if provider_id not in self.providers:
57
+ return False
58
+
59
+ provider = self.providers[provider_id]
60
+ success = provider.authenticate()
61
+
62
+ # Instantly refresh localized engine metrics on tracking maps
63
+ self.update_provider_state(provider_id)
64
+ return success
65
+
66
+ def update_provider_state(self, provider_id: str) -> None:
67
+ """Queries explicit metric fields to populate current view data boundaries."""
68
+ provider = self.providers[provider_id]
69
+ is_auth = provider.refresh_session()
70
+
71
+ metrics = {"status": "Unauthenticated", "input_tokens": 0, "output_tokens": 0}
72
+ if is_auth:
73
+ metrics = provider.get_usage()
74
+
75
+ self.shared_state[provider_id] = {
76
+ "display_name": provider.display_name,
77
+ "is_authenticated": is_auth,
78
+ "metrics": metrics
79
+ }
80
+
81
+ def _execution_loop(self) -> None:
82
+ """Continuous low-priority background thread execution lifecycle loop."""
83
+ while self.is_running:
84
+ enabled_targets = self.config.get("enabled_providers", [])
85
+
86
+ for p_id in enabled_targets:
87
+ if p_id in self.providers:
88
+ try:
89
+ self.update_provider_state(p_id)
90
+ except Exception as loop_err:
91
+ print(f"[Engine] Exception during active polling cycle for {p_id}: {loop_err}")
92
+
93
+ interval = self.config.get("polling_interval_seconds", 60)
94
+ # Sleep in tiny blocks to allow fast application exit steps
95
+ for _ in range(int(interval)):
96
+ if not self.is_running:
97
+ break
98
+ time.sleep(1)
99
+
100
+ def get_latest_state(self) -> Dict[str, Any]:
101
+ """Provides the presentation interface with thread-safe current execution states."""
102
+ return self.shared_state.copy()
@@ -0,0 +1,50 @@
1
+ import os
2
+ import inspect
3
+ import importlib
4
+ from typing import Dict, Type
5
+ from ai_tokenkeeper.providers.base import BaseProvider
6
+
7
+ class PluginLoader:
8
+ """
9
+ Discovers, inspects, and registers provider plugins within the package environment.
10
+ """
11
+
12
+ @staticmethod
13
+ def discover_providers() -> Dict[str, Type[BaseProvider]]:
14
+ """
15
+ Scans the providers directory, looks for valid subclasses of BaseProvider,
16
+ and builds a registration dictionary mapped by their provider_id.
17
+ """
18
+ discovered: Dict[str, Type[BaseProvider]] = {}
19
+
20
+ # Locate the absolute directory paths relative to this runtime file
21
+ current_dir = os.path.dirname(os.path.abspath(__file__))
22
+ providers_dir = os.path.normpath(os.path.join(current_dir, "..", "providers"))
23
+
24
+ if not os.path.exists(providers_dir):
25
+ return discovered
26
+
27
+ # Scan folder files
28
+ for filename in os.listdir(providers_dir):
29
+ if filename.endswith(".py") and filename != "base.py" and not filename.startswith("__"):
30
+ module_name = f"ai_tokenkeeper.providers.{filename[:-3]}"
31
+
32
+ try:
33
+ module = importlib.import_module(module_name)
34
+
35
+ # Inspect every class declared inside the found python file
36
+ for _, obj in inspect.getmembers(module, inspect.isclass):
37
+ # Ensure the class inherits from BaseProvider but isn't BaseProvider itself
38
+ if issubclass(obj, BaseProvider) and obj is not BaseProvider:
39
+ # Safely read properties to register the identifier string
40
+ try:
41
+ provider_instance = obj.__new__(obj)
42
+ p_id = provider_instance.provider_id
43
+ discovered[p_id] = obj
44
+ except Exception as instantiation_err:
45
+ print(f"[PluginLoader] Failed tracking attributes for class {obj.__name__}: {instantiation_err}")
46
+
47
+ except Exception as import_err:
48
+ print(f"[PluginLoader] Could not load provider module {module_name}: {import_err}")
49
+
50
+ return discovered
@@ -0,0 +1,54 @@
1
+ import json
2
+ import os
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ class UsageHistory:
8
+ """
9
+ Lightweight append-only time-series data store mapping historical usage percentages
10
+ to drive UI sparkline visualization components.
11
+ """
12
+ def __init__(self):
13
+ self.data_dir = Path(os.path.expanduser("~/.config/ai_tokenkeeper"))
14
+ self.history_file = self.data_dir / "usage_history.json"
15
+ self._init_store()
16
+
17
+ def _init_store(self) -> None:
18
+ self.data_dir.mkdir(parents=True, exist_ok=True)
19
+ if not self.history_file.exists():
20
+ with open(self.history_file, "w") as f:
21
+ json.dump([], f)
22
+
23
+ def log_entry(self, provider_id: str, session_pct: int, weekly_pct: int) -> None:
24
+ """Appends a snapshot metric into the local time-series repository."""
25
+ try:
26
+ with open(self.history_file, "r") as f:
27
+ history = json.load(f)
28
+
29
+ entry = {
30
+ "timestamp": datetime.now().isoformat(),
31
+ "provider_id": provider_id,
32
+ "session_pct": session_pct,
33
+ "weekly_pct": weekly_pct
34
+ }
35
+ history.append(entry)
36
+
37
+ # Keep log bounded to last 200 data tracks to prevent infinite disk leaks
38
+ if len(history) > 200:
39
+ history = history[-200:]
40
+
41
+ with open(self.history_file, "w") as f:
42
+ json.dump(history, f, indent=2)
43
+ except Exception as e:
44
+ print(f"[History Store] Log write failure: {e}")
45
+
46
+ def get_records(self, provider_id: str, limit: int = 20) -> List[Dict[str, Any]]:
47
+ """Retrieves filtered chronological records for a designated core provider."""
48
+ try:
49
+ with open(self.history_file, "r") as f:
50
+ history = json.load(f)
51
+ filtered = [r for r in history if r.get("provider_id") == provider_id]
52
+ return filtered[-limit:]
53
+ except Exception:
54
+ return []
@@ -0,0 +1,108 @@
1
+ import os
2
+ import sys
3
+
4
+ class AutoStartManager:
5
+ """
6
+ Manages OS-specific hooks to register or deregister the utility from system startup lists.
7
+ """
8
+ APP_NAME = "AITokenKeeper"
9
+
10
+ @classmethod
11
+ def enable(cls) -> bool:
12
+ """Registers the application executable into the target platform startup lifecycle."""
13
+ platform = sys.platform
14
+ # Determine the accurate target running script handle
15
+ exec_command = f'"{sys.executable}" -m ai_tokenkeeper'
16
+
17
+ try:
18
+ if platform == "win32":
19
+ import winreg
20
+ key = winreg.OpenKey(
21
+ winreg.HKEY_CURRENT_USER,
22
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
23
+ 0, winreg.KEY_SET_VALUE
24
+ )
25
+ winreg.SetValueEx(key, cls.APP_NAME, 0, winreg.REG_SZ, exec_command)
26
+ winreg.CloseKey(key)
27
+ return True
28
+
29
+ elif platform == "darwin": # macOS Launchd Agent
30
+ plist_dir = os.path.expanduser("~/Library/LaunchAgents")
31
+ os.makedirs(plist_dir, exist_ok=True)
32
+ plist_path = os.path.join(plist_dir, f"com.tokenkeeper.app.plist")
33
+
34
+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
35
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
36
+ <plist version="1.0">
37
+ <dict>
38
+ <key>Label</key>
39
+ <string>com.tokenkeeper.app</string>
40
+ <key>ProgramArguments</key>
41
+ <array>
42
+ <string>{sys.executable}</string>
43
+ <string>-m</string>
44
+ <string>ai_tokenkeeper</string>
45
+ </array>
46
+ <key>RunAtLoad</key>
47
+ <true/>
48
+ </dict>
49
+ </plist>"""
50
+ with open(plist_path, "w") as f:
51
+ f.write(plist_content)
52
+ return True
53
+
54
+ elif platform.startswith("linux"): # XDG Desktop Autostart standard
55
+ autostart_dir = os.path.expanduser("~/.config/autostart")
56
+ os.makedirs(autostart_dir, exist_ok=True)
57
+ desktop_path = os.path.join(autostart_dir, "ai_tokenkeeper.desktop")
58
+
59
+ desktop_content = f"""[Desktop Entry]
60
+ Type=Application
61
+ Name=AI TokenKeeper
62
+ Exec={sys.executable} -m ai_tokenkeeper
63
+ Hidden=false
64
+ NoDisplay=false
65
+ X-GNOME-Autostart-enabled=true
66
+ Comment=LLM Session Recovery Agent
67
+ """
68
+ with open(desktop_path, "w") as f:
69
+ f.write(desktop_content)
70
+ return True
71
+
72
+ except Exception as e:
73
+ print(f"[AutoStart] Failed to enable startup registration: {e}")
74
+ return False
75
+
76
+ @classmethod
77
+ def disable(cls) -> bool:
78
+ """Removes the application registration from the system startup sequences."""
79
+ platform = sys.platform
80
+ try:
81
+ if platform == "win32":
82
+ import winreg
83
+ key = winreg.OpenKey(
84
+ winreg.HKEY_CURRENT_USER,
85
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
86
+ 0, winreg.KEY_SET_VALUE
87
+ )
88
+ try:
89
+ winreg.DeleteValue(key, cls.APP_NAME)
90
+ except FileNotFoundError:
91
+ pass
92
+ winreg.CloseKey(key)
93
+ return True
94
+
95
+ elif platform == "darwin":
96
+ plist_path = os.path.expanduser("~/Library/LaunchAgents/com.tokenkeeper.app.plist")
97
+ if os.path.exists(plist_path):
98
+ os.remove(plist_path)
99
+ return True
100
+
101
+ elif platform.startswith("linux"):
102
+ desktop_path = os.path.expanduser("~/.config/autostart/ai_tokenkeeper.desktop")
103
+ if os.path.exists(desktop_path):
104
+ os.remove(desktop_path)
105
+ return True
106
+ except Exception as e:
107
+ print(f"[AutoStart] Failed to tear down autostart configs: {e}")
108
+ return False
@@ -0,0 +1,45 @@
1
+ import os
2
+ import sys
3
+
4
+ class BrowserDetector:
5
+ """
6
+ Locates compatible chromium browser engines across diverse operating systems
7
+ to enable targeted stealth automation loops.
8
+ """
9
+
10
+ @staticmethod
11
+ def get_edge_path() -> str:
12
+ """
13
+ Scans predictable system directory frames to return the binary location of Edge.
14
+ Raises FileNotFoundError if no match is discovered.
15
+ """
16
+ platform = sys.platform
17
+ paths = []
18
+
19
+ if platform == "win32":
20
+ paths = [
21
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
22
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
23
+ os.path.join(os.environ.get("LOCALAPPDATA", ""), "Microsoft", "Edge", "Application", "msedge.exe")
24
+ ]
25
+ elif platform == "darwin": # macOS
26
+ paths = [
27
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
28
+ os.path.expanduser("~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")
29
+ ]
30
+ elif platform.startswith("linux"):
31
+ paths = [
32
+ "/usr/bin/microsoft-edge",
33
+ "/usr/bin/microsoft-edge-stable",
34
+ "/usr/local/bin/microsoft-edge",
35
+ "/snap/bin/microsoft-edge"
36
+ ]
37
+
38
+ for path in paths:
39
+ if os.path.exists(path):
40
+ return path
41
+
42
+ raise FileNotFoundError(
43
+ f"Microsoft Edge binary could not be automatically detected on platform: {platform}. "
44
+ "Please ensure it is installed or specify the path manually."
45
+ )
@@ -0,0 +1,49 @@
1
+ import threading
2
+ from typing import Callable
3
+ import pystray
4
+ from PIL import Image, ImageDraw
5
+
6
+ class SystemTrayManager:
7
+ """
8
+ Controls the application presence in the OS taskbar/tray area.
9
+ Keeps the engine processing tokens even when the dashboard window is hidden.
10
+ """
11
+ def __init__(self, on_open_dashboard: Callable, on_force_refresh: Callable, on_exit: Callable):
12
+ self.on_open_dashboard = on_open_dashboard
13
+ self.on_force_refresh = on_force_refresh
14
+ self.on_exit = on_exit
15
+ self.icon = None
16
+
17
+ def _create_fallback_icon(self) -> Image.Image:
18
+ """Generates a simple geometric placeholder image if a dedicated PNG asset is missing."""
19
+ image = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
20
+ dc = ImageDraw.Draw(image)
21
+ # Draw a clean circular indicator frame
22
+ dc.ellipse([8, 8, 56, 56], fill=(30, 41, 59), outline=(99, 102, 241), width=4)
23
+ dc.text((24, 22), "TK", fill=(255, 255, 255))
24
+ return image
25
+
26
+ def run_tray_icon(self) -> None:
27
+ """Spawns the system tray instance inside an unblocked running environment loop."""
28
+ menu = pystray.Menu(
29
+ pystray.MenuItem("Open Dashboard", self.on_open_dashboard, default=True),
30
+ pystray.MenuItem("Force Refresh Sync", self.on_force_refresh),
31
+ pystray.Menu.SEPARATOR,
32
+ pystray.MenuItem("Exit Utility", self.on_quit_triggered)
33
+ )
34
+
35
+ self.icon = pystray.Icon(
36
+ "AITokenKeeper",
37
+ icon=self._create_fallback_icon(),
38
+ title="AI TokenKeeper (Running)",
39
+ menu=menu
40
+ )
41
+
42
+ # Runs the internal platform monitoring process
43
+ self.icon.run()
44
+
45
+ def on_quit_triggered(self, icon, item) -> None:
46
+ """Internal callback to stop the icon instance before handing execution off to global hooks."""
47
+ if self.icon:
48
+ self.icon.stop()
49
+ self.on_exit()