tokenhoggers 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Token Hoggers Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenhoggers
3
+ Version: 0.1.0
4
+ Summary: Track your AI usage across Claude Code, Cursor, ChatGPT, Gemini, and more
5
+ Author: Vikrant Tyagi
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AIPeek/token-hoggers
8
+ Project-URL: Repository, https://github.com/AIPeek/token-hoggers
9
+ Keywords: ai,analytics,leaderboard,claude,cursor,llm
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # Token Hoggers
20
+
21
+ **Gamified, tool-agnostic AI usage leaderboard.** Track and compete on your AI usage across Claude Code, Cursor, Gemini, ChatGPT, Copilot, and more.
22
+
23
+ Inspired by [claude_rank](https://github.com/bicro/claude_rank) but tool-agnostic, with an RPG character system built on the **AMI (AI Maturity Index)** framework.
24
+
25
+ ## How It Works
26
+
27
+ 1. **Install the CLI** — `pip install tokenhoggers`
28
+ 2. **Sign in** — Google OAuth at [tokenhoggers.com](https://web-system42.vercel.app)
29
+ 3. **Sync** — `tokenhoggers sync` reads your local AI tool stats (counts only, never content)
30
+ 4. **Compete** — See your rank, level, tier, and RPG stats on the leaderboard
31
+
32
+ ## Privacy-First
33
+
34
+ We collect **counts and timestamps only**. Never prompt text, code, file paths, or API keys.
35
+
36
+ | Collected | Never Touched |
37
+ |---|---|
38
+ | Token counts (in/out) | Prompt/response text |
39
+ | Message & session counts | Project/repo names |
40
+ | Tool call counts | File paths, code, diffs |
41
+ | Model names | API keys, credentials |
42
+ | Timestamps | Working directories |
43
+
44
+ ## RPG Character System
45
+
46
+ Built on the **AMI (AI Maturity Index)** — 4 dimensions that measure how you use AI, not just how much.
47
+
48
+ ```
49
+ MST (Mastery)
50
+ |
51
+ PRC ──────+────── VRS
52
+ |
53
+ END (Endurance)
54
+ ```
55
+
56
+ | Stat | Measures |
57
+ |---|---|
58
+ | **Mastery** | Power-user behavior (tool calls/session, model diversity) |
59
+ | **Precision** | Output per effort (efficiency) |
60
+ | **Versatility** | Multi-tool range |
61
+ | **Endurance** | Consistency (streaks, active days) |
62
+
63
+ **Tiers:** Bronze → Silver → Gold → Platinum → Diamond
64
+
65
+ ## Supported Tools
66
+
67
+ | Tool | Data Source | Parser |
68
+ |---|---|---|
69
+ | Claude Code | `~/.claude/stats-cache.json` | CLI |
70
+ | Cursor | `state.vscdb` (SQLite, read-only) | CLI |
71
+ | Gemini CLI | `~/.gemini/tmp/*/logs.json` | CLI |
72
+ | ChatGPT | Web request counting | Browser Extension |
73
+ | Gemini Web | Web request counting | Browser Extension |
74
+ | Claude Web | Web request counting | Browser Extension |
75
+
76
+ ## Quick Start
77
+
78
+ ### CLI
79
+
80
+ ```bash
81
+ pip install tokenhoggers
82
+
83
+ # Check what data is available locally
84
+ tokenhoggers status
85
+
86
+ # Sign in at the web app, copy your token, then:
87
+ tokenhoggers config --url https://aaxkbwyolbhjgovmcael.supabase.co/functions/v1/sync
88
+ tokenhoggers config --token <YOUR_TOKEN>
89
+ tokenhoggers sync
90
+
91
+ # Re-sync all historical data
92
+ tokenhoggers sync --force
93
+ ```
94
+
95
+ ### Desktop App (macOS)
96
+
97
+ Menu bar app that shows your stats at a glance.
98
+
99
+ ```bash
100
+ cd desktop/src-tauri
101
+ source ~/.cargo/env
102
+ cargo run
103
+ ```
104
+
105
+ ### Browser Extension
106
+
107
+ Tracks request counts on ChatGPT, Gemini, and Claude web (never captures content).
108
+
109
+ ```bash
110
+ cd extension/icons && python3 generate.py
111
+ # Chrome → chrome://extensions → Developer Mode → Load unpacked → select extension/
112
+ ```
113
+
114
+ ## Architecture
115
+
116
+ ```
117
+ collectors/cli/ Python CLI — parses local AI tool stats
118
+ web/ Frontend (vanilla HTML/JS/CSS) — hosted on Vercel
119
+ supabase/ Backend — PostgreSQL, Auth, Edge Functions, Realtime
120
+ desktop/ Tauri 2.x macOS menu bar app
121
+ extension/ Chrome MV3 browser extension
122
+ ```
123
+
124
+ ## Tech Stack
125
+
126
+ - **Backend:** Supabase (PostgreSQL + Auth + Edge Functions + Realtime)
127
+ - **Frontend:** Vanilla HTML/JS/CSS (Claude Rank-inspired design)
128
+ - **CLI:** Python 3.9+
129
+ - **Desktop:** Rust / Tauri 2.x
130
+ - **Extension:** Chrome MV3
131
+
132
+ ## Contributing
133
+
134
+ PRs welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
135
+
136
+ ## License
137
+
138
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,120 @@
1
+ # Token Hoggers
2
+
3
+ **Gamified, tool-agnostic AI usage leaderboard.** Track and compete on your AI usage across Claude Code, Cursor, Gemini, ChatGPT, Copilot, and more.
4
+
5
+ Inspired by [claude_rank](https://github.com/bicro/claude_rank) but tool-agnostic, with an RPG character system built on the **AMI (AI Maturity Index)** framework.
6
+
7
+ ## How It Works
8
+
9
+ 1. **Install the CLI** — `pip install tokenhoggers`
10
+ 2. **Sign in** — Google OAuth at [tokenhoggers.com](https://web-system42.vercel.app)
11
+ 3. **Sync** — `tokenhoggers sync` reads your local AI tool stats (counts only, never content)
12
+ 4. **Compete** — See your rank, level, tier, and RPG stats on the leaderboard
13
+
14
+ ## Privacy-First
15
+
16
+ We collect **counts and timestamps only**. Never prompt text, code, file paths, or API keys.
17
+
18
+ | Collected | Never Touched |
19
+ |---|---|
20
+ | Token counts (in/out) | Prompt/response text |
21
+ | Message & session counts | Project/repo names |
22
+ | Tool call counts | File paths, code, diffs |
23
+ | Model names | API keys, credentials |
24
+ | Timestamps | Working directories |
25
+
26
+ ## RPG Character System
27
+
28
+ Built on the **AMI (AI Maturity Index)** — 4 dimensions that measure how you use AI, not just how much.
29
+
30
+ ```
31
+ MST (Mastery)
32
+ |
33
+ PRC ──────+────── VRS
34
+ |
35
+ END (Endurance)
36
+ ```
37
+
38
+ | Stat | Measures |
39
+ |---|---|
40
+ | **Mastery** | Power-user behavior (tool calls/session, model diversity) |
41
+ | **Precision** | Output per effort (efficiency) |
42
+ | **Versatility** | Multi-tool range |
43
+ | **Endurance** | Consistency (streaks, active days) |
44
+
45
+ **Tiers:** Bronze → Silver → Gold → Platinum → Diamond
46
+
47
+ ## Supported Tools
48
+
49
+ | Tool | Data Source | Parser |
50
+ |---|---|---|
51
+ | Claude Code | `~/.claude/stats-cache.json` | CLI |
52
+ | Cursor | `state.vscdb` (SQLite, read-only) | CLI |
53
+ | Gemini CLI | `~/.gemini/tmp/*/logs.json` | CLI |
54
+ | ChatGPT | Web request counting | Browser Extension |
55
+ | Gemini Web | Web request counting | Browser Extension |
56
+ | Claude Web | Web request counting | Browser Extension |
57
+
58
+ ## Quick Start
59
+
60
+ ### CLI
61
+
62
+ ```bash
63
+ pip install tokenhoggers
64
+
65
+ # Check what data is available locally
66
+ tokenhoggers status
67
+
68
+ # Sign in at the web app, copy your token, then:
69
+ tokenhoggers config --url https://aaxkbwyolbhjgovmcael.supabase.co/functions/v1/sync
70
+ tokenhoggers config --token <YOUR_TOKEN>
71
+ tokenhoggers sync
72
+
73
+ # Re-sync all historical data
74
+ tokenhoggers sync --force
75
+ ```
76
+
77
+ ### Desktop App (macOS)
78
+
79
+ Menu bar app that shows your stats at a glance.
80
+
81
+ ```bash
82
+ cd desktop/src-tauri
83
+ source ~/.cargo/env
84
+ cargo run
85
+ ```
86
+
87
+ ### Browser Extension
88
+
89
+ Tracks request counts on ChatGPT, Gemini, and Claude web (never captures content).
90
+
91
+ ```bash
92
+ cd extension/icons && python3 generate.py
93
+ # Chrome → chrome://extensions → Developer Mode → Load unpacked → select extension/
94
+ ```
95
+
96
+ ## Architecture
97
+
98
+ ```
99
+ collectors/cli/ Python CLI — parses local AI tool stats
100
+ web/ Frontend (vanilla HTML/JS/CSS) — hosted on Vercel
101
+ supabase/ Backend — PostgreSQL, Auth, Edge Functions, Realtime
102
+ desktop/ Tauri 2.x macOS menu bar app
103
+ extension/ Chrome MV3 browser extension
104
+ ```
105
+
106
+ ## Tech Stack
107
+
108
+ - **Backend:** Supabase (PostgreSQL + Auth + Edge Functions + Realtime)
109
+ - **Frontend:** Vanilla HTML/JS/CSS (Claude Rank-inspired design)
110
+ - **CLI:** Python 3.9+
111
+ - **Desktop:** Rust / Tauri 2.x
112
+ - **Extension:** Chrome MV3
113
+
114
+ ## Contributing
115
+
116
+ PRs welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
117
+
118
+ ## License
119
+
120
+ MIT — see [LICENSE](LICENSE).
File without changes
File without changes
@@ -0,0 +1,247 @@
1
+ """Browser-based OAuth login and token refresh for Token Hoggers CLI."""
2
+
3
+ import base64
4
+ import json
5
+ import socket
6
+ import sys
7
+ import threading
8
+ import time
9
+ import webbrowser
10
+ from http.server import HTTPServer, BaseHTTPRequestHandler
11
+ from typing import Optional
12
+ from urllib.error import HTTPError
13
+ from urllib.request import Request, urlopen
14
+
15
+ from collectors.cli.config import Config
16
+
17
+ SUPABASE_URL = "https://aaxkbwyolbhjgovmcael.supabase.co"
18
+ SUPABASE_ANON_KEY = (
19
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
20
+ "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFheGtid3lvbGJoamdvdm1jYWVsIiwi"
21
+ "cm9sZSI6ImFub24iLCJpYXQiOjE3NzQxODM4MTUsImV4cCI6MjA4OTc1OTgxNX0."
22
+ "nNDUcYm2xzpGW6ervqNQEIXQdENi1qYXpQo4Rp0JrI8"
23
+ )
24
+ SYNC_API_URL = f"{SUPABASE_URL}/functions/v1/sync"
25
+
26
+ _PORT_RANGE = range(19876, 19900)
27
+ _LOGIN_TIMEOUT = 120 # seconds
28
+
29
+ # HTML page served at /callback — reads hash fragment and POSTs tokens back
30
+ _CALLBACK_HTML = """<!DOCTYPE html>
31
+ <html>
32
+ <head>
33
+ <title>Token Hoggers</title>
34
+ <style>
35
+ body { font-family: -apple-system, system-ui, sans-serif; display: flex;
36
+ justify-content: center; align-items: center; min-height: 100vh;
37
+ margin: 0; background: #0f0f0f; color: #e0e0e0; }
38
+ .card { text-align: center; padding: 48px; border-radius: 16px;
39
+ background: #1a1a1a; border: 1px solid #333; max-width: 400px; }
40
+ h2 { margin: 0 0 8px; color: #fff; }
41
+ p { margin: 0; color: #888; font-size: 14px; }
42
+ .err { color: #ff6b6b; }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <div class="card">
47
+ <div id="status"><h2>Completing login...</h2></div>
48
+ </div>
49
+ <script>
50
+ (function() {
51
+ var hash = window.location.hash.substring(1);
52
+ if (!hash) {
53
+ document.getElementById('status').innerHTML =
54
+ '<h2 class="err">Login failed</h2><p>No auth tokens received. Please try again.</p>';
55
+ return;
56
+ }
57
+ var params = new URLSearchParams(hash);
58
+ var data = {
59
+ access_token: params.get('access_token'),
60
+ refresh_token: params.get('refresh_token'),
61
+ expires_in: parseInt(params.get('expires_in') || '3600', 10),
62
+ token_type: params.get('token_type')
63
+ };
64
+ if (!data.access_token) {
65
+ document.getElementById('status').innerHTML =
66
+ '<h2 class="err">Login failed</h2><p>Missing access token. Please try again.</p>';
67
+ return;
68
+ }
69
+ fetch('/receive-tokens', {
70
+ method: 'POST',
71
+ headers: {'Content-Type': 'application/json'},
72
+ body: JSON.stringify(data)
73
+ }).then(function(r) { return r.json(); }).then(function() {
74
+ document.getElementById('status').innerHTML =
75
+ '<h2>Login successful!</h2><p>You can close this tab and return to your terminal.</p>';
76
+ }).catch(function(e) {
77
+ document.getElementById('status').innerHTML =
78
+ '<h2 class="err">Error</h2><p>' + e.message + '</p>';
79
+ });
80
+ })();
81
+ </script>
82
+ </body>
83
+ </html>"""
84
+
85
+
86
+ def _find_available_port() -> int:
87
+ """Find an available port in the designated range."""
88
+ for port in _PORT_RANGE:
89
+ try:
90
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
91
+ s.bind(("127.0.0.1", port))
92
+ return port
93
+ except OSError:
94
+ continue
95
+ raise RuntimeError(
96
+ f"No available port in range {_PORT_RANGE.start}-{_PORT_RANGE.stop - 1}. "
97
+ "Close other applications or use: tokenhoggers config --token <JWT>"
98
+ )
99
+
100
+
101
+ def browser_login() -> dict:
102
+ """Open browser for Google OAuth and capture tokens via local HTTP server.
103
+
104
+ Returns dict with access_token, refresh_token, expires_in.
105
+ Raises TimeoutError if user doesn't complete login within the timeout.
106
+ Raises RuntimeError on other failures.
107
+ """
108
+ port = _find_available_port()
109
+ tokens = {}
110
+ token_event = threading.Event()
111
+
112
+ class CallbackHandler(BaseHTTPRequestHandler):
113
+ def do_GET(self):
114
+ if self.path.startswith("/callback"):
115
+ self.send_response(200)
116
+ self.send_header("Content-Type", "text/html")
117
+ self.end_headers()
118
+ self.wfile.write(_CALLBACK_HTML.encode())
119
+ else:
120
+ self.send_response(404)
121
+ self.end_headers()
122
+
123
+ def do_POST(self):
124
+ if self.path == "/receive-tokens":
125
+ length = int(self.headers.get("Content-Length", 0))
126
+ body = json.loads(self.rfile.read(length))
127
+ tokens.update(body)
128
+ token_event.set()
129
+ self.send_response(200)
130
+ self.send_header("Content-Type", "application/json")
131
+ self.end_headers()
132
+ self.wfile.write(b'{"ok":true}')
133
+ else:
134
+ self.send_response(404)
135
+ self.end_headers()
136
+
137
+ def log_message(self, format, *args):
138
+ pass # suppress HTTP server logging
139
+
140
+ server = HTTPServer(("127.0.0.1", port), CallbackHandler)
141
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
142
+ server_thread.start()
143
+
144
+ auth_url = (
145
+ f"{SUPABASE_URL}/auth/v1/authorize"
146
+ f"?provider=google"
147
+ f"&redirect_to=http://localhost:{port}/callback"
148
+ )
149
+
150
+ opened = webbrowser.open(auth_url)
151
+ if not opened:
152
+ print(f"\n Could not open browser. Please visit this URL manually:\n")
153
+ print(f" {auth_url}\n")
154
+
155
+ if not token_event.wait(timeout=_LOGIN_TIMEOUT):
156
+ server.shutdown()
157
+ raise TimeoutError("Login timed out. Please try again.")
158
+
159
+ server.shutdown()
160
+
161
+ if not tokens.get("access_token"):
162
+ raise RuntimeError("No access token received.")
163
+
164
+ return tokens
165
+
166
+
167
+ def decode_jwt_exp(token: str) -> int:
168
+ """Decode a JWT and return the exp (expiry) claim as a Unix timestamp.
169
+
170
+ Does NOT verify the signature — only used for local expiry checks.
171
+ Returns 0 if the token is malformed or has no exp claim.
172
+ """
173
+ try:
174
+ parts = token.split(".")
175
+ if len(parts) != 3:
176
+ return 0
177
+ payload = parts[1]
178
+ # Add base64 padding
179
+ padding = 4 - len(payload) % 4
180
+ if padding != 4:
181
+ payload += "=" * padding
182
+ decoded = base64.urlsafe_b64decode(payload)
183
+ claims = json.loads(decoded)
184
+ return int(claims.get("exp", 0))
185
+ except Exception:
186
+ return 0
187
+
188
+
189
+ def refresh_access_token(refresh_token: str) -> dict:
190
+ """Exchange a refresh token for a new access token pair.
191
+
192
+ Returns dict with access_token, refresh_token, expires_in.
193
+ Raises RuntimeError if refresh fails (token revoked/expired).
194
+ """
195
+ url = f"{SUPABASE_URL}/auth/v1/token?grant_type=refresh_token"
196
+ body = json.dumps({"refresh_token": refresh_token}).encode()
197
+ req = Request(
198
+ url,
199
+ data=body,
200
+ headers={
201
+ "apikey": SUPABASE_ANON_KEY,
202
+ "Content-Type": "application/json",
203
+ },
204
+ method="POST",
205
+ )
206
+ try:
207
+ with urlopen(req, timeout=15) as resp:
208
+ data = json.loads(resp.read().decode())
209
+ return {
210
+ "access_token": data["access_token"],
211
+ "refresh_token": data["refresh_token"],
212
+ "expires_in": data.get("expires_in", 3600),
213
+ }
214
+ except HTTPError as e:
215
+ if e.code in (400, 401, 403):
216
+ raise RuntimeError(
217
+ "Session expired. Please run `tokenhoggers login` again."
218
+ )
219
+ raise RuntimeError(f"Token refresh failed (HTTP {e.code})")
220
+
221
+
222
+ def refresh_if_needed(config: Config) -> Config:
223
+ """Check if the access token is expired and refresh it if possible.
224
+
225
+ Modifies and saves config in-place if a refresh occurs.
226
+ Returns the (possibly updated) config.
227
+ """
228
+ if not config.token:
229
+ return config
230
+
231
+ exp = decode_jwt_exp(config.token)
232
+ now = int(time.time())
233
+
234
+ # Token still valid for more than 60 seconds
235
+ if exp > 0 and (exp - now) > 60:
236
+ return config
237
+
238
+ # No refresh token available (manual token setup)
239
+ if not config.refresh_token:
240
+ return config
241
+
242
+ tokens = refresh_access_token(config.refresh_token)
243
+ config.token = tokens["access_token"]
244
+ config.refresh_token = tokens["refresh_token"]
245
+ config.token_expiry = now + tokens.get("expires_in", 3600)
246
+ config.save()
247
+ return config
@@ -0,0 +1,44 @@
1
+ """User configuration for the Token Hoggers CLI."""
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+ from collectors.cli.paths import config_dir
8
+
9
+
10
+ @dataclass
11
+ class Config:
12
+ api_url: str = ""
13
+ token: str = ""
14
+ refresh_token: str = ""
15
+ token_expiry: int = 0
16
+ enable_fluency: bool = False
17
+
18
+ @classmethod
19
+ def load(cls) -> "Config":
20
+ path = config_dir() / "config.json"
21
+ if path.exists():
22
+ data = json.loads(path.read_text())
23
+ return cls(
24
+ api_url=data.get("api_url", ""),
25
+ token=data.get("token", ""),
26
+ refresh_token=data.get("refresh_token", ""),
27
+ token_expiry=data.get("token_expiry", 0),
28
+ enable_fluency=data.get("enable_fluency", False),
29
+ )
30
+ return cls()
31
+
32
+ def save(self) -> None:
33
+ path = config_dir() / "config.json"
34
+ path.write_text(json.dumps({
35
+ "api_url": self.api_url,
36
+ "token": self.token,
37
+ "refresh_token": self.refresh_token,
38
+ "token_expiry": self.token_expiry,
39
+ "enable_fluency": self.enable_fluency,
40
+ }, indent=2))
41
+
42
+ @property
43
+ def is_configured(self) -> bool:
44
+ return bool(self.api_url and self.token)