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 +54 -0
- claudometer-1.0.0.dist-info/METADATA +345 -0
- claudometer-1.0.0.dist-info/RECORD +16 -0
- claudometer-1.0.0.dist-info/WHEEL +5 -0
- claudometer-1.0.0.dist-info/entry_points.txt +2 -0
- claudometer-1.0.0.dist-info/licenses/LICENSE +21 -0
- claudometer-1.0.0.dist-info/top_level.txt +10 -0
- config.py +35 -0
- cost.py +121 -0
- menubar_mac.py +153 -0
- render.py +587 -0
- resume.py +145 -0
- settings.py +287 -0
- tray_windows.py +146 -0
- usage_core.py +642 -0
- widget_bar.py +1326 -0
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 & 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** (🟢 <50% · 🟡 50–80% · 🔴 >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,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.
|
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())
|