clauddy 1.21.1 → 1.21.2

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/auth.js +25 -3
  2. package/main.js +27 -0
  3. package/package.json +1 -1
package/auth.js CHANGED
@@ -35,12 +35,25 @@ function setDataDir(dir) {
35
35
  tokens = null
36
36
  profile = null
37
37
  pending = null
38
+ unreadable = false
38
39
  }
39
40
 
41
+ // Set when the file itself could not be read for a reason other than "it is not
42
+ // there" — a disk still waking up, say. That must not read as a logout, or the
43
+ // widget asks for a login it doesn't need and every account looks gone. A file
44
+ // we did read but cannot parse is a different matter: that one really is dead.
45
+ let unreadable = false
40
46
  function load() {
41
47
  if (tokens) return tokens
48
+ let raw = null
42
49
  try {
43
- tokens = JSON.parse(fs.readFileSync(tokenPath, 'utf8'))
50
+ raw = fs.readFileSync(tokenPath, 'utf8')
51
+ unreadable = false
52
+ } catch (e) {
53
+ unreadable = e?.code !== 'ENOENT'
54
+ }
55
+ try {
56
+ tokens = raw == null ? null : JSON.parse(raw)
44
57
  } catch {
45
58
  tokens = null
46
59
  }
@@ -56,12 +69,15 @@ function save(t) {
56
69
  function clear() {
57
70
  tokens = null
58
71
  profile = null
72
+ unreadable = false
59
73
  try {
60
74
  fs.unlinkSync(tokenPath)
61
75
  } catch {}
62
76
  }
63
77
  function isConnected() {
64
- return !!load()
78
+ // a token we merely failed to read still counts: the account is connected,
79
+ // we just couldn't see it this instant
80
+ return !!load() || unreadable
65
81
  }
66
82
 
67
83
  // Step 1: build the authorize URL (opens in the browser)
@@ -152,7 +168,13 @@ async function refresh() {
152
168
 
153
169
  async function validToken() {
154
170
  const t = load()
155
- if (!t) throw Object.assign(new Error('not connected'), { status: 401 })
171
+ // status 0 is "try again later", not "log in again": only a token that is
172
+ // genuinely absent should send the user back through the browser
173
+ if (!t) {
174
+ throw Object.assign(new Error(unreadable ? 'token unreadable' : 'not connected'), {
175
+ status: unreadable ? 0 : 401,
176
+ })
177
+ }
156
178
  if (!t.expires_at || t.expires_at - Date.now() < 60000) await refresh()
157
179
  return load().access_token
158
180
  }
package/main.js CHANGED
@@ -8,6 +8,7 @@ const {
8
8
  Tray,
9
9
  Menu,
10
10
  nativeImage,
11
+ powerMonitor,
11
12
  } = require('electron')
12
13
  const path = require('node:path')
13
14
  const fs = require('node:fs')
@@ -336,6 +337,7 @@ function createWindow() {
336
337
  for (const a of accounts.list()) pruneEmpty(a.id)
337
338
  accounts.pruneOrphans() // folders those slots left behind
338
339
  applyAccount(accounts.active())
340
+ powerMonitor?.on?.('resume', onResume)
339
341
  const { workAreaSize } = screen.getPrimaryDisplay()
340
342
  const H = 480
341
343
 
@@ -648,6 +650,7 @@ function watchDebug() {
648
650
  // ---- real usage via OAuth (authoritative %), polled slowly with 429 backoff ----
649
651
  let usageTimer = null
650
652
  let usageBackoff = 5 * 60 * 1000
653
+ let authFails = 0 // consecutive 401s — see pollUsage
651
654
  function scheduleUsagePoll() {
652
655
  clearTimeout(usageTimer)
653
656
  if (auth.isConnected()) usageTimer = setTimeout(pollUsage, usageBackoff)
@@ -656,11 +659,21 @@ async function pollUsage() {
656
659
  try {
657
660
  const u = await auth.fetchUsage()
658
661
  usageBackoff = 5 * 60 * 1000
662
+ authFails = 0
659
663
  pushRealUsage(u)
660
664
  } catch (e) {
661
665
  if (e && e.status === 429) {
662
666
  usageBackoff = Math.min(usageBackoff * 2, 30 * 60 * 1000)
663
667
  } else if (e && e.status === 401) {
668
+ // Refreshing rotates the refresh token, so a request that raced the one
669
+ // that rotated it comes back rejected while the session is perfectly
670
+ // alive. Clearing on that first answer throws away a good login, so the
671
+ // second rejection in a row is what counts.
672
+ if (++authFails < 2) {
673
+ usageBackoff = 60 * 1000
674
+ scheduleUsagePoll()
675
+ return
676
+ }
664
677
  auth.clear()
665
678
  pushRealUsage(null)
666
679
  sendAccounts()
@@ -674,9 +687,23 @@ async function pollUsage() {
674
687
  scheduleUsagePoll()
675
688
  }
676
689
  function startUsagePoll() {
690
+ authFails = 0
677
691
  if (auth.isConnected()) pollUsage()
678
692
  }
679
693
 
694
+ // Waking up: the poll timer was frozen through the sleep, so the numbers on
695
+ // screen are as old as the nap. Re-state what we know — a read that failed on
696
+ // the way down would otherwise leave a stale "log in" panel up — and poll
697
+ // again, after a beat, since the network is rarely back the instant we are.
698
+ function onResume() {
699
+ if (!auth.isConnected()) return
700
+ if (win && !win.isDestroyed()) win.webContents.send('auth-state', { connected: true })
701
+ clearTimeout(usageTimer)
702
+ usageBackoff = 5 * 60 * 1000
703
+ usageTimer = setTimeout(pollUsage, 5000)
704
+ sendProfile()
705
+ }
706
+
680
707
  // push the logged-in account's identity (email + plan) to the renderer
681
708
  let profileTimer = null
682
709
  async function sendProfile(tries = 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.21.1",
4
+ "version": "1.21.2",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {