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.
- package/.claude/settings.local.json +9 -0
- package/.idea/vcs.xml +5 -0
- package/README.md +104 -7
- package/analyze.js +245 -55
- package/cursor-usage-analyzer-0.2.0.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/.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
|
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.
|
|
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
|
|
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,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
|
-
|
|
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')}
|
|
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
|