clauddy 1.11.1 → 1.12.0
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.
- package/README.md +25 -10
- package/package.json +6 -4
- package/renderer/index.html +1 -0
- package/renderer/pet.js +85 -0
- package/renderer/style.css +12 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A cute pixel-art desktop pet for macOS that tracks your Claude Code usage — mi
|
|
|
9
9
|
|
|
10
10
|
## What it shows
|
|
11
11
|
|
|
12
|
-
- **Current session** — real % used + **"resets in Xh Ym"** + session tokens
|
|
12
|
+
- **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))
|
|
13
13
|
- **Weekly · all models** — real % used + tokens over the last 7 days
|
|
14
14
|
- **Status line** under the pet: `● working · 1.6M tok/min` (or today's tokens when idle)
|
|
15
15
|
- **By model · 7 days** — Opus / Sonnet / Haiku / Fable, in tokens
|
|
@@ -26,6 +26,19 @@ The session/weekly **%** comes straight from your Anthropic account, so it match
|
|
|
26
26
|
|
|
27
27
|
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.
|
|
28
28
|
|
|
29
|
+
## Burn rate
|
|
30
|
+
|
|
31
|
+
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:
|
|
32
|
+
|
|
33
|
+
- **`~35m left at this pace`** (in coral) — you'd run out before the window resets. Ease off, or wrap up.
|
|
34
|
+
- **`resets before you run out`** — the reset gets there first. Carry on.
|
|
35
|
+
|
|
36
|
+
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 ~5 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.
|
|
37
|
+
|
|
38
|
+
It reads your **recent** pace, not the session average: go quiet for a few minutes and the projection eases off, which is the point.
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
29
42
|
## The pet's states
|
|
30
43
|
|
|
31
44
|
<table>
|
|
@@ -109,9 +122,7 @@ The quickest path works the same as macOS — with [Bun](https://bun.sh) or Node
|
|
|
109
122
|
bunx clauddy # or: npx clauddy
|
|
110
123
|
```
|
|
111
124
|
|
|
112
|
-
Prefer a standalone app with no Node/Bun? Grab the **portable zip** (`Clauddy-<version>-win.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.
|
|
113
|
-
|
|
114
|
-
> Windows builds are produced by the **Build** workflow (Actions ▸ Build) — attaching them to every release automatically is on the roadmap.
|
|
125
|
+
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.
|
|
115
126
|
|
|
116
127
|
### Linux (x64)
|
|
117
128
|
|
|
@@ -121,15 +132,13 @@ The quickest path works the same as macOS — with [Bun](https://bun.sh) or Node
|
|
|
121
132
|
bunx clauddy # or: npx clauddy
|
|
122
133
|
```
|
|
123
134
|
|
|
124
|
-
Prefer a standalone app? Grab the **AppImage** or **tar.gz** (`Clauddy-<version
|
|
135
|
+
Prefer a standalone app? Grab the **AppImage** or **tar.gz** (`Clauddy-<version>-linux-x64.AppImage` / `Clauddy-<version>-linux-x64.tar.gz`) from the [latest release](https://github.com/renatoaug/claude-usage-monitor/releases), then:
|
|
125
136
|
|
|
126
137
|
```bash
|
|
127
138
|
chmod +x Clauddy-*.AppImage
|
|
128
139
|
./Clauddy-*.AppImage
|
|
129
140
|
```
|
|
130
141
|
|
|
131
|
-
> Linux builds are produced by the **Build** workflow (Actions ▸ Build) — attaching them to every release automatically is on the roadmap.
|
|
132
|
-
|
|
133
142
|
> 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/`.
|
|
134
143
|
|
|
135
144
|
> The app keeps its data in `~/.claude-usage-monitor`, regardless of platform or how you run it.
|
|
@@ -218,9 +227,15 @@ Nothing leaves your machine except the OAuth calls to Anthropic's own login and
|
|
|
218
227
|
Releases are **fully automated**. Every push to `main` runs
|
|
219
228
|
[semantic-release](https://semantic-release.gitbook.io) (`.github/workflows/release.yml`):
|
|
220
229
|
it reads the **Conventional Commits** and, when there's something to ship,
|
|
221
|
-
computes the version, builds the macOS
|
|
222
|
-
|
|
223
|
-
your PRs.
|
|
230
|
+
computes the version, builds the app for **macOS, Windows and Linux** on their
|
|
231
|
+
own runners, publishes `clauddy` to npm, and cuts a GitHub Release with every
|
|
232
|
+
artifact attached. Nothing to do by hand — just merge your PRs.
|
|
233
|
+
|
|
234
|
+
The pipeline runs in three stages, because electron-builder can't cross-build
|
|
235
|
+
Windows/Linux from macOS: `version` (a semantic-release dry-run that computes
|
|
236
|
+
the next version) → `build` (a matrix that stamps that version into
|
|
237
|
+
`package.json` so the filenames are right) → `publish` (downloads every
|
|
238
|
+
artifact and runs semantic-release for real).
|
|
224
239
|
|
|
225
240
|
- `feat:` → minor, `fix:` → patch, `feat!:`/`BREAKING CHANGE` → major.
|
|
226
241
|
- `docs:`/`chore:`/`ci:` etc. don't trigger a release.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clauddy",
|
|
3
3
|
"desktopName": "clauddy.desktop",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.12.0",
|
|
5
5
|
"description": "A cute desktop pet that tracks your Claude Code usage",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"bin": {
|
|
@@ -80,17 +80,20 @@
|
|
|
80
80
|
"category": "public.app-category.developer-tools",
|
|
81
81
|
"target": "dir",
|
|
82
82
|
"identity": null,
|
|
83
|
-
"icon": "build/icon.icns"
|
|
83
|
+
"icon": "build/icon.icns",
|
|
84
|
+
"artifactName": "${productName}-${version}-mac-${arch}.${ext}"
|
|
84
85
|
},
|
|
85
86
|
"win": {
|
|
86
87
|
"target": "zip",
|
|
87
|
-
"icon": "build/icon.ico"
|
|
88
|
+
"icon": "build/icon.ico",
|
|
89
|
+
"artifactName": "${productName}-${version}-win-${arch}.${ext}"
|
|
88
90
|
},
|
|
89
91
|
"linux": {
|
|
90
92
|
"target": [
|
|
91
93
|
"tar.gz",
|
|
92
94
|
"AppImage"
|
|
93
95
|
],
|
|
96
|
+
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
|
|
94
97
|
"category": "Utility",
|
|
95
98
|
"icon": "build/icon.png",
|
|
96
99
|
"syncDesktopName": true
|
|
@@ -101,7 +104,6 @@
|
|
|
101
104
|
},
|
|
102
105
|
"devDependencies": {
|
|
103
106
|
"@biomejs/biome": "^2.5.0",
|
|
104
|
-
"@semantic-release/exec": "^7.1.0",
|
|
105
107
|
"electron-builder": "^26.15.3",
|
|
106
108
|
"semantic-release": "^25.0.5"
|
|
107
109
|
}
|
package/renderer/index.html
CHANGED
|
@@ -246,6 +246,7 @@
|
|
|
246
246
|
<div class="meter-top"><span>current session</span><span id="session-pct">0%</span></div>
|
|
247
247
|
<div class="track"><div class="fill" id="session-fill"></div></div>
|
|
248
248
|
<div class="sub" id="session-sub">no active session</div>
|
|
249
|
+
<div class="sub proj" id="session-proj" hidden></div>
|
|
249
250
|
</div>
|
|
250
251
|
<div class="meter">
|
|
251
252
|
<div class="meter-top"><span>weekly · all models</span><span id="week-pct">0%</span></div>
|
package/renderer/pet.js
CHANGED
|
@@ -154,6 +154,80 @@ function fmtReset(ms) {
|
|
|
154
154
|
const m = Math.floor((ms % 3600000) / 60000)
|
|
155
155
|
return h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
156
156
|
}
|
|
157
|
+
// ---- burn-rate projection ----
|
|
158
|
+
// The panel knows where you are (82%) and when the window resets (1h 12m); it
|
|
159
|
+
// can also say where you're headed — fit a slope through recent usage and
|
|
160
|
+
// project the crossing of 100%. If the reset lands first there's nothing to
|
|
161
|
+
// worry about, which is worth saying out loud rather than leaving blank.
|
|
162
|
+
//
|
|
163
|
+
// The slope is fitted over session *tokens*, not the account %. The % is the
|
|
164
|
+
// number we ultimately care about, but it arrives as a whole number every ~5
|
|
165
|
+
// min: over a short window the whole signal is a single 16 → 17 step, which
|
|
166
|
+
// makes the fitted pace wrong by multiples. Local-log tokens step too — one
|
|
167
|
+
// jump per assistant turn, with plateaus in between — but in increments some
|
|
168
|
+
// 10-20x finer, so they carry a far steadier slope. The
|
|
169
|
+
// account % still anchors it: pct/tokens converts tokens/ms into %/ms and
|
|
170
|
+
// re-calibrates on every poll, so the projection stays tied to the real number.
|
|
171
|
+
const PROJ_WINDOW_MS = 45 * 60 * 1000 // only fit recent samples — pace changes
|
|
172
|
+
const PROJ_MIN_SAMPLES = 4
|
|
173
|
+
const PROJ_MIN_SPAN_MS = 5 * 60 * 1000 // shorter than a % fit affords, and steadier
|
|
174
|
+
const PROJ_SAMPLE_EVERY_MS = 30 * 1000 // usage polls every few seconds; thin it out
|
|
175
|
+
let tokTrail = []
|
|
176
|
+
|
|
177
|
+
function noteTokens(tokens, active) {
|
|
178
|
+
if (!active || !(tokens > 0)) {
|
|
179
|
+
tokTrail = [] // no session, no trend
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
// session tokens only climb within a window — a drop means it rolled over,
|
|
183
|
+
// and the fresh sample starts the new trail rather than being throttled away
|
|
184
|
+
let last = tokTrail[tokTrail.length - 1]
|
|
185
|
+
if (last && tokens < last.tokens) {
|
|
186
|
+
tokTrail = []
|
|
187
|
+
last = undefined
|
|
188
|
+
}
|
|
189
|
+
const now = Date.now()
|
|
190
|
+
if (last && now - last.t < PROJ_SAMPLE_EVERY_MS) return
|
|
191
|
+
tokTrail.push({ t: now, tokens })
|
|
192
|
+
tokTrail = tokTrail.filter((s) => s.t >= now - PROJ_WINDOW_MS)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// least-squares slope in tokens per ms, or null when there isn't enough to say
|
|
196
|
+
function burnSlope() {
|
|
197
|
+
if (tokTrail.length < PROJ_MIN_SAMPLES) return null
|
|
198
|
+
const span = tokTrail[tokTrail.length - 1].t - tokTrail[0].t
|
|
199
|
+
if (span < PROJ_MIN_SPAN_MS) return null
|
|
200
|
+
const n = tokTrail.length
|
|
201
|
+
const t0 = tokTrail[0].t
|
|
202
|
+
let sx = 0
|
|
203
|
+
let sy = 0
|
|
204
|
+
let sxy = 0
|
|
205
|
+
let sxx = 0
|
|
206
|
+
for (const s of tokTrail) {
|
|
207
|
+
const x = s.t - t0
|
|
208
|
+
sx += x
|
|
209
|
+
sy += s.tokens
|
|
210
|
+
sxy += x * s.tokens
|
|
211
|
+
sxx += x * x
|
|
212
|
+
}
|
|
213
|
+
const denom = n * sxx - sx * sx
|
|
214
|
+
if (denom === 0) return null
|
|
215
|
+
return (n * sxy - sx * sy) / denom
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// → { kind: 'eta', ms } when you'd run out first, { kind: 'safe' } when the
|
|
219
|
+
// reset beats you there, or null when we can't tell yet.
|
|
220
|
+
function project(pct, resetMs, tokens) {
|
|
221
|
+
if (!(pct > 0) || !(tokens > 0)) return null // no anchor to convert tokens → %
|
|
222
|
+
const tokPerMs = burnSlope()
|
|
223
|
+
if (tokPerMs == null || tokPerMs <= 0) return null // idle or flat
|
|
224
|
+
const pctPerMs = tokPerMs * (pct / tokens)
|
|
225
|
+
const eta = (100 - pct) / pctPerMs
|
|
226
|
+
if (!Number.isFinite(eta) || eta <= 0) return null
|
|
227
|
+
if (resetMs != null && eta >= resetMs) return { kind: 'safe' }
|
|
228
|
+
return { kind: 'eta', ms: eta }
|
|
229
|
+
}
|
|
230
|
+
|
|
157
231
|
function setState(name) {
|
|
158
232
|
const b = document.body
|
|
159
233
|
;[...b.classList].forEach((c) => {
|
|
@@ -500,6 +574,17 @@ function render(d) {
|
|
|
500
574
|
? `resets in ${fmtReset(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
|
|
501
575
|
: 'no active session'
|
|
502
576
|
|
|
577
|
+
// where this pace is taking you — hidden until there's enough trail to tell
|
|
578
|
+
noteTokens(d.session.tokens, sessActive)
|
|
579
|
+
const proj = sessActive && sessPct < 100 ? project(sessPct, sessReset, d.session.tokens) : null
|
|
580
|
+
const pe = el('session-proj')
|
|
581
|
+
pe.hidden = !proj
|
|
582
|
+
pe.classList.toggle('tight', proj?.kind === 'eta')
|
|
583
|
+
if (proj) {
|
|
584
|
+
pe.textContent =
|
|
585
|
+
proj.kind === 'eta' ? `~${fmtReset(proj.ms)} left at this pace` : 'resets before you run out'
|
|
586
|
+
}
|
|
587
|
+
|
|
503
588
|
el('week-pct').textContent = `${Math.round(wkPct)}%`
|
|
504
589
|
const wf = el('week-fill')
|
|
505
590
|
wf.style.width = `${wkPct}%`
|
package/renderer/style.css
CHANGED
|
@@ -1618,6 +1618,18 @@ body.state-tired #status-text {
|
|
|
1618
1618
|
letter-spacing: 0.2px;
|
|
1619
1619
|
}
|
|
1620
1620
|
|
|
1621
|
+
/* burn-rate projection — calm by default, warm when you'd run out first */
|
|
1622
|
+
.sub.proj {
|
|
1623
|
+
margin-top: 2px;
|
|
1624
|
+
font-style: italic;
|
|
1625
|
+
opacity: 0.85;
|
|
1626
|
+
}
|
|
1627
|
+
.sub.proj.tight {
|
|
1628
|
+
color: var(--coral);
|
|
1629
|
+
font-style: normal;
|
|
1630
|
+
opacity: 1;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1621
1633
|
/* 30-day map */
|
|
1622
1634
|
#heat {
|
|
1623
1635
|
margin-top: 12px;
|