clauddy 1.3.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ๐ŸŸซ Claude Usage Monitor
1
+ # ๐ŸŸซ Clauddy
2
2
 
3
3
  A cute pixel-art desktop pet for macOS that tracks your Claude Code usage โ€” mirroring the official **Settings โ†’ Usage** panel (current session + weekly limits, in tokens & %), with animations.
4
4
 
5
5
  <p align="center">
6
- <img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/docs/media/overview.gif" width="300" alt="Claude Usage Monitor โ€” the full widget showing session, weekly, by-model and 30-day usage" /><br />
6
+ <img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/docs/media/overview.gif" width="300" alt="Clauddy โ€” the full widget showing session, weekly, by-model and 30-day usage" /><br />
7
7
  <em>A little terracotta creature that lives in the corner of your screen, eats your tokens, and naps when you're idle.</em>
8
8
  </p>
9
9
 
@@ -60,7 +60,9 @@ One command โ€” it downloads the latest release and drops it in `/Applications`:
60
60
  curl -fsSL https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/install.sh | bash
61
61
  ```
62
62
 
63
- No Gatekeeper warning: files fetched with `curl` aren't quarantined like browser downloads, so the (unsigned) app just opens. It registers itself in **Login Items**, so it **starts automatically** with your Mac โ€” set it and forget it.
63
+ **Clauddy is free and open source** โ€” the command above just downloads the latest release from this repo and drops it in `/Applications`, nothing else (you can read [`install.sh`](install.sh) first if you'd like).
64
+
65
+ Why not a normal download? macOS blocks **unsigned** apps downloaded through a browser with a scary *"damaged, move to Trash"* warning โ€” even when they're perfectly safe. It's a false alarm: the only way to silence it is to pay Apple **$99/year** to sign + notarize, which a free hobby app skips. Files fetched with `curl` aren't flagged, so this method simply **lets your Mac open the app** without the block. It then registers in **Login Items** and starts with your Mac โ€” set it and forget it.
64
66
 
65
67
  ### 2. Run it via `bunx` (no install)
66
68
 
@@ -74,19 +76,6 @@ The first run downloads Electron, so give it a moment. Handy for a quick run, bu
74
76
 
75
77
  > The app keeps its data in `~/.claude-usage-monitor`, regardless of how you run it.
76
78
 
77
- <details>
78
- <summary>Prefer to download the app by hand?</summary>
79
-
80
- Grab the **`โ€ฆ-mac.zip`** from [Releases](https://github.com/renatoaug/claude-usage-monitor/releases/latest), unzip, and drag the app to `/Applications`. The browser quarantines it, so macOS will call it *"damaged"* โ€” clear the flag once (Terminal needs **Full Disk Access**):
81
-
82
- ```bash
83
- xattr -dr com.apple.quarantine "/Applications/Claude Usage Monitor.app"
84
- ```
85
-
86
- The `curl` installer above avoids all of this.
87
-
88
- </details>
89
-
90
79
  ## Controls
91
80
 
92
81
  - **Drag** the widget anywhere on screen
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
@@ -77,7 +77,7 @@ function checkAlerts(config, d) {
77
77
  if (!armed.has(key)) {
78
78
  armed.add(key)
79
79
  new Notification({
80
- title: 'Claude Usage Monitor',
80
+ title: 'Clauddy',
81
81
  body: `Your ${name} is over ${t}% โ€” now at ${Math.round(pct)}%`,
82
82
  silent: false,
83
83
  }).show()
@@ -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.3.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": {
@@ -55,8 +55,8 @@
55
55
  "node": ">=24"
56
56
  },
57
57
  "build": {
58
- "appId": "com.renato.claude-usage-monitor",
59
- "productName": "Claude Usage Monitor",
58
+ "appId": "app.clauddy",
59
+ "productName": "Clauddy",
60
60
  "directories": {
61
61
  "output": "dist"
62
62
  },
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),
@@ -3,11 +3,16 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Claude Usage Monitor</title>
6
+ <title>Clauddy</title>
7
7
  <link rel="stylesheet" href="style.css" />
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
  }