clauddy 1.29.0 → 2.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/codex.js DELETED
@@ -1,236 +0,0 @@
1
- const fs = require('node:fs')
2
- const path = require('node:path')
3
- const os = require('node:os')
4
-
5
- // Codex (CLI and the desktop app alike) writes one `rollout-*.jsonl` per
6
- // session under ~/.codex/sessions/YYYY/MM/DD. Its `token_count` events carry
7
- // both the token totals and the account's rate limits, so everything here comes
8
- // from local files — no login needed.
9
- let codexDir = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
10
-
11
- function setCodexDir(dir) {
12
- codexDir = dir || process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
13
- fileCache.clear()
14
- }
15
-
16
- const DAY = 86400000
17
- const DAYS = 30
18
- const WEEK = 7 * DAY
19
- const ACTIVE_MS = 60000 // a rollout touched within the last minute = Codex working
20
- const DAY_MIN = 24 * 60
21
- const TOP_PROJECTS = 5 // same fold as Claude's panel
22
-
23
- // per file: how far we've read, plus what we got out of it. Rollouts only ever
24
- // grow, so a poll reads just the bytes appended since the last one.
25
- const fileCache = new Map()
26
-
27
- function blankSummary() {
28
- return { offset: 0, rest: '', lastTotal: 0, events: [], limits: null, model: null, cwd: null }
29
- }
30
-
31
- // a token_count's rate limits. Codex reports several buckets; some (the
32
- // `premium` one on Plus, say) come with null windows — only a bucket with a
33
- // real window is worth showing.
34
- function limitsOf(rl, ts) {
35
- if (!rl || (!rl.primary && !rl.secondary)) return null
36
- const win = (w) =>
37
- w && typeof w.used_percent === 'number'
38
- ? { pct: w.used_percent, resetsAt: w.resets_at ? w.resets_at * 1000 : null }
39
- : null
40
- // Plus reports a 5-hour window and a weekly one; some plans (Pro Lite, say)
41
- // only the weekly one, in the `primary` slot. Sort by length, not by slot.
42
- let session = null
43
- let weekly = null
44
- for (const w of [rl.primary, rl.secondary]) {
45
- const v = win(w)
46
- if (!v) continue
47
- const long = w.window_minutes != null ? w.window_minutes >= DAY_MIN : w === rl.secondary
48
- if (long) weekly = v
49
- else session = v
50
- }
51
- return { ts, plan: rl.plan_type || null, session, weekly }
52
- }
53
-
54
- function ingestLine(sum, line) {
55
- // cheap filter before JSON.parse — rollouts are mostly message bodies
56
- if (!line.includes('"token_count"') && !line.includes('"turn_context"')) return
57
- let o
58
- try {
59
- o = JSON.parse(line)
60
- } catch {
61
- return
62
- }
63
- const p = o.payload || {}
64
- const ts = Date.parse(o.timestamp) || 0
65
- if (o.type === 'turn_context') {
66
- if (p.model) sum.model = p.model
67
- if (p.cwd) sum.cwd = p.cwd
68
- return
69
- }
70
- if (p.type !== 'token_count') return
71
- // totals are cumulative per session, and the same count is often emitted
72
- // twice — the delta is what this event actually spent. The model and project
73
- // are the ones in effect *now*, so a mid-session switch is attributed right.
74
- const total = p.info?.total_token_usage?.total_tokens
75
- if (typeof total === 'number' && total > sum.lastTotal) {
76
- sum.events.push({ ts, tokens: total - sum.lastTotal, model: sum.model, cwd: sum.cwd })
77
- sum.lastTotal = total
78
- }
79
- const lim = limitsOf(p.rate_limits, ts)
80
- if (lim && (!sum.limits || lim.ts >= sum.limits.ts)) sum.limits = lim
81
- }
82
-
83
- function readFile(file, st) {
84
- let sum = fileCache.get(file)
85
- // shrunk or replaced: start over
86
- if (!sum || st.size < sum.offset) sum = blankSummary()
87
- if (st.size > sum.offset) {
88
- const fd = fs.openSync(file, 'r')
89
- try {
90
- const buf = Buffer.alloc(st.size - sum.offset)
91
- fs.readSync(fd, buf, 0, buf.length, sum.offset)
92
- sum.offset = st.size
93
- const lines = (sum.rest + buf.toString('utf8')).split('\n')
94
- // the last piece may be a line still being written
95
- sum.rest = lines.pop()
96
- for (const line of lines) ingestLine(sum, line)
97
- } finally {
98
- fs.closeSync(fd)
99
- }
100
- }
101
- sum.mtime = st.mtimeMs
102
- fileCache.set(file, sum)
103
- return sum
104
- }
105
-
106
- // every rollout, with its stat; `since` drops the ones untouched since then
107
- function rollouts(since) {
108
- const out = []
109
- const walk = (dir, depth) => {
110
- let kids
111
- try {
112
- kids = fs.readdirSync(dir, { withFileTypes: true })
113
- } catch {
114
- return
115
- }
116
- for (const k of kids) {
117
- const full = path.join(dir, k.name)
118
- if (k.isDirectory() && depth < 3) walk(full, depth + 1)
119
- else if (k.isFile() && k.name.endsWith('.jsonl')) {
120
- try {
121
- const st = fs.statSync(full)
122
- if (st.mtimeMs >= since) out.push({ file: full, st })
123
- } catch {}
124
- }
125
- }
126
- }
127
- walk(path.join(codexDir, 'sessions'), 0)
128
- return out
129
- }
130
-
131
- // Is there a Codex session on this machine? The folder alone is not enough.
132
- function detectCodex() {
133
- const files = rollouts(0)
134
- if (!files.length) return { found: false, plan: null }
135
- files.sort((a, b) => b.st.mtimeMs - a.st.mtimeMs)
136
- let plan = null
137
- for (const { file, st } of files.slice(0, 5)) {
138
- const lim = readFile(file, st).limits
139
- if (lim?.plan) {
140
- plan = lim.plan
141
- break
142
- }
143
- }
144
- return { found: true, plan }
145
- }
146
-
147
- // A window whose reset has passed tells us nothing about today: the log is
148
- // older than the window, so the % is unknown until Codex writes a fresh one.
149
- function windowNow(w, now) {
150
- if (!w) return null
151
- const expired = w.resetsAt != null && w.resetsAt <= now
152
- return {
153
- pct: expired ? null : w.pct,
154
- resetMs: w.resetsAt != null && !expired ? w.resetsAt - now : null,
155
- expired,
156
- }
157
- }
158
-
159
- function ranked(map) {
160
- return [...map.entries()]
161
- .map(([label, tokens]) => ({ label, tokens }))
162
- .sort((a, b) => b.tokens - a.tokens)
163
- }
164
-
165
- function getCodexUsage(now = Date.now()) {
166
- if (!fs.existsSync(path.join(codexDir, 'sessions'))) return null
167
- const startOfDay = new Date(now).setHours(0, 0, 0, 0)
168
- const start30 = startOfDay - (DAYS - 1) * DAY
169
- const files = rollouts(start30)
170
- const seen = new Set()
171
- let limits = null
172
- let latest = null
173
- let tokensToday = 0
174
- let tokens5h = 0
175
- let tokensWeek = 0
176
- let monthTokens = 0
177
- const days30 = new Array(DAYS).fill(0)
178
- const byModel = new Map()
179
- const byProject = new Map()
180
- for (const { file, st } of files) {
181
- seen.add(file)
182
- const sum = readFile(file, st)
183
- if (sum.limits && (!limits || sum.limits.ts > limits.ts)) limits = sum.limits
184
- if (!latest || sum.mtime > latest.mtime) latest = sum
185
- for (const e of sum.events) {
186
- if (e.ts < start30) continue
187
- monthTokens += e.tokens
188
- days30[Math.min(DAYS - 1, Math.floor((e.ts - start30) / DAY))] += e.tokens
189
- if (e.ts >= startOfDay) tokensToday += e.tokens
190
- if (now - e.ts < 5 * 3600000) tokens5h += e.tokens
191
- if (now - e.ts < WEEK) {
192
- tokensWeek += e.tokens
193
- const m = e.model || 'unknown'
194
- byModel.set(m, (byModel.get(m) || 0) + e.tokens)
195
- if (e.cwd) {
196
- const p = path.basename(e.cwd)
197
- byProject.set(p, (byProject.get(p) || 0) + e.tokens)
198
- }
199
- }
200
- }
201
- }
202
- // forget files that aged out of the window
203
- for (const f of fileCache.keys()) if (!seen.has(f)) fileCache.delete(f)
204
-
205
- const projects = ranked(byProject)
206
- const byProjectArr = projects.slice(0, TOP_PROJECTS)
207
- const rest = projects.slice(TOP_PROJECTS)
208
- if (rest.length) {
209
- byProjectArr.push({
210
- label: `other · ${rest.length}`,
211
- tokens: rest.reduce((n, p) => n + p.tokens, 0),
212
- })
213
- }
214
-
215
- const weeklyOnly = !!limits?.weekly && !limits.session
216
- return {
217
- active: !!latest && now - latest.mtime < ACTIVE_MS,
218
- lastSeen: latest ? latest.mtime : null,
219
- plan: limits?.plan || null,
220
- // with no 5-hour window, the week is the budget that binds: it takes the
221
- // session's place, the way Cursor's month does
222
- session: limits ? windowNow(weeklyOnly ? limits.weekly : limits.session, now) : null,
223
- weekly: limits && !weeklyOnly ? windowNow(limits.weekly, now) : null,
224
- weeklyOnly,
225
- limitsAt: limits?.ts || null,
226
- tokensToday,
227
- tokens5h,
228
- tokensWeek,
229
- byModel: ranked(byModel),
230
- byProject: byProjectArr,
231
- days30,
232
- monthTokens,
233
- }
234
- }
235
-
236
- module.exports = { getCodexUsage, detectCodex, setCodexDir }
package/config.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "_note": "Defaults bundled with the app. User settings are saved to ~/.claude-usage-monitor/config.json. Session/weekly % come from your account (log in via Settings).",
3
- "mode": "floating",
4
- "alerts": true,
5
- "alertThresholds": [80, 95],
6
- "fireThreshold": 90,
7
- "zoom": 100,
8
- "pollIntervalMs": 4000,
9
- "activeThresholdMs": 20000,
10
- "sleepThresholdMs": 300000
11
- }
package/cursor.js DELETED
@@ -1,278 +0,0 @@
1
- const fs = require('node:fs')
2
- const path = require('node:path')
3
- const os = require('node:os')
4
-
5
- // Cursor keeps no usage in local files. Its app stores a session token in
6
- // `state.vscdb` (SQLite), and the same dashboard endpoints cursor.com uses
7
- // answer with it — unofficial, so every field is read defensively. Activity
8
- // comes from the agent transcripts under ~/.cursor/projects, which log each
9
- // tool call much like Claude Code's own logs.
10
- const API = 'https://api2.cursor.sh/aiserver.v1.DashboardService/'
11
- const ACTIVE_MS = 60000 // a transcript that says nothing counts only while it's this fresh
12
- // an unfinished turn this long without a write was abandoned (Cursor closed mid-run)
13
- const OPEN_TURN_MS = 30 * 60000
14
- const RECENT_TRANSCRIPTS = 8
15
- const FETCH_MS = 5 * 60000 // a monthly budget moves slowly; don't hammer the API
16
- const RETRY_MS = 60000
17
- const TAIL_BYTES = 64 * 1024
18
-
19
- function defaultStateDb() {
20
- const tail = ['Cursor', 'User', 'globalStorage', 'state.vscdb']
21
- if (process.platform === 'darwin')
22
- return path.join(os.homedir(), 'Library', 'Application Support', ...tail)
23
- if (process.platform === 'win32')
24
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), ...tail)
25
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), ...tail)
26
- }
27
-
28
- let homeDir = path.join(os.homedir(), '.cursor')
29
- let stateDb = defaultStateDb()
30
- let fetchImpl = (...args) => globalThis.fetch(...args)
31
- let limits = null // last good reading: { ts, plan, endMs, total, api }
32
- let lastTry = -Infinity
33
- let lastFailed = false
34
- let signedOut = false
35
- let inflight = null
36
-
37
- // tests point these elsewhere; a change of place forgets the old reading
38
- function configureCursor(opts = {}) {
39
- if ('fetch' in opts) fetchImpl = opts.fetch || ((...args) => globalThis.fetch(...args))
40
- if (!('home' in opts) && !('stateDb' in opts)) return
41
- if ('home' in opts) homeDir = opts.home || path.join(os.homedir(), '.cursor')
42
- if ('stateDb' in opts) stateDb = opts.stateDb || defaultStateDb()
43
- limits = null
44
- lastTry = -Infinity
45
- lastFailed = false
46
- signedOut = false
47
- inflight = null
48
- }
49
-
50
- const text = (v) => (v == null ? null : typeof v === 'string' ? v : Buffer.from(v).toString('utf8'))
51
-
52
- // Electron's Node has `node:sqlite`; Bun, which runs the tests, has its own
53
- // with the same prepare/get/close
54
- function openReadOnly(file) {
55
- try {
56
- const { DatabaseSync } = require('node:sqlite')
57
- return new DatabaseSync(file, { readOnly: true })
58
- } catch (err) {
59
- if (!process.versions.bun) throw err
60
- const { Database } = require('bun:sqlite')
61
- return new Database(file, { readonly: true })
62
- }
63
- }
64
-
65
- // the app's own session: its token, plus the plan it last cached
66
- function readAuth() {
67
- if (!fs.existsSync(stateDb)) return null
68
- let db
69
- try {
70
- db = openReadOnly(stateDb)
71
- const q = db.prepare('SELECT value FROM ItemTable WHERE key = ?')
72
- const get = (k) => text(q.get(k)?.value)
73
- return { token: get('cursorAuth/accessToken'), plan: get('cursorAuth/stripeMembershipType') }
74
- } catch {
75
- return null
76
- } finally {
77
- db?.close()
78
- }
79
- }
80
-
81
- // Settings → Connect: only a signed-in app has something to read
82
- function detectCursor() {
83
- const auth = readAuth()
84
- return { found: !!auth?.token, plan: auth?.plan || null }
85
- }
86
-
87
- async function post(method, token) {
88
- const res = await fetchImpl(API + method, {
89
- method: 'POST',
90
- headers: {
91
- Authorization: `Bearer ${token}`,
92
- 'Content-Type': 'application/json',
93
- 'Connect-Protocol-Version': '1',
94
- },
95
- body: '{}',
96
- signal: AbortSignal.timeout(15000),
97
- })
98
- if (!res.ok) throw new Error(`cursor ${method}: HTTP ${res.status}`)
99
- return res.json()
100
- }
101
-
102
- const pctOf = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null)
103
-
104
- // One fetch at a time. The token is read fresh each time: the Cursor app
105
- // refreshes it on its own, so there is nothing to renew here.
106
- function refreshLimits(now = Date.now()) {
107
- if (inflight) return inflight
108
- lastTry = now
109
- inflight = (async () => {
110
- const auth = readAuth()
111
- signedOut = !auth?.token
112
- if (signedOut) return
113
- const [usage, info] = await Promise.all([
114
- post('GetCurrentPeriodUsage', auth.token),
115
- post('GetPlanInfo', auth.token).catch(() => null),
116
- ])
117
- const pu = usage?.planUsage || {}
118
- limits = {
119
- ts: now,
120
- plan: info?.planInfo?.planName?.toLowerCase() || auth.plan || null,
121
- endMs: Number(usage?.billingCycleEnd) || null,
122
- total: pctOf(pu.totalPercentUsed),
123
- api: pctOf(pu.apiPercentUsed),
124
- }
125
- })()
126
- .then(() => {
127
- lastFailed = false
128
- })
129
- .catch((err) => {
130
- lastFailed = true
131
- console.error('cursor:', err.message)
132
- })
133
- .finally(() => {
134
- inflight = null
135
- })
136
- return inflight
137
- }
138
-
139
- // the billing cycle as a window; past its end, the % belongs to the old cycle
140
- function cycleNow(pct, endMs, now) {
141
- const expired = endMs != null && endMs <= now
142
- return {
143
- pct: expired ? null : pct,
144
- resetMs: endMs != null && !expired ? endMs - now : null,
145
- expired,
146
- }
147
- }
148
-
149
- // ---- activity ---------------------------------------------------------------
150
- const ACTIVITY = {
151
- Read: 'reading',
152
- ReadFile: 'reading',
153
- Grep: 'reading',
154
- Glob: 'reading',
155
- LS: 'reading',
156
- ListDir: 'reading',
157
- SemanticSearch: 'reading',
158
- ReadLints: 'reading',
159
- Write: 'editing',
160
- StrReplace: 'editing',
161
- Edit: 'editing',
162
- MultiEdit: 'editing',
163
- ApplyPatch: 'editing',
164
- Delete: 'editing',
165
- EditNotebook: 'editing',
166
- Shell: 'running',
167
- WebSearch: 'researching',
168
- WebFetch: 'researching',
169
- Task: 'delegating',
170
- TodoWrite: 'planning',
171
- CreatePlan: 'planning',
172
- AskQuestion: 'waiting',
173
- }
174
-
175
- // what the newest transcript lines say: a finished turn means nothing is
176
- // running (null), an open one names its scene, and undefined means no telling
177
- function activityOf(lines) {
178
- for (let i = lines.length - 1; i >= 0; i--) {
179
- let o
180
- try {
181
- o = JSON.parse(lines[i])
182
- } catch {
183
- continue
184
- }
185
- if (o.type === 'turn_ended') return null
186
- if (o.role === 'assistant') {
187
- const blocks = Array.isArray(o.message?.content) ? o.message.content : []
188
- const tool = blocks.findLast((b) => b?.type === 'tool_use')
189
- return (tool && ACTIVITY[tool.name]) || 'working'
190
- }
191
- if (o.role === 'user') return 'working' // just asked: the agent is thinking
192
- }
193
- return undefined
194
- }
195
-
196
- const dirs = (dir) => {
197
- try {
198
- return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => !d.name.startsWith('.'))
199
- } catch {
200
- return []
201
- }
202
- }
203
-
204
- // every transcript, subagents included: projects/<p>/agent-transcripts/<id>/…
205
- function transcripts() {
206
- const out = []
207
- for (const p of dirs(path.join(homeDir, 'projects'))) {
208
- if (!p.isDirectory()) continue
209
- const root = path.join(homeDir, 'projects', p.name, 'agent-transcripts')
210
- for (const id of dirs(root)) {
211
- if (!id.isDirectory()) continue
212
- const dir = path.join(root, id.name)
213
- for (const f of dirs(dir)) {
214
- if (f.isFile() && f.name.endsWith('.jsonl')) out.push(path.join(dir, f.name))
215
- else if (f.isDirectory() && f.name === 'subagents') {
216
- for (const s of dirs(path.join(dir, f.name)))
217
- if (s.isFile() && s.name.endsWith('.jsonl')) out.push(path.join(dir, f.name, s.name))
218
- }
219
- }
220
- }
221
- }
222
- return out
223
- }
224
-
225
- function tailLines(file, size) {
226
- const len = Math.min(size, TAIL_BYTES)
227
- const buf = Buffer.alloc(len)
228
- const fd = fs.openSync(file, 'r')
229
- try {
230
- fs.readSync(fd, buf, 0, len, size - len)
231
- } finally {
232
- fs.closeSync(fd)
233
- }
234
- const lines = buf.toString('utf8').split('\n')
235
- if (len < size) lines.shift() // cut mid-line
236
- return lines.filter(Boolean)
237
- }
238
-
239
- // A turn stays open while a long command runs or the model thinks, without a
240
- // write, so an open turn in any recent transcript is Cursor at work. A
241
- // transcript with nothing to say counts only while it's fresh.
242
- function readActivity(now) {
243
- const recent = []
244
- let lastSeen = null
245
- for (const file of transcripts()) {
246
- try {
247
- const st = fs.statSync(file)
248
- if (lastSeen == null || st.mtimeMs > lastSeen) lastSeen = st.mtimeMs
249
- if (now - st.mtimeMs < OPEN_TURN_MS) recent.push({ file, st })
250
- } catch {}
251
- }
252
- recent.sort((a, b) => b.st.mtimeMs - a.st.mtimeMs)
253
- for (const { file, st } of recent.slice(0, RECENT_TRANSCRIPTS)) {
254
- let said
255
- try {
256
- said = activityOf(tailLines(file, st.size))
257
- } catch {}
258
- const activity = said === undefined ? (now - st.mtimeMs < ACTIVE_MS ? 'working' : null) : said
259
- if (activity) return { active: true, activity, lastSeen }
260
- }
261
- return { active: false, activity: null, lastSeen }
262
- }
263
-
264
- // The poll's view of Cursor: activity from disk every tick, limits from the
265
- // last fetch (a new one starts in the background when it's due).
266
- function getCursorUsage(now = Date.now()) {
267
- if (now - lastTry >= (limits && !lastFailed ? FETCH_MS : RETRY_MS)) refreshLimits(now)
268
- return {
269
- ...readActivity(now),
270
- plan: limits?.plan ?? null,
271
- session: limits ? cycleNow(limits.total, limits.endMs, now) : null,
272
- api: limits ? cycleNow(limits.api, limits.endMs, now) : null,
273
- limitsAt: limits?.ts ?? null,
274
- signedOut,
275
- }
276
- }
277
-
278
- module.exports = { getCursorUsage, refreshLimits, detectCursor, configureCursor, activityOf }