clauddy 1.21.2 → 1.22.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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/auth.js +29 -1
  3. package/main.js +23 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,7 @@ Knowing you're at **82%** with **1h 12m** left on the window still leaves you do
46
46
  - **`~35m left at this pace`** (in coral) — you'd run out before the window resets. Ease off, or wrap up.
47
47
  - **`resets before you run out`** — the reset gets there first. Carry on.
48
48
 
49
- 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.
49
+ 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.
50
50
 
51
51
  It reads your **recent** pace, not the session average: go quiet for a few minutes and the projection eases off, which is the point.
52
52
 
package/auth.js CHANGED
@@ -211,6 +211,24 @@ function scopedWeeks(j) {
211
211
  return out
212
212
  }
213
213
 
214
+ // What the endpoint says its own budget is. Undocumented, so we only ever read
215
+ // it — nothing depends on it yet. Knowing the real numbers is what tells us how
216
+ // close the activity poll's floor can get.
217
+ let rateLimit = null
218
+ function readRateLimit(res) {
219
+ const out = {}
220
+ try {
221
+ for (const [k, v] of res.headers) {
222
+ if (k.startsWith('anthropic-ratelimit-')) out[k.slice(20)] = v
223
+ }
224
+ } catch {
225
+ return // no headers to read — nothing here is worth failing a fetch over
226
+ }
227
+ if (!Object.keys(out).length) return
228
+ if (JSON.stringify(out) !== JSON.stringify(rateLimit)) console.log('usage rate limit', out)
229
+ rateLimit = out
230
+ }
231
+
214
232
  // Step 3: fetch the authoritative usage
215
233
  async function fetchUsage() {
216
234
  const token = await validToken()
@@ -228,6 +246,7 @@ async function fetchUsage() {
228
246
  status: res.status,
229
247
  })
230
248
  }
249
+ readRateLimit(res)
231
250
  const j = await res.json()
232
251
  return {
233
252
  session: win(j.five_hour) || { pct: 0, resetMs: null },
@@ -261,4 +280,13 @@ async function fetchProfile() {
261
280
  return profile
262
281
  }
263
282
 
264
- module.exports = { begin, complete, fetchUsage, fetchProfile, clear, isConnected, setDataDir }
283
+ module.exports = {
284
+ rateLimit: () => rateLimit,
285
+ begin,
286
+ complete,
287
+ fetchUsage,
288
+ fetchProfile,
289
+ clear,
290
+ isConnected,
291
+ setDataDir,
292
+ }
package/main.js CHANGED
@@ -234,6 +234,7 @@ function applyAccount(acc) {
234
234
  usage.setClaudeDir(acc.claudeDir)
235
235
  armed = new Set()
236
236
  lastSessionPct = null
237
+ lastSeenTokens = null // the new account's first read is a baseline, not a spend
237
238
  loadAlertState()
238
239
  }
239
240
 
@@ -402,6 +403,7 @@ function createWindow() {
402
403
  const data = getUsage(config)
403
404
  win.webContents.send('usage', data)
404
405
  checkAlerts(config, data)
406
+ onLocalActivity(data)
405
407
  } catch (err) {
406
408
  win.webContents.send('usage-error', String(err))
407
409
  }
@@ -651,11 +653,32 @@ function watchDebug() {
651
653
  let usageTimer = null
652
654
  let usageBackoff = 5 * 60 * 1000
653
655
  let authFails = 0 // consecutive 401s — see pollUsage
656
+ let lastPollAt = 0
657
+
658
+ // The authoritative % only moves when tokens are actually spent, and the local
659
+ // logs show that within a tick. Polling off that beats a faster clock: it
660
+ // answers while the user is working and asks for nothing while they are not.
661
+ // The floor is what bounds the cost — never more than one call per 90s, well
662
+ // under what a fixed one-minute poll would spend.
663
+ const POLL_FLOOR_MS = 90 * 1000
664
+ let lastSeenTokens = null
665
+ function onLocalActivity(data) {
666
+ const t = data?.session?.tokens
667
+ if (typeof t !== 'number') return
668
+ const grew = lastSeenTokens != null && t > lastSeenTokens
669
+ lastSeenTokens = t
670
+ if (!grew || !auth.isConnected()) return
671
+ if (Date.now() - lastPollAt < POLL_FLOOR_MS) return
672
+ clearTimeout(usageTimer) // pollUsage schedules the next heartbeat itself
673
+ pollUsage()
674
+ }
675
+
654
676
  function scheduleUsagePoll() {
655
677
  clearTimeout(usageTimer)
656
678
  if (auth.isConnected()) usageTimer = setTimeout(pollUsage, usageBackoff)
657
679
  }
658
680
  async function pollUsage() {
681
+ lastPollAt = Date.now()
659
682
  try {
660
683
  const u = await auth.fetchUsage()
661
684
  usageBackoff = 5 * 60 * 1000
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.21.2",
4
+ "version": "1.22.0",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {