cursor-usage-analyzer 0.2.1 → 0.3.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/.idea/copilot.data.migration.agent.xml +6 -0
- package/analyze.js +94 -11
- package/package.json +5 -3
package/analyze.js
CHANGED
|
@@ -3,10 +3,83 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
6
|
import { generateHTMLReport } from './html-template.js';
|
|
8
7
|
import { parse } from 'csv-parse/sync';
|
|
9
8
|
|
|
9
|
+
// Database abstraction - try better-sqlite3 first, fall back to sql.js
|
|
10
|
+
let dbEngine = null;
|
|
11
|
+
|
|
12
|
+
async function loadDatabaseEngine() {
|
|
13
|
+
// Try better-sqlite3 first (faster, handles large files)
|
|
14
|
+
try {
|
|
15
|
+
const { default: Database } = await import('better-sqlite3');
|
|
16
|
+
// Test that it actually works (native bindings compiled)
|
|
17
|
+
const testDb = new Database(':memory:');
|
|
18
|
+
testDb.close();
|
|
19
|
+
dbEngine = { type: 'better-sqlite3', Database };
|
|
20
|
+
return;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.log('Note: better-sqlite3 unavailable, trying sql.js fallback...');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fall back to sql.js (pure JS, works everywhere, but 2GB limit)
|
|
26
|
+
try {
|
|
27
|
+
const { default: initSqlJs } = await import('sql.js');
|
|
28
|
+
const SQL = await initSqlJs();
|
|
29
|
+
dbEngine = { type: 'sql.js', SQL };
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error('No SQLite library available. Please run: npm install -g cursor-usage-analyzer');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function openDatabase(dbPath) {
|
|
36
|
+
if (dbEngine.type === 'better-sqlite3') {
|
|
37
|
+
return new dbEngine.Database(dbPath, { readonly: true });
|
|
38
|
+
} else {
|
|
39
|
+
const fileSize = fs.statSync(dbPath).size;
|
|
40
|
+
if (fileSize > 2 * 1024 * 1024 * 1024) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Database file (${(fileSize / 1024 / 1024 / 1024).toFixed(2)} GB) exceeds sql.js 2GB limit.\n` +
|
|
43
|
+
`To fix this, install globally with native compilation:\n` +
|
|
44
|
+
` npm install -g cursor-usage-analyzer\n` +
|
|
45
|
+
`Or clone and build locally:\n` +
|
|
46
|
+
` git clone https://github.com/xaverric/cursor-usage-analyzer.git\n` +
|
|
47
|
+
` cd cursor-usage-analyzer && npm install && npm rebuild better-sqlite3`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const dbBuffer = fs.readFileSync(dbPath);
|
|
51
|
+
return new dbEngine.SQL.Database(dbBuffer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dbQueryAll(db, sql, params = []) {
|
|
56
|
+
if (dbEngine.type === 'better-sqlite3') {
|
|
57
|
+
return db.prepare(sql).all(...params);
|
|
58
|
+
} else {
|
|
59
|
+
const stmt = db.prepare(sql);
|
|
60
|
+
if (params.length > 0) stmt.bind(params);
|
|
61
|
+
const results = [];
|
|
62
|
+
while (stmt.step()) {
|
|
63
|
+
results.push(stmt.getAsObject());
|
|
64
|
+
}
|
|
65
|
+
stmt.free();
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function dbQueryOne(db, sql, params = []) {
|
|
71
|
+
if (dbEngine.type === 'better-sqlite3') {
|
|
72
|
+
return db.prepare(sql).get(...params);
|
|
73
|
+
} else {
|
|
74
|
+
const results = dbQueryAll(db, sql, params);
|
|
75
|
+
return results.length > 0 ? results[0] : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function dbClose(db) {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
|
|
10
83
|
// Get Cursor storage paths based on OS
|
|
11
84
|
function getCursorPaths() {
|
|
12
85
|
const platform = os.platform();
|
|
@@ -246,7 +319,7 @@ function findWorkspaceFromPath(filePath) {
|
|
|
246
319
|
}
|
|
247
320
|
|
|
248
321
|
// Determine workspace name from multiple sources
|
|
249
|
-
function resolveWorkspace(composerData, headers,
|
|
322
|
+
function resolveWorkspace(composerData, headers, composerId) {
|
|
250
323
|
// Try workspaceId first
|
|
251
324
|
if (composerData.workspaceId) {
|
|
252
325
|
const name = readWorkspaceJson(composerData.workspaceId);
|
|
@@ -268,11 +341,20 @@ function resolveWorkspace(composerData, headers, db, composerId) {
|
|
|
268
341
|
}
|
|
269
342
|
}
|
|
270
343
|
|
|
344
|
+
return 'unknown';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Determine workspace name with database access for messageRequestContext
|
|
348
|
+
function resolveWorkspaceWithDb(composerData, headers, db, composerId) {
|
|
349
|
+
// Try basic resolution first
|
|
350
|
+
const basicResult = resolveWorkspace(composerData, headers, composerId);
|
|
351
|
+
if (basicResult !== 'unknown') return basicResult;
|
|
352
|
+
|
|
271
353
|
// Last resort: messageRequestContext
|
|
272
354
|
if (headers?.length > 0) {
|
|
273
355
|
try {
|
|
274
356
|
const contextKey = `messageRequestContext:${composerId}:${headers[0].bubbleId}`;
|
|
275
|
-
const row = db
|
|
357
|
+
const row = dbQueryOne(db, "SELECT value FROM cursorDiskKV WHERE key = ?", [contextKey]);
|
|
276
358
|
if (row) {
|
|
277
359
|
const context = JSON.parse(row.value);
|
|
278
360
|
if (context.projectLayouts?.length > 0) {
|
|
@@ -288,7 +370,7 @@ function resolveWorkspace(composerData, headers, db, composerId) {
|
|
|
288
370
|
}
|
|
289
371
|
|
|
290
372
|
// Extract conversations from database
|
|
291
|
-
function extractConversations(startTime, endTime, apiCalls = []) {
|
|
373
|
+
async function extractConversations(startTime, endTime, apiCalls = []) {
|
|
292
374
|
const dbPath = path.join(CURSOR_GLOBAL_STORAGE, 'state.vscdb');
|
|
293
375
|
|
|
294
376
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -297,11 +379,11 @@ function extractConversations(startTime, endTime, apiCalls = []) {
|
|
|
297
379
|
}
|
|
298
380
|
|
|
299
381
|
try {
|
|
300
|
-
const db =
|
|
382
|
+
const db = openDatabase(dbPath);
|
|
301
383
|
|
|
302
384
|
// Load all bubbles
|
|
303
385
|
const bubbleMap = {};
|
|
304
|
-
const bubbleRows = db
|
|
386
|
+
const bubbleRows = dbQueryAll(db, "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'");
|
|
305
387
|
|
|
306
388
|
for (const row of bubbleRows) {
|
|
307
389
|
try {
|
|
@@ -312,9 +394,9 @@ function extractConversations(startTime, endTime, apiCalls = []) {
|
|
|
312
394
|
}
|
|
313
395
|
|
|
314
396
|
// Load composers
|
|
315
|
-
const composerRows = db
|
|
397
|
+
const composerRows = dbQueryAll(db,
|
|
316
398
|
"SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND value LIKE '%fullConversationHeadersOnly%'"
|
|
317
|
-
)
|
|
399
|
+
);
|
|
318
400
|
|
|
319
401
|
const conversations = [];
|
|
320
402
|
|
|
@@ -354,7 +436,7 @@ function extractConversations(startTime, endTime, apiCalls = []) {
|
|
|
354
436
|
timestamp,
|
|
355
437
|
messages,
|
|
356
438
|
messageCount: messages.length,
|
|
357
|
-
workspace:
|
|
439
|
+
workspace: resolveWorkspaceWithDb(composer, headers, db, composerId),
|
|
358
440
|
model: composer.modelConfig?.modelName || 'unknown',
|
|
359
441
|
contextTokensUsed: composer.contextTokensUsed || 0,
|
|
360
442
|
contextTokenLimit: composer.contextTokenLimit || 0,
|
|
@@ -419,7 +501,7 @@ function extractConversations(startTime, endTime, apiCalls = []) {
|
|
|
419
501
|
});
|
|
420
502
|
}
|
|
421
503
|
|
|
422
|
-
db
|
|
504
|
+
dbClose(db);
|
|
423
505
|
return conversations;
|
|
424
506
|
} catch (error) {
|
|
425
507
|
console.error('Database error:', error.message);
|
|
@@ -612,7 +694,8 @@ async function main() {
|
|
|
612
694
|
|
|
613
695
|
console.log('Extracting conversations...');
|
|
614
696
|
|
|
615
|
-
|
|
697
|
+
await loadDatabaseEngine();
|
|
698
|
+
const conversations = await extractConversations(startOfDay, endOfDay, apiCalls);
|
|
616
699
|
|
|
617
700
|
console.log(`Found ${conversations.length} conversations\n`);
|
|
618
701
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-usage-analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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",
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
"today": "node analyze.js --today",
|
|
13
13
|
"yesterday": "node analyze.js --yesterday",
|
|
14
14
|
"this-month": "node analyze.js --this-month",
|
|
15
|
-
"last-month": "node analyze.js --last-month"
|
|
15
|
+
"last-month": "node analyze.js --last-month",
|
|
16
|
+
"postinstall": "npm rebuild better-sqlite3 2>/dev/null || echo 'Note: better-sqlite3 native build failed, will use sql.js fallback (2GB limit)'"
|
|
16
17
|
},
|
|
17
18
|
"keywords": ["cursor", "ai", "analytics", "usage", "chat", "tokens", "statistics"],
|
|
18
19
|
"author": "Daniel Jílek",
|
|
19
20
|
"license": "MIT",
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"better-sqlite3": "^
|
|
22
|
+
"better-sqlite3": "^11.7.0",
|
|
23
|
+
"sql.js": "^1.11.0",
|
|
22
24
|
"csv-parse": "^5.5.3"
|
|
23
25
|
},
|
|
24
26
|
"repository": {
|