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/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 }