cursor-usage-analyzer 0.1.0 → 0.2.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/.claude/settings.local.json +13 -0
- package/.idea/vcs.xml +5 -0
- package/README.md +104 -7
- package/analyze.js +285 -55
- package/cursor-usage-analyzer-0.2.0.tgz +0 -0
- package/cursor-usage-analyzer-0.2.1.tgz +0 -0
- package/html-template.js +33 -0
- package/package.json +4 -3
- package/team-usage-events-10287858-2025-12-18.csv +600 -0
package/analyze.js
CHANGED
|
@@ -5,10 +5,40 @@ import path from 'path';
|
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import Database from 'better-sqlite3';
|
|
7
7
|
import { generateHTMLReport } from './html-template.js';
|
|
8
|
+
import { parse } from 'csv-parse/sync';
|
|
9
|
+
|
|
10
|
+
// Get Cursor storage paths based on OS
|
|
11
|
+
function getCursorPaths() {
|
|
12
|
+
const platform = os.platform();
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
|
|
15
|
+
if (platform === 'darwin') {
|
|
16
|
+
// macOS
|
|
17
|
+
return {
|
|
18
|
+
global: path.join(home, 'Library/Application Support/Cursor/User/globalStorage'),
|
|
19
|
+
workspace: path.join(home, 'Library/Application Support/Cursor/User/workspaceStorage')
|
|
20
|
+
};
|
|
21
|
+
} else if (platform === 'win32') {
|
|
22
|
+
// Windows
|
|
23
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData/Roaming');
|
|
24
|
+
return {
|
|
25
|
+
global: path.join(appData, 'Cursor/User/globalStorage'),
|
|
26
|
+
workspace: path.join(appData, 'Cursor/User/workspaceStorage')
|
|
27
|
+
};
|
|
28
|
+
} else {
|
|
29
|
+
// Linux
|
|
30
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
31
|
+
return {
|
|
32
|
+
global: path.join(configHome, 'Cursor/User/globalStorage'),
|
|
33
|
+
workspace: path.join(configHome, 'Cursor/User/workspaceStorage')
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
8
37
|
|
|
9
38
|
// Constants
|
|
10
|
-
const
|
|
11
|
-
const
|
|
39
|
+
const CURSOR_PATHS = getCursorPaths();
|
|
40
|
+
const CURSOR_GLOBAL_STORAGE = CURSOR_PATHS.global;
|
|
41
|
+
const CURSOR_WORKSPACE_STORAGE = CURSOR_PATHS.workspace;
|
|
12
42
|
const OUTPUT_DIR = path.join(process.cwd(), 'cursor-logs-export');
|
|
13
43
|
const CHATS_DIR = path.join(OUTPUT_DIR, 'chats');
|
|
14
44
|
const REPORT_PATH = path.join(OUTPUT_DIR, 'report.html');
|
|
@@ -25,8 +55,14 @@ const setDayBounds = (date, start = true) => {
|
|
|
25
55
|
// Parse command line arguments
|
|
26
56
|
function parseArgs() {
|
|
27
57
|
const args = process.argv.slice(2);
|
|
28
|
-
let startDate, endDate, dateStr;
|
|
29
|
-
|
|
58
|
+
let startDate, endDate, dateStr, csvPath;
|
|
59
|
+
|
|
60
|
+
// Check for CSV path
|
|
61
|
+
const csvIdx = args.indexOf('--csv');
|
|
62
|
+
if (csvIdx !== -1 && args[csvIdx + 1]) {
|
|
63
|
+
csvPath = args[csvIdx + 1];
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
if (args.includes('--last-month')) {
|
|
31
67
|
const now = new Date();
|
|
32
68
|
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
@@ -57,14 +93,81 @@ function parseArgs() {
|
|
|
57
93
|
startDate = endDate = new Date();
|
|
58
94
|
dateStr = formatDate(startDate);
|
|
59
95
|
}
|
|
60
|
-
|
|
96
|
+
|
|
61
97
|
return {
|
|
62
98
|
startOfDay: setDayBounds(startDate, true).getTime(),
|
|
63
99
|
endOfDay: setDayBounds(endDate, false).getTime(),
|
|
64
|
-
dateStr
|
|
100
|
+
dateStr,
|
|
101
|
+
csvPath
|
|
65
102
|
};
|
|
66
103
|
}
|
|
67
104
|
|
|
105
|
+
// Parse CSV usage data from Cursor dashboard export
|
|
106
|
+
function parseCSVUsage(csvPath) {
|
|
107
|
+
if (!csvPath || !fs.existsSync(csvPath)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const content = fs.readFileSync(csvPath, 'utf-8');
|
|
113
|
+
const records = parse(content, {
|
|
114
|
+
columns: true,
|
|
115
|
+
skip_empty_lines: true
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return records.map(r => ({
|
|
119
|
+
timestamp: new Date(r.Date).getTime(),
|
|
120
|
+
user: r.User,
|
|
121
|
+
kind: r.Kind,
|
|
122
|
+
model: r.Model,
|
|
123
|
+
maxMode: r['Max Mode'],
|
|
124
|
+
inputWithCache: parseInt(r['Input (w/ Cache Write)']) || 0,
|
|
125
|
+
inputWithoutCache: parseInt(r['Input (w/o Cache Write)']) || 0,
|
|
126
|
+
cacheRead: parseInt(r['Cache Read']) || 0,
|
|
127
|
+
outputTokens: parseInt(r['Output Tokens']) || 0,
|
|
128
|
+
totalTokens: parseInt(r['Total Tokens']) || 0,
|
|
129
|
+
cost: parseFloat(r.Cost) || 0
|
|
130
|
+
}));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error('Error parsing CSV:', e.message);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Match API calls to a conversation based on timestamp and model
|
|
138
|
+
function matchAPICallsToConversation(conv, apiCalls, timeWindow = 5 * 60 * 1000) {
|
|
139
|
+
if (!apiCalls || apiCalls.length === 0) return [];
|
|
140
|
+
|
|
141
|
+
const convStart = conv.timestamp;
|
|
142
|
+
const convEnd = conv.messages.length > 0
|
|
143
|
+
? Math.max(...conv.messages.map(m => m.timestamp))
|
|
144
|
+
: convStart;
|
|
145
|
+
|
|
146
|
+
const normalizeModel = (model) => {
|
|
147
|
+
if (!model) return 'unknown';
|
|
148
|
+
const m = model.toLowerCase();
|
|
149
|
+
if (m.includes('sonnet')) return 'sonnet';
|
|
150
|
+
if (m.includes('opus')) return 'opus';
|
|
151
|
+
if (m.includes('composer')) return 'composer';
|
|
152
|
+
if (m === 'auto' || m === 'default' || m === 'unknown') return 'any';
|
|
153
|
+
return model;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const convModel = normalizeModel(conv.model);
|
|
157
|
+
|
|
158
|
+
return apiCalls.filter(call => {
|
|
159
|
+
const callModel = normalizeModel(call.model);
|
|
160
|
+
const timeMatch = call.timestamp >= (convStart - timeWindow) &&
|
|
161
|
+
call.timestamp <= (convEnd + timeWindow);
|
|
162
|
+
|
|
163
|
+
const modelMatch = convModel === 'any' ||
|
|
164
|
+
callModel === 'any' ||
|
|
165
|
+
convModel === callModel;
|
|
166
|
+
|
|
167
|
+
return timeMatch && modelMatch;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
68
171
|
// Extract text from various bubble formats
|
|
69
172
|
function extractTextFromBubble(bubble) {
|
|
70
173
|
if (bubble.text?.trim()) return bubble.text;
|
|
@@ -185,21 +288,21 @@ function resolveWorkspace(composerData, headers, db, composerId) {
|
|
|
185
288
|
}
|
|
186
289
|
|
|
187
290
|
// Extract conversations from database
|
|
188
|
-
function extractConversations(startTime, endTime) {
|
|
291
|
+
function extractConversations(startTime, endTime, apiCalls = []) {
|
|
189
292
|
const dbPath = path.join(CURSOR_GLOBAL_STORAGE, 'state.vscdb');
|
|
190
|
-
|
|
293
|
+
|
|
191
294
|
if (!fs.existsSync(dbPath)) {
|
|
192
295
|
console.log('Database not found');
|
|
193
296
|
return [];
|
|
194
297
|
}
|
|
195
|
-
|
|
298
|
+
|
|
196
299
|
try {
|
|
197
300
|
const db = new Database(dbPath, { readonly: true });
|
|
198
|
-
|
|
301
|
+
|
|
199
302
|
// Load all bubbles
|
|
200
303
|
const bubbleMap = {};
|
|
201
304
|
const bubbleRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").all();
|
|
202
|
-
|
|
305
|
+
|
|
203
306
|
for (const row of bubbleRows) {
|
|
204
307
|
try {
|
|
205
308
|
const bubbleId = row.key.split(':')[2];
|
|
@@ -207,34 +310,34 @@ function extractConversations(startTime, endTime) {
|
|
|
207
310
|
if (bubble) bubbleMap[bubbleId] = bubble;
|
|
208
311
|
} catch (e) {}
|
|
209
312
|
}
|
|
210
|
-
|
|
313
|
+
|
|
211
314
|
// Load composers
|
|
212
315
|
const composerRows = db.prepare(
|
|
213
316
|
"SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND value LIKE '%fullConversationHeadersOnly%'"
|
|
214
317
|
).all();
|
|
215
|
-
|
|
318
|
+
|
|
216
319
|
const conversations = [];
|
|
217
|
-
|
|
320
|
+
|
|
218
321
|
for (const row of composerRows) {
|
|
219
322
|
try {
|
|
220
323
|
const composerId = row.key.split(':')[1];
|
|
221
324
|
const composer = JSON.parse(row.value);
|
|
222
325
|
const timestamp = composer.lastUpdatedAt || composer.createdAt || Date.now();
|
|
223
|
-
|
|
326
|
+
|
|
224
327
|
if (timestamp < startTime || timestamp > endTime) continue;
|
|
225
|
-
|
|
328
|
+
|
|
226
329
|
const headers = composer.fullConversationHeadersOnly || [];
|
|
227
330
|
if (headers.length === 0) continue;
|
|
228
|
-
|
|
331
|
+
|
|
229
332
|
// Extract messages
|
|
230
333
|
const messages = headers
|
|
231
334
|
.map(h => {
|
|
232
335
|
const bubble = bubbleMap[h.bubbleId];
|
|
233
336
|
if (!bubble) return null;
|
|
234
|
-
|
|
337
|
+
|
|
235
338
|
const text = extractTextFromBubble(bubble);
|
|
236
339
|
if (!text?.trim()) return null;
|
|
237
|
-
|
|
340
|
+
|
|
238
341
|
return {
|
|
239
342
|
role: h.type === 1 ? 'user' : 'assistant',
|
|
240
343
|
text: text.trim(),
|
|
@@ -242,10 +345,10 @@ function extractConversations(startTime, endTime) {
|
|
|
242
345
|
};
|
|
243
346
|
})
|
|
244
347
|
.filter(Boolean);
|
|
245
|
-
|
|
348
|
+
|
|
246
349
|
if (messages.length === 0) continue;
|
|
247
|
-
|
|
248
|
-
|
|
350
|
+
|
|
351
|
+
const conv = {
|
|
249
352
|
composerId,
|
|
250
353
|
name: composer.name || 'Untitled Chat',
|
|
251
354
|
timestamp,
|
|
@@ -258,12 +361,64 @@ function extractConversations(startTime, endTime) {
|
|
|
258
361
|
totalLinesAdded: composer.totalLinesAdded || 0,
|
|
259
362
|
totalLinesRemoved: composer.totalLinesRemoved || 0,
|
|
260
363
|
filesChangedCount: composer.filesChangedCount || 0
|
|
261
|
-
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
conversations.push(conv);
|
|
262
367
|
} catch (e) {
|
|
263
368
|
// Silently skip invalid composers
|
|
264
369
|
}
|
|
265
370
|
}
|
|
266
|
-
|
|
371
|
+
|
|
372
|
+
// Match API calls to conversations WITHOUT double-counting
|
|
373
|
+
// Each API call should only be matched to ONE conversation
|
|
374
|
+
if (apiCalls.length > 0) {
|
|
375
|
+
const usedApiCallIndices = new Set();
|
|
376
|
+
|
|
377
|
+
for (const conv of conversations) {
|
|
378
|
+
const availableApiCalls = apiCalls.filter((_, idx) => !usedApiCallIndices.has(idx));
|
|
379
|
+
const matchedCalls = matchAPICallsToConversation(conv, availableApiCalls);
|
|
380
|
+
|
|
381
|
+
// Mark these API calls as used
|
|
382
|
+
matchedCalls.forEach(call => {
|
|
383
|
+
const originalIndex = apiCalls.findIndex(c =>
|
|
384
|
+
c.timestamp === call.timestamp &&
|
|
385
|
+
c.model === call.model &&
|
|
386
|
+
c.totalTokens === call.totalTokens
|
|
387
|
+
);
|
|
388
|
+
if (originalIndex !== -1) {
|
|
389
|
+
usedApiCallIndices.add(originalIndex);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
conv.apiCalls = matchedCalls;
|
|
394
|
+
conv.apiCallCount = matchedCalls.length;
|
|
395
|
+
|
|
396
|
+
// Sum up API tokens
|
|
397
|
+
conv.apiTokens = {
|
|
398
|
+
inputWithCache: matchedCalls.reduce((sum, c) => sum + c.inputWithCache, 0),
|
|
399
|
+
inputWithoutCache: matchedCalls.reduce((sum, c) => sum + c.inputWithoutCache, 0),
|
|
400
|
+
cacheRead: matchedCalls.reduce((sum, c) => sum + c.cacheRead, 0),
|
|
401
|
+
outputTokens: matchedCalls.reduce((sum, c) => sum + c.outputTokens, 0),
|
|
402
|
+
totalTokens: matchedCalls.reduce((sum, c) => sum + c.totalTokens, 0),
|
|
403
|
+
cost: matchedCalls.reduce((sum, c) => sum + c.cost, 0)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
// No API calls to match
|
|
408
|
+
conversations.forEach(conv => {
|
|
409
|
+
conv.apiCalls = [];
|
|
410
|
+
conv.apiCallCount = 0;
|
|
411
|
+
conv.apiTokens = {
|
|
412
|
+
inputWithCache: 0,
|
|
413
|
+
inputWithoutCache: 0,
|
|
414
|
+
cacheRead: 0,
|
|
415
|
+
outputTokens: 0,
|
|
416
|
+
totalTokens: 0,
|
|
417
|
+
cost: 0
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
267
422
|
db.close();
|
|
268
423
|
return conversations;
|
|
269
424
|
} catch (error) {
|
|
@@ -278,9 +433,9 @@ function exportConversation(conv, index, dateStr) {
|
|
|
278
433
|
const timeStr = timestamp.toTimeString().split(' ')[0].replace(/:/g, '-');
|
|
279
434
|
const workspaceShort = conv.workspace.substring(0, 15).replace(/[^a-zA-Z0-9]/g, '_');
|
|
280
435
|
const filename = `${dateStr}_${timeStr}_${workspaceShort}_conv${index}.txt`;
|
|
281
|
-
|
|
436
|
+
|
|
282
437
|
const percentUsed = (conv.contextTokensUsed / conv.contextTokenLimit * 100).toFixed(1);
|
|
283
|
-
|
|
438
|
+
|
|
284
439
|
const lines = [
|
|
285
440
|
'='.repeat(80),
|
|
286
441
|
`CONVERSATION #${index}`,
|
|
@@ -288,18 +443,34 @@ function exportConversation(conv, index, dateStr) {
|
|
|
288
443
|
`Workspace: ${conv.workspace}`,
|
|
289
444
|
`Time: ${timestamp.toLocaleString('en-US')}`,
|
|
290
445
|
`Model: ${conv.model}`,
|
|
291
|
-
`Tokens: ${conv.contextTokensUsed.toLocaleString()} / ${conv.contextTokenLimit.toLocaleString()} (${percentUsed}%)`,
|
|
446
|
+
`Context Tokens: ${conv.contextTokensUsed.toLocaleString()} / ${conv.contextTokenLimit.toLocaleString()} (${percentUsed}%)`,
|
|
292
447
|
`Changes: +${conv.totalLinesAdded} -${conv.totalLinesRemoved} lines in ${conv.filesChangedCount} files`,
|
|
293
448
|
`Messages: ${conv.messageCount}`,
|
|
294
449
|
`Composer ID: ${conv.composerId}`,
|
|
295
|
-
'='.repeat(80),
|
|
296
450
|
''
|
|
297
451
|
];
|
|
298
|
-
|
|
452
|
+
|
|
453
|
+
// Add API token information if available
|
|
454
|
+
if (conv.apiCallCount > 0) {
|
|
455
|
+
lines.push(
|
|
456
|
+
'API TOKEN USAGE (from dashboard export):',
|
|
457
|
+
` API Calls: ${conv.apiCallCount}`,
|
|
458
|
+
` Input (w/ Cache Write): ${conv.apiTokens.inputWithCache.toLocaleString()}`,
|
|
459
|
+
` Input (w/o Cache Write): ${conv.apiTokens.inputWithoutCache.toLocaleString()}`,
|
|
460
|
+
` Cache Read: ${conv.apiTokens.cacheRead.toLocaleString()}`,
|
|
461
|
+
` Output Tokens: ${conv.apiTokens.outputTokens.toLocaleString()}`,
|
|
462
|
+
` Total API Tokens: ${conv.apiTokens.totalTokens.toLocaleString()}`,
|
|
463
|
+
` Cost: $${conv.apiTokens.cost.toFixed(2)}`,
|
|
464
|
+
''
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
lines.push('='.repeat(80), '');
|
|
469
|
+
|
|
299
470
|
for (const msg of conv.messages) {
|
|
300
471
|
const msgTime = new Date(msg.timestamp).toLocaleTimeString('en-US');
|
|
301
472
|
const role = msg.role.toUpperCase();
|
|
302
|
-
|
|
473
|
+
|
|
303
474
|
lines.push(
|
|
304
475
|
'',
|
|
305
476
|
'-'.repeat(80),
|
|
@@ -308,9 +479,9 @@ function exportConversation(conv, index, dateStr) {
|
|
|
308
479
|
msg.text
|
|
309
480
|
);
|
|
310
481
|
}
|
|
311
|
-
|
|
482
|
+
|
|
312
483
|
lines.push('', '='.repeat(80), 'End of conversation', '='.repeat(80));
|
|
313
|
-
|
|
484
|
+
|
|
314
485
|
fs.writeFileSync(path.join(CHATS_DIR, filename), lines.join('\n'), 'utf-8');
|
|
315
486
|
return filename;
|
|
316
487
|
}
|
|
@@ -318,7 +489,7 @@ function exportConversation(conv, index, dateStr) {
|
|
|
318
489
|
// Generate statistics
|
|
319
490
|
function generateStats(conversations, startOfDay, endOfDay) {
|
|
320
491
|
const daysDiff = Math.ceil((endOfDay - startOfDay) / (1000 * 60 * 60 * 24));
|
|
321
|
-
|
|
492
|
+
|
|
322
493
|
const stats = {
|
|
323
494
|
totalConversations: conversations.length,
|
|
324
495
|
totalMessages: 0,
|
|
@@ -326,6 +497,15 @@ function generateStats(conversations, startOfDay, endOfDay) {
|
|
|
326
497
|
totalLinesAdded: 0,
|
|
327
498
|
totalLinesRemoved: 0,
|
|
328
499
|
totalFilesChanged: 0,
|
|
500
|
+
totalApiCalls: 0,
|
|
501
|
+
totalApiTokens: {
|
|
502
|
+
inputWithCache: 0,
|
|
503
|
+
inputWithoutCache: 0,
|
|
504
|
+
cacheRead: 0,
|
|
505
|
+
outputTokens: 0,
|
|
506
|
+
totalTokens: 0,
|
|
507
|
+
cost: 0
|
|
508
|
+
},
|
|
329
509
|
modelUsage: {},
|
|
330
510
|
workspaceUsage: {},
|
|
331
511
|
hourlyDistribution: Array(24).fill(0),
|
|
@@ -334,26 +514,37 @@ function generateStats(conversations, startOfDay, endOfDay) {
|
|
|
334
514
|
conversations: [],
|
|
335
515
|
generatedAt: new Date().toLocaleString('en-US')
|
|
336
516
|
};
|
|
337
|
-
|
|
517
|
+
|
|
338
518
|
for (const conv of conversations) {
|
|
339
519
|
const ts = new Date(conv.timestamp);
|
|
340
|
-
|
|
520
|
+
|
|
341
521
|
stats.totalMessages += conv.messageCount;
|
|
342
522
|
stats.totalTokens += conv.contextTokensUsed;
|
|
343
523
|
stats.totalLinesAdded += conv.totalLinesAdded;
|
|
344
524
|
stats.totalLinesRemoved += conv.totalLinesRemoved;
|
|
345
525
|
stats.totalFilesChanged += conv.filesChangedCount;
|
|
346
|
-
|
|
526
|
+
|
|
527
|
+
// Add API token stats
|
|
528
|
+
if (conv.apiCallCount > 0) {
|
|
529
|
+
stats.totalApiCalls += conv.apiCallCount;
|
|
530
|
+
stats.totalApiTokens.inputWithCache += conv.apiTokens.inputWithCache;
|
|
531
|
+
stats.totalApiTokens.inputWithoutCache += conv.apiTokens.inputWithoutCache;
|
|
532
|
+
stats.totalApiTokens.cacheRead += conv.apiTokens.cacheRead;
|
|
533
|
+
stats.totalApiTokens.outputTokens += conv.apiTokens.outputTokens;
|
|
534
|
+
stats.totalApiTokens.totalTokens += conv.apiTokens.totalTokens;
|
|
535
|
+
stats.totalApiTokens.cost += conv.apiTokens.cost;
|
|
536
|
+
}
|
|
537
|
+
|
|
347
538
|
stats.modelUsage[conv.model] = (stats.modelUsage[conv.model] || 0) + 1;
|
|
348
539
|
stats.workspaceUsage[conv.workspace] = (stats.workspaceUsage[conv.workspace] || 0) + 1;
|
|
349
540
|
stats.hourlyDistribution[ts.getHours()]++;
|
|
350
|
-
|
|
541
|
+
|
|
351
542
|
const dateKey = ts.toLocaleDateString('en-US');
|
|
352
543
|
stats.dailyDistribution[dateKey] = (stats.dailyDistribution[dateKey] || 0) + 1;
|
|
353
|
-
|
|
544
|
+
|
|
354
545
|
const firstUserMsg = conv.messages.find(m => m.role === 'user');
|
|
355
546
|
const preview = firstUserMsg?.text.substring(0, 100) + (firstUserMsg?.text.length > 100 ? '...' : '') || '(no user message)';
|
|
356
|
-
|
|
547
|
+
|
|
357
548
|
stats.conversations.push({
|
|
358
549
|
timestamp: conv.timestamp,
|
|
359
550
|
time: ts.toLocaleTimeString('en-US'),
|
|
@@ -365,49 +556,79 @@ function generateStats(conversations, startOfDay, endOfDay) {
|
|
|
365
556
|
messages: conv.messageCount,
|
|
366
557
|
tokens: conv.contextTokensUsed,
|
|
367
558
|
contextLimit: conv.contextTokenLimit,
|
|
559
|
+
apiCallCount: conv.apiCallCount || 0,
|
|
560
|
+
apiTokens: conv.apiTokens,
|
|
368
561
|
linesChanged: `+${conv.totalLinesAdded}/-${conv.totalLinesRemoved}`,
|
|
369
562
|
files: conv.filesChangedCount,
|
|
370
563
|
preview
|
|
371
564
|
});
|
|
372
565
|
}
|
|
373
|
-
|
|
566
|
+
|
|
374
567
|
stats.conversations.sort((a, b) => a.timestamp - b.timestamp);
|
|
375
|
-
|
|
568
|
+
|
|
376
569
|
return stats;
|
|
377
570
|
}
|
|
378
571
|
|
|
379
572
|
// Main
|
|
380
573
|
async function main() {
|
|
381
574
|
console.log('Cursor Usage Analyzer v2\n');
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
575
|
+
|
|
576
|
+
// Check if Cursor storage exists
|
|
577
|
+
if (!fs.existsSync(CURSOR_GLOBAL_STORAGE)) {
|
|
578
|
+
console.error('Error: Cursor storage directory not found!');
|
|
579
|
+
console.error(`Expected location: ${CURSOR_GLOBAL_STORAGE}`);
|
|
580
|
+
console.error('\nPossible reasons:');
|
|
581
|
+
console.error(' 1. Cursor is not installed');
|
|
582
|
+
console.error(' 2. Cursor has never been run');
|
|
583
|
+
console.error(' 3. Different installation path\n');
|
|
584
|
+
console.error(`Detected OS: ${os.platform()}`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const { startOfDay, endOfDay, dateStr, csvPath } = parseArgs();
|
|
589
|
+
|
|
385
590
|
console.log(`Analyzing: ${dateStr}`);
|
|
386
|
-
console.log(`Period: ${new Date(startOfDay).toLocaleString('en-US')} - ${new Date(endOfDay).toLocaleString('en-US')}
|
|
387
|
-
|
|
591
|
+
console.log(`Period: ${new Date(startOfDay).toLocaleString('en-US')} - ${new Date(endOfDay).toLocaleString('en-US')}`);
|
|
592
|
+
console.log(`Platform: ${os.platform()}`);
|
|
593
|
+
|
|
594
|
+
// Parse CSV if provided
|
|
595
|
+
let apiCalls = [];
|
|
596
|
+
if (csvPath) {
|
|
597
|
+
console.log(`CSV file: ${csvPath}`);
|
|
598
|
+
const allApiCalls = parseCSVUsage(csvPath);
|
|
599
|
+
// Filter API calls to only those within the date range
|
|
600
|
+
apiCalls = allApiCalls.filter(call =>
|
|
601
|
+
call.timestamp >= startOfDay && call.timestamp <= endOfDay
|
|
602
|
+
);
|
|
603
|
+
console.log(`Parsed ${allApiCalls.length} API calls from CSV`);
|
|
604
|
+
console.log(`Filtered to ${apiCalls.length} API calls within date range\n`);
|
|
605
|
+
} else {
|
|
606
|
+
console.log('No CSV file provided (use --csv path/to/file.csv to include API token data)\n');
|
|
607
|
+
}
|
|
608
|
+
|
|
388
609
|
// Prepare output folders
|
|
389
610
|
if (fs.existsSync(OUTPUT_DIR)) fs.rmSync(OUTPUT_DIR, { recursive: true });
|
|
390
611
|
fs.mkdirSync(CHATS_DIR, { recursive: true });
|
|
391
|
-
|
|
612
|
+
|
|
392
613
|
console.log('Extracting conversations...');
|
|
393
|
-
|
|
394
|
-
const conversations = extractConversations(startOfDay, endOfDay);
|
|
395
|
-
|
|
614
|
+
|
|
615
|
+
const conversations = extractConversations(startOfDay, endOfDay, apiCalls);
|
|
616
|
+
|
|
396
617
|
console.log(`Found ${conversations.length} conversations\n`);
|
|
397
|
-
|
|
618
|
+
|
|
398
619
|
if (conversations.length === 0) {
|
|
399
620
|
console.log('No conversations found in specified period');
|
|
400
621
|
return;
|
|
401
622
|
}
|
|
402
|
-
|
|
623
|
+
|
|
403
624
|
console.log('Exporting conversations...');
|
|
404
625
|
conversations.forEach((conv, i) => {
|
|
405
626
|
exportConversation(conv, i + 1, dateStr);
|
|
406
627
|
});
|
|
407
|
-
|
|
628
|
+
|
|
408
629
|
console.log('Generating statistics...');
|
|
409
630
|
const stats = generateStats(conversations, startOfDay, endOfDay);
|
|
410
|
-
|
|
631
|
+
|
|
411
632
|
console.log('Generating HTML report...');
|
|
412
633
|
try {
|
|
413
634
|
generateHTMLReport(stats, dateStr, REPORT_PATH);
|
|
@@ -416,11 +637,20 @@ async function main() {
|
|
|
416
637
|
console.error(e.stack);
|
|
417
638
|
return;
|
|
418
639
|
}
|
|
419
|
-
|
|
640
|
+
|
|
420
641
|
console.log('\nDone!\n');
|
|
421
642
|
console.log(`Export folder: ${OUTPUT_DIR}`);
|
|
422
643
|
console.log(`Conversations: ${conversations.length}`);
|
|
644
|
+
if (apiCalls.length > 0) {
|
|
645
|
+
console.log(`API Calls matched: ${stats.totalApiCalls}`);
|
|
646
|
+
console.log(`Total API tokens: ${stats.totalApiTokens.totalTokens.toLocaleString()}`);
|
|
647
|
+
console.log(`Total cost: $${stats.totalApiTokens.cost.toFixed(2)}`);
|
|
648
|
+
}
|
|
423
649
|
console.log(`Report: ${REPORT_PATH}`);
|
|
650
|
+
|
|
651
|
+
// Platform-specific open command hint
|
|
652
|
+
const openCmd = os.platform() === 'win32' ? 'start' : os.platform() === 'darwin' ? 'open' : 'xdg-open';
|
|
653
|
+
console.log(`\nOpen report: ${openCmd} ${REPORT_PATH}`);
|
|
424
654
|
}
|
|
425
655
|
|
|
426
656
|
main().catch(console.error);
|
|
Binary file
|
|
Binary file
|
package/html-template.js
CHANGED
|
@@ -151,6 +151,29 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
151
151
|
<div class="stat-label">Context Tokens</div>
|
|
152
152
|
<div class="stat-sublabel">Ø ${avgTokens.toLocaleString()} per conversation</div>
|
|
153
153
|
</div>
|
|
154
|
+
${stats.totalApiCalls > 0 ? `
|
|
155
|
+
<div class="stat">
|
|
156
|
+
<div class="stat-value">${stats.totalApiTokens.totalTokens.toLocaleString()}</div>
|
|
157
|
+
<div class="stat-label">API Tokens (Total)</div>
|
|
158
|
+
<div class="stat-sublabel">$${stats.totalApiTokens.cost.toFixed(2)} total cost</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="stat">
|
|
161
|
+
<div class="stat-value">${stats.totalApiTokens.inputWithCache.toLocaleString()}</div>
|
|
162
|
+
<div class="stat-label">Input (w/ Cache Write)</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="stat">
|
|
165
|
+
<div class="stat-value">${stats.totalApiTokens.cacheRead.toLocaleString()}</div>
|
|
166
|
+
<div class="stat-label">Cache Read</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="stat">
|
|
169
|
+
<div class="stat-value">${stats.totalApiTokens.outputTokens.toLocaleString()}</div>
|
|
170
|
+
<div class="stat-label">Output Tokens</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="stat">
|
|
173
|
+
<div class="stat-value">${stats.totalApiCalls}</div>
|
|
174
|
+
<div class="stat-label">API Calls</div>
|
|
175
|
+
</div>
|
|
176
|
+
` : ''}
|
|
154
177
|
<div class="stat">
|
|
155
178
|
<div class="stat-value">+${stats.totalLinesAdded.toLocaleString()}</div>
|
|
156
179
|
<div class="stat-label">Lines Added</div>
|
|
@@ -235,6 +258,11 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
235
258
|
<th class="sortable" data-column="model" data-type="string">Model</th>
|
|
236
259
|
<th class="sortable" data-column="messages" data-type="number">Messages</th>
|
|
237
260
|
<th class="sortable" data-column="tokens" data-type="number">Context Tokens</th>
|
|
261
|
+
${stats.totalApiCalls > 0 ? `
|
|
262
|
+
<th class="sortable" data-column="apiTokens" data-type="number">API Tokens</th>
|
|
263
|
+
<th class="sortable" data-column="apiCost" data-type="number">Cost</th>
|
|
264
|
+
<th class="sortable" data-column="apiCalls" data-type="number">API Calls</th>
|
|
265
|
+
` : ''}
|
|
238
266
|
<th class="sortable" data-column="linesChanged" data-type="string">Changes</th>
|
|
239
267
|
<th class="sortable" data-column="files" data-type="number">Files</th>
|
|
240
268
|
</tr>
|
|
@@ -248,6 +276,11 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
248
276
|
<td data-value="${c.model}"><small>${c.model}</small></td>
|
|
249
277
|
<td data-value="${c.messages}">${c.messages}</td>
|
|
250
278
|
<td data-value="${c.tokens}"><small>${c.tokens.toLocaleString()} / ${c.contextLimit.toLocaleString()}</small></td>
|
|
279
|
+
${stats.totalApiCalls > 0 ? `
|
|
280
|
+
<td data-value="${c.apiTokens?.totalTokens || 0}"><small>${(c.apiTokens?.totalTokens || 0).toLocaleString()}</small></td>
|
|
281
|
+
<td data-value="${c.apiTokens?.cost || 0}"><small>$${(c.apiTokens?.cost || 0).toFixed(2)}</small></td>
|
|
282
|
+
<td data-value="${c.apiCallCount || 0}">${c.apiCallCount || 0}</td>
|
|
283
|
+
` : ''}
|
|
251
284
|
<td data-value="${c.linesChanged}">${c.linesChanged}</td>
|
|
252
285
|
<td data-value="${c.files}">${c.files}</td>
|
|
253
286
|
</tr>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-usage-analyzer",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Analyze and visualize your Cursor AI editor usage with interactive reports",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Analyze and visualize your Cursor AI editor usage with interactive reports including API token tracking and costs",
|
|
5
5
|
"main": "analyze.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"author": "Daniel Jílek",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"better-sqlite3": "^9.2.2"
|
|
21
|
+
"better-sqlite3": "^9.2.2",
|
|
22
|
+
"csv-parse": "^5.5.3"
|
|
22
23
|
},
|
|
23
24
|
"repository": {
|
|
24
25
|
"type": "git",
|