clauddy 1.4.0 → 1.5.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/auth.js CHANGED
@@ -9,6 +9,7 @@ const REDIRECT = 'https://platform.claude.com/oauth/code/callback'
9
9
  const AUTHORIZE = 'https://claude.ai/oauth/authorize'
10
10
  const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token'
11
11
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage'
12
+ const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile'
12
13
  const SCOPE = 'org:create_api_key user:profile user:inference'
13
14
  const UA = 'claude-cli/2.1.181 (external, cli)'
14
15
  // when CLAUDE_CONFIG_DIR is set (e.g. via direnv for multi-account setups),
@@ -23,6 +24,7 @@ const b64url = (buf) =>
23
24
 
24
25
  let tokens = null // { access_token, refresh_token, expires_at }
25
26
  let pending = null // { verifier, state }
27
+ let profile = null // { email, name, plan } — cached account identity
26
28
 
27
29
  function load() {
28
30
  if (tokens) return tokens
@@ -42,6 +44,7 @@ function save(t) {
42
44
  }
43
45
  function clear() {
44
46
  tokens = null
47
+ profile = null
45
48
  try {
46
49
  fs.unlinkSync(TOKEN_PATH)
47
50
  } catch {}
@@ -169,4 +172,29 @@ async function fetchUsage() {
169
172
  }
170
173
  }
171
174
 
172
- module.exports = { begin, complete, fetchUsage, clear, isConnected }
175
+ // The logged-in account's identity (email + plan tier). Cached for the session;
176
+ // only changes on logout/login, so we don't re-fetch on every usage poll.
177
+ async function fetchProfile() {
178
+ if (profile) return profile
179
+ const token = await validToken()
180
+ const res = await fetch(PROFILE_URL, {
181
+ headers: {
182
+ Authorization: `Bearer ${token}`,
183
+ 'anthropic-beta': 'oauth-2025-04-20',
184
+ 'anthropic-version': '2023-06-01',
185
+ 'User-Agent': UA,
186
+ },
187
+ })
188
+ if (!res.ok) {
189
+ throw Object.assign(new Error(`profile ${res.status}`), { status: res.status })
190
+ }
191
+ const a = (await res.json()).account || {}
192
+ profile = {
193
+ email: a.email || null,
194
+ name: a.display_name || a.full_name || null,
195
+ plan: a.has_claude_max ? 'Max' : a.has_claude_pro ? 'Pro' : null,
196
+ }
197
+ return profile
198
+ }
199
+
200
+ module.exports = { begin, complete, fetchUsage, fetchProfile, clear, isConnected }
package/main.js CHANGED
@@ -135,6 +135,7 @@ function createWindow() {
135
135
  win.webContents.send('auth-state', { connected: auth.isConnected() })
136
136
  pollTimer = setInterval(tick, config.pollIntervalMs)
137
137
  startUsagePoll()
138
+ sendProfile()
138
139
  watchDebug()
139
140
  })
140
141
  }
@@ -182,6 +183,7 @@ async function pollUsage() {
182
183
  if (win && !win.isDestroyed()) {
183
184
  win.webContents.send('auth-state', { connected: false })
184
185
  win.webContents.send('real-usage', null)
186
+ win.webContents.send('profile', null)
185
187
  }
186
188
  }
187
189
  }
@@ -191,6 +193,15 @@ function startUsagePoll() {
191
193
  if (auth.isConnected()) pollUsage()
192
194
  }
193
195
 
196
+ // push the logged-in account's identity (email + plan) to the renderer
197
+ async function sendProfile() {
198
+ if (!auth.isConnected()) return
199
+ try {
200
+ const p = await auth.fetchProfile()
201
+ if (win && !win.isDestroyed()) win.webContents.send('profile', p)
202
+ } catch {} // non-fatal: the chip just stays hidden
203
+ }
204
+
194
205
  ipcMain.on('auth-start', () => shell.openExternal(auth.begin()))
195
206
  ipcMain.on('auth-code', async (_e, code) => {
196
207
  const ok = () => {
@@ -198,6 +209,7 @@ ipcMain.on('auth-code', async (_e, code) => {
198
209
  win.webContents.send('auth-state', { connected: true })
199
210
  win.webContents.send('auth-result', { ok: true })
200
211
  }
212
+ sendProfile()
201
213
  }
202
214
  try {
203
215
  await auth.complete(code)
@@ -227,6 +239,7 @@ ipcMain.on('auth-logout', () => {
227
239
  if (win && !win.isDestroyed()) {
228
240
  win.webContents.send('auth-state', { connected: false })
229
241
  win.webContents.send('real-usage', null)
242
+ win.webContents.send('profile', null)
230
243
  }
231
244
  })
232
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauddy",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A cute desktop pet that tracks your Claude Code usage",
5
5
  "main": "main.js",
6
6
  "bin": {
package/preload.js CHANGED
@@ -9,6 +9,7 @@ contextBridge.exposeInMainWorld('api', {
9
9
  openUsage: () => ipcRenderer.send('open-usage'),
10
10
  onRealUsage: (cb) => ipcRenderer.on('real-usage', (_e, u) => cb(u)),
11
11
  onAuthState: (cb) => ipcRenderer.on('auth-state', (_e, s) => cb(s)),
12
+ onProfile: (cb) => ipcRenderer.on('profile', (_e, p) => cb(p)),
12
13
  onAuthResult: (cb) => ipcRenderer.on('auth-result', (_e, r) => cb(r)),
13
14
  authStart: () => ipcRenderer.send('auth-start'),
14
15
  authCode: (code) => ipcRenderer.send('auth-code', code),
@@ -8,6 +8,11 @@
8
8
  </head>
9
9
  <body class="state-idle">
10
10
  <div id="card">
11
+ <div id="account-chip" hidden>
12
+ <span class="ac-dot"></span>
13
+ <span id="ac-email"></span>
14
+ <span id="ac-plan" class="plan-badge" hidden></span>
15
+ </div>
11
16
  <div id="controls">
12
17
  <button id="gear" title="Settings">⚙</button>
13
18
  <button id="usage" title="Open Usage page">
@@ -47,6 +52,10 @@
47
52
 
48
53
  <!-- shown only when collapsed -->
49
54
  <div id="mini">
55
+ <div id="mini-acct" hidden>
56
+ <span id="mini-acct-name"></span>
57
+ <span id="mini-acct-plan" class="plan-badge" hidden></span>
58
+ </div>
50
59
  <div class="mini-line"><span id="mini-dot"></span><span id="mini-text">idle</span></div>
51
60
  <div id="mini-pct-row"><span id="mini-pct">0%</span> <span class="lbl">session</span></div>
52
61
  </div>
package/renderer/pet.js CHANGED
@@ -499,9 +499,39 @@ window.api.onAuthState((s) => {
499
499
  realUsage = null
500
500
  document.body.classList.remove('live')
501
501
  el('acc-paste').classList.remove('show')
502
+ showProfile(null)
502
503
  }
503
504
  if (document.body.classList.contains('settings-open')) fitSize()
504
505
  })
506
+
507
+ // logged-in account chip (email + plan) shown top-left when connected
508
+ function showProfile(p) {
509
+ const chip = el('account-chip')
510
+ const mini = el('mini-acct')
511
+ if (!p?.email) {
512
+ chip.hidden = true
513
+ mini.hidden = true
514
+ return
515
+ }
516
+ // expanded: full email chip in the top bar
517
+ el('ac-email').textContent = p.email
518
+ chip.title = p.name ? `${p.name} · ${p.email}` : p.email
519
+ setPlan(el('ac-plan'), p.plan)
520
+ chip.hidden = false
521
+ // collapsed: short name + plan in the mini block (email won't fit at 116px)
522
+ el('mini-acct-name').textContent = p.name || p.email.split('@')[0]
523
+ setPlan(el('mini-acct-plan'), p.plan)
524
+ mini.hidden = false
525
+ }
526
+ function setPlan(node, plan) {
527
+ if (plan) {
528
+ node.textContent = plan
529
+ node.hidden = false
530
+ } else {
531
+ node.hidden = true
532
+ }
533
+ }
534
+ window.api.onProfile(showProfile)
505
535
  window.api.onAuthResult((r) => {
506
536
  if (r?.ok) {
507
537
  el('acc-msg').textContent = ''
@@ -1,7 +1,7 @@
1
1
  :root {
2
2
  --coral: #d97757;
3
3
  --coral-soft: #e6a07f;
4
- --pixel: #bd7853; /* pet terracotta (earthier, like the original) */
4
+ --pixel: #d57658; /* pet terracotta matches the real Claude logo */
5
5
  --eye: #221b16;
6
6
  --text: #f3ebe3;
7
7
  --muted: #9d9085;
@@ -129,6 +129,54 @@ body.collapsed #usage {
129
129
  display: none;
130
130
  }
131
131
 
132
+ /* logged-in account chip (top-left, mirrors #controls) */
133
+ #account-chip {
134
+ position: absolute;
135
+ top: 6px;
136
+ left: 10px;
137
+ z-index: 10;
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 5px;
141
+ height: 19px; /* match #controls buttons so text centers on the same line */
142
+ max-width: 138px;
143
+ font-size: 9.5px;
144
+ line-height: 1;
145
+ color: var(--muted);
146
+ }
147
+ #account-chip[hidden] {
148
+ display: none;
149
+ }
150
+ .ac-dot {
151
+ flex: none;
152
+ width: 6px;
153
+ height: 6px;
154
+ border-radius: 50%;
155
+ background: var(--pixel);
156
+ }
157
+ #ac-email {
158
+ overflow: hidden;
159
+ text-overflow: ellipsis;
160
+ white-space: nowrap;
161
+ }
162
+ .plan-badge {
163
+ flex: none;
164
+ padding: 1.5px 5px;
165
+ border-radius: 6px;
166
+ font-size: 8px;
167
+ font-weight: 700;
168
+ letter-spacing: 0.3px;
169
+ color: var(--coral-soft);
170
+ background: rgba(217, 119, 87, 0.16);
171
+ }
172
+ .plan-badge[hidden] {
173
+ display: none;
174
+ }
175
+ /* no room for the chip when collapsed */
176
+ body.collapsed #account-chip {
177
+ display: none;
178
+ }
179
+
132
180
  /* settings panel */
133
181
  #settings {
134
182
  display: none;
@@ -440,6 +488,25 @@ body:not(.live) #mini-pct-row {
440
488
  text-align: center;
441
489
  margin-top: 2px;
442
490
  }
491
+ /* account name + plan, shown above the status line when collapsed */
492
+ #mini-acct {
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: center;
496
+ gap: 5px;
497
+ margin-bottom: 3px;
498
+ }
499
+ #mini-acct[hidden] {
500
+ display: none;
501
+ }
502
+ #mini-acct-name {
503
+ max-width: 56px;
504
+ overflow: hidden;
505
+ text-overflow: ellipsis;
506
+ white-space: nowrap;
507
+ font-size: 9.5px;
508
+ color: var(--muted);
509
+ }
443
510
  .mini-line {
444
511
  display: flex;
445
512
  align-items: center;
@@ -757,9 +824,6 @@ body.state-tired #sweat {
757
824
  body.state-working #claude {
758
825
  animation: hop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
759
826
  }
760
- body.state-working #body rect {
761
- fill: #d68a64;
762
- }
763
827
  body.state-stressed #body rect {
764
828
  fill: #cb5642;
765
829
  }