cursor-usage 0.1.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,350 @@
1
+ /**
2
+ * Event loading utilities for detailed usage analysis
3
+ * Fetches granular usage events for building daily/weekly reports
4
+ */
5
+ import { CURSOR_API_URL_EVENTS, DEFAULT_TIMEOUT, USER_AGENT, } from './_consts';
6
+ import { logger } from './logger';
7
+ /**
8
+ * Fetch usage events for a date range
9
+ */
10
+ export async function fetchUsageEvents(credentials, startDate, endDate, pageSize = 100) {
11
+ try {
12
+ const sessionToken = `${credentials.userId}::${credentials.accessToken}`;
13
+ const headers = {
14
+ Accept: 'application/json',
15
+ 'Content-Type': 'application/json',
16
+ 'User-Agent': USER_AGENT,
17
+ Origin: 'https://cursor.com',
18
+ };
19
+ const cookieHeader = `WorkosCursorSessionToken=${encodeURIComponent(sessionToken)}`;
20
+ // Convert dates to millisecond timestamps
21
+ const startTimestamp = startDate.getTime().toString();
22
+ const endTimestamp = endDate.getTime().toString();
23
+ const body = {
24
+ teamId: 0,
25
+ startDate: startTimestamp,
26
+ endDate: endTimestamp,
27
+ page: 1,
28
+ pageSize,
29
+ };
30
+ const controller = new AbortController();
31
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
32
+ const response = await fetch(CURSOR_API_URL_EVENTS, {
33
+ method: 'POST',
34
+ headers: {
35
+ ...headers,
36
+ Cookie: cookieHeader,
37
+ },
38
+ body: JSON.stringify(body),
39
+ signal: controller.signal,
40
+ });
41
+ clearTimeout(timeoutId);
42
+ if (!response.ok) {
43
+ logger.error(`Events API request failed with status ${response.status}: ${response.statusText}`);
44
+ return [];
45
+ }
46
+ const data = (await response.json());
47
+ return parseEvents(data);
48
+ }
49
+ catch (error) {
50
+ logger.error(`Error fetching usage events: ${String(error)}`);
51
+ return [];
52
+ }
53
+ }
54
+ /**
55
+ * Parse and validate events response
56
+ */
57
+ function parseEvents(data) {
58
+ try {
59
+ if (!Array.isArray(data?.usageEventsDisplay)) {
60
+ return [];
61
+ }
62
+ return data.usageEventsDisplay.map((event) => {
63
+ const tokenUsage = event.tokenUsage || {};
64
+ const totalTokens = (tokenUsage.inputTokens || 0) +
65
+ (tokenUsage.outputTokens || 0) +
66
+ (tokenUsage.cacheWriteTokens || 0) +
67
+ (tokenUsage.cacheReadTokens || 0);
68
+ return {
69
+ id: event.id || `${event.timestamp}-${event.model}`,
70
+ timestamp: Number(event.timestamp) || Date.now(),
71
+ model: event.model || 'unknown',
72
+ tokens: totalTokens,
73
+ inputTokens: Number(tokenUsage.inputTokens) || 0,
74
+ outputTokens: Number(tokenUsage.outputTokens) || 0,
75
+ cacheWriteTokens: Number(tokenUsage.cacheWriteTokens) || 0,
76
+ cacheReadTokens: Number(tokenUsage.cacheReadTokens) || 0,
77
+ type: 'usage',
78
+ kind: event.kind || 'unknown',
79
+ cost: Number(tokenUsage.totalCents) / 100 || 0, // Convert cents to dollars
80
+ maxMode: event.maxMode || false,
81
+ };
82
+ });
83
+ }
84
+ catch (error) {
85
+ logger.error(`Error parsing events: ${String(error)}`);
86
+ return [];
87
+ }
88
+ }
89
+ /**
90
+ * Group events by day
91
+ */
92
+ export function groupByDay(events) {
93
+ const grouped = new Map();
94
+ events.forEach((event) => {
95
+ const date = new Date(event.timestamp);
96
+ const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
97
+ if (!grouped.has(dateKey)) {
98
+ grouped.set(dateKey, []);
99
+ }
100
+ grouped.get(dateKey).push(event);
101
+ });
102
+ return grouped;
103
+ }
104
+ export function calculateDailyStats(groupedEvents) {
105
+ const stats = [];
106
+ Array.from(groupedEvents.entries())
107
+ .sort()
108
+ .forEach(([date, events]) => {
109
+ const models = new Map();
110
+ let totalTokens = 0;
111
+ let inputTokens = 0;
112
+ let outputTokens = 0;
113
+ let totalCost = 0;
114
+ events.forEach((event) => {
115
+ totalTokens += event.tokens;
116
+ inputTokens += event.inputTokens;
117
+ outputTokens += event.outputTokens;
118
+ totalCost += event.cost || 0;
119
+ const count = models.get(event.model) || 0;
120
+ models.set(event.model, count + 1);
121
+ });
122
+ stats.push({
123
+ date,
124
+ eventCount: events.length,
125
+ totalTokens,
126
+ inputTokens,
127
+ outputTokens,
128
+ totalCost,
129
+ models,
130
+ });
131
+ });
132
+ return stats;
133
+ }
134
+ /**
135
+ * Format daily stats for table display
136
+ */
137
+ export function formatDailyStatsTable(stats) {
138
+ return stats.map((day) => {
139
+ const modelList = Array.from(day.models.entries())
140
+ .map(([model, count]) => `${model}(${count})`)
141
+ .join(', ');
142
+ return [
143
+ day.date,
144
+ day.eventCount.toString(),
145
+ day.totalTokens.toLocaleString(),
146
+ day.inputTokens.toLocaleString(),
147
+ day.outputTokens.toLocaleString(),
148
+ `$${day.totalCost.toFixed(2)}`,
149
+ modelList || 'N/A',
150
+ ];
151
+ });
152
+ }
153
+ /**
154
+ * Group events by month
155
+ */
156
+ export function groupByMonth(events) {
157
+ const grouped = new Map();
158
+ events.forEach((event) => {
159
+ const date = new Date(event.timestamp);
160
+ const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; // YYYY-MM
161
+ if (!grouped.has(monthKey)) {
162
+ grouped.set(monthKey, []);
163
+ }
164
+ grouped.get(monthKey).push(event);
165
+ });
166
+ return grouped;
167
+ }
168
+ /**
169
+ * Calculate monthly statistics
170
+ */
171
+ export function calculateMonthlyStats(groupedEvents) {
172
+ const monthNames = [
173
+ 'January', 'February', 'March', 'April', 'May', 'June',
174
+ 'July', 'August', 'September', 'October', 'November', 'December',
175
+ ];
176
+ const stats = [];
177
+ Array.from(groupedEvents.entries())
178
+ .sort()
179
+ .forEach(([monthKey, events]) => {
180
+ const [yearStr, monthStr] = monthKey.split('-');
181
+ const year = Number(yearStr);
182
+ const month = Number(monthStr);
183
+ const models = new Map();
184
+ let totalTokens = 0;
185
+ let inputTokens = 0;
186
+ let outputTokens = 0;
187
+ let totalCost = 0;
188
+ events.forEach((event) => {
189
+ totalTokens += event.tokens;
190
+ inputTokens += event.inputTokens;
191
+ outputTokens += event.outputTokens;
192
+ totalCost += event.cost || 0;
193
+ const count = models.get(event.model) || 0;
194
+ models.set(event.model, count + 1);
195
+ });
196
+ stats.push({
197
+ year,
198
+ month,
199
+ monthName: monthNames[month - 1],
200
+ monthKey,
201
+ eventCount: events.length,
202
+ totalTokens,
203
+ inputTokens,
204
+ outputTokens,
205
+ totalCost,
206
+ models,
207
+ });
208
+ });
209
+ return stats;
210
+ }
211
+ /**
212
+ * Format monthly stats for table display
213
+ */
214
+ export function formatMonthlyStatsTable(stats) {
215
+ return stats.map((month) => {
216
+ const modelList = Array.from(month.models.entries())
217
+ .map(([model, count]) => `${model}(${count})`)
218
+ .join(', ');
219
+ return [
220
+ `${month.monthName} ${month.year}`,
221
+ month.eventCount.toString(),
222
+ month.totalTokens.toLocaleString(),
223
+ month.inputTokens.toLocaleString(),
224
+ month.outputTokens.toLocaleString(),
225
+ `$${month.totalCost.toFixed(2)}`,
226
+ modelList || 'N/A',
227
+ ];
228
+ });
229
+ }
230
+ /**
231
+ * Calculate per-model breakdown
232
+ */
233
+ export function calculateModelBreakdown(events) {
234
+ const breakdown = new Map();
235
+ events.forEach((event) => {
236
+ const model = event.model || 'unknown';
237
+ if (!breakdown.has(model)) {
238
+ breakdown.set(model, {
239
+ model,
240
+ count: 0,
241
+ totalTokens: 0,
242
+ inputTokens: 0,
243
+ outputTokens: 0,
244
+ totalCost: 0,
245
+ });
246
+ }
247
+ const stats = breakdown.get(model);
248
+ stats.count += 1;
249
+ stats.totalTokens += event.tokens;
250
+ stats.inputTokens += event.inputTokens;
251
+ stats.outputTokens += event.outputTokens;
252
+ stats.totalCost += event.cost || 0;
253
+ });
254
+ return breakdown;
255
+ }
256
+ /**
257
+ * Format model breakdown for display
258
+ */
259
+ export function formatModelBreakdownTable(breakdown, totalTokens, totalCost) {
260
+ const sorted = Array.from(breakdown.values()).sort((a, b) => b.totalTokens - a.totalTokens);
261
+ return sorted.map((model) => {
262
+ const tokenPercent = totalTokens > 0 ? (model.totalTokens / totalTokens * 100).toFixed(1) : '0.0';
263
+ const costPercent = totalCost > 0 ? (model.totalCost / totalCost * 100).toFixed(1) : '0.0';
264
+ return [
265
+ model.model,
266
+ model.count.toString(),
267
+ model.totalTokens.toLocaleString(),
268
+ `$${model.totalCost.toFixed(2)}`,
269
+ `${tokenPercent}%`,
270
+ `${costPercent}%`,
271
+ ];
272
+ });
273
+ }
274
+ /**
275
+ * Group events by week
276
+ */
277
+ export function groupByWeek(events) {
278
+ const grouped = new Map();
279
+ events.forEach((event) => {
280
+ const date = new Date(event.timestamp);
281
+ // ISO week starts on Monday
282
+ const weekStart = new Date(date);
283
+ const day = weekStart.getUTCDay();
284
+ const diff = weekStart.getUTCDate() - day + (day === 0 ? -6 : 1);
285
+ weekStart.setUTCDate(diff);
286
+ weekStart.setUTCHours(0, 0, 0, 0);
287
+ const weekKey = weekStart.toISOString().split('T')[0];
288
+ if (!grouped.has(weekKey)) {
289
+ grouped.set(weekKey, []);
290
+ }
291
+ grouped.get(weekKey).push(event);
292
+ });
293
+ return grouped;
294
+ }
295
+ /**
296
+ * Calculate weekly statistics
297
+ */
298
+ export function calculateWeeklyStats(groupedEvents) {
299
+ const stats = [];
300
+ Array.from(groupedEvents.entries())
301
+ .sort()
302
+ .forEach(([weekKey, events]) => {
303
+ const weekStart = new Date(weekKey);
304
+ const weekEnd = new Date(weekStart);
305
+ weekEnd.setDate(weekEnd.getDate() + 6);
306
+ const models = new Map();
307
+ let totalTokens = 0;
308
+ let inputTokens = 0;
309
+ let outputTokens = 0;
310
+ let totalCost = 0;
311
+ events.forEach((event) => {
312
+ totalTokens += event.tokens;
313
+ inputTokens += event.inputTokens;
314
+ outputTokens += event.outputTokens;
315
+ totalCost += event.cost || 0;
316
+ const count = models.get(event.model) || 0;
317
+ models.set(event.model, count + 1);
318
+ });
319
+ stats.push({
320
+ weekStart: weekKey,
321
+ weekEnd: weekEnd.toISOString().split('T')[0],
322
+ eventCount: events.length,
323
+ totalTokens,
324
+ inputTokens,
325
+ outputTokens,
326
+ totalCost,
327
+ models,
328
+ });
329
+ });
330
+ return stats;
331
+ }
332
+ /**
333
+ * Format weekly stats for table display
334
+ */
335
+ export function formatWeeklyStatsTable(stats) {
336
+ return stats.map((week) => {
337
+ const modelList = Array.from(week.models.entries())
338
+ .map(([model, count]) => `${model}(${count})`)
339
+ .join(', ');
340
+ return [
341
+ `${week.weekStart} to ${week.weekEnd}`,
342
+ week.eventCount.toString(),
343
+ week.totalTokens.toLocaleString(),
344
+ week.inputTokens.toLocaleString(),
345
+ week.outputTokens.toLocaleString(),
346
+ `$${week.totalCost.toFixed(2)}`,
347
+ modelList || 'N/A',
348
+ ];
349
+ });
350
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor Usage Analyzer
4
+ * A CLI tool for analyzing Cursor API usage and token consumption
5
+ */
6
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor Usage Analyzer
4
+ * A CLI tool for analyzing Cursor API usage and token consumption
5
+ */
6
+ import { runCLI } from './cli.js';
7
+ const argv = process.argv.slice(2);
8
+ runCLI(argv).catch((error) => {
9
+ console.error(`Fatal error: ${String(error)}`);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Simple logging utilities
3
+ */
4
+ export declare const logger: {
5
+ log: (message: string) => void;
6
+ info: (message: string) => void;
7
+ success: (message: string) => void;
8
+ warn: (message: string) => void;
9
+ error: (message: string) => void;
10
+ debug: (message: string) => void;
11
+ };
package/dist/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Simple logging utilities
3
+ */
4
+ const COLORS = {
5
+ reset: '\x1b[0m',
6
+ dim: '\x1b[2m',
7
+ red: '\x1b[31m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ blue: '\x1b[34m',
11
+ cyan: '\x1b[36m',
12
+ };
13
+ export const logger = {
14
+ log: (message) => {
15
+ console.log(message);
16
+ },
17
+ info: (message) => {
18
+ console.log(`${COLORS.cyan}ℹ ${COLORS.reset}${message}`);
19
+ },
20
+ success: (message) => {
21
+ console.log(`${COLORS.green}✓ ${COLORS.reset}${message}`);
22
+ },
23
+ warn: (message) => {
24
+ console.warn(`${COLORS.yellow}⚠ ${COLORS.reset}${message}`);
25
+ },
26
+ error: (message) => {
27
+ console.error(`${COLORS.red}✗ ${COLORS.reset}${message}`);
28
+ },
29
+ debug: (message) => {
30
+ if (process.env.DEBUG) {
31
+ console.log(`${COLORS.dim}[DEBUG] ${message}${COLORS.reset}`);
32
+ }
33
+ },
34
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Simple table formatting utilities
3
+ */
4
+ export interface TableOptions {
5
+ headers: string[];
6
+ rows: string[][];
7
+ maxWidth?: number;
8
+ }
9
+ /**
10
+ * Format data as a simple ASCII table
11
+ */
12
+ export declare function formatTable(options: TableOptions): string;
13
+ /**
14
+ * Create a simple data table with borders
15
+ */
16
+ export declare function createTable(headers: string[], rows: string[][]): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Simple table formatting utilities
3
+ */
4
+ /**
5
+ * Format data as a simple ASCII table
6
+ */
7
+ export function formatTable(options) {
8
+ const { headers, rows } = options;
9
+ if (rows.length === 0) {
10
+ return 'No data to display';
11
+ }
12
+ // Calculate column widths
13
+ const colWidths = headers.map((h, i) => {
14
+ const headerWidth = h.length;
15
+ const maxRowWidth = Math.max(...rows.map((row) => (row[i] ? row[i].length : 0)));
16
+ return Math.max(headerWidth, maxRowWidth);
17
+ });
18
+ // Build separator line
19
+ const separator = '+' + colWidths.map((w) => '-'.repeat(w + 2)).join('+') + '+';
20
+ // Build header line
21
+ const headerLine = '| ' +
22
+ headers
23
+ .map((h, i) => h.padEnd(colWidths[i]))
24
+ .join(' | ') +
25
+ ' |';
26
+ // Build data lines
27
+ const dataLines = rows.map((row) => '| ' +
28
+ row
29
+ .map((cell, i) => (cell || '').padEnd(colWidths[i]))
30
+ .join(' | ') +
31
+ ' |');
32
+ // Combine all parts
33
+ return [separator, headerLine, separator, ...dataLines, separator].join('\n');
34
+ }
35
+ /**
36
+ * Create a simple data table with borders
37
+ */
38
+ export function createTable(headers, rows) {
39
+ return formatTable({ headers, rows });
40
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "cursor-usage",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "CLI tool for analyzing Cursor usage and token consumption",
6
+ "author": "yifen <anthonyeef@gmail.com>",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Anthonyeef/cursor-usage.git"
11
+ },
12
+ "keywords": [
13
+ "cursor",
14
+ "cli",
15
+ "usage",
16
+ "analytics",
17
+ "tokens",
18
+ "api",
19
+ "cursor-ai"
20
+ ],
21
+ "bugs": {
22
+ "url": "https://github.com/Anthonyeef/cursor-usage/issues"
23
+ },
24
+ "homepage": "https://github.com/Anthonyeef/cursor-usage#readme",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "bin": "dist/index.js",
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "dev": "tsx src/index.ts",
35
+ "build": "tsc",
36
+ "prepublishOnly": "npm run build",
37
+ "start": "tsx src/index.ts",
38
+ "test": "vitest",
39
+ "lint": "eslint src/",
40
+ "format": "prettier --write src/"
41
+ },
42
+ "dependencies": {
43
+ "sql.js": "^1.8.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.10.6",
47
+ "typescript": "^5.3.3",
48
+ "tsx": "^4.7.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ }
53
+ }