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.
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AgentMigrationStateService">
4
+ <option name="migrationStatus" value="COMPLETED" />
5
+ </component>
6
+ </project>
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, db, composerId) {
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.prepare("SELECT value FROM cursorDiskKV WHERE key = ?").get(contextKey);
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 = new Database(dbPath, { readonly: true });
382
+ const db = openDatabase(dbPath);
301
383
 
302
384
  // Load all bubbles
303
385
  const bubbleMap = {};
304
- const bubbleRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").all();
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.prepare(
397
+ const composerRows = dbQueryAll(db,
316
398
  "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND value LIKE '%fullConversationHeadersOnly%'"
317
- ).all();
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: resolveWorkspace(composer, headers, db, composerId),
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.close();
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
- const conversations = extractConversations(startOfDay, endOfDay, apiCalls);
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.2.1",
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": "^9.2.2",
22
+ "better-sqlite3": "^11.7.0",
23
+ "sql.js": "^1.11.0",
22
24
  "csv-parse": "^5.5.3"
23
25
  },
24
26
  "repository": {