clauddy 1.29.1 → 2.0.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/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 }