cursor-usage-analyzer 0.1.0 → 0.2.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,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install)",
5
+ "Bash(sqlite3:*)",
6
+ "Bash(python3:*)"
7
+ ]
8
+ }
9
+ }
package/.idea/vcs.xml CHANGED
@@ -1,5 +1,10 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <project version="4">
3
+ <component name="GitSharedSettings">
4
+ <option name="FORCE_PUSH_PROHIBITED_PATTERNS">
5
+ <list />
6
+ </option>
7
+ </component>
3
8
  <component name="VcsDirectoryMappings">
4
9
  <mapping directory="" vcs="Git" />
5
10
  </component>
package/README.md CHANGED
@@ -5,11 +5,13 @@ A powerful Node.js tool to analyze and visualize your Cursor AI editor usage. Ex
5
5
  ## Features
6
6
 
7
7
  - 📊 **Comprehensive Analytics**: Track conversations, messages, tokens, code changes, and more
8
+ - 💰 **API Token Tracking**: Import CSV from Cursor dashboard to track actual API usage, costs, and billing
8
9
  - 📈 **Interactive Charts**: Visualize usage patterns with Chart.js-powered graphs
9
10
  - 🔍 **Smart Filtering**: Filter and sort conversations by project, model, date, and name
10
11
  - 📁 **Full Export**: Export complete conversation histories as readable text files
11
12
  - 🎯 **Project Detection**: Automatically resolves workspace names from Cursor's database
12
13
  - 📅 **Flexible Date Ranges**: Analyze single days, months, or custom periods
14
+ - 🖥️ **Cross-Platform**: Works on Windows, macOS, and Linux
13
15
 
14
16
  ## Installation
15
17
 
@@ -86,8 +88,35 @@ npx cursor-usage-analyzer --last-month
86
88
 
87
89
  # This month
88
90
  npx cursor-usage-analyzer --this-month
91
+
92
+ # Include API token usage from CSV export
93
+ npx cursor-usage-analyzer --csv path/to/team-usage-events.csv
89
94
  ```
90
95
 
96
+ ### API Token Tracking (CSV Import)
97
+
98
+ For detailed API usage tracking including actual tokens sent to Claude's API and costs, you can import CSV data from the Cursor dashboard:
99
+
100
+ 1. **Export CSV from Cursor**:
101
+ - Go to [Cursor Dashboard](https://cursor.com/dashboard)
102
+ - Navigate to Usage tab
103
+ - Click "Export" to download your usage CSV
104
+
105
+ 2. **Run analyzer with CSV**:
106
+ ```bash
107
+ npx cursor-usage-analyzer --csv ~/Downloads/team-usage-events-XXXXX-2025-12-18.csv
108
+ ```
109
+
110
+ 3. **What you get**:
111
+ - **Input Tokens** (with/without cache write)
112
+ - **Cache Read tokens**
113
+ - **Output tokens**
114
+ - **Total API tokens** (actual usage sent to Claude API)
115
+ - **Cost per conversation** (in USD)
116
+ - **API call count** per conversation
117
+
118
+ The tool automatically matches API calls to conversations based on timestamp and model, giving you complete visibility into your actual usage and costs.
119
+
91
120
  ### NPM Scripts (Local Installation Only)
92
121
 
93
122
  If you cloned the repo locally, you can use these convenient shortcuts:
@@ -111,11 +140,22 @@ Name: Feature implementation
111
140
  Workspace: my-project
112
141
  Time: 12/8/2025, 2:30:45 PM
113
142
  Model: claude-4.5-sonnet-thinking
114
- Tokens: 15,234 / 200,000 (7.6%)
143
+ Context Tokens: 15,234 / 200,000 (7.6%)
115
144
  Changes: +245 -12 lines in 8 files
116
145
  Messages: 23
146
+
147
+ API TOKEN USAGE (from dashboard export):
148
+ API Calls: 3
149
+ Input (w/ Cache Write): 12,456
150
+ Input (w/o Cache Write): 1,234
151
+ Cache Read: 45,678
152
+ Output Tokens: 2,345
153
+ Total API Tokens: 61,713
154
+ Cost: $0.23
117
155
  ```
118
156
 
157
+ **Note**: API token data only appears when using `--csv` flag
158
+
119
159
  ### 2. HTML Report (`report.html`)
120
160
 
121
161
  Interactive dashboard featuring:
@@ -137,25 +177,69 @@ Interactive dashboard featuring:
137
177
  - Filter by project, model, name, or date range
138
178
  - View complete conversation metadata
139
179
 
140
- ## Data Source
180
+ ### Opening the Report
181
+
182
+ After generation, open the HTML report with:
183
+
184
+ **macOS:**
185
+ ```bash
186
+ open cursor-logs-export/report.html
187
+ ```
188
+
189
+ **Windows:**
190
+ ```cmd
191
+ start cursor-logs-export/report.html
192
+ ```
193
+
194
+ **Linux:**
195
+ ```bash
196
+ xdg-open cursor-logs-export/report.html
197
+ ```
198
+
199
+ Or simply double-click `report.html` in your file explorer.
141
200
 
142
- The analyzer reads from Cursor's local SQLite database:
201
+ ## Data Sources
202
+
203
+ ### 1. Local SQLite Database (Required)
204
+
205
+ The analyzer reads from Cursor's local SQLite database at platform-specific locations:
206
+
207
+ **macOS:**
143
208
  ```
144
209
  ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
145
210
  ```
146
211
 
147
- It extracts:
212
+ **Windows:**
213
+ ```
214
+ %APPDATA%\Cursor\User\globalStorage\state.vscdb
215
+ ```
216
+
217
+ **Linux:**
218
+ ```
219
+ ~/.config/Cursor/User/globalStorage/state.vscdb
220
+ ```
221
+
222
+ From the database, it extracts:
148
223
  - **Conversation metadata**: Names, timestamps, models
149
224
  - **Message content**: Full chat history (user prompts & AI responses)
150
225
  - **Code changes**: Lines added/removed, files modified
151
- - **Token usage**: Context tokens used and limits
226
+ - **Context token usage**: Tokens in conversation context window
152
227
  - **Project information**: Workspace paths and names
153
228
 
229
+ ### 2. CSV Export (Optional, for API Token Tracking)
230
+
231
+ When you provide a CSV export from Cursor's dashboard using `--csv`:
232
+ - **API token usage**: Actual tokens sent to Claude API (input with/without cache, cache reads, output)
233
+ - **Cost tracking**: Exact costs in USD per conversation
234
+ - **API call details**: Number of API calls, model used, timestamps
235
+
236
+ The tool automatically matches CSV data to conversations based on timestamps and models, giving you a complete picture of both context usage and actual API consumption.
237
+
154
238
  ## Requirements
155
239
 
156
240
  - **Node.js**: v14 or higher
157
241
  - **Cursor Editor**: Must have conversations stored locally
158
- - **OS**: macOS (paths are macOS-specific)
242
+ - **OS**: Windows, macOS, or Linux
159
243
 
160
244
  ## Project Structure
161
245
 
@@ -200,11 +284,24 @@ npx cursor-usage-analyzer --date 2025-11-15
200
284
  ## Troubleshooting
201
285
 
202
286
  ### "Database not found"
203
- Ensure Cursor has been used and conversations exist. Database path:
287
+ Ensure Cursor has been used and conversations exist. Check the database path for your platform:
288
+
289
+ **macOS:**
204
290
  ```
205
291
  ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
206
292
  ```
207
293
 
294
+ **Windows:**
295
+ ```
296
+ %APPDATA%\Cursor\User\globalStorage\state.vscdb
297
+ ```
298
+ (Usually: `C:\Users\YourUsername\AppData\Roaming\Cursor\User\globalStorage\state.vscdb`)
299
+
300
+ **Linux:**
301
+ ```
302
+ ~/.config/Cursor/User/globalStorage/state.vscdb
303
+ ```
304
+
208
305
  ### "No conversations found"
209
306
  - Check the date range is correct
210
307
  - Verify you have conversations in Cursor from that period
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 CURSOR_GLOBAL_STORAGE = path.join(os.homedir(), 'Library/Application Support/Cursor/User/globalStorage');
11
- const CURSOR_WORKSPACE_STORAGE = path.join(os.homedir(), 'Library/Application Support/Cursor/User/workspaceStorage');
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
- conversations.push({
350
+
351
+ const conv = {
249
352
  composerId,
250
353
  name: composer.name || 'Untitled Chat',
251
354
  timestamp,
@@ -258,12 +361,29 @@ 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
+ // Match API calls if available
367
+ const matchedCalls = matchAPICallsToConversation(conv, apiCalls);
368
+ conv.apiCalls = matchedCalls;
369
+ conv.apiCallCount = matchedCalls.length;
370
+
371
+ // Sum up API tokens
372
+ conv.apiTokens = {
373
+ inputWithCache: matchedCalls.reduce((sum, c) => sum + c.inputWithCache, 0),
374
+ inputWithoutCache: matchedCalls.reduce((sum, c) => sum + c.inputWithoutCache, 0),
375
+ cacheRead: matchedCalls.reduce((sum, c) => sum + c.cacheRead, 0),
376
+ outputTokens: matchedCalls.reduce((sum, c) => sum + c.outputTokens, 0),
377
+ totalTokens: matchedCalls.reduce((sum, c) => sum + c.totalTokens, 0),
378
+ cost: matchedCalls.reduce((sum, c) => sum + c.cost, 0)
379
+ };
380
+
381
+ conversations.push(conv);
262
382
  } catch (e) {
263
383
  // Silently skip invalid composers
264
384
  }
265
385
  }
266
-
386
+
267
387
  db.close();
268
388
  return conversations;
269
389
  } catch (error) {
@@ -278,9 +398,9 @@ function exportConversation(conv, index, dateStr) {
278
398
  const timeStr = timestamp.toTimeString().split(' ')[0].replace(/:/g, '-');
279
399
  const workspaceShort = conv.workspace.substring(0, 15).replace(/[^a-zA-Z0-9]/g, '_');
280
400
  const filename = `${dateStr}_${timeStr}_${workspaceShort}_conv${index}.txt`;
281
-
401
+
282
402
  const percentUsed = (conv.contextTokensUsed / conv.contextTokenLimit * 100).toFixed(1);
283
-
403
+
284
404
  const lines = [
285
405
  '='.repeat(80),
286
406
  `CONVERSATION #${index}`,
@@ -288,18 +408,34 @@ function exportConversation(conv, index, dateStr) {
288
408
  `Workspace: ${conv.workspace}`,
289
409
  `Time: ${timestamp.toLocaleString('en-US')}`,
290
410
  `Model: ${conv.model}`,
291
- `Tokens: ${conv.contextTokensUsed.toLocaleString()} / ${conv.contextTokenLimit.toLocaleString()} (${percentUsed}%)`,
411
+ `Context Tokens: ${conv.contextTokensUsed.toLocaleString()} / ${conv.contextTokenLimit.toLocaleString()} (${percentUsed}%)`,
292
412
  `Changes: +${conv.totalLinesAdded} -${conv.totalLinesRemoved} lines in ${conv.filesChangedCount} files`,
293
413
  `Messages: ${conv.messageCount}`,
294
414
  `Composer ID: ${conv.composerId}`,
295
- '='.repeat(80),
296
415
  ''
297
416
  ];
298
-
417
+
418
+ // Add API token information if available
419
+ if (conv.apiCallCount > 0) {
420
+ lines.push(
421
+ 'API TOKEN USAGE (from dashboard export):',
422
+ ` API Calls: ${conv.apiCallCount}`,
423
+ ` Input (w/ Cache Write): ${conv.apiTokens.inputWithCache.toLocaleString()}`,
424
+ ` Input (w/o Cache Write): ${conv.apiTokens.inputWithoutCache.toLocaleString()}`,
425
+ ` Cache Read: ${conv.apiTokens.cacheRead.toLocaleString()}`,
426
+ ` Output Tokens: ${conv.apiTokens.outputTokens.toLocaleString()}`,
427
+ ` Total API Tokens: ${conv.apiTokens.totalTokens.toLocaleString()}`,
428
+ ` Cost: $${conv.apiTokens.cost.toFixed(2)}`,
429
+ ''
430
+ );
431
+ }
432
+
433
+ lines.push('='.repeat(80), '');
434
+
299
435
  for (const msg of conv.messages) {
300
436
  const msgTime = new Date(msg.timestamp).toLocaleTimeString('en-US');
301
437
  const role = msg.role.toUpperCase();
302
-
438
+
303
439
  lines.push(
304
440
  '',
305
441
  '-'.repeat(80),
@@ -308,9 +444,9 @@ function exportConversation(conv, index, dateStr) {
308
444
  msg.text
309
445
  );
310
446
  }
311
-
447
+
312
448
  lines.push('', '='.repeat(80), 'End of conversation', '='.repeat(80));
313
-
449
+
314
450
  fs.writeFileSync(path.join(CHATS_DIR, filename), lines.join('\n'), 'utf-8');
315
451
  return filename;
316
452
  }
@@ -318,7 +454,7 @@ function exportConversation(conv, index, dateStr) {
318
454
  // Generate statistics
319
455
  function generateStats(conversations, startOfDay, endOfDay) {
320
456
  const daysDiff = Math.ceil((endOfDay - startOfDay) / (1000 * 60 * 60 * 24));
321
-
457
+
322
458
  const stats = {
323
459
  totalConversations: conversations.length,
324
460
  totalMessages: 0,
@@ -326,6 +462,15 @@ function generateStats(conversations, startOfDay, endOfDay) {
326
462
  totalLinesAdded: 0,
327
463
  totalLinesRemoved: 0,
328
464
  totalFilesChanged: 0,
465
+ totalApiCalls: 0,
466
+ totalApiTokens: {
467
+ inputWithCache: 0,
468
+ inputWithoutCache: 0,
469
+ cacheRead: 0,
470
+ outputTokens: 0,
471
+ totalTokens: 0,
472
+ cost: 0
473
+ },
329
474
  modelUsage: {},
330
475
  workspaceUsage: {},
331
476
  hourlyDistribution: Array(24).fill(0),
@@ -334,26 +479,37 @@ function generateStats(conversations, startOfDay, endOfDay) {
334
479
  conversations: [],
335
480
  generatedAt: new Date().toLocaleString('en-US')
336
481
  };
337
-
482
+
338
483
  for (const conv of conversations) {
339
484
  const ts = new Date(conv.timestamp);
340
-
485
+
341
486
  stats.totalMessages += conv.messageCount;
342
487
  stats.totalTokens += conv.contextTokensUsed;
343
488
  stats.totalLinesAdded += conv.totalLinesAdded;
344
489
  stats.totalLinesRemoved += conv.totalLinesRemoved;
345
490
  stats.totalFilesChanged += conv.filesChangedCount;
346
-
491
+
492
+ // Add API token stats
493
+ if (conv.apiCallCount > 0) {
494
+ stats.totalApiCalls += conv.apiCallCount;
495
+ stats.totalApiTokens.inputWithCache += conv.apiTokens.inputWithCache;
496
+ stats.totalApiTokens.inputWithoutCache += conv.apiTokens.inputWithoutCache;
497
+ stats.totalApiTokens.cacheRead += conv.apiTokens.cacheRead;
498
+ stats.totalApiTokens.outputTokens += conv.apiTokens.outputTokens;
499
+ stats.totalApiTokens.totalTokens += conv.apiTokens.totalTokens;
500
+ stats.totalApiTokens.cost += conv.apiTokens.cost;
501
+ }
502
+
347
503
  stats.modelUsage[conv.model] = (stats.modelUsage[conv.model] || 0) + 1;
348
504
  stats.workspaceUsage[conv.workspace] = (stats.workspaceUsage[conv.workspace] || 0) + 1;
349
505
  stats.hourlyDistribution[ts.getHours()]++;
350
-
506
+
351
507
  const dateKey = ts.toLocaleDateString('en-US');
352
508
  stats.dailyDistribution[dateKey] = (stats.dailyDistribution[dateKey] || 0) + 1;
353
-
509
+
354
510
  const firstUserMsg = conv.messages.find(m => m.role === 'user');
355
511
  const preview = firstUserMsg?.text.substring(0, 100) + (firstUserMsg?.text.length > 100 ? '...' : '') || '(no user message)';
356
-
512
+
357
513
  stats.conversations.push({
358
514
  timestamp: conv.timestamp,
359
515
  time: ts.toLocaleTimeString('en-US'),
@@ -365,49 +521,74 @@ function generateStats(conversations, startOfDay, endOfDay) {
365
521
  messages: conv.messageCount,
366
522
  tokens: conv.contextTokensUsed,
367
523
  contextLimit: conv.contextTokenLimit,
524
+ apiCallCount: conv.apiCallCount || 0,
525
+ apiTokens: conv.apiTokens,
368
526
  linesChanged: `+${conv.totalLinesAdded}/-${conv.totalLinesRemoved}`,
369
527
  files: conv.filesChangedCount,
370
528
  preview
371
529
  });
372
530
  }
373
-
531
+
374
532
  stats.conversations.sort((a, b) => a.timestamp - b.timestamp);
375
-
533
+
376
534
  return stats;
377
535
  }
378
536
 
379
537
  // Main
380
538
  async function main() {
381
539
  console.log('Cursor Usage Analyzer v2\n');
382
-
383
- const { startOfDay, endOfDay, dateStr } = parseArgs();
384
-
540
+
541
+ // Check if Cursor storage exists
542
+ if (!fs.existsSync(CURSOR_GLOBAL_STORAGE)) {
543
+ console.error('Error: Cursor storage directory not found!');
544
+ console.error(`Expected location: ${CURSOR_GLOBAL_STORAGE}`);
545
+ console.error('\nPossible reasons:');
546
+ console.error(' 1. Cursor is not installed');
547
+ console.error(' 2. Cursor has never been run');
548
+ console.error(' 3. Different installation path\n');
549
+ console.error(`Detected OS: ${os.platform()}`);
550
+ process.exit(1);
551
+ }
552
+
553
+ const { startOfDay, endOfDay, dateStr, csvPath } = parseArgs();
554
+
385
555
  console.log(`Analyzing: ${dateStr}`);
386
- console.log(`Period: ${new Date(startOfDay).toLocaleString('en-US')} - ${new Date(endOfDay).toLocaleString('en-US')}\n`);
387
-
556
+ console.log(`Period: ${new Date(startOfDay).toLocaleString('en-US')} - ${new Date(endOfDay).toLocaleString('en-US')}`);
557
+ console.log(`Platform: ${os.platform()}`);
558
+
559
+ // Parse CSV if provided
560
+ let apiCalls = [];
561
+ if (csvPath) {
562
+ console.log(`CSV file: ${csvPath}`);
563
+ apiCalls = parseCSVUsage(csvPath);
564
+ console.log(`Parsed ${apiCalls.length} API calls from CSV\n`);
565
+ } else {
566
+ console.log('No CSV file provided (use --csv path/to/file.csv to include API token data)\n');
567
+ }
568
+
388
569
  // Prepare output folders
389
570
  if (fs.existsSync(OUTPUT_DIR)) fs.rmSync(OUTPUT_DIR, { recursive: true });
390
571
  fs.mkdirSync(CHATS_DIR, { recursive: true });
391
-
572
+
392
573
  console.log('Extracting conversations...');
393
-
394
- const conversations = extractConversations(startOfDay, endOfDay);
395
-
574
+
575
+ const conversations = extractConversations(startOfDay, endOfDay, apiCalls);
576
+
396
577
  console.log(`Found ${conversations.length} conversations\n`);
397
-
578
+
398
579
  if (conversations.length === 0) {
399
580
  console.log('No conversations found in specified period');
400
581
  return;
401
582
  }
402
-
583
+
403
584
  console.log('Exporting conversations...');
404
585
  conversations.forEach((conv, i) => {
405
586
  exportConversation(conv, i + 1, dateStr);
406
587
  });
407
-
588
+
408
589
  console.log('Generating statistics...');
409
590
  const stats = generateStats(conversations, startOfDay, endOfDay);
410
-
591
+
411
592
  console.log('Generating HTML report...');
412
593
  try {
413
594
  generateHTMLReport(stats, dateStr, REPORT_PATH);
@@ -416,11 +597,20 @@ async function main() {
416
597
  console.error(e.stack);
417
598
  return;
418
599
  }
419
-
600
+
420
601
  console.log('\nDone!\n');
421
602
  console.log(`Export folder: ${OUTPUT_DIR}`);
422
603
  console.log(`Conversations: ${conversations.length}`);
604
+ if (apiCalls.length > 0) {
605
+ console.log(`API Calls matched: ${stats.totalApiCalls}`);
606
+ console.log(`Total API tokens: ${stats.totalApiTokens.totalTokens.toLocaleString()}`);
607
+ console.log(`Total cost: $${stats.totalApiTokens.cost.toFixed(2)}`);
608
+ }
423
609
  console.log(`Report: ${REPORT_PATH}`);
610
+
611
+ // Platform-specific open command hint
612
+ const openCmd = os.platform() === 'win32' ? 'start' : os.platform() === 'darwin' ? 'open' : 'xdg-open';
613
+ console.log(`\nOpen report: ${openCmd} ${REPORT_PATH}`);
424
614
  }
425
615
 
426
616
  main().catch(console.error);
Binary file