clauddy 1.29.0 โ†’ 1.29.1

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 (3) hide show
  1. package/README.md +39 -339
  2. package/codex.js +112 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,378 +1,78 @@
1
1
  # ๐ŸŸซ Clauddy
2
2
 
3
- A cute pixel-art desktop pet for macOS that tracks your Claude Code usage โ€” mirroring the official **Settings โ†’ Usage** panel (current session + weekly limits, in tokens & %), with animations. Use Codex or Cursor too? It can track those alongside (see [Codex](#codex) and [Cursor](#cursor)).
3
+ A pixel-art desktop pet that keeps an eye on your Claude Code usage: the real session and weekly % from your account, the tokens from your local logs, and a pet that works, sleeps and catches fire along with it. It can follow Codex and Cursor too, and you can dress it as a pumpkin, a ghost or Santa.
4
4
 
5
5
  <p align="center">
6
-
7
- https://github.com/user-attachments/assets/be8d339c-ac69-4980-96a7-8e286f8b080f
8
-
9
- <em>A little terracotta creature that lives in the corner of your screen, eats your tokens, and naps when you're idle.</em>
6
+ <a href="#running">๐Ÿ“š Get started</a> ยท
7
+ <a href="docs/design/02-features.md">โœจ Features</a> ยท
8
+ <a href="docs/design/01-architecture.md">๐Ÿ—๏ธ Architecture</a> ยท
9
+ <a href="docs/design/03-setup.md">โš™๏ธ Setup</a> ยท
10
+ <a href="docs/decisions/">๐Ÿงญ Decisions</a>
10
11
  </p>
11
12
 
12
- ## What it shows
13
-
14
- - **Current session** โ€” real % used + **"resets in Xh Ym"** + session tokens, and a projection of where that pace is taking you (see [Burn rate](#burn-rate))
15
- - **Weekly ยท all models** โ€” real % used + tokens over the last 7 days
16
- - **Status line** under the pet: `โ— working ยท 1.6M tok/min` (or today's tokens when idle)
17
- - **By model ยท 7 days** โ€” Opus / Sonnet / Haiku / Fable, in tokens
18
- - **By project ยท 7 days** โ€” which repo actually ate the week, ranked, with the tail folded into `other`
19
- - **30-day map** โ€” colored squares by daily tokens (green = light โ†’ red = heavy), with the monthly total
20
-
21
- The **percentages are real**, pulled from your account (you log in once โ€” see below). The token counts, the by-model and by-project breakdowns, activity status, and 30-day map come from your local logs (`~/.claude/projects/**/*.jsonl`). Everything is token-based โ€” no dollars.
22
-
23
- ## Account & live usage
24
-
25
- The session/weekly **%** comes straight from your Anthropic account, so it matches the official panel exactly. You connect once via a browser login:
26
-
27
- 1. Open **โš™ Settings โ†’ Accounts โ†’ Claude โ†’ Connect** โ€” your browser opens an Anthropic auth page.
28
- 2. Log in, copy the **authentication code** shown, and paste it back into the app โ†’ **Connect**.
29
-
30
- The token is saved locally (see [Data & privacy](#data--privacy)) and refreshed automatically. **Until you connect**, the limits area shows a _"Connect your account"_ prompt instead of percentages.
31
-
32
- ### Several subscriptions
33
-
34
- Got more than one Claude account โ€” say a personal Pro and a Max from work? The **account chip** in the top-left corner is the switcher: click it for the list of accounts, with the active one marked, plus **"+ Add another account"** โ€” which opens the same browser login and drops the token it brings back into a new slot. The tray icon has the same list under its **Account** submenu. Give up halfway and the empty slot disappears on its own โ€” the list only ever holds accounts you actually logged into.
35
-
36
- The widget follows **one account at a time**: the one you pick is the one whose % is shown, whose logs are counted, and the only one that can notify you. Each account keeps its own token and its own armed alerts, so switching never replays a notification you already dismissed elsewhere. Everything else โ€” window position, display mode, zoom, thresholds โ€” is shared.
37
-
38
- Removing an account (the **ร—** on its row) deletes its token from disk. The one you're currently on can't be removed โ€” switch away first โ€” and neither can the last one left. Removing the first account clears its token without touching the settings that live in the same folder.
39
-
40
- > Prefer one widget per account instead? Setting `CLAUDE_CONFIG_DIR` still isolates a whole instance โ€” token, settings and logs โ€” so you can run two Clauddys side by side.
41
-
42
- ## Codex
43
-
44
- Clauddy can follow [Codex](https://openai.com/codex) (the CLI and the desktop app) next to Claude. Open **โš™ Settings โ†’ Accounts โ†’ Codex โ†’ Connect**: it looks for a Codex session on this computer and starts monitoring if it finds one. There's nothing to log into โ€” Codex writes its token counts and rate limits to `~/.codex/sessions` (or `$CODEX_HOME`), and Clauddy only reads those files.
45
-
46
- With both connected, **tabs under the pet** pick which service the whole panel shows โ€” account chip, pet mood, session and weekly %, the breakdowns, the 30-day map and the **โ†—** Usage link. The other tab keeps its % in view and gets a **!** when it crosses an alert threshold. Minimized, the ring becomes one reading per service; click one to switch.
47
-
48
- A few things differ from Claude:
49
-
50
- - **The % updates when Codex runs.** Its logs are only written during a session, so an idle Codex shows the last reading with its age (`read 1h 30m ago`). A window that has already reset since then shows **โ€”** rather than a made-up 0%.
51
- - **It can lag the account page.** Codex limits are shared with other ChatGPT products (Work, Agents, Excel) that don't write to these logs.
52
- - **No burn-rate projection and no activity scenes** โ€” the logs don't carry enough to draw them honestly.
53
-
54
- Disconnecting (the **ร—** on the Codex card) only stops monitoring in Clauddy; your Codex login and logs are untouched.
55
-
56
- ## Cursor
57
-
58
- Clauddy can follow [Cursor](https://cursor.com) too. Open **โš™ Settings โ†’ Accounts โ†’ Cursor โ†’ Connect**: it needs the Cursor app signed in on this computer, and starts monitoring once it finds that login. Clauddy reads the app's session token from its local storage and asks Cursor's own dashboard endpoints for your usage, the same numbers as the Usage page on cursor.com.
59
-
60
- With three services connected, the tabs under the pet show each logo and %, and the minimized dock gets a third line.
61
-
62
- Cursor works differently from the other two:
63
-
64
- - **Its budget is monthly.** The Cursor tab shows **this month ยท included** (the whole plan) and **API models** (the pool for named third-party models), both resetting on your billing date. There is no 5-hour session or weekly window.
65
- - **Activity scenes work.** Cursor's agent writes a transcript of every tool call under `~/.cursor/projects`, so the pet reads, edits, runs commands and researches along with it, as it does for Claude.
66
- - **No token counts.** Neither source reports tokens, so the Cursor tab has no breakdowns or 30-day map, and its status line says when the agent last ran instead.
67
- - **The endpoints are unofficial.** They're the ones the cursor.com dashboard uses, checked every 5 minutes. If Cursor changes them, the tab shows the last reading with its age until Clauddy is updated.
68
-
69
- Disconnecting (the **ร—** on the Cursor card) only stops monitoring in Clauddy; the Cursor app and its login are untouched.
70
-
71
- ## Burn rate
72
-
73
- Knowing you're at **82%** with **1h 12m** left on the window still leaves you doing arithmetic in your head. So Clauddy does it for you: it fits the slope of your recent usage and projects when you'd hit 100% โ€” showing one extra line under the session bar:
74
-
75
- - **`~35m left at this pace`** (in coral) โ€” you'd run out before the window resets. Ease off, or wrap up.
76
- - **`resets before you run out`** โ€” the reset gets there first. Carry on.
77
-
78
- The slope is fitted over your **session tokens** rather than the account %. The % is the number you care about, but it arrives as a whole number every few minutes โ€” over a short window the whole signal is a single `16 โ†’ 17` step, which throws the fitted pace off by multiples. Local-log tokens step too โ€” one jump per assistant turn โ€” but in increments some 10โ€“20ร— finer, so the slope is far steadier; the account % then anchors it, converting tokens into % and re-calibrating on every poll.
79
-
80
- It reads your **recent** pace, not the session average: go quiet for a few minutes and the projection eases off, which is the point.
81
-
82
- It only appears once there's enough to say honestly โ€” roughly 5 minutes into a session โ€” and stays hidden while you're idle, when the pace is flat, or right after a reset. A projection is a projection: change your pace and it changes with you.
83
-
84
- ## The pet's states
85
-
86
- <table>
87
- <tr>
88
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/idle.gif" width="280" alt="idle" /><br /><b>idle</b><br /><sub>breathes &amp; blinks</sub></td>
89
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/working.gif" width="280" alt="working" /><br /><b>working</b><br /><sub>hops &amp; eats token coins</sub></td>
90
- </tr>
91
- <tr>
92
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/on-fire.gif" width="280" alt="on fire" /><br /><b>on fire</b><br /><sub>session โ‰ฅ 90% โ†’ red, shivers, flames</sub></td>
93
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/tired.gif" width="280" alt="maxed out" /><br /><b>maxed out</b><br /><sub>session at 100% โ†’ drained, slumped, sweating</sub></td>
94
- </tr>
95
- <tr>
96
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/sleeping.gif" width="280" alt="sleeping" /><br /><b>sleeping</b><br /><sub>idle 5+ min โ†’ blue zzz &amp; moonlight</sub></td>
97
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/poke.gif" width="280" alt="poke" /><br /><b>poke</b><br /><sub>click the pet โ†’ squish &amp; hearts</sub></td>
98
- </tr>
99
- <tr>
100
- <td colspan="2" align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/celebrate.gif" width="280" alt="celebrate" /><br /><b>celebrate</b><br /><sub>session resets โ†’ jump &amp; confetti</sub></td>
101
- </tr>
102
- </table>
103
-
104
- Plus a welcome **wave** on launch. You can [poke the pet from the terminal](#play-with-the-pet) too.
105
-
106
- ### Pick your pet
107
-
108
- Clauddy is the default, but **โš™ Settings โ†’ Appearance โ†’ pet** has more: a Wizard, a Halloween pack (Pumpkin, Skull, Ghost, Bat, Black cat) and a Christmas pack (Snowman, Santa, Reindeer, Gingerbread). Click one to preview it and **Save** to keep it. Each pet stays still and blinks when nothing is running, and has its own way of moving while a provider works: the Pumpkin rocks, the Bat flaps, the Wizard levitates while an owl flies by.
109
-
110
- On holidays a pet from the season's pack takes over for the day, whichever pet you picked: one Halloween pet on October 31, one Christmas pet on December 24 and 25. The pick changes every year and holds all day. Your own pet comes back the next day.
111
-
112
- ### What Claude's up to
113
-
114
- While Claude Code is actively working, the pet sets up a little desk scene that
115
- mirrors **what it's doing right now** โ€” inferred from your local logs (the last
116
- tool it used). The status line names the activity, and three of them get their
117
- own animated scene:
118
-
119
- <table>
120
- <tr>
121
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/reading.gif" width="280" alt="reading" /><br /><b>reading</b><br /><sub>an open book under the reading lamp</sub></td>
122
- <td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/editing.gif" width="280" alt="editing" /><br /><b>editing</b><br /><sub>types at the desk, coffee in reach</sub></td>
123
- </tr>
124
- <tr>
125
- <td colspan="2" align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/a8491cea34c4a8547965eadc3b24618ba50218e1/docs/media/running.gif" width="280" alt="running" /><br /><b>running</b><br /><sub>watches a task log tick through its checks</sub></td>
126
- </tr>
127
- </table>
128
-
129
- Other activities โ€” **planning**, **researching**, **delegating**, **waiting** โ€”
130
- show up in the status line as they happen. When Claude goes quiet, the pet drops
131
- back to plain **working** / **idle**.
132
-
133
- ## The pet talks
134
-
135
- Now and then the pet says something in a speech bubble. It only speaks when something changes, never on a timer, and it keeps quiet when it has nothing specific to say. Remarks are at least 10 minutes apart; the big moments can cut that line.
136
-
137
- | Moment | What it says (expanded) | Minimized | Blip |
138
- | --- | --- | --- | --- |
139
- | First launch of the day | _Good morning! Yesterday was your heaviest day this week, 12M tokens._ | _Morning! Big day yesterday._ | โœ“ |
140
- | Session crosses your fire threshold | _Getting warm, 91% already. At this pace you'll run out in about 40m._ | _91%, getting warm!_ | โœ“ |
141
- | The session window resets | _Fresh window! That was 34M tokens over 4h 12m, mostly editing code._ | _Fresh window! 34M last time._ | โœ“ |
142
- | The session hits 100% | _That's the limit. I'll be back at 2:35 PM._ | _Maxed out till 2:35 PM._ | |
143
- | You're back after 2+ hours away | _Welcome back! You were gone 3h 12m. The session's at 12%._ | _Welcome back!_ | |
144
- | 90 minutes of unbroken work | _You've been at it for 1h 35m straight. Stretch break?_ | _Stretch break?_ | |
145
- | A new record day (30-day best) | _New record! 45M tokens today, your biggest day in a month._ | _New record: 45M!_ | |
146
- | Codex crosses fire, hits 100%, or resets | _Codex is at 91% now. It resets in 1h 12m._ | _Codex at 91%!_ | โœ“ (fire, reset) |
147
- | Cursor crosses fire, hits 100%, or starts a new month | _Cursor is at 91% now. It resets in 6d 4h._ | _Cursor at 91%!_ | โœ“ (fire, reset) |
148
-
149
- The greeting follows your clock (_Good morning_, _Good afternoon_, _Good evening_, or _Still up?_ before 5 AM) and appears once a day, and so does the record. The reset recap covers only what the pet actually saw, so if you launch it mid-session it says less. Click the bubble to dismiss it.
150
-
151
- ### The voice
152
-
153
- Optionally the pet also **blips** as it talks: short chiptune notes, one per letter, generated live with Web Audio (square waves, no sound files). It blips faster and higher when it's on fire, slower and lower when it's sleepy. Only the headline moments blip (the โœ“ rows above), because a sound on every remark wears out fast.
154
-
155
- Audio should never catch you off guard in a meeting:
156
-
157
- - **Off by default.** Turn it on under **โš™ Settings โ†’ Appearance โ†’ Chiptune voice** (you'll hear a sample).
158
- - **One-click mute.** While the voice is on, a ๐Ÿ”Š button sits in the title bar. Click it to mute for an hour, and again to unmute.
159
- - **Always silent** in menu-bar mode and while the pet is minimized.
13
+ https://github.com/user-attachments/assets/be8d339c-ac69-4980-96a7-8e286f8b080f
160
14
 
161
- **Speech bubbles** can be switched off entirely in the same place. Try any remark from the terminal with `bunx clauddy say <kind>` ([see below](#play-with-the-pet)).
15
+ ## Stack
162
16
 
163
- ## Install
17
+ An Electron widget in vanilla JS, with Bun for the scripts and the tests, and Biome for format and lint.
164
18
 
165
- **macOS (Apple Silicon)** is the first-class build. **Windows (x64)** and **Linux (x64)** work too. The `bunx`/`npx` route below runs on all of them today.
19
+ More details, with every tool and version, in [`docs/design/01-architecture.md`](docs/design/01-architecture.md#stack).
166
20
 
167
- ### macOS
21
+ ## Requirements
168
22
 
169
- Two ways, depending on what you want:
23
+ - macOS on Apple Silicon (the first-class build), Windows x64 or Linux x64
24
+ - [Claude Code](https://claude.com/claude-code), whose logs in `~/.claude/projects` give the token counts; a Claude login, done once in Settings, gives the real %
25
+ - [Bun](https://bun.sh), or Node 24 for `npx`, only to run it without installing or to work on the code
26
+ - Optional: [Codex](https://openai.com/codex) or the [Cursor](https://cursor.com) app on the same machine, to follow them too
170
27
 
171
- #### 1. Install as an app โ€” opens at login (recommended)
28
+ ## Running
172
29
 
173
- One command โ€” it downloads the latest release and drops it in `/Applications`:
30
+ On macOS, as an app that opens at login:
174
31
 
175
32
  ```bash
176
33
  curl -fsSL https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/install.sh | bash
177
34
  ```
178
35
 
179
- **Clauddy is free and open source** โ€” the command above just downloads the latest release from this repo and drops it in `/Applications`, nothing else (you can read [`install.sh`](install.sh) first if you'd like).
180
-
181
- Once installed, you can update in-app: **โš™ Settings โ†’ Check for updates โ†’ Update now** runs the same installer and relaunches the new build.
182
-
183
- Why not a normal download? macOS blocks **unsigned** apps downloaded through a browser with a scary *"damaged, move to Trash"* warning โ€” even when they're perfectly safe. It's a false alarm: the only way to silence it is to pay Apple **$99/year** to sign + notarize, which a free hobby app skips. Files fetched with `curl` aren't flagged, so this method simply **lets your Mac open the app** without the block. It then registers in **Login Items** and starts with your Mac โ€” set it and forget it.
184
-
185
- #### 2. Run it via `bunx` (no install)
186
-
187
- Needs [Bun](https://bun.sh) (or use `npx` with Node 24):
36
+ Anywhere, without installing:
188
37
 
189
38
  ```bash
190
39
  bunx clauddy
191
40
  ```
192
41
 
193
- The first run downloads Electron, so give it a moment. Handy for a quick run, but it stays up **only while that command is open** and won't start on its own. Quit it with the **ร—** button.
194
-
195
- ### Windows (x64)
196
-
197
- The quickest path works the same as macOS โ€” with [Bun](https://bun.sh) or Node 24 installed:
198
-
199
- ```powershell
200
- bunx clauddy # or: npx clauddy
201
- ```
202
-
203
- Prefer a standalone app with no Node/Bun? Grab the **portable zip** (`Clauddy-<version>-win-x64.zip`) from the [latest release](https://github.com/renatoaug/claude-usage-monitor/releases), unzip it anywhere, and run `Clauddy.exe`. Because the app is unsigned, Windows **SmartScreen** shows a "Windows protected your PC" prompt the first time โ€” click **More info โ†’ Run anyway**. From then on it starts with Windows.
204
-
205
- ### Linux (x64)
206
-
207
- The quickest path works the same as macOS โ€” with [Bun](https://bun.sh) or Node 24 installed:
208
-
209
- ```bash
210
- bunx clauddy # or: npx clauddy
211
- ```
212
-
213
- Prefer a standalone app? Grab the **AppImage** or **tar.gz** (`Clauddy-<version>-linux-x86_64.AppImage` / `Clauddy-<version>-linux-x64.tar.gz`) from the [latest release](https://github.com/renatoaug/claude-usage-monitor/releases), then:
214
-
215
- ```bash
216
- chmod +x Clauddy-*.AppImage
217
- ./Clauddy-*.AppImage
218
- ```
219
-
220
- Want the pet icon to show up in your app menu / taskbar too, instead of a generic icon? Running the AppImage directly doesn't register it anywhere โ€” GNOME (and most Wayland desktops) only pick up an app's icon from an installed `.desktop` entry. One-time setup, right next to the AppImage:
221
-
222
- ```bash
223
- ./Clauddy-*.AppImage --appimage-extract clauddy.desktop >/dev/null
224
- ./Clauddy-*.AppImage --appimage-extract usr/share/icons/hicolor/512x512/apps/clauddy.png >/dev/null
225
- mkdir -p ~/.local/share/applications ~/.local/share/icons/hicolor/512x512/apps
226
- cp squashfs-root/usr/share/icons/hicolor/512x512/apps/clauddy.png ~/.local/share/icons/hicolor/512x512/apps/clauddy.png
227
- sed "s|^Exec=.*|Exec=$(readlink -f Clauddy-*.AppImage) --no-sandbox %U|" squashfs-root/clauddy.desktop > ~/.local/share/applications/clauddy.desktop
228
- rm -rf squashfs-root
229
- update-desktop-database ~/.local/share/applications 2>/dev/null
230
- ```
231
-
232
- > The system tray icon needs an indicator extension on vanilla GNOME (e.g. "AppIndicator and KStatusNotifier Item Support") โ€” it works out of the box on Cinnamon, KDE, and XFCE. Autostart-at-login is wired up via an XDG `.desktop` entry in `~/.config/autostart/`.
233
-
234
- > The app keeps its data in `~/.claude-usage-monitor`, regardless of platform or how you run it.
235
-
236
- ## Controls
237
-
238
- - **Drag** the widget anywhere on screen
239
- - **โ€“** minimizes to just the pet, ringed by the live session % (the number sits inside the ring) โ€” or, with more than one service connected, one reading per service; the **โคข** button or a double-click on the pet expands it back
240
- - **Pet icon** in the compact header switches to **Just the pet** (floating mode). Drag the pet itself to move it. Hovering over the pet (or focusing it) pops up the usage glance as the pet's own speech bubble, one reading and a usage bar per service; the small expand button in it returns to compact. Keyboard users can focus the pet and press Enter. The chosen size is remembered. In compact and pet-only modes, the pet follows whoever is working, with provider logos identifying Claude, Codex, Cursor, or several at once. Usage selection stays manual. When neither is active, the hottest session still sets the mood: the pet catches fire (or maxes out) with that provider's logo beside it, and otherwise rests.
241
- - **โš™** opens settings (log in, toggle alerts, set thresholds, pick the display mode)
242
- - **โ†—** opens the official Usage page of the service on screen
243
- - **ร—** quits
244
-
245
- ### Floating or menu bar
246
-
247
- Under **โš™ Settings โ†’ Appearance** you can pick where Clauddy lives:
248
-
249
- - **Floating pet** โ€” the always-on widget in the corner (default).
250
- - **Menu bar** โ€” a small pet icon in the macOS menu bar showing your live session **%** (it turns ๐Ÿ”ฅ near your limit; with Codex or Cursor too it reads `C 64% ยท X 28% ยท Cu 5%`). Click it to pop open the full pet + usage panel; click away to dismiss. Right-click for a quick menu.
251
-
252
- Switching is instant โ€” no restart. (On Windows/Linux the icon lives in the system tray; the live % shows in its tooltip.)
253
-
254
- ### Provider reactions
255
-
256
- When a provider's activity changes, the pet briefly glances toward it, with no caption. Both can be working at once. A glance never changes your selected provider, and glances share a 30-second cooldown. Inactivity is treated as a pause, not a completed task.
257
-
258
- ## Alerts
259
-
260
- Optional **macOS notifications**, toggled (with their thresholds) in **โš™ Settings โ†’ Alerts**:
261
-
262
- | Notification | When |
263
- | --- | --- |
264
- | _Session at 82%_ โ€” `2h 39m left ยท resets 6:50 PM` | Your session crosses a threshold (default **80%** and **95%**) |
265
- | _Weekly usage at 84%_ โ€” `resets Fri 7:00 AM` | Same, for the weekly limit |
266
- | _Fable weekly at 84%_ โ€” `resets Fri 7:00 AM` | Same, for a per-model weekly limit |
267
- | _Codex session at 82%_ / _Codex weekly at 84%_ | Same, for Codex, when it's connected |
268
- | _Cursor usage at 82%_ / _Cursor API models at 84%_ โ€” `resets Oct 11` | Same, for Cursor's month, when it's connected |
269
- | _Session window reset_ โ€” `full budget again` | A session you had pushed past 80% rolls over |
270
- | _Clauddy lost access to your usage_ | The OAuth token expired or was revoked, so the % went back to being an estimate |
271
-
272
- The last threshold you set is the only one that makes a sound; the earlier ones arrive silently. Clicking any of them brings the widget to the front. Each fires once and re-arms when usage drops back below โ€” remembered across restarts, so relaunching at 85% doesn't repeat an alert you already dismissed.
273
-
274
- Percentages come from your account when you're logged in, and fall back to the local token estimate when you're not.
275
-
276
- The first alert asks macOS for permission; after that the app shows up in **System Settings > Notifications** like any other.
277
-
278
- ### Remind me at the session reset
279
-
280
- When the selected session crosses your first alert threshold, **Notify at reset** appears beneath the session details and in the compact monitor. Click to arm it; click **Reminder on** to cancel. This is a one-shot request, independent of the automatic threshold-alert toggle.
281
-
282
- Reminders survive restarts, belong to the Claude account, Codex or Cursor connection that created them, and are canceled when you disconnect that source. Clauddy must be running to notify; an overdue reminder is delivered on reopening or waking up. A fresh, lower reading confirms that session budget returned and the pet celebrates. If usage hasn't refreshed โ€” common with idle Codex logs โ€” the reminder only says the scheduled reset time arrived and asks you to check usage.
283
-
284
- ## Configure (`config.json`)
285
-
286
- Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you can tweak them without rebuilding:
287
-
288
- ```jsonc
289
- {
290
- "mode": "floating", // "floating" pet in the corner, or "menubar" popover
291
- "alerts": true, // macOS notifications on/off
292
- "alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
293
- "fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
294
- "pollIntervalMs": 4000, // how often local logs are re-read
295
- "activeThresholdMs": 20000, // fallback "active" window, for a log whose tail doesn't say whether a turn is open
296
- "sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
297
- "codex": { "enabled": false }, // set by Settings โ†’ Codex โ†’ Connect
298
- "cursor": { "enabled": false }, // set by Settings โ†’ Cursor โ†’ Connect
299
- "talk": true, // speech bubbles on transitions
300
- "sound": false, // chiptune blips, opt-in
301
- "soundMutedUntil": 0, // set by the ๐Ÿ”Š button: muted until this epoch ms
302
- }
303
- ```
304
-
305
- ## Play with the pet
42
+ [`docs/design/03-setup.md`](docs/design/03-setup.md) has Windows and Linux, updating, running from the code and every setting in `config.json`.
306
43
 
307
- With the widget running, poke it from the terminal โ€” just for fun:
44
+ ## Test
308
45
 
309
46
  ```bash
310
- bunx clauddy poke # ๐Ÿ’• squish + hearts
311
- bunx clauddy celebrate # ๐ŸŽ‰ jump + confetti
312
- bunx clauddy fire # ๐Ÿ”ฅ on fire
313
- bunx clauddy sleeping # ๐Ÿ˜ด blue zzz
314
- bunx clauddy working # ๐Ÿด eats token coins
315
- bunx clauddy tired # ๐Ÿฅต maxed out
316
- bunx clauddy idle # ๐Ÿ™‚ calm
317
- bunx clauddy auto # โ†ฉ๏ธ back to your real usage
318
- bunx clauddy say # ๐Ÿ’ฌ a remark in the speech bubble
319
- bunx clauddy say fire # or a specific one: greeting, fire, reset, maxed, welcome, streak, record, codex, cursor
47
+ bun run check # Biome format and lint (autofix)
48
+ bun run test # the three test groups, each in its own process
49
+ bun run test:coverage # the same, failing under 80% overall or 60% per file
50
+ bun run pack # builds dist/mac-arm64/Clauddy.app
51
+ ./pet fire # forces a pet state in the running widget (./pet auto to go back)
320
52
  ```
321
53
 
322
- Each state is written to the data dir the running widget watches, so it reacts
323
- live. (Installed globally? Drop the `bunx`: `clauddy poke`. Working on the repo?
324
- `./pet <state>` does the same.)
325
-
326
- ## How it works
327
-
328
- - **`main.js`** โ€” Electron main process: frameless, transparent, always-on-top window; polls usage; fires macOS notifications; watches `config.json` and `debug.json`.
329
- - **`usage.js`** โ€” reads `~/.claude/projects/**/*.jsonl`, sums tokens per model/project/day, detects the rolling 5-hour session window, the working/sleeping status, and which activity (reading/editing/running/โ€ฆ) Claude is on from its latest tool use.
330
- - **`auth.js`** โ€” OAuth login (PKCE, same public client as Claude Code) that fetches the authoritative usage %. Token stored locally, never committed.
331
- - **`renderer/`** โ€” the pet itself: an SVG pixel sprite, CSS animations, and the Web Animations API for particles. `voice.js` writes its remarks and synthesizes the blips.
332
- - **`make-icon.js`** โ€” generates the app icon from the pixel sprite (`build/icon.icns`).
333
-
334
- ## Data & privacy
335
-
336
- Everything lives on your machine, in `~/.claude-usage-monitor/`:
54
+ Never run bare `bun test`: the groups under `test/` must each run in their own process ([ADR-002](docs/decisions/ADR-002-three-test-processes.md)). `bun install` turns on a pre-commit hook that formats the staged files and blocks on lint errors. CI runs the coverage gate on every pull request.
337
55
 
338
- - `auth.json` โ€” your OAuth token (file mode `600`, never committed)
339
- - `config.json` โ€” your alert settings
340
- - `alerts.json` โ€” which notifications are already armed, so a restart doesn't repeat them
341
- - `accounts.json` โ€” your list of accounts and which one is active
56
+ ## Releases
342
57
 
343
- With Codex connected, Clauddy also reads (never writes) `~/.codex/sessions`. With Cursor connected, it reads the Cursor app's login from its local storage and the agent transcripts in `~/.cursor/projects`, also without writing.
344
- - `debug.json` โ€” scratch file for the `./pet` simulator
345
- - `accounts/<id>/` โ€” the same `auth.json` + `alerts.json`, for each extra account
58
+ Every merge to `main` can be a release: [semantic-release](https://semantic-release.gitbook.io) reads the Conventional Commit titles since the last tag (a `feat` bumps the minor, a `fix` or `perf` the patch, `docs` and `chore` nothing), builds the app on macOS, Windows and Linux runners, publishes the GitHub release and `clauddy` on npm. The whole pipeline is in [`docs/design/07-releases.md`](docs/design/07-releases.md).
346
59
 
347
- Nothing leaves your machine except the OAuth calls to Anthropic's own login and usage endpoints, and, with Cursor connected, the usage calls to Cursor's own API with the Cursor app's token.
60
+ ## Docs
348
61
 
349
- ## Contributing
62
+ Everything about how Clauddy works, runs and was decided is indexed in [`docs/`](docs/README.md). Code rules and the checks before calling a change done are in [`AGENTS.md`](AGENTS.md).
350
63
 
351
- Bug reports and ideas are welcome โ€” see **[CONTRIBUTING.md](CONTRIBUTING.md)**
352
- for setup and the few gotchas worth knowing before a first PR.
64
+ We follow [Conventional Commits](https://www.conventionalcommits.org).
353
65
 
354
- ## Dev tooling
66
+ ## Privacy
355
67
 
356
- - **Bun** for install/scripts, **Node 24** pinned in `.nvmrc`
357
- - **Tests**: `bun run test` (never bare `bun test` โ€” the groups under `test/`
358
- must each run in their own process). `bun run test:coverage` enforces the
359
- floor; every PR runs both.
360
- - **Biome** for format + lint (`bun run check`); a versioned **pre-commit hook** (`.githooks/pre-commit`) auto-formats staged files and blocks on errors. It's wired up automatically on `bun install` (via the `prepare` script).
68
+ Everything stays on your machine, in `~/.claude-usage-monitor/`. Nothing leaves it except the OAuth and usage calls to Anthropic and, with Cursor connected, the usage calls to Cursor with the Cursor app's own token. What is kept and what is read: [`docs/design/01-architecture.md`](docs/design/01-architecture.md#data-on-disk).
361
69
 
362
- ### Releasing
70
+ ## Community
363
71
 
364
- Releases are **fully automated**. Every push to `main` runs
365
- [semantic-release](https://semantic-release.gitbook.io) (`.github/workflows/release.yml`):
366
- it reads the **Conventional Commits** and, when there's something to ship,
367
- computes the version, builds the app for **macOS, Windows and Linux** on their
368
- own runners, publishes `clauddy` to npm, and cuts a GitHub Release with every
369
- artifact attached. Nothing to do by hand โ€” just merge your PRs.
72
+ - ๐Ÿ› [GitHub Issues](https://github.com/renatoaug/claude-usage-monitor/issues): report a bug or propose a feature
73
+ - ๐Ÿ”ง Pull requests are welcome: read [`CONTRIBUTING.md`](CONTRIBUTING.md) first for how to start and the checks
74
+ - ๐Ÿ™ [@renatoaug on GitHub](https://github.com/renatoaug): the maintainer
370
75
 
371
- The pipeline runs in three stages, because electron-builder can't cross-build
372
- Windows/Linux from macOS: `version` (a semantic-release dry-run that computes
373
- the next version) โ†’ `build` (a matrix that stamps that version into
374
- `package.json` so the filenames are right) โ†’ `publish` (downloads every
375
- artifact and runs semantic-release for real).
76
+ ## License
376
77
 
377
- - `feat:` โ†’ minor, `fix:` โ†’ patch, `feat!:`/`BREAKING CHANGE` โ†’ major.
378
- - `docs:`/`chore:`/`ci:` etc. don't trigger a release.
78
+ Clauddy is licensed under the [MIT License](LICENSE).
package/codex.js CHANGED
@@ -1,16 +1,26 @@
1
+ const { spawn } = require('node:child_process')
1
2
  const fs = require('node:fs')
2
3
  const path = require('node:path')
3
4
  const os = require('node:os')
4
5
 
5
6
  // Codex (CLI and the desktop app alike) writes one `rollout-*.jsonl` per
6
7
  // session under ~/.codex/sessions/YYYY/MM/DD. Its `token_count` events carry
7
- // both the token totals and the account's rate limits, so everything here comes
8
- // from local files โ€” no login needed.
8
+ // both the token totals and the account's rate limits, so the tokens and history
9
+ // come from local files. Other ChatGPT products share those limits without
10
+ // writing rollouts, so the % also comes from Codex's own app-server.
9
11
  let codexDir = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
10
12
 
11
13
  function setCodexDir(dir) {
12
14
  codexDir = dir || process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
13
15
  fileCache.clear()
16
+ liveLimits = null
17
+ lastLiveTry = 0
18
+ lastLiveFailed = false
19
+ }
20
+
21
+ // tests point this at a stub; false turns the live read off
22
+ function setCodexBin(bin) {
23
+ codexBin = bin
14
24
  }
15
25
 
16
26
  const DAY = 86400000
@@ -19,6 +29,14 @@ const WEEK = 7 * DAY
19
29
  const ACTIVE_MS = 60000 // a rollout touched within the last minute = Codex working
20
30
  const DAY_MIN = 24 * 60
21
31
  const TOP_PROJECTS = 5 // same fold as Claude's panel
32
+ const LIVE_MS = 2 * 60000 // how often to ask the app-server for the limits
33
+ const LIVE_RETRY_MS = 5 * 60000 // a process spawn per try: back off when it fails
34
+ const LIVE_TIMEOUT_MS = 15000
35
+
36
+ let codexBin
37
+ let liveLimits = null
38
+ let lastLiveTry = 0
39
+ let lastLiveFailed = false
22
40
 
23
41
  // per file: how far we've read, plus what we got out of it. Rollouts only ever
24
42
  // grow, so a poll reads just the bytes appended since the last one.
@@ -51,6 +69,94 @@ function limitsOf(rl, ts) {
51
69
  return { ts, plan: rl.plan_type || null, session, weekly }
52
70
  }
53
71
 
72
+ // the app-server reports the same windows as the rollouts, in camelCase
73
+ function limitsFromLive(result, ts) {
74
+ const rl = result?.rateLimitsByLimitId?.codex || result?.rateLimits
75
+ if (!rl) return null
76
+ const win = (w) =>
77
+ w && {
78
+ used_percent: w.usedPercent,
79
+ window_minutes: w.windowDurationMins,
80
+ resets_at: w.resetsAt,
81
+ }
82
+ return limitsOf(
83
+ { primary: win(rl.primary), secondary: win(rl.secondary), plan_type: rl.planType },
84
+ ts,
85
+ )
86
+ }
87
+
88
+ // the desktop app's bundled CLI first: a GUI app doesn't get the shell's PATH
89
+ function findCodexBin() {
90
+ if (codexBin !== undefined) return codexBin
91
+ const home = os.homedir()
92
+ const bundled = ['/Applications', path.join(home, 'Applications')].flatMap((dir) => [
93
+ path.join(dir, 'ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex'),
94
+ path.join(dir, 'Codex.app/Contents/Resources/codex'),
95
+ ])
96
+ return bundled.find((f) => fs.existsSync(f)) || 'codex'
97
+ }
98
+
99
+ // One `account/rateLimits/read` over the app-server's stdio JSON-RPC. Codex keeps
100
+ // its own login and token refresh: Clauddy never touches the token.
101
+ function readLiveLimits(bin, ts) {
102
+ return new Promise((resolve) => {
103
+ let child
104
+ try {
105
+ child = spawn(bin, ['app-server'], {
106
+ env: { ...process.env, CODEX_HOME: codexDir },
107
+ stdio: ['pipe', 'pipe', 'ignore'],
108
+ })
109
+ } catch {
110
+ resolve(null)
111
+ return
112
+ }
113
+ const finish = (limits) => {
114
+ clearTimeout(timer)
115
+ child.kill()
116
+ resolve(limits)
117
+ }
118
+ const timer = setTimeout(() => finish(null), LIVE_TIMEOUT_MS)
119
+ const send = (msg) => child.stdin.write(`${JSON.stringify(msg)}\n`)
120
+ child.on('error', () => finish(null))
121
+ child.on('exit', () => finish(null))
122
+ child.stdin.on('error', () => finish(null))
123
+ let rest = ''
124
+ child.stdout.on('data', (chunk) => {
125
+ const lines = (rest + chunk).split('\n')
126
+ rest = lines.pop()
127
+ for (const line of lines) {
128
+ let msg
129
+ try {
130
+ msg = JSON.parse(line)
131
+ } catch {
132
+ continue
133
+ }
134
+ if (msg.id === 1) {
135
+ send({ method: 'initialized' })
136
+ send({ id: 2, method: 'account/rateLimits/read' })
137
+ } else if (msg.id === 2) finish(limitsFromLive(msg.result, ts))
138
+ }
139
+ })
140
+ send({
141
+ id: 1,
142
+ method: 'initialize',
143
+ params: { clientInfo: { name: 'clauddy', version: '0' } },
144
+ })
145
+ })
146
+ }
147
+
148
+ // A failed read keeps the last good one: it ages into stale, never into zero.
149
+ async function refreshLimits(now = Date.now()) {
150
+ const bin = findCodexBin()
151
+ if (!bin) return
152
+ lastLiveTry = now
153
+ const dir = codexDir
154
+ const limits = await readLiveLimits(bin, now)
155
+ if (dir !== codexDir) return // the source changed while this one was running
156
+ lastLiveFailed = !limits
157
+ if (limits) liveLimits = limits
158
+ }
159
+
54
160
  function ingestLine(sum, line) {
55
161
  // cheap filter before JSON.parse โ€” rollouts are mostly message bodies
56
162
  if (!line.includes('"token_count"') && !line.includes('"turn_context"')) return
@@ -164,6 +270,7 @@ function ranked(map) {
164
270
 
165
271
  function getCodexUsage(now = Date.now()) {
166
272
  if (!fs.existsSync(path.join(codexDir, 'sessions'))) return null
273
+ if (now - lastLiveTry >= (lastLiveFailed ? LIVE_RETRY_MS : LIVE_MS)) refreshLimits(now)
167
274
  const startOfDay = new Date(now).setHours(0, 0, 0, 0)
168
275
  const start30 = startOfDay - (DAYS - 1) * DAY
169
276
  const files = rollouts(start30)
@@ -199,6 +306,8 @@ function getCodexUsage(now = Date.now()) {
199
306
  }
200
307
  }
201
308
  }
309
+ // whichever is newer: a rollout written after the last live read beats it
310
+ if (liveLimits && (!limits || liveLimits.ts >= limits.ts)) limits = liveLimits
202
311
  // forget files that aged out of the window
203
312
  for (const f of fileCache.keys()) if (!seen.has(f)) fileCache.delete(f)
204
313
 
@@ -233,4 +342,4 @@ function getCodexUsage(now = Date.now()) {
233
342
  }
234
343
  }
235
344
 
236
- module.exports = { getCodexUsage, detectCodex, setCodexDir }
345
+ module.exports = { getCodexUsage, refreshLimits, detectCodex, setCodexDir, setCodexBin }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.29.0",
4
+ "version": "1.29.1",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {