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.
Files changed (29) hide show
  1. claude_dongle-1.0.0/LICENSE +21 -0
  2. claude_dongle-1.0.0/PKG-INFO +222 -0
  3. claude_dongle-1.0.0/README.md +181 -0
  4. claude_dongle-1.0.0/claude_dongle/__init__.py +2 -0
  5. claude_dongle-1.0.0/claude_dongle/__main__.py +3 -0
  6. claude_dongle-1.0.0/claude_dongle/autostart.py +176 -0
  7. claude_dongle-1.0.0/claude_dongle/config.py +63 -0
  8. claude_dongle-1.0.0/claude_dongle/dashboard_ui.py +1044 -0
  9. claude_dongle-1.0.0/claude_dongle/dongle.py +473 -0
  10. claude_dongle-1.0.0/claude_dongle/history.py +201 -0
  11. claude_dongle-1.0.0/claude_dongle/main.py +75 -0
  12. claude_dongle-1.0.0/claude_dongle/monitor.py +210 -0
  13. claude_dongle-1.0.0/claude_dongle/notifier.py +202 -0
  14. claude_dongle-1.0.0/claude_dongle/projects.py +177 -0
  15. claude_dongle-1.0.0/claude_dongle/tray.py +42 -0
  16. claude_dongle-1.0.0/claude_dongle/usage_api.py +273 -0
  17. claude_dongle-1.0.0/claude_dongle/utils.py +60 -0
  18. claude_dongle-1.0.0/claude_dongle.egg-info/PKG-INFO +222 -0
  19. claude_dongle-1.0.0/claude_dongle.egg-info/SOURCES.txt +27 -0
  20. claude_dongle-1.0.0/claude_dongle.egg-info/dependency_links.txt +1 -0
  21. claude_dongle-1.0.0/claude_dongle.egg-info/entry_points.txt +2 -0
  22. claude_dongle-1.0.0/claude_dongle.egg-info/requires.txt +1 -0
  23. claude_dongle-1.0.0/claude_dongle.egg-info/top_level.txt +1 -0
  24. claude_dongle-1.0.0/pyproject.toml +31 -0
  25. claude_dongle-1.0.0/setup.cfg +4 -0
  26. claude_dongle-1.0.0/tests/test_history.py +33 -0
  27. claude_dongle-1.0.0/tests/test_notifier.py +76 -0
  28. claude_dongle-1.0.0/tests/test_usage_api.py +51 -0
  29. 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 &amp; 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 &amp; 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,2 @@
1
+ """claude-dongle — Claude Code rate-limit monitor (floating dongle + dashboard)."""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ main()
@@ -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))