claudometer 1.0.0__py3-none-any.whl

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.
app.py ADDED
@@ -0,0 +1,54 @@
1
+ """Entry point.
2
+
3
+ Usage (no arg default: macOS = menu bar, Windows = taskbar strip, Linux = tray):
4
+ py app.py # default per platform (above)
5
+ py app.py bar # Windows: floating readable numbers over the taskbar
6
+ py app.py tray # Windows/Linux: tray icon only
7
+ py app.py both # Windows: tray icon + floating taskbar readout
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ import sys
13
+
14
+
15
+ def _run_tray():
16
+ from tray_windows import TrayApp
17
+ TrayApp().run()
18
+
19
+
20
+ def _run_bar():
21
+ from widget_bar import BarWidget
22
+ BarWidget().run()
23
+
24
+
25
+ def main() -> None:
26
+ mode = (sys.argv[1] if len(sys.argv) > 1 else "").lower()
27
+
28
+ if sys.platform == "darwin":
29
+ from menubar_mac import MenuApp
30
+ MenuApp().run()
31
+ return
32
+
33
+ if not mode: # packaged/no-arg default: the flagship on Windows, tray on Linux
34
+ mode = "bar" if sys.platform == "win32" else "tray"
35
+ if mode in ("bar", "both") and sys.platform != "win32":
36
+ mode = "tray" # bar/both are Windows-only (widget_bar needs ctypes.windll)
37
+
38
+ if mode == "bar":
39
+ _run_bar()
40
+ elif mode == "both":
41
+ # Run the tray in a separate process (its own message loop) and the
42
+ # floating bar here on the main thread.
43
+ if getattr(sys, "frozen", False): # packaged .exe re-reads argv
44
+ subprocess.Popen([sys.executable, "tray"])
45
+ else:
46
+ here = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app.py")
47
+ subprocess.Popen([sys.executable, here, "tray"])
48
+ _run_bar()
49
+ else: # "", "tray", win32 / linux default
50
+ _run_tray()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,345 @@
1
+ Metadata-Version: 2.4
2
+ Name: claudometer
3
+ Version: 1.0.0
4
+ Summary: Live Claude usage limits (session & weekly) on your taskbar / menu bar — so you never hit a limit by surprise.
5
+ Author-email: Muhammad Ali <ali.dev178@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ali-dev178/claudometer
8
+ Project-URL: Repository, https://github.com/ali-dev178/claudometer
9
+ Project-URL: Issues, https://github.com/ali-dev178/claudometer/issues
10
+ Keywords: claude,claude-code,anthropic,usage,taskbar,menubar,widget,tray
11
+ Classifier: Environment :: Win32 (MS Windows)
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Environment :: X11 Applications
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: requests>=2.31
25
+ Requires-Dist: pystray>=0.19; sys_platform != "darwin"
26
+ Requires-Dist: Pillow>=10.1; sys_platform != "darwin"
27
+ Requires-Dist: rumps>=0.4; sys_platform == "darwin"
28
+ Dynamic: license-file
29
+
30
+ <p align="center"><img src="assets/icon.png" width="84" alt="Claudometer"></p>
31
+ <h1 align="center">Claudometer</h1>
32
+
33
+ <p align="center">
34
+ <b>Your Claude usage limits, always visible — right on your taskbar.</b><br>
35
+ A tiny, elegant desktop widget that shows your live session &amp; weekly usage so you never hit a limit by surprise.
36
+ </p>
37
+
38
+ <p align="center">
39
+ <img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-informational" alt="platform">
40
+ <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="python">
41
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="license">
42
+ <img src="https://img.shields.io/badge/setup-zero%20config-success" alt="zero config">
43
+ </p>
44
+
45
+ <p align="center">
46
+ <img src="assets/hero-windows.png" alt="Claudometer on Windows" width="820">
47
+ </p>
48
+
49
+ > **Unofficial project — not affiliated with, or endorsed by, Anthropic.** See the [disclaimer](#-disclaimer).
50
+
51
+ ---
52
+
53
+ ## The problem
54
+
55
+ Claude's plans (Pro / Max / Team) enforce **usage limits** — a rolling **5‑hour session** limit and **weekly** limits. If you use Claude heavily (Claude Code, long sessions), it's easy to burn through them without realizing… until you're suddenly rate‑limited in the middle of something important.
56
+
57
+ Today, checking where you stand means **opening the `/usage` panel or the app and reading it** — a context switch you have to *remember* to do. There's no ambient, at‑a‑glance signal.
58
+
59
+ ## The solution
60
+
61
+ **Claudometer keeps your usage on‑screen at all times**, as clean floating text on your taskbar:
62
+
63
+ <p align="center"><img src="assets/strip.png" alt="Taskbar strip" width="300"></p>
64
+
65
+ - **`Session 61%`** — how much of your current 5‑hour window is used, with a **live countdown** to reset (`1h 21m left`).
66
+ - **`Weekly 18%`** — your 7‑day all‑models usage.
67
+ - A **color‑coded status dot** (🟢 &lt;50% · 🟡 50–80% · 🔴 &gt;80%) so severity registers in a glance — turning to a clear **"limit reached"** when you're maxed out, plus a graceful *offline* state when there's no data:
68
+
69
+ <p align="center"><img src="assets/strip-states.png" alt="Color-coded severity and offline states" width="500"></p>
70
+
71
+ **Click it** for a polished breakdown with per‑meter reset times and per‑model (e.g. Fable) usage:
72
+
73
+ <p align="center"><img src="assets/popover-themes.png" alt="Light and dark popover" width="820"></p>
74
+
75
+ ### Why you'll want it
76
+ - 🎯 **Pace yourself** — see when you're approaching a limit *before* you hit it.
77
+ - ⚡ **Zero context‑switch** — the number is already there; no panel to open.
78
+ - 🔒 **Zero setup** — reuses your existing Claude login. Nothing to configure.
79
+ - 🪶 **Featherweight** — ~0.03% CPU idle, ~50 MB RAM. You won't notice it.
80
+ - 🖥️ **Stays out of the way** — optionally auto‑hides over fullscreen movies, games, and presentations (or set it to always show).
81
+ - 🔔 **Warns you in time** — optional desktop alerts when you cross 80% / 90%.
82
+ - ⏭️ **Picks up where you left off** — when your session limit resets, one click resumes the interrupted work (or auto‑resume, if you opt in).
83
+ - 🎨 **Looks the part** — supersampled rendering, light/dark aware, adapts to your taskbar.
84
+ - ⚙️ **Yours to tune** — a built‑in **settings panel** (no file editing) for interval, theme, meters, alerts, accent, cost view, fullscreen behavior and resume.
85
+
86
+ ---
87
+
88
+ ## Screenshots
89
+
90
+ **Windows** — floating strip on the taskbar + click‑to‑open popover with usage meters:
91
+
92
+ <p align="center"><img src="assets/hero-windows.png" alt="Windows" width="760"></p>
93
+
94
+ **macOS** — native menu‑bar item with a dropdown breakdown:
95
+
96
+ <p align="center"><img src="assets/macos-menubar.png" alt="macOS menu bar" width="760"></p>
97
+
98
+ **Threshold alerts** — a desktop toast the moment you cross a limit you set (default 80% and 90%), for both session and weekly:
99
+
100
+ <p align="center"><img src="assets/alerts.png" alt="Threshold alert toasts" width="760"></p>
101
+
102
+ **Estimated cost** *(opt‑in)* — turn on `show_cost` for a today's‑tokens and rough‑dollar line in the popover (a local estimate from your session logs — not a bill):
103
+
104
+ <p align="center"><img src="assets/popover-cost.png" alt="Estimated cost line in the popover" width="430"></p>
105
+
106
+ **Always‑visible mode** — set `hide_on_fullscreen = false` and your stats stay readable even over a fullscreen movie, game, or presentation:
107
+
108
+ <p align="center"><img src="assets/fullscreen.png" alt="Visible over a fullscreen movie" width="760"></p>
109
+
110
+ ---
111
+
112
+ ## Resume when your limit resets
113
+
114
+ Hit the 5‑hour session limit mid‑task and everything grinds to a halt? Claudometer
115
+ watches your usage recover, so it can help you pick right back up the moment it does.
116
+
117
+ <p align="center"><img src="assets/resume.png" alt="Resume notifications" width="820"></p>
118
+
119
+ - **Tier 1 — notify + one click** *(default, safe).* When your session resets, a
120
+ notification appears; click **Resume** and it opens a terminal in the interrupted
121
+ session's folder running `claude --resume <id>` for you to continue — supervised.
122
+ - **Tier 2 — auto‑resume** *(opt‑in, off by default).* After a short *"resuming in
123
+ 20s — click to cancel"* window, it resumes **unattended and headless** so work
124
+ continues while you're away.
125
+
126
+ > ⚠️ **Tier 2 runs Claude Code with nobody watching.** It's gated behind
127
+ > `resume_auto = true` and ships with guard rails: a turn cap (`--max-turns`) and
128
+ > the safer `acceptEdits` permission mode by default (full
129
+ > `--dangerously-skip-permissions` only if you *also* set
130
+ > `resume_skip_permissions = true`). Enable it only for work you trust to run on
131
+ > its own. Output is written to a log in `~/.claude/`.
132
+
133
+ Configure both in the [config file](#configuration).
134
+
135
+ ---
136
+
137
+ ## Install
138
+
139
+ You need a **Claude Pro / Max / Team** subscription and to have **signed into Claude Code** at least once (that's where the credentials live).
140
+
141
+ > **Heads up:** the `pipx`, `scoop`, `brew`, and download methods below light up once the **first release is published** (a one‑time maintainer step — see [`packaging/`](packaging/README.md)). Until then, **[install from source](#from-source-python-39)** — that always works.
142
+
143
+ ### Easiest — one command (any OS) 🏆
144
+ Installs in an isolated environment and puts `claudometer` on your PATH:
145
+ ```bash
146
+ pipx install claudometer # don't have pipx? → python -m pip install --user pipx
147
+ claudometer # launch it
148
+ ```
149
+ Update anytime with `pipx upgrade claudometer`.
150
+
151
+ ### Windows
152
+ ```powershell
153
+ scoop install https://raw.githubusercontent.com/ali-dev178/claudometer/main/packaging/scoop/claudometer.json
154
+ ```
155
+ …or grab the **installer** (`ClaudometerSetup.exe`, ticks "start on sign‑in" for you) or the portable **`Claudometer.exe`** from
156
+ [**Releases**](https://github.com/ali-dev178/claudometer/releases) and double‑click.
157
+
158
+ ### macOS
159
+ ```bash
160
+ brew install --cask ali-dev178/claudometer/claudometer
161
+ ```
162
+ …or download **`Claudometer.dmg`** from [**Releases**](https://github.com/ali-dev178/claudometer/releases) and drag it to Applications.
163
+
164
+ ### From source (Python 3.9+)
165
+ ```bash
166
+ git clone https://github.com/ali-dev178/claudometer.git && cd claudometer
167
+ pip install -r requirements.txt
168
+ pythonw.exe app.py bar # Windows (no console) · python3 app.py # macOS
169
+ ```
170
+
171
+ > **Unsigned builds:** the downloads aren't code‑signed yet, so on first launch Windows SmartScreen ("More info → Run anyway") or macOS Gatekeeper (right‑click → **Open**) may ask you to confirm. `pipx`, `scoop` and `brew` avoid this.
172
+ >
173
+ > **First‑run tip (Windows):** Windows 11 may tuck new taskbar items away — drag Claudometer where you want it; it remembers the spot.
174
+
175
+ ## Usage
176
+
177
+ | Command | What you get |
178
+ |---|---|
179
+ | `app.py bar` | **Windows:** floating taskbar strip + popover *(recommended)* |
180
+ | `app.py tray` | **Windows/Linux:** notification‑area tray icon |
181
+ | `app.py both` | **Windows:** taskbar strip **and** tray icon |
182
+ | `app.py` | Default — **macOS:** menu bar · **Windows:** taskbar strip · **Linux:** tray |
183
+
184
+ **Interactions (taskbar strip):** left‑click = open/close popover · drag = move (remembered) · right‑click = Details / Refresh / Quit.
185
+
186
+ ## Auto‑start on login
187
+
188
+ **Windows** — press `Win`+`R`, run `shell:startup`, and add a shortcut to:
189
+ ```
190
+ pythonw.exe "C:\path\to\claudometer\app.py" bar
191
+ ```
192
+
193
+ **macOS** — add a LaunchAgent at `~/Library/LaunchAgents/com.claudometer.plist`:
194
+ ```xml
195
+ <?xml version="1.0" encoding="UTF-8"?>
196
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
197
+ <plist version="1.0"><dict>
198
+ <key>Label</key><string>com.claudometer</string>
199
+ <key>ProgramArguments</key>
200
+ <array><string>/usr/bin/python3</string><string>/absolute/path/to/claudometer/app.py</string></array>
201
+ <key>RunAtLoad</key><true/>
202
+ </dict></plist>
203
+ ```
204
+ then `launchctl load ~/Library/LaunchAgents/com.claudometer.plist`. (This plist is
205
+ for the from-source install; if you use the standalone `Claudometer.app`, just add
206
+ it to **System Settings → General → Login Items** instead.)
207
+
208
+ ## Configuration
209
+
210
+ Two ways to set things up — use whichever you prefer:
211
+
212
+ ### ⚙ In‑app settings panel (recommended)
213
+
214
+ Click **⚙ Settings** in the popover (or right‑click the strip → *Settings…*). Adjust
215
+ theme, meters, accent, poll interval, alerts, the cost view, fullscreen behavior and
216
+ resume — changes apply **immediately** (no restart) and are written to
217
+ `~/.claudometer.toml` for you, so you never have to touch the file.
218
+
219
+ <p align="center"><img src="assets/settings.png" alt="In-app settings panel (light & dark)" width="820"></p>
220
+
221
+ > **macOS:** the menu‑bar app is a lighter adapter — its **Settings** submenu exposes
222
+ > what it actually honors (which meters, poll interval) plus **Open config file…** for
223
+ > the rest. Alerts, cost, resume, accent and theme currently apply to the **Windows**
224
+ > strip only (see [Platform support](#platform-support)).
225
+
226
+ ### ✎ Or edit the file by hand
227
+
228
+ Everything works with no config. To customise manually, copy
229
+ [`claudometer.example.toml`](claudometer.example.toml) to `~/.claudometer.toml`:
230
+
231
+ ```toml
232
+ poll = 90 # seconds between polls (60–300)
233
+ theme = "auto" # auto | light | dark
234
+ metrics = ["session", "weekly"] # which meters on the strip
235
+ hide_on_fullscreen = true # false = keep visible even over fullscreen apps
236
+ alerts = true # desktop toast on threshold crossings
237
+ alert_thresholds = [80, 90]
238
+ show_cost = false # estimated token/$ line in the popover
239
+ # accent = "#d97757" # override the accent color
240
+
241
+ resume_notify = true # one-click resume when the session limit resets
242
+ resume_auto = false # Tier 2: unattended auto-resume (opt-in, risky)
243
+ resume_prompt = "Continue where you left off."
244
+ resume_max_turns = 30 # Tier 2: cap agentic turns
245
+ # resume_skip_permissions = false # Tier 2: --dangerously-skip-permissions (else acceptEdits)
246
+ ```
247
+
248
+ Environment overrides:
249
+
250
+ | Env var | Purpose |
251
+ |---|---|
252
+ | `CLAUDOMETER_CONFIG` | Path to the config file (default `~/.claudometer.toml`). |
253
+ | `CLAUDE_CONFIG_DIR` | Where to read Claude credentials/transcripts (default `~/.claude`). |
254
+ | `CLAUDE_WIDGET_POLL` | Poll interval in seconds (60–300). The interval source for the Windows/Linux **tray** (the macOS menu bar polls at a fixed 90s). |
255
+ | `CLAUDE_WIDGET_FAKE` | Testing: `"95,40,0"` = session,weekly,scoped % (skips the network). |
256
+
257
+ Preview the red/alert state with no real load:
258
+ ```powershell
259
+ $env:CLAUDE_WIDGET_FAKE="95,40,0"; py app.py bar
260
+ ```
261
+
262
+ ---
263
+
264
+ ## How it works
265
+
266
+ Claudometer reads the OAuth token Claude Code already stores locally
267
+ (`~/.claude/.credentials.json`, or the macOS Keychain) and polls Anthropic's
268
+ **server‑reported** usage endpoint — the same source the `/usage` panel uses:
269
+
270
+ ```
271
+ GET https://api.anthropic.com/api/oauth/usage
272
+ ```
273
+
274
+ The response maps directly onto the UI: the 5‑hour window → **Session**, the
275
+ 7‑day all‑models window → **Weekly**, and any per‑model scoped limits → per‑model
276
+ rows (e.g. **Fable**), shown only when your account actually has one. Tokens are
277
+ refreshed automatically when they expire.
278
+
279
+ > This is *true plan %* from Anthropic's backend — **not** a local token‑cost
280
+ > estimate (unlike tools that add up `*.jsonl` transcript costs).
281
+
282
+ ### Privacy
283
+ - Your token is read **locally**; the only network call is the authenticated
284
+ request to `api.anthropic.com`.
285
+ - **No** third‑party servers, **no** telemetry, **no** analytics.
286
+
287
+ ### Behavior notes
288
+ - **Fullscreen auto‑hide** (optional) — by default Claudometer hides itself
289
+ whenever a fullscreen app is active (movies, games, presentations), using
290
+ Windows' own `SHQueryUserNotificationState` plus a foreground‑covers‑the‑screen
291
+ check, then reappears when you exit. Set `hide_on_fullscreen = false` to keep it
292
+ visible even over fullscreen apps. On macOS the menu bar hides in fullscreen
293
+ natively.
294
+ - **Performance** — ~0.03% of total CPU idle, ~0.2% while you have the popover
295
+ open, ~50 MB RAM. The strip only re‑renders when a value changes; the popover
296
+ only while it's open.
297
+
298
+ ## Platform support
299
+
300
+ | Platform | UI | Status |
301
+ |---|---|---|
302
+ | **Windows 10/11** | Taskbar strip + click‑to‑open popover | ✅ Full |
303
+ | **macOS** | Menu‑bar item + dropdown | ✅ Menu bar |
304
+ | **Linux** | Notification‑area tray icon | 🧪 Experimental (`app.py tray`) |
305
+
306
+ > **Feature scope:** the taskbar strip (Windows) has the full feature set —
307
+ > click‑to‑open popover, alerts, estimated cost, resume‑on‑reset, themes, accent,
308
+ > and the config file. The macOS menu bar and Linux tray currently show live
309
+ > usage only. Unifying these is on the roadmap.
310
+
311
+ ## Roadmap
312
+
313
+ **Shipped:** ✅ desktop alerts · ✅ config file · ✅ estimated cost view · ✅ standalone binaries + release CI.
314
+
315
+ **Next up:**
316
+ - 📈 A tiny **usage sparkline** over the session
317
+ - 🍎 Unified floating popover on macOS (currently a native menu‑bar item)
318
+ - 🧮 Per‑model cost breakdown & weekly totals
319
+ - 📥 Published **winget** / **Homebrew** listings
320
+
321
+ Contributions and ideas welcome — open an issue or PR.
322
+
323
+ ## Contributing
324
+
325
+ ```bash
326
+ py -m pip install -r requirements.txt
327
+ py app.py bar
328
+ ```
329
+ `usage_core.py` holds the data/auth logic (no UI deps), `render.py` does all the
330
+ Pillow drawing, `settings.py` / `cost.py` / `resume.py` add config, cost
331
+ estimation, and session‑resume, and the platform adapters (`widget_bar.py`,
332
+ `menubar_mac.py`, `tray_windows.py`) are thin. Regenerate the README images with
333
+ `py assets/make_assets.py`.
334
+
335
+ ## ⚠️ Disclaimer
336
+
337
+ Claudometer is an **independent, unofficial** tool. It is **not affiliated with,
338
+ authorized, or endorsed by Anthropic**. It relies on an **undocumented** usage
339
+ endpoint that may change or break at any time, and reads the local Claude Code
340
+ credentials on your own machine. Use at your own risk, and in accordance with
341
+ Anthropic's Terms of Service. "Claude" is a trademark of Anthropic, PBC.
342
+
343
+ ## License
344
+
345
+ [MIT](LICENSE) © 2026 Muhammad Ali
@@ -0,0 +1,16 @@
1
+ app.py,sha256=tElOL5h3rFAqlj1y9daAdBneohxyYLp0D6la_VTNDWY,1652
2
+ config.py,sha256=qIGkwX5RQRwFXsA94sD3So3RxqQGp8xu31dlEed9_vI,1308
3
+ cost.py,sha256=yfppGKOXaSALLk1sLK-9cn6hGaneXN-MuF0YwiwMPMs,5136
4
+ menubar_mac.py,sha256=CIlyy33WGdU2H9-fwd56IusDurG8MzhdTOsXgROWRB4,5840
5
+ render.py,sha256=t3k813bx7kWG1bGrf8ySUVRf3rgs_BM5xPG6cHhs9tI,23571
6
+ resume.py,sha256=CW5Gav2If0YwB0SdgiHA3pGSDIdGzJuMBJncD2hOlQU,5813
7
+ settings.py,sha256=acFCRKGE8kJq6K3k5tsnWzul1-SJ1n8_u8yBn08dmPw,10542
8
+ tray_windows.py,sha256=I8-GJoIIiQxd9BhYgqK8tpddUhR8jJ3YE6YBBP4vIRM,5103
9
+ usage_core.py,sha256=PvqU8KxU1G_9N-J8fqmwTfttQQeP4Tpa2F1H1bsG6Pw,22060
10
+ widget_bar.py,sha256=g5aYelvFAtZl2b6Dfu_s_wtpvD_55nG_4Wk_HMTLWes,51666
11
+ claudometer-1.0.0.dist-info/licenses/LICENSE,sha256=3G7a7s2gyvMvNmAgBZYlqTBRP70e2CkHhxRtAMpOY5M,1069
12
+ claudometer-1.0.0.dist-info/METADATA,sha256=uFTNUrp201wOmWks30VUnl0vKePHJVyLVXmIncSGt48,16330
13
+ claudometer-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
+ claudometer-1.0.0.dist-info/entry_points.txt,sha256=wBNAagEQT7imBzJYaQTX4naex-VeNA6ZGHBzSE8D1Ds,37
15
+ claudometer-1.0.0.dist-info/top_level.txt,sha256=RdiM4_6nk1E63UlhhgmsiN89Cadz8JABDim1GLqlf8o,86
16
+ claudometer-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [gui_scripts]
2
+ claudometer = app:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Ali
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,10 @@
1
+ app
2
+ config
3
+ cost
4
+ menubar_mac
5
+ render
6
+ resume
7
+ settings
8
+ tray_windows
9
+ usage_core
10
+ widget_bar
config.py ADDED
@@ -0,0 +1,35 @@
1
+ """Static configuration for the Claude usage widget."""
2
+
3
+ # This app.
4
+ APP_VERSION = "1.0.0"
5
+ REPO_URL = "https://github.com/ali-dev178/claudometer"
6
+
7
+ # Server-reported plan-usage endpoint (same one Claude Code's /usage panel calls).
8
+ USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
9
+
10
+ # OAuth token-refresh endpoints. Anthropic is mid-migration from console -> platform,
11
+ # so we try console first and fall back to platform on connection error / 404.
12
+ TOKEN_URLS = [
13
+ "https://console.anthropic.com/v1/oauth/token",
14
+ "https://platform.claude.com/v1/oauth/token",
15
+ ]
16
+
17
+ # Well-known Claude Code OAuth client id (public).
18
+ OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
19
+
20
+ # Required beta header for the OAuth usage endpoint.
21
+ BETA_HEADER = "oauth-2025-04-20"
22
+
23
+ # Network timeout (seconds) for both the usage call and token refresh.
24
+ HTTP_TIMEOUT = 15
25
+
26
+ # Default poll interval (seconds). Overridable via env CLAUDE_WIDGET_POLL,
27
+ # clamped to [60, 300]. Community tools use 90-180s to stay under rate limits.
28
+ DEFAULT_POLL = 90
29
+
30
+ # Refresh the access token this many ms before it expires.
31
+ REFRESH_SKEW_MS = 5 * 60 * 1000
32
+
33
+ # Used in the mandatory "User-Agent: claude-code/<version>" header when the real
34
+ # installed version can't be read. A generic UA gets persistent HTTP 429s.
35
+ FALLBACK_VERSION = "2.1.173"
cost.py ADDED
@@ -0,0 +1,121 @@
1
+ """Estimate today's token usage and cost from local Claude Code transcripts.
2
+
3
+ Reads ``~/.claude/projects/**/*.jsonl`` (the session logs Claude Code writes),
4
+ sums per-message token counts, and applies Anthropic list prices. This is an
5
+ ESTIMATE of what the same tokens would cost at API rates — it is not your actual
6
+ plan billing (Pro/Max are flat-rate).
7
+
8
+ Prices: USD per million tokens, verified from the official pricing page
9
+ (platform.claude.com) on 2026-07-10. `cache_write` = 5-minute cache write
10
+ (1.25x input); `cache_read` = cache hit (0.1x input). Prices are matched by model
11
+ family (opus/sonnet/haiku/fable) and assume the current generation — a future
12
+ model sharing a family name but priced differently would use these rates.
13
+ """
14
+
15
+ import json
16
+ import os
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+ PRICING = {
21
+ "opus": {"input": 5, "output": 25, "cache_write": 6.25, "cache_read": 0.50},
22
+ "sonnet": {"input": 3, "output": 15, "cache_write": 3.75, "cache_read": 0.30},
23
+ "haiku": {"input": 1, "output": 5, "cache_write": 1.25, "cache_read": 0.10},
24
+ "fable": {"input": 10, "output": 50, "cache_write": 12.50, "cache_read": 1.00},
25
+ }
26
+
27
+
28
+ def _key_for(model: str):
29
+ m = str(model or "").lower()
30
+ if "fable" in m or "mythos" in m: # Mythos 5 shares Fable 5 pricing
31
+ return "fable"
32
+ if "opus" in m:
33
+ return "opus"
34
+ if "haiku" in m:
35
+ return "haiku"
36
+ if "sonnet" in m:
37
+ return "sonnet"
38
+ return None
39
+
40
+
41
+ def _n(usage: dict, k: str) -> float:
42
+ v = usage.get(k)
43
+ return v if isinstance(v, (int, float)) and not isinstance(v, bool) else 0
44
+
45
+
46
+ def _tok(usage: dict) -> float:
47
+ return (_n(usage, "input_tokens") + _n(usage, "output_tokens")
48
+ + _n(usage, "cache_creation_input_tokens") + _n(usage, "cache_read_input_tokens"))
49
+
50
+
51
+ def _line_cost(usage: dict, key: str) -> float:
52
+ p = PRICING[key]
53
+ return (_n(usage, "input_tokens") * p["input"]
54
+ + _n(usage, "output_tokens") * p["output"]
55
+ + _n(usage, "cache_creation_input_tokens") * p["cache_write"]
56
+ + _n(usage, "cache_read_input_tokens") * p["cache_read"]) / 1_000_000.0
57
+
58
+
59
+ def compute_today(config_dir=None):
60
+ """Return {"tokens": int, "cost": float} for usage since local midnight,
61
+ or None if there's nothing to read."""
62
+ base = Path(config_dir or os.environ.get("CLAUDE_CONFIG_DIR") or (Path.home() / ".claude"))
63
+ proj = base / "projects"
64
+ if not proj.exists():
65
+ return None
66
+
67
+ start = datetime.now().astimezone().replace(hour=0, minute=0, second=0, microsecond=0)
68
+ cutoff = start.timestamp()
69
+ # Claude Code writes one line per content block for an assistant message,
70
+ # all sharing the message id. Input/cache tokens are identical across them
71
+ # but output_tokens GROW (streaming snapshots), so keep the LAST occurrence
72
+ # per id (the complete cumulative usage) rather than summing or keeping first.
73
+ by_id = {} # message id -> (usage, key), last wins
74
+ extra_tokens, extra_cost = 0, 0.0 # lines without an id (counted as-is)
75
+
76
+ for f in proj.rglob("*.jsonl"):
77
+ try:
78
+ if f.stat().st_mtime < cutoff: # file untouched today -> skip fast
79
+ continue
80
+ except OSError:
81
+ continue
82
+ try:
83
+ with f.open("r", encoding="utf-8") as fh:
84
+ for line in fh:
85
+ if '"usage"' not in line:
86
+ continue
87
+ try: # one malformed line must not abort the whole scan
88
+ obj = json.loads(line)
89
+ if obj.get("type") != "assistant":
90
+ continue
91
+ ts = obj.get("timestamp")
92
+ if isinstance(ts, str):
93
+ dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
94
+ if dt.tzinfo is None:
95
+ dt = dt.replace(tzinfo=timezone.utc)
96
+ if dt < start:
97
+ continue
98
+ else:
99
+ continue # no valid timestamp -> not counted as "today"
100
+ msg = obj.get("message", {})
101
+ key = _key_for(msg.get("model", "")) # may be None (unpriced)
102
+ u = msg.get("usage", {})
103
+ mid = msg.get("id")
104
+ if mid is not None:
105
+ by_id[mid] = (u, key) # tokens still count; cost only if key
106
+ else:
107
+ extra_tokens += _tok(u)
108
+ if key:
109
+ extra_cost += _line_cost(u, key)
110
+ except Exception:
111
+ continue
112
+ except OSError:
113
+ continue
114
+
115
+ total_tokens = extra_tokens + sum(_tok(u) for u, _ in by_id.values())
116
+ total_cost = extra_cost + sum(_line_cost(u, key) for u, key in by_id.values() if key)
117
+ return {"tokens": int(total_tokens), "cost": total_cost}
118
+
119
+
120
+ if __name__ == "__main__":
121
+ print(compute_today())