clauddy 1.0.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 +154 -0
- package/auth.js +167 -0
- package/bin/clauddy.js +12 -0
- package/config.json +8 -0
- package/main.js +256 -0
- package/package.json +87 -0
- package/preload.js +18 -0
- package/renderer/index.html +162 -0
- package/renderer/pet.js +614 -0
- package/renderer/style.css +1195 -0
- package/usage.js +219 -0
package/usage.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
|
|
5
|
+
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects')
|
|
6
|
+
|
|
7
|
+
function labelFor(model) {
|
|
8
|
+
if (!model) return 'desconhecido'
|
|
9
|
+
const m = model.toLowerCase()
|
|
10
|
+
const fam = m.includes('opus')
|
|
11
|
+
? 'Opus'
|
|
12
|
+
: m.includes('sonnet')
|
|
13
|
+
? 'Sonnet'
|
|
14
|
+
: m.includes('haiku')
|
|
15
|
+
? 'Haiku'
|
|
16
|
+
: m.includes('fable')
|
|
17
|
+
? 'Fable'
|
|
18
|
+
: m.includes('mythos')
|
|
19
|
+
? 'Mythos'
|
|
20
|
+
: null
|
|
21
|
+
const ver = m.match(/-(\d)-(\d+)/)
|
|
22
|
+
if (fam && ver) return `${fam} ${ver[1]}.${ver[2]}`
|
|
23
|
+
if (fam) return fam
|
|
24
|
+
return model
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tokensOf(entry) {
|
|
28
|
+
const u = entry.usage || {}
|
|
29
|
+
return (
|
|
30
|
+
(u.input_tokens || 0) +
|
|
31
|
+
(u.output_tokens || 0) +
|
|
32
|
+
(u.cache_read_input_tokens || 0) +
|
|
33
|
+
(u.cache_creation_input_tokens || 0)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fileCache = new Map()
|
|
38
|
+
|
|
39
|
+
function walkJsonl(dir, out, cutoffMs) {
|
|
40
|
+
let items
|
|
41
|
+
try {
|
|
42
|
+
items = fs.readdirSync(dir, { withFileTypes: true })
|
|
43
|
+
} catch {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
for (const it of items) {
|
|
47
|
+
const full = path.join(dir, it.name)
|
|
48
|
+
if (it.isDirectory()) {
|
|
49
|
+
walkJsonl(full, out, cutoffMs)
|
|
50
|
+
} else if (it.isFile() && it.name.endsWith('.jsonl')) {
|
|
51
|
+
let st
|
|
52
|
+
try {
|
|
53
|
+
st = fs.statSync(full)
|
|
54
|
+
} catch {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (st.mtimeMs < cutoffMs) continue
|
|
58
|
+
out.push({ full, st })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseFile(full, st) {
|
|
64
|
+
const cached = fileCache.get(full)
|
|
65
|
+
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
|
|
66
|
+
return cached.entries
|
|
67
|
+
}
|
|
68
|
+
const entries = []
|
|
69
|
+
let raw
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(full, 'utf8')
|
|
72
|
+
} catch {
|
|
73
|
+
return entries
|
|
74
|
+
}
|
|
75
|
+
for (const line of raw.split('\n')) {
|
|
76
|
+
if (!line) continue
|
|
77
|
+
let obj
|
|
78
|
+
try {
|
|
79
|
+
obj = JSON.parse(line)
|
|
80
|
+
} catch {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) continue
|
|
84
|
+
if (obj.message.model === '<synthetic>') continue // Claude Code internal messages
|
|
85
|
+
const ts = Date.parse(obj.timestamp || obj.message?.timestamp || 0)
|
|
86
|
+
if (!ts) continue
|
|
87
|
+
entries.push({
|
|
88
|
+
ts,
|
|
89
|
+
model: obj.message.model,
|
|
90
|
+
tokens: tokensOf({ usage: obj.message.usage }),
|
|
91
|
+
key: `${obj.message.id || ''}:${obj.requestId || obj.uuid || ''}`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
fileCache.set(full, { mtimeMs: st.mtimeMs, size: st.size, entries })
|
|
95
|
+
return entries
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DAYS = 30
|
|
99
|
+
const SESSION_MS = 5 * 3600 * 1000
|
|
100
|
+
|
|
101
|
+
// Plan presets (token budgets ~= 100%). Calibrated for Max 5x from the official
|
|
102
|
+
// panel (5h ~24% at 152M tokens, weekly ~62% at 2.14B); Pro/Max20x scaled by the
|
|
103
|
+
// plan multiplier. ESTIMATES — Anthropic doesn't publish exact numbers.
|
|
104
|
+
const PLAN_BUDGETS = {
|
|
105
|
+
pro: { session: 126e6, week: 690e6 },
|
|
106
|
+
max5x: { session: 630e6, week: 3450e6 },
|
|
107
|
+
max20x: { session: 2520e6, week: 13800e6 },
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getUsage(config) {
|
|
111
|
+
const now = Date.now()
|
|
112
|
+
const dayMs = 24 * 3600 * 1000
|
|
113
|
+
const startOfToday = new Date()
|
|
114
|
+
startOfToday.setHours(0, 0, 0, 0)
|
|
115
|
+
const todayMs = startOfToday.getTime()
|
|
116
|
+
let weekStart = now - 7 * dayMs
|
|
117
|
+
let weekResetMs = null
|
|
118
|
+
if (config.weeklyAnchorIso) {
|
|
119
|
+
const anchor = Date.parse(config.weeklyAnchorIso)
|
|
120
|
+
if (!Number.isNaN(anchor)) {
|
|
121
|
+
const period = 7 * dayMs
|
|
122
|
+
const lastReset = anchor + Math.floor((now - anchor) / period) * period
|
|
123
|
+
weekStart = lastReset
|
|
124
|
+
weekResetMs = lastReset + period - now
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const start30 = todayMs - (DAYS - 1) * dayMs
|
|
128
|
+
const fiveMinAgo = now - 5 * 60000
|
|
129
|
+
const recentCutoff = now - 12 * 3600 * 1000
|
|
130
|
+
const scanCutoff = start30 - dayMs
|
|
131
|
+
|
|
132
|
+
const files = []
|
|
133
|
+
walkJsonl(PROJECTS_DIR, files, scanCutoff)
|
|
134
|
+
|
|
135
|
+
let lastMtime = 0
|
|
136
|
+
for (const f of files) lastMtime = Math.max(lastMtime, f.st.mtimeMs)
|
|
137
|
+
|
|
138
|
+
const seen = new Set()
|
|
139
|
+
let todayTokens = 0
|
|
140
|
+
let weekTokens = 0
|
|
141
|
+
let monthTokens = 0
|
|
142
|
+
const byModel = new Map() // tokens per model, 7 days
|
|
143
|
+
const days30 = new Array(DAYS).fill(0) // tokens per day
|
|
144
|
+
const recent = [] // last 12h, to detect the 5h session
|
|
145
|
+
let last5mTokens = 0
|
|
146
|
+
|
|
147
|
+
for (const f of files) {
|
|
148
|
+
const entries = parseFile(f.full, f.st)
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
if (e.ts < start30) continue
|
|
151
|
+
if (e.key && e.key !== ':' && seen.has(e.key)) continue
|
|
152
|
+
if (e.key && e.key !== ':') seen.add(e.key)
|
|
153
|
+
const t = e.tokens
|
|
154
|
+
monthTokens += t
|
|
155
|
+
|
|
156
|
+
const dayIdx = Math.min(DAYS - 1, Math.max(0, Math.floor((e.ts - start30) / dayMs)))
|
|
157
|
+
days30[dayIdx] += t
|
|
158
|
+
|
|
159
|
+
if (e.ts >= weekStart) {
|
|
160
|
+
weekTokens += t
|
|
161
|
+
const lbl = labelFor(e.model)
|
|
162
|
+
byModel.set(lbl, (byModel.get(lbl) || 0) + t)
|
|
163
|
+
}
|
|
164
|
+
if (e.ts >= todayMs) todayTokens += t
|
|
165
|
+
if (e.ts >= recentCutoff) recent.push({ ts: e.ts, tokens: t })
|
|
166
|
+
if (e.ts >= fiveMinAgo) last5mTokens += t
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 5h session window (like /usage "Current session")
|
|
171
|
+
recent.sort((a, b) => a.ts - b.ts)
|
|
172
|
+
let sStart = null
|
|
173
|
+
let sEnd = null
|
|
174
|
+
for (const e of recent) {
|
|
175
|
+
if (sStart === null || e.ts >= sEnd) {
|
|
176
|
+
sStart = e.ts
|
|
177
|
+
sEnd = sStart + SESSION_MS
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const planB = PLAN_BUDGETS[config.plan]
|
|
181
|
+
const sessionBudget = planB ? planB.session : config.sessionTokenBudget
|
|
182
|
+
const weeklyBudget = planB ? planB.week : config.weeklyTokenBudget
|
|
183
|
+
|
|
184
|
+
const session = { tokens: 0, pct: 0, resetMs: 0, active: false }
|
|
185
|
+
if (sStart !== null && now < sEnd) {
|
|
186
|
+
let tk = 0
|
|
187
|
+
for (const e of recent) if (e.ts >= sStart && e.ts < sEnd) tk += e.tokens
|
|
188
|
+
session.tokens = tk
|
|
189
|
+
session.active = true
|
|
190
|
+
session.resetMs = sEnd - now
|
|
191
|
+
session.pct = sessionBudget ? Math.min(100, (tk / sessionBudget) * 100) : 0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const weekPct = weeklyBudget ? Math.min(100, (weekTokens / weeklyBudget) * 100) : 0
|
|
195
|
+
|
|
196
|
+
const lastActivityMs = lastMtime ? now - lastMtime : Infinity
|
|
197
|
+
const active = lastActivityMs <= (config.activeThresholdMs || 8000)
|
|
198
|
+
const sleeping = lastActivityMs >= (config.sleepThresholdMs || 300000)
|
|
199
|
+
|
|
200
|
+
const byModelArr = [...byModel.entries()]
|
|
201
|
+
.map(([label, tokens]) => ({ label, tokens }))
|
|
202
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
session,
|
|
206
|
+
week: { tokens: weekTokens, pct: weekPct, resetMs: weekResetMs },
|
|
207
|
+
today: { tokens: todayTokens },
|
|
208
|
+
byModel: byModelArr,
|
|
209
|
+
days30,
|
|
210
|
+
monthTokens,
|
|
211
|
+
tokensPerMin: Math.round(last5mTokens / 5),
|
|
212
|
+
active,
|
|
213
|
+
sleeping,
|
|
214
|
+
lastActivityMs,
|
|
215
|
+
ts: now,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { getUsage }
|