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/README.md +11 -9
- package/bin/clauddy.js +82 -79
- package/package.json +10 -82
- package/accounts.js +0 -181
- package/auth.js +0 -300
- package/codex.js +0 -345
- package/config.json +0 -11
- package/cursor.js +0 -278
- package/main.js +0 -1305
- package/preload.js +0 -43
- package/reminders.js +0 -54
- package/renderer/burn.js +0 -98
- package/renderer/companion.js +0 -52
- package/renderer/index.html +0 -527
- package/renderer/motion.css +0 -646
- package/renderer/pet.js +0 -2394
- package/renderer/pets.css +0 -916
- package/renderer/pets.js +0 -579
- package/renderer/style.css +0 -3758
- package/renderer/voice.js +0 -244
- package/usage.js +0 -539
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 }
|