claude-dongle 1.0.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.
- claude_dongle-1.0.0/LICENSE +21 -0
- claude_dongle-1.0.0/PKG-INFO +222 -0
- claude_dongle-1.0.0/README.md +181 -0
- claude_dongle-1.0.0/claude_dongle/__init__.py +2 -0
- claude_dongle-1.0.0/claude_dongle/__main__.py +3 -0
- claude_dongle-1.0.0/claude_dongle/autostart.py +176 -0
- claude_dongle-1.0.0/claude_dongle/config.py +63 -0
- claude_dongle-1.0.0/claude_dongle/dashboard_ui.py +1044 -0
- claude_dongle-1.0.0/claude_dongle/dongle.py +473 -0
- claude_dongle-1.0.0/claude_dongle/history.py +201 -0
- claude_dongle-1.0.0/claude_dongle/main.py +75 -0
- claude_dongle-1.0.0/claude_dongle/monitor.py +210 -0
- claude_dongle-1.0.0/claude_dongle/notifier.py +202 -0
- claude_dongle-1.0.0/claude_dongle/projects.py +177 -0
- claude_dongle-1.0.0/claude_dongle/tray.py +42 -0
- claude_dongle-1.0.0/claude_dongle/usage_api.py +273 -0
- claude_dongle-1.0.0/claude_dongle/utils.py +60 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/PKG-INFO +222 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/SOURCES.txt +27 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/dependency_links.txt +1 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/entry_points.txt +2 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/requires.txt +1 -0
- claude_dongle-1.0.0/claude_dongle.egg-info/top_level.txt +1 -0
- claude_dongle-1.0.0/pyproject.toml +31 -0
- claude_dongle-1.0.0/setup.cfg +4 -0
- claude_dongle-1.0.0/tests/test_history.py +33 -0
- claude_dongle-1.0.0/tests/test_notifier.py +76 -0
- claude_dongle-1.0.0/tests/test_usage_api.py +51 -0
- claude_dongle-1.0.0/tests/test_utils.py +16 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Henrique
|
|
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,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-dongle
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Claude Code rate-limit monitor: floating dongle + dashboard (burn rate, overflow forecast, per-project usage).
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Pedro Henrique
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Repository, https://github.com/PedroHenrique0713/claude-dongle
|
|
28
|
+
Project-URL: Issues, https://github.com/PedroHenrique0713/claude-dongle/issues
|
|
29
|
+
Keywords: claude,claude-code,usage,rate-limit,monitor,tray
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
32
|
+
Classifier: Operating System :: MacOS
|
|
33
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
36
|
+
Requires-Python: >=3.9
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: PyQt6>=6.5
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
# claude-dongle
|
|
45
|
+
|
|
46
|
+
**Your Claude Code usage limits, live — without breaking your flow.**
|
|
47
|
+
|
|
48
|
+
A floating pill that shows how much of your usage windows you've burned
|
|
49
|
+
(5-hour session and weekly, per model) and predicts when you'll hit the wall.
|
|
50
|
+
It reads everything from Claude Code's own local token: no proxy, no extra
|
|
51
|
+
login, nothing sent anywhere.
|
|
52
|
+
|
|
53
|
+
<br>
|
|
54
|
+
|
|
55
|
+
<img src="docs/dongle.png" alt="the dongle" width="240">
|
|
56
|
+
|
|
57
|
+
<br><br>
|
|
58
|
+
|
|
59
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-5c8bff?style=flat-square" alt="License: MIT"></a>
|
|
60
|
+
<img src="https://img.shields.io/badge/python-3.9%2B-5c8bff?style=flat-square" alt="Python 3.9+">
|
|
61
|
+
<img src="https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-2b2b33?style=flat-square" alt="Platforms">
|
|
62
|
+
<img src="https://img.shields.io/badge/UI-PyQt6,%20hand--drawn-2b2b33?style=flat-square" alt="PyQt6">
|
|
63
|
+
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
> **Unofficial.** This project is not affiliated with Anthropic. It reads the
|
|
69
|
+
> same undocumented usage endpoint that powers Claude Code's own limit
|
|
70
|
+
> warnings (`api.anthropic.com/api/oauth/usage`) — if Anthropic changes it,
|
|
71
|
+
> the monitor may stop showing data until updated.
|
|
72
|
+
|
|
73
|
+
## ✨ Features
|
|
74
|
+
|
|
75
|
+
- **Always-on dongle** — a discreet pill in the corner with your session (5h),
|
|
76
|
+
week, and week-per-model usage. It appears only when it makes sense (e.g. with
|
|
77
|
+
your editor or terminal open) and hides when you're not working.
|
|
78
|
+
- **Overflow forecast** — computes your *burn rate* by regression over recent
|
|
79
|
+
usage and estimates the ETA to 100%. The border pulses amber when, at the
|
|
80
|
+
current pace, you'll run out before the reset.
|
|
81
|
+
- **Pace marker** — every ring has a tick for *where you'd be at a linear pace*.
|
|
82
|
+
Fill ahead of the tick = burning fast; behind it = comfortable. You read your
|
|
83
|
+
pace at a glance, no math.
|
|
84
|
+
- **Per-project & per-model usage** — from Claude Code's local logs, it shows
|
|
85
|
+
which projects and models ate your week (with a 14-day heatmap).
|
|
86
|
+
- **Limit notifications** — alerts when you cross a threshold and on the overflow
|
|
87
|
+
forecast. Works even with the dongle closed, via a background timer.
|
|
88
|
+
- **Cross-platform** — Linux, macOS and Windows, with native autostart on each.
|
|
89
|
+
|
|
90
|
+
## 🖼️ Preview
|
|
91
|
+
|
|
92
|
+
<div align="center">
|
|
93
|
+
<img src="docs/rings.gif" alt="usage rings animating" width="440">
|
|
94
|
+
<br>
|
|
95
|
+
<em>The usage gauges, live</em>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<table>
|
|
99
|
+
<tr>
|
|
100
|
+
<td width="50%"><img src="docs/dashboard.png" alt="dashboard"></td>
|
|
101
|
+
<td width="50%"><img src="docs/dashboard-full.png" alt="dashboard, expanded"></td>
|
|
102
|
+
</tr>
|
|
103
|
+
<tr>
|
|
104
|
+
<td align="center"><em>Dashboard</em></td>
|
|
105
|
+
<td align="center"><em>Forecast & per-project usage, expanded</em></td>
|
|
106
|
+
</tr>
|
|
107
|
+
</table>
|
|
108
|
+
|
|
109
|
+
<div align="center">
|
|
110
|
+
<img src="docs/notifications.png" alt="notifications" width="420">
|
|
111
|
+
<br>
|
|
112
|
+
<em>Limit and overflow-forecast alerts — they fire even with the dongle closed</em>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
## 📦 Installation
|
|
116
|
+
|
|
117
|
+
Requirements: **Python 3.9+** and **Claude Code** installed and logged in on the
|
|
118
|
+
machine.
|
|
119
|
+
|
|
120
|
+
With [pipx](https://pipx.pypa.io) (recommended — installs into an isolated env):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pipx install git+https://github.com/PedroHenrique0713/claude-dongle
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or with pip:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install --user git+https://github.com/PedroHenrique0713/claude-dongle
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
claude-dongle tray # open the dongle
|
|
136
|
+
claude-dongle setup # (optional) launch it automatically on login
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`setup` wires up autostart the native way on each OS — **systemd user** on Linux,
|
|
140
|
+
**LaunchAgent** on macOS, **Startup folder** on Windows. Undo it with
|
|
141
|
+
`claude-dongle uninstall`.
|
|
142
|
+
|
|
143
|
+
## ⚙️ Usage
|
|
144
|
+
|
|
145
|
+
| Command | What it does |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `claude-dongle tray` | open the floating dongle (normal use) |
|
|
148
|
+
| `claude-dongle status` | print the current state as JSON |
|
|
149
|
+
| `claude-dongle notify` | check the limits once and notify |
|
|
150
|
+
| `claude-dongle config` | open just the settings panel |
|
|
151
|
+
| `claude-dongle setup` | set up autostart on login |
|
|
152
|
+
| `claude-dongle uninstall` | remove autostart |
|
|
153
|
+
|
|
154
|
+
**Dongle interactions:** drag to reposition (it snaps to edges); click to open the
|
|
155
|
+
dashboard; middle-click to refresh now.
|
|
156
|
+
|
|
157
|
+
## 🔧 Configuration
|
|
158
|
+
|
|
159
|
+
Tune it from the panel or by editing `~/.config/claude-dongle/config.json`:
|
|
160
|
+
|
|
161
|
+
| Key | Default | Description |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `thresholds` | `[50, 70, 85, 95]` | percentages that trigger a notification |
|
|
164
|
+
| `show_mode` | `"dev"` | when to show the dongle: `always`, `claude`, `dev` or `custom` |
|
|
165
|
+
| `poll_interval` | `5` | seconds between dongle refreshes |
|
|
166
|
+
| `api_poll_interval` | `300` | minimum interval between API calls (the endpoint rate-limits aggressive polling) |
|
|
167
|
+
| `dongle_opacity` | `0.85` | dongle opacity (0 to 1) |
|
|
168
|
+
| `notify_on_threshold` | `true` | notify when a threshold is crossed |
|
|
169
|
+
| `notify_on_limit` | `true` | notify when 100% is reached |
|
|
170
|
+
| `forecast_notify` | `true` | notify on a predicted overflow before the reset |
|
|
171
|
+
| `reset_day` / `reset_time` / `reset_timezone` | `null` | manual weekly-reset fallback, used only if the API never answered (`null` timezone = system local) |
|
|
172
|
+
|
|
173
|
+
## 🔍 How it works
|
|
174
|
+
|
|
175
|
+
Claude Code keeps an OAuth token in `~/.claude/.credentials.json` (on macOS, in
|
|
176
|
+
the Keychain). claude-dongle uses that token to query Anthropic's usage endpoint
|
|
177
|
+
(`api.anthropic.com/api/oauth/usage`) — the same one that powers Claude Code's own
|
|
178
|
+
limit warnings. From there:
|
|
179
|
+
|
|
180
|
+
- `monitor` assembles the state; with no real source (API down and no cache) it
|
|
181
|
+
shows `--` instead of inventing a number.
|
|
182
|
+
- `history` keeps a local time series (SQLite) for the burn rate and forecast.
|
|
183
|
+
- `projects` aggregates tokens per project/model by reading the JSONL files in
|
|
184
|
+
`~/.claude/projects`.
|
|
185
|
+
- `dongle` and `dashboard` (PyQt6, hand-drawn) render everything; `notifier`
|
|
186
|
+
raises the alerts.
|
|
187
|
+
|
|
188
|
+
## 🔒 Privacy
|
|
189
|
+
|
|
190
|
+
Everything is local. The monitor **reads** Claude Code's token and talks
|
|
191
|
+
**directly** to Anthropic's official API — no data is sent to any third party, and
|
|
192
|
+
the token never leaves the machine nor gets rewritten (the monitor keeps what it
|
|
193
|
+
needs in its own cache, with owner-only file permissions, without touching Claude
|
|
194
|
+
Code's file).
|
|
195
|
+
|
|
196
|
+
## 🛠️ Development
|
|
197
|
+
|
|
198
|
+
Run from the repo without installing:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
./run.sh tray # Linux/macOS
|
|
202
|
+
python -m claude_dongle tray # any OS
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Run the tests:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install pytest
|
|
209
|
+
pytest tests/
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Regenerate the README assets (rendered by the app itself, offscreen, with
|
|
213
|
+
fictional data):
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
python scripts/gen_screenshots.py # dongle, dashboard, notifications
|
|
217
|
+
python scripts/gen_gif.py # the animated usage rings
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 📄 License
|
|
221
|
+
|
|
222
|
+
MIT © Pedro Henrique — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# claude-dongle
|
|
4
|
+
|
|
5
|
+
**Your Claude Code usage limits, live — without breaking your flow.**
|
|
6
|
+
|
|
7
|
+
A floating pill that shows how much of your usage windows you've burned
|
|
8
|
+
(5-hour session and weekly, per model) and predicts when you'll hit the wall.
|
|
9
|
+
It reads everything from Claude Code's own local token: no proxy, no extra
|
|
10
|
+
login, nothing sent anywhere.
|
|
11
|
+
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
<img src="docs/dongle.png" alt="the dongle" width="240">
|
|
15
|
+
|
|
16
|
+
<br><br>
|
|
17
|
+
|
|
18
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-5c8bff?style=flat-square" alt="License: MIT"></a>
|
|
19
|
+
<img src="https://img.shields.io/badge/python-3.9%2B-5c8bff?style=flat-square" alt="Python 3.9+">
|
|
20
|
+
<img src="https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-2b2b33?style=flat-square" alt="Platforms">
|
|
21
|
+
<img src="https://img.shields.io/badge/UI-PyQt6,%20hand--drawn-2b2b33?style=flat-square" alt="PyQt6">
|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
> **Unofficial.** This project is not affiliated with Anthropic. It reads the
|
|
28
|
+
> same undocumented usage endpoint that powers Claude Code's own limit
|
|
29
|
+
> warnings (`api.anthropic.com/api/oauth/usage`) — if Anthropic changes it,
|
|
30
|
+
> the monitor may stop showing data until updated.
|
|
31
|
+
|
|
32
|
+
## ✨ Features
|
|
33
|
+
|
|
34
|
+
- **Always-on dongle** — a discreet pill in the corner with your session (5h),
|
|
35
|
+
week, and week-per-model usage. It appears only when it makes sense (e.g. with
|
|
36
|
+
your editor or terminal open) and hides when you're not working.
|
|
37
|
+
- **Overflow forecast** — computes your *burn rate* by regression over recent
|
|
38
|
+
usage and estimates the ETA to 100%. The border pulses amber when, at the
|
|
39
|
+
current pace, you'll run out before the reset.
|
|
40
|
+
- **Pace marker** — every ring has a tick for *where you'd be at a linear pace*.
|
|
41
|
+
Fill ahead of the tick = burning fast; behind it = comfortable. You read your
|
|
42
|
+
pace at a glance, no math.
|
|
43
|
+
- **Per-project & per-model usage** — from Claude Code's local logs, it shows
|
|
44
|
+
which projects and models ate your week (with a 14-day heatmap).
|
|
45
|
+
- **Limit notifications** — alerts when you cross a threshold and on the overflow
|
|
46
|
+
forecast. Works even with the dongle closed, via a background timer.
|
|
47
|
+
- **Cross-platform** — Linux, macOS and Windows, with native autostart on each.
|
|
48
|
+
|
|
49
|
+
## 🖼️ Preview
|
|
50
|
+
|
|
51
|
+
<div align="center">
|
|
52
|
+
<img src="docs/rings.gif" alt="usage rings animating" width="440">
|
|
53
|
+
<br>
|
|
54
|
+
<em>The usage gauges, live</em>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<table>
|
|
58
|
+
<tr>
|
|
59
|
+
<td width="50%"><img src="docs/dashboard.png" alt="dashboard"></td>
|
|
60
|
+
<td width="50%"><img src="docs/dashboard-full.png" alt="dashboard, expanded"></td>
|
|
61
|
+
</tr>
|
|
62
|
+
<tr>
|
|
63
|
+
<td align="center"><em>Dashboard</em></td>
|
|
64
|
+
<td align="center"><em>Forecast & per-project usage, expanded</em></td>
|
|
65
|
+
</tr>
|
|
66
|
+
</table>
|
|
67
|
+
|
|
68
|
+
<div align="center">
|
|
69
|
+
<img src="docs/notifications.png" alt="notifications" width="420">
|
|
70
|
+
<br>
|
|
71
|
+
<em>Limit and overflow-forecast alerts — they fire even with the dongle closed</em>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
## 📦 Installation
|
|
75
|
+
|
|
76
|
+
Requirements: **Python 3.9+** and **Claude Code** installed and logged in on the
|
|
77
|
+
machine.
|
|
78
|
+
|
|
79
|
+
With [pipx](https://pipx.pypa.io) (recommended — installs into an isolated env):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pipx install git+https://github.com/PedroHenrique0713/claude-dongle
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or with pip:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install --user git+https://github.com/PedroHenrique0713/claude-dongle
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude-dongle tray # open the dongle
|
|
95
|
+
claude-dongle setup # (optional) launch it automatically on login
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`setup` wires up autostart the native way on each OS — **systemd user** on Linux,
|
|
99
|
+
**LaunchAgent** on macOS, **Startup folder** on Windows. Undo it with
|
|
100
|
+
`claude-dongle uninstall`.
|
|
101
|
+
|
|
102
|
+
## ⚙️ Usage
|
|
103
|
+
|
|
104
|
+
| Command | What it does |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `claude-dongle tray` | open the floating dongle (normal use) |
|
|
107
|
+
| `claude-dongle status` | print the current state as JSON |
|
|
108
|
+
| `claude-dongle notify` | check the limits once and notify |
|
|
109
|
+
| `claude-dongle config` | open just the settings panel |
|
|
110
|
+
| `claude-dongle setup` | set up autostart on login |
|
|
111
|
+
| `claude-dongle uninstall` | remove autostart |
|
|
112
|
+
|
|
113
|
+
**Dongle interactions:** drag to reposition (it snaps to edges); click to open the
|
|
114
|
+
dashboard; middle-click to refresh now.
|
|
115
|
+
|
|
116
|
+
## 🔧 Configuration
|
|
117
|
+
|
|
118
|
+
Tune it from the panel or by editing `~/.config/claude-dongle/config.json`:
|
|
119
|
+
|
|
120
|
+
| Key | Default | Description |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `thresholds` | `[50, 70, 85, 95]` | percentages that trigger a notification |
|
|
123
|
+
| `show_mode` | `"dev"` | when to show the dongle: `always`, `claude`, `dev` or `custom` |
|
|
124
|
+
| `poll_interval` | `5` | seconds between dongle refreshes |
|
|
125
|
+
| `api_poll_interval` | `300` | minimum interval between API calls (the endpoint rate-limits aggressive polling) |
|
|
126
|
+
| `dongle_opacity` | `0.85` | dongle opacity (0 to 1) |
|
|
127
|
+
| `notify_on_threshold` | `true` | notify when a threshold is crossed |
|
|
128
|
+
| `notify_on_limit` | `true` | notify when 100% is reached |
|
|
129
|
+
| `forecast_notify` | `true` | notify on a predicted overflow before the reset |
|
|
130
|
+
| `reset_day` / `reset_time` / `reset_timezone` | `null` | manual weekly-reset fallback, used only if the API never answered (`null` timezone = system local) |
|
|
131
|
+
|
|
132
|
+
## 🔍 How it works
|
|
133
|
+
|
|
134
|
+
Claude Code keeps an OAuth token in `~/.claude/.credentials.json` (on macOS, in
|
|
135
|
+
the Keychain). claude-dongle uses that token to query Anthropic's usage endpoint
|
|
136
|
+
(`api.anthropic.com/api/oauth/usage`) — the same one that powers Claude Code's own
|
|
137
|
+
limit warnings. From there:
|
|
138
|
+
|
|
139
|
+
- `monitor` assembles the state; with no real source (API down and no cache) it
|
|
140
|
+
shows `--` instead of inventing a number.
|
|
141
|
+
- `history` keeps a local time series (SQLite) for the burn rate and forecast.
|
|
142
|
+
- `projects` aggregates tokens per project/model by reading the JSONL files in
|
|
143
|
+
`~/.claude/projects`.
|
|
144
|
+
- `dongle` and `dashboard` (PyQt6, hand-drawn) render everything; `notifier`
|
|
145
|
+
raises the alerts.
|
|
146
|
+
|
|
147
|
+
## 🔒 Privacy
|
|
148
|
+
|
|
149
|
+
Everything is local. The monitor **reads** Claude Code's token and talks
|
|
150
|
+
**directly** to Anthropic's official API — no data is sent to any third party, and
|
|
151
|
+
the token never leaves the machine nor gets rewritten (the monitor keeps what it
|
|
152
|
+
needs in its own cache, with owner-only file permissions, without touching Claude
|
|
153
|
+
Code's file).
|
|
154
|
+
|
|
155
|
+
## 🛠️ Development
|
|
156
|
+
|
|
157
|
+
Run from the repo without installing:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
./run.sh tray # Linux/macOS
|
|
161
|
+
python -m claude_dongle tray # any OS
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Run the tests:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install pytest
|
|
168
|
+
pytest tests/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Regenerate the README assets (rendered by the app itself, offscreen, with
|
|
172
|
+
fictional data):
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python scripts/gen_screenshots.py # dongle, dashboard, notifications
|
|
176
|
+
python scripts/gen_gif.py # the animated usage rings
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 📄 License
|
|
180
|
+
|
|
181
|
+
MIT © Pedro Henrique — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Install/remove the dongle's login autostart, per operating system:
|
|
2
|
+
Linux (systemd user), macOS (LaunchAgent), Windows (Startup folder)."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_LABEL = "com.claudedongle.dongle"
|
|
10
|
+
# Pre-rename artifacts (the project used to be called claude-monitor):
|
|
11
|
+
# removed on install/uninstall so both autostarts never run side by side.
|
|
12
|
+
_LEGACY_LABEL = "com.claudemonitor.dongle"
|
|
13
|
+
_LEGACY_UNITS = ("claude-monitor.service", "claude-monitor-notify.service",
|
|
14
|
+
"claude-monitor-notify.timer")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _tray_cmd():
|
|
18
|
+
"""Command that starts the dongle. Prefers the installed console script;
|
|
19
|
+
falls back to the module. On Windows uses pythonw (no console window)."""
|
|
20
|
+
exe = shutil.which("claude-dongle")
|
|
21
|
+
if exe and sys.platform != "win32":
|
|
22
|
+
return [exe, "tray"]
|
|
23
|
+
py = sys.executable
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
pyw = Path(py).with_name("pythonw.exe")
|
|
26
|
+
py = str(pyw) if pyw.exists() else py
|
|
27
|
+
return [py, "-m", "claude_dongle", "tray"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------- Linux (systemd --user) ----------
|
|
31
|
+
|
|
32
|
+
def _linux_unit_dir():
|
|
33
|
+
return Path.home() / ".config" / "systemd" / "user"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cleanup_legacy_linux():
|
|
37
|
+
d = _linux_unit_dir()
|
|
38
|
+
if not any((d / u).exists() for u in _LEGACY_UNITS):
|
|
39
|
+
return
|
|
40
|
+
for unit in ("claude-monitor.service", "claude-monitor-notify.timer"):
|
|
41
|
+
subprocess.run(["systemctl", "--user", "disable", "--now", unit],
|
|
42
|
+
check=False, capture_output=True)
|
|
43
|
+
for f in _LEGACY_UNITS:
|
|
44
|
+
(d / f).unlink(missing_ok=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _install_linux():
|
|
48
|
+
_cleanup_legacy_linux()
|
|
49
|
+
d = _linux_unit_dir()
|
|
50
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
exec_start = " ".join(_tray_cmd())
|
|
52
|
+
(d / "claude-dongle.service").write_text(
|
|
53
|
+
"[Unit]\n"
|
|
54
|
+
"Description=Claude Code usage monitor — dongle\n"
|
|
55
|
+
"After=graphical-session.target\n"
|
|
56
|
+
"PartOf=graphical-session.target\n\n"
|
|
57
|
+
"[Service]\nType=simple\n"
|
|
58
|
+
f"ExecStart={exec_start}\n"
|
|
59
|
+
"Restart=on-failure\nRestartSec=5\n\n"
|
|
60
|
+
"[Install]\nWantedBy=graphical-session.target\n")
|
|
61
|
+
notify = " ".join(_tray_cmd()[:-1] + ["notify"])
|
|
62
|
+
(d / "claude-dongle-notify.service").write_text(
|
|
63
|
+
"[Unit]\nDescription=Claude Code usage monitor — limit check\n"
|
|
64
|
+
"After=graphical-session.target\n\n"
|
|
65
|
+
f"[Service]\nType=oneshot\nExecStart={notify}\n")
|
|
66
|
+
(d / "claude-dongle-notify.timer").write_text(
|
|
67
|
+
"[Unit]\nDescription=Checks Claude Code limits periodically\n\n"
|
|
68
|
+
"[Timer]\nOnBootSec=2min\nOnUnitActiveSec=10min\nPersistent=true\n\n"
|
|
69
|
+
"[Install]\nWantedBy=timers.target\n")
|
|
70
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
|
|
71
|
+
subprocess.run(["systemctl", "--user", "enable", "--now",
|
|
72
|
+
"claude-dongle.service"], check=False)
|
|
73
|
+
subprocess.run(["systemctl", "--user", "enable", "--now",
|
|
74
|
+
"claude-dongle-notify.timer"], check=False)
|
|
75
|
+
return "systemd user service enabled (starts on login)."
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _uninstall_linux():
|
|
79
|
+
_cleanup_legacy_linux()
|
|
80
|
+
for unit in ("claude-dongle.service", "claude-dongle-notify.timer"):
|
|
81
|
+
subprocess.run(["systemctl", "--user", "disable", "--now", unit],
|
|
82
|
+
check=False)
|
|
83
|
+
d = _linux_unit_dir()
|
|
84
|
+
for f in ("claude-dongle.service", "claude-dongle-notify.service",
|
|
85
|
+
"claude-dongle-notify.timer"):
|
|
86
|
+
(d / f).unlink(missing_ok=True)
|
|
87
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
|
|
88
|
+
return "systemd user service removed."
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------- macOS (LaunchAgent) ----------
|
|
92
|
+
|
|
93
|
+
def _mac_plist(label=_LABEL):
|
|
94
|
+
return Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _cleanup_legacy_mac():
|
|
98
|
+
p = _mac_plist(_LEGACY_LABEL)
|
|
99
|
+
if p.exists():
|
|
100
|
+
subprocess.run(["launchctl", "unload", str(p)], check=False,
|
|
101
|
+
capture_output=True)
|
|
102
|
+
p.unlink(missing_ok=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _install_mac():
|
|
106
|
+
_cleanup_legacy_mac()
|
|
107
|
+
p = _mac_plist()
|
|
108
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
args = "".join(f"<string>{a}</string>" for a in _tray_cmd())
|
|
110
|
+
p.write_text(
|
|
111
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
112
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
|
|
113
|
+
'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
|
114
|
+
'<plist version="1.0"><dict>\n'
|
|
115
|
+
f' <key>Label</key><string>{_LABEL}</string>\n'
|
|
116
|
+
f' <key>ProgramArguments</key><array>{args}</array>\n'
|
|
117
|
+
' <key>RunAtLoad</key><true/>\n'
|
|
118
|
+
' <key>KeepAlive</key><true/>\n'
|
|
119
|
+
'</dict></plist>\n')
|
|
120
|
+
subprocess.run(["launchctl", "unload", str(p)], check=False,
|
|
121
|
+
capture_output=True)
|
|
122
|
+
subprocess.run(["launchctl", "load", str(p)], check=False)
|
|
123
|
+
return "LaunchAgent installed (starts on login)."
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _uninstall_mac():
|
|
127
|
+
_cleanup_legacy_mac()
|
|
128
|
+
p = _mac_plist()
|
|
129
|
+
subprocess.run(["launchctl", "unload", str(p)], check=False,
|
|
130
|
+
capture_output=True)
|
|
131
|
+
p.unlink(missing_ok=True)
|
|
132
|
+
return "LaunchAgent removed."
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------- Windows (Startup folder) ----------
|
|
136
|
+
|
|
137
|
+
def _win_startup_bat(name="claude-dongle.bat"):
|
|
138
|
+
base = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
|
|
139
|
+
return (Path(base) / "Microsoft" / "Windows" / "Start Menu" /
|
|
140
|
+
"Programs" / "Startup" / name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _install_windows():
|
|
144
|
+
_win_startup_bat("claude-monitor.bat").unlink(missing_ok=True) # legacy
|
|
145
|
+
bat = _win_startup_bat()
|
|
146
|
+
bat.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
cmd = " ".join(f'"{a}"' if " " in a else a for a in _tray_cmd())
|
|
148
|
+
bat.write_text(f'@start "" {cmd}\r\n')
|
|
149
|
+
subprocess.Popen(_tray_cmd()) # start now, without waiting for next login
|
|
150
|
+
return "shortcut in the Startup folder (starts on login)."
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _uninstall_windows():
|
|
154
|
+
_win_startup_bat("claude-monitor.bat").unlink(missing_ok=True) # legacy
|
|
155
|
+
_win_startup_bat().unlink(missing_ok=True)
|
|
156
|
+
return "Startup folder shortcut removed."
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def install():
|
|
160
|
+
if sys.platform.startswith("linux"):
|
|
161
|
+
return _install_linux()
|
|
162
|
+
if sys.platform == "darwin":
|
|
163
|
+
return _install_mac()
|
|
164
|
+
if sys.platform == "win32":
|
|
165
|
+
return _install_windows()
|
|
166
|
+
raise RuntimeError(f"unsupported platform: {sys.platform}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def uninstall():
|
|
170
|
+
if sys.platform.startswith("linux"):
|
|
171
|
+
return _uninstall_linux()
|
|
172
|
+
if sys.platform == "darwin":
|
|
173
|
+
return _uninstall_mac()
|
|
174
|
+
if sys.platform == "win32":
|
|
175
|
+
return _uninstall_windows()
|
|
176
|
+
raise RuntimeError(f"unsupported platform: {sys.platform}")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR = Path.home() / ".config" / "claude-dongle"
|
|
6
|
+
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
7
|
+
# Pre-rename config dir (the project used to be called claude-monitor):
|
|
8
|
+
# copied once, on first load, so settings/history survive the rename.
|
|
9
|
+
LEGACY_CONFIG_DIR = Path.home() / ".config" / "claude-monitor"
|
|
10
|
+
|
|
11
|
+
DEFAULTS = {
|
|
12
|
+
# Manual fallback for the weekly reset, used only when the API never
|
|
13
|
+
# answered (it normally provides the real resets_at). null = unknown:
|
|
14
|
+
# the UI shows "--" instead of guessing. Example: "thursday", "18:00",
|
|
15
|
+
# "America/New_York" (reset_timezone null = system local timezone).
|
|
16
|
+
"reset_day": None,
|
|
17
|
+
"reset_time": None,
|
|
18
|
+
"reset_timezone": None,
|
|
19
|
+
"poll_interval": 5,
|
|
20
|
+
"thresholds": [50, 70, 85, 95],
|
|
21
|
+
"notify_on_threshold": True,
|
|
22
|
+
"notify_on_limit": True,
|
|
23
|
+
"telemetry_stale_minutes": 60, # no fresh data for X min → warn once
|
|
24
|
+
"claude_dir": str(Path.home() / ".claude"),
|
|
25
|
+
"dongle_opacity": 0.85,
|
|
26
|
+
"dongle_always_on_top": True,
|
|
27
|
+
"dongle_pos": None, # [x, y] of the last dragged position (null = default corner)
|
|
28
|
+
"show_mode": "dev",
|
|
29
|
+
"idle_quit_minutes": 10, # hidden for X min → the service exits (0 = never)
|
|
30
|
+
"show_processes": ["code", "claude"], # process names for show_mode=custom
|
|
31
|
+
"api_poll_interval": 300, # the usage endpoint rate-limits aggressive polling
|
|
32
|
+
"history_retention_days": 21, # burn-rate time series (GC'd on start)
|
|
33
|
+
"burn_lookback_minutes": 60, # burn-rate regression window
|
|
34
|
+
"burn_min_points": 3, # minimum points to show a forecast
|
|
35
|
+
"forecast_notify": True, # notify on predicted overflow before the reset
|
|
36
|
+
"forecast_expanded": False, # Forecast section starts collapsed
|
|
37
|
+
"projects_expanded": False, # "By project" section starts collapsed
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _migrate_legacy():
|
|
42
|
+
if CONFIG_DIR.exists() or not LEGACY_CONFIG_DIR.exists():
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
shutil.copytree(LEGACY_CONFIG_DIR, CONFIG_DIR)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load():
|
|
51
|
+
_migrate_legacy()
|
|
52
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
if CONFIG_PATH.exists():
|
|
54
|
+
merged = DEFAULTS.copy()
|
|
55
|
+
merged.update(json.loads(CONFIG_PATH.read_text()))
|
|
56
|
+
return merged
|
|
57
|
+
save(DEFAULTS)
|
|
58
|
+
return DEFAULTS.copy()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def save(cfg):
|
|
62
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
CONFIG_PATH.write_text(json.dumps(cfg, indent=2))
|