clauddy 1.4.0 → 1.5.1
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 +36 -2
- package/main.js +13 -0
- package/package.json +1 -1
- package/preload.js +1 -0
- package/renderer/index.html +9 -0
- package/renderer/pet.js +30 -0
- package/renderer/style.css +68 -4
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 {}
|
|
@@ -121,7 +124,13 @@ async function refresh() {
|
|
|
121
124
|
client_id: CLIENT_ID,
|
|
122
125
|
}),
|
|
123
126
|
})
|
|
124
|
-
if (!res.ok)
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
// a rejected grant (expired/revoked refresh token) means the session is
|
|
129
|
+
// dead — surface it as 401 so callers clear the token and prompt login.
|
|
130
|
+
// transient failures (429, 5xx) keep their status so we retry, not logout.
|
|
131
|
+
const dead = res.status === 400 || res.status === 403
|
|
132
|
+
throw Object.assign(new Error('refresh failed'), { status: dead ? 401 : res.status })
|
|
133
|
+
}
|
|
125
134
|
const j = await res.json()
|
|
126
135
|
save({
|
|
127
136
|
access_token: j.access_token,
|
|
@@ -169,4 +178,29 @@ async function fetchUsage() {
|
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
// The logged-in account's identity (email + plan tier). Cached for the session;
|
|
182
|
+
// only changes on logout/login, so we don't re-fetch on every usage poll.
|
|
183
|
+
async function fetchProfile() {
|
|
184
|
+
if (profile) return profile
|
|
185
|
+
const token = await validToken()
|
|
186
|
+
const res = await fetch(PROFILE_URL, {
|
|
187
|
+
headers: {
|
|
188
|
+
Authorization: `Bearer ${token}`,
|
|
189
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
190
|
+
'anthropic-version': '2023-06-01',
|
|
191
|
+
'User-Agent': UA,
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
throw Object.assign(new Error(`profile ${res.status}`), { status: res.status })
|
|
196
|
+
}
|
|
197
|
+
const a = (await res.json()).account || {}
|
|
198
|
+
profile = {
|
|
199
|
+
email: a.email || null,
|
|
200
|
+
name: a.display_name || a.full_name || null,
|
|
201
|
+
plan: a.has_claude_max ? 'Max' : a.has_claude_pro ? 'Pro' : null,
|
|
202
|
+
}
|
|
203
|
+
return profile
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
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
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),
|
package/renderer/index.html
CHANGED
|
@@ -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 = ''
|
package/renderer/style.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--coral: #d97757;
|
|
3
3
|
--coral-soft: #e6a07f;
|
|
4
|
-
--pixel: #
|
|
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
|
}
|