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 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>.AppImage` / `clauddy-<version>.tar.gz`) from the [latest release](https://github.com/renatoaug/claude-usage-monitor/releases), then:
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 app, publishes `clauddy` to npm, and
222
- cuts a GitHub Release with the `.app` zip. Nothing to do by hand just merge
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.11.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
  }
@@ -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}%`
@@ -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;