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,273 @@
1
+ /**
2
+ * CLI command handlers
3
+ */
4
+ import { fetchUsageEvents, groupByDay, calculateDailyStats, groupByMonth, calculateMonthlyStats, formatMonthlyStatsTable, groupByWeek, calculateWeeklyStats, formatWeeklyStatsTable, calculateModelBreakdown, formatModelBreakdownTable, } from './event-loader';
5
+ import { createTable } from './table-formatter';
6
+ import { logger } from './logger';
7
+ /**
8
+ * Convert stats to JSON
9
+ */
10
+ export function statsToJSON(command, stats, events, options = {}) {
11
+ const totalTokens = events.reduce((sum, e) => sum + e.tokens, 0);
12
+ const totalCost = events.reduce((sum, e) => sum + (e.cost || 0), 0);
13
+ const totalEvents = events.length;
14
+ const data = {
15
+ command,
16
+ generatedAt: new Date().toISOString(),
17
+ period: options.period || command,
18
+ timeRange: options.timeRange,
19
+ summary: {
20
+ totalEvents,
21
+ totalTokens,
22
+ totalCost: parseFloat(totalCost.toFixed(2)),
23
+ },
24
+ data: stats,
25
+ };
26
+ if (options.breakdown) {
27
+ const breakdown = calculateModelBreakdown(events);
28
+ data.breakdown = Array.from(breakdown.values()).map((model) => ({
29
+ model: model.model,
30
+ count: model.count,
31
+ totalTokens: model.totalTokens,
32
+ inputTokens: model.inputTokens,
33
+ outputTokens: model.outputTokens,
34
+ totalCost: parseFloat(model.totalCost.toFixed(2)),
35
+ tokenPercent: parseFloat(((model.totalTokens / totalTokens) * 100).toFixed(2)),
36
+ costPercent: parseFloat(((model.totalCost / totalCost) * 100).toFixed(2)),
37
+ }));
38
+ }
39
+ return JSON.stringify(data, null, 2);
40
+ }
41
+ /**
42
+ * Display model breakdown
43
+ */
44
+ function displayBreakdown(events, title = 'MODEL BREAKDOWN') {
45
+ const breakdown = calculateModelBreakdown(events);
46
+ const totalTokens = events.reduce((sum, e) => sum + e.tokens, 0);
47
+ const totalCost = events.reduce((sum, e) => sum + (e.cost || 0), 0);
48
+ logger.log('\n' + '='.repeat(100));
49
+ logger.log(title);
50
+ logger.log('='.repeat(100) + '\n');
51
+ const tableData = formatModelBreakdownTable(breakdown, totalTokens, totalCost);
52
+ const table = createTable(['Model', 'Events', 'Total Tokens', 'Cost', 'Token %', 'Cost %'], tableData);
53
+ logger.log(table + '\n');
54
+ }
55
+ /**
56
+ * Show daily usage report for a date range
57
+ */
58
+ export async function showDailyReport(credentials, days = 7, options = {}, outputJson = false) {
59
+ let startDate = options.startDate;
60
+ let endDate = options.endDate;
61
+ if (!startDate || !endDate) {
62
+ endDate = endDate || new Date();
63
+ startDate = startDate || new Date();
64
+ startDate.setDate(startDate.getDate() - days);
65
+ // Set to start of day
66
+ startDate.setHours(0, 0, 0, 0);
67
+ endDate.setHours(23, 59, 59, 999);
68
+ }
69
+ const events = await fetchUsageEvents(credentials, startDate, endDate);
70
+ if (events.length === 0) {
71
+ logger.warn('No usage events found for this period');
72
+ return;
73
+ }
74
+ logger.log('');
75
+ // Group and calculate stats
76
+ const grouped = groupByDay(events);
77
+ const stats = calculateDailyStats(grouped);
78
+ // Display table
79
+ logger.log('\n' + '='.repeat(100));
80
+ logger.log(`DAILY USAGE REPORT (Last ${days} days)`);
81
+ logger.log('='.repeat(100) + '\n');
82
+ const tableData = stats.map((day) => {
83
+ const modelList = Array.from(day.models.entries())
84
+ .map(([model, count]) => `${model}(${count})`)
85
+ .join(', ');
86
+ return [
87
+ day.date,
88
+ day.eventCount.toString(),
89
+ day.totalTokens.toLocaleString(),
90
+ day.inputTokens.toLocaleString(),
91
+ day.outputTokens.toLocaleString(),
92
+ `$${day.totalCost.toFixed(2)}`,
93
+ modelList || 'N/A',
94
+ ];
95
+ });
96
+ const table = createTable(['Date', 'Events', 'Total Tokens', 'Input', 'Output', 'Cost', 'Models'], tableData);
97
+ logger.log(table);
98
+ // Summary
99
+ const totalEvents = stats.reduce((sum, day) => sum + day.eventCount, 0);
100
+ const totalTokens = stats.reduce((sum, day) => sum + day.totalTokens, 0);
101
+ const totalCost = stats.reduce((sum, day) => sum + day.totalCost, 0);
102
+ logger.log('\n' + '='.repeat(100));
103
+ logger.log('SUMMARY');
104
+ logger.log('='.repeat(100));
105
+ logger.log(`Total Events: ${totalEvents}`);
106
+ logger.log(`Total Tokens: ${totalTokens.toLocaleString()}`);
107
+ logger.log(`Total Cost: $${totalCost.toFixed(2)}`);
108
+ logger.log('='.repeat(100) + '\n');
109
+ // Show breakdown if requested
110
+ if (options.breakdown) {
111
+ displayBreakdown(events, 'PER-MODEL BREAKDOWN');
112
+ }
113
+ // Output JSON if requested
114
+ if (outputJson) {
115
+ const jsonOutput = statsToJSON('daily', stats, events, { breakdown: options.breakdown, period: `last ${days} days` });
116
+ console.log(jsonOutput);
117
+ }
118
+ }
119
+ /**
120
+ * Show monthly usage report
121
+ */
122
+ export async function showMonthlyReport(credentials, months = 3, options = {}, outputJson = false) {
123
+ let startDate = options.startDate;
124
+ let endDate = options.endDate;
125
+ if (!startDate || !endDate) {
126
+ endDate = endDate || new Date();
127
+ startDate = startDate || new Date();
128
+ startDate.setMonth(startDate.getMonth() - months);
129
+ // Set to start of month
130
+ startDate.setDate(1);
131
+ startDate.setHours(0, 0, 0, 0);
132
+ endDate.setHours(23, 59, 59, 999);
133
+ }
134
+ const events = await fetchUsageEvents(credentials, startDate, endDate);
135
+ if (events.length === 0) {
136
+ logger.warn('No usage events found for this period');
137
+ return;
138
+ }
139
+ logger.log('');
140
+ // Group and calculate stats
141
+ const grouped = groupByMonth(events);
142
+ const stats = calculateMonthlyStats(grouped);
143
+ // Display table
144
+ logger.log('\n' + '='.repeat(100));
145
+ logger.log(`MONTHLY USAGE REPORT (Last ${months} months)`);
146
+ logger.log('='.repeat(100) + '\n');
147
+ const tableData = formatMonthlyStatsTable(stats);
148
+ const table = createTable(['Month', 'Events', 'Total Tokens', 'Input', 'Output', 'Cost', 'Models'], tableData);
149
+ logger.log(table);
150
+ // Summary
151
+ const totalEvents = stats.reduce((sum, month) => sum + month.eventCount, 0);
152
+ const totalTokens = stats.reduce((sum, month) => sum + month.totalTokens, 0);
153
+ const totalCost = stats.reduce((sum, month) => sum + month.totalCost, 0);
154
+ logger.log('\n' + '='.repeat(100));
155
+ logger.log('SUMMARY');
156
+ logger.log('='.repeat(100));
157
+ logger.log(`Total Events: ${totalEvents}`);
158
+ logger.log(`Total Tokens: ${totalTokens.toLocaleString()}`);
159
+ logger.log(`Total Cost: $${totalCost.toFixed(2)}`);
160
+ logger.log(`Average per month: ${(totalTokens / Math.max(1, stats.length)).toLocaleString()} tokens, $${(totalCost / Math.max(1, stats.length)).toFixed(2)}`);
161
+ logger.log('='.repeat(100) + '\n');
162
+ // Show breakdown if requested
163
+ if (options.breakdown) {
164
+ displayBreakdown(events, 'PER-MODEL BREAKDOWN');
165
+ }
166
+ // Output JSON if requested
167
+ if (outputJson) {
168
+ const jsonOutput = statsToJSON('monthly', stats, events, { breakdown: options.breakdown, period: `last ${months} months` });
169
+ console.log(jsonOutput);
170
+ }
171
+ }
172
+ /**
173
+ * Show weekly usage report
174
+ */
175
+ export async function showWeeklyReport(credentials, weeks = 4, options = {}, outputJson = false) {
176
+ let startDate = options.startDate;
177
+ let endDate = options.endDate;
178
+ if (!startDate || !endDate) {
179
+ endDate = endDate || new Date();
180
+ startDate = startDate || new Date();
181
+ startDate.setDate(startDate.getDate() - weeks * 7);
182
+ // Set to start of day
183
+ startDate.setHours(0, 0, 0, 0);
184
+ endDate.setHours(23, 59, 59, 999);
185
+ }
186
+ const events = await fetchUsageEvents(credentials, startDate, endDate);
187
+ if (events.length === 0) {
188
+ logger.warn('No usage events found for this period');
189
+ return;
190
+ }
191
+ logger.log('');
192
+ // Group and calculate stats
193
+ const grouped = groupByWeek(events);
194
+ const stats = calculateWeeklyStats(grouped);
195
+ // Display table
196
+ logger.log('\n' + '='.repeat(120));
197
+ logger.log(`WEEKLY USAGE REPORT (Last ${weeks} weeks)`);
198
+ logger.log('='.repeat(120) + '\n');
199
+ const tableData = formatWeeklyStatsTable(stats);
200
+ const table = createTable(['Week', 'Events', 'Total Tokens', 'Input', 'Output', 'Cost', 'Models'], tableData);
201
+ logger.log(table);
202
+ // Summary
203
+ const totalEvents = stats.reduce((sum, week) => sum + week.eventCount, 0);
204
+ const totalTokens = stats.reduce((sum, week) => sum + week.totalTokens, 0);
205
+ const totalCost = stats.reduce((sum, week) => sum + week.totalCost, 0);
206
+ logger.log('\n' + '='.repeat(120));
207
+ logger.log('SUMMARY');
208
+ logger.log('='.repeat(120));
209
+ logger.log(`Total Events: ${totalEvents}`);
210
+ logger.log(`Total Tokens: ${totalTokens.toLocaleString()}`);
211
+ logger.log(`Total Cost: $${totalCost.toFixed(2)}`);
212
+ logger.log(`Average per week: ${(totalTokens / Math.max(1, stats.length)).toLocaleString()} tokens, $${(totalCost / Math.max(1, stats.length)).toFixed(2)}`);
213
+ logger.log('='.repeat(120) + '\n');
214
+ // Show breakdown if requested
215
+ if (options.breakdown) {
216
+ displayBreakdown(events, 'PER-MODEL BREAKDOWN');
217
+ }
218
+ // Output JSON if requested
219
+ if (outputJson) {
220
+ const jsonOutput = statsToJSON('weekly', stats, events, { breakdown: options.breakdown, period: `last ${weeks} weeks` });
221
+ console.log(jsonOutput);
222
+ }
223
+ }
224
+ /**
225
+ * Show usage for a specific date
226
+ */
227
+ export async function showDateReport(credentials, date, options = {}, outputJson = false) {
228
+ const startDate = new Date(date);
229
+ startDate.setHours(0, 0, 0, 0);
230
+ const endDate = new Date(date);
231
+ endDate.setHours(23, 59, 59, 999);
232
+ const events = await fetchUsageEvents(credentials, startDate, endDate);
233
+ if (events.length === 0) {
234
+ logger.warn('No usage events found for this date');
235
+ return;
236
+ }
237
+ logger.log('');
238
+ // Display detailed events
239
+ logger.log('\n' + '='.repeat(120));
240
+ logger.log(`USAGE EVENTS FOR ${date.toDateString()}`);
241
+ logger.log('='.repeat(120) + '\n');
242
+ const tableData = events.map((event) => {
243
+ const timestamp = new Date(event.timestamp).toLocaleTimeString();
244
+ return [
245
+ timestamp,
246
+ event.model,
247
+ event.type,
248
+ event.inputTokens.toLocaleString(),
249
+ event.outputTokens.toLocaleString(),
250
+ event.tokens.toLocaleString(),
251
+ `$${event.cost?.toFixed(4) || '0.0000'}`,
252
+ ];
253
+ });
254
+ const table = createTable(['Time', 'Model', 'Type', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Cost'], tableData);
255
+ logger.log(table);
256
+ // Summary
257
+ const totalTokens = events.reduce((sum, e) => sum + e.tokens, 0);
258
+ const totalCost = events.reduce((sum, e) => sum + (e.cost || 0), 0);
259
+ logger.log('\n' + '='.repeat(120));
260
+ logger.log(`Total Events: ${events.length}`);
261
+ logger.log(`Total Tokens: ${totalTokens.toLocaleString()}`);
262
+ logger.log(`Total Cost: $${totalCost.toFixed(2)}`);
263
+ logger.log('='.repeat(120) + '\n');
264
+ // Show breakdown if requested
265
+ if (options.breakdown) {
266
+ displayBreakdown(events, 'PER-MODEL BREAKDOWN');
267
+ }
268
+ // Output JSON if requested
269
+ if (outputJson) {
270
+ const jsonOutput = statsToJSON('today', events.map((e, i) => ({ ...e, eventIndex: i })), events, { breakdown: options.breakdown, period: 'today' });
271
+ console.log(jsonOutput);
272
+ }
273
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Data loading utilities for Cursor usage analysis
3
+ * Extracts credentials from local database and fetches usage data from API
4
+ */
5
+ import type { CursorCredentials, UsageSummary } from './_types';
6
+ /**
7
+ * Extract Cursor credentials from local database
8
+ */
9
+ export declare function getCursorCredentials(): Promise<CursorCredentials | null>;
10
+ /**
11
+ * Fetch usage data from Cursor API
12
+ */
13
+ export declare function fetchUsageData(credentials: CursorCredentials): Promise<UsageSummary | null>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Data loading utilities for Cursor usage analysis
3
+ * Extracts credentials from local database and fetches usage data from API
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { CURSOR_API_URL, CURSOR_DB_PATH, DB_KEY_ACCESS_TOKEN, DB_KEY_EMAIL, DB_KEY_MEMBERSHIP, DB_KEY_STATSIG_BOOTSTRAP, DEFAULT_TIMEOUT, USER_AGENT, } from './_consts';
7
+ import { logger } from './logger';
8
+ import initSqlJs from 'sql.js';
9
+ let sqlJs = null;
10
+ /**
11
+ * Initialize SQL.js
12
+ */
13
+ async function initSql() {
14
+ if (!sqlJs) {
15
+ sqlJs = await initSqlJs();
16
+ }
17
+ return sqlJs;
18
+ }
19
+ /**
20
+ * Query database value
21
+ */
22
+ function queryDB(db, key) {
23
+ try {
24
+ const stmt = db.prepare(`SELECT value FROM ItemTable WHERE key = ?`);
25
+ stmt.bind([key]);
26
+ if (stmt.step()) {
27
+ const row = stmt.getAsObject();
28
+ return row.value || null;
29
+ }
30
+ stmt.free();
31
+ return null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Extract Cursor credentials from local database
39
+ */
40
+ export async function getCursorCredentials() {
41
+ try {
42
+ const sql = await initSql();
43
+ const data = readFileSync(CURSOR_DB_PATH);
44
+ const db = new sql.Database(data);
45
+ // Get user ID from statsigBootstrap
46
+ let userId = null;
47
+ const bootstrapValue = queryDB(db, DB_KEY_STATSIG_BOOTSTRAP);
48
+ if (bootstrapValue) {
49
+ try {
50
+ const parsed = JSON.parse(bootstrapValue);
51
+ userId = parsed?.user?.userID;
52
+ }
53
+ catch (e) {
54
+ logger.warn('Failed to extract user ID from statsigBootstrap');
55
+ }
56
+ }
57
+ // Get access token
58
+ const accessToken = queryDB(db, DB_KEY_ACCESS_TOKEN);
59
+ // Get email
60
+ const email = queryDB(db, DB_KEY_EMAIL) || undefined;
61
+ // Get membership
62
+ const membership = queryDB(db, DB_KEY_MEMBERSHIP) || undefined;
63
+ db.close();
64
+ if (!userId) {
65
+ logger.error('Could not extract user ID from database');
66
+ return null;
67
+ }
68
+ if (!accessToken) {
69
+ logger.error('Could not extract access token from database');
70
+ return null;
71
+ }
72
+ return {
73
+ userId,
74
+ accessToken,
75
+ email,
76
+ membership,
77
+ };
78
+ }
79
+ catch (error) {
80
+ logger.error(`Error reading database: ${String(error)}`);
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Fetch usage data from Cursor API
86
+ */
87
+ export async function fetchUsageData(credentials) {
88
+ try {
89
+ const sessionToken = `${credentials.userId}::${credentials.accessToken}`;
90
+ const headers = {
91
+ Accept: 'application/json',
92
+ 'Content-Type': 'application/json',
93
+ 'User-Agent': USER_AGENT,
94
+ };
95
+ const cookieHeader = `WorkosCursorSessionToken=${encodeURIComponent(sessionToken)}`;
96
+ const controller = new AbortController();
97
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
98
+ const response = await fetch(CURSOR_API_URL, {
99
+ method: 'GET',
100
+ headers: {
101
+ ...headers,
102
+ Cookie: cookieHeader,
103
+ },
104
+ signal: controller.signal,
105
+ });
106
+ clearTimeout(timeoutId);
107
+ if (!response.ok) {
108
+ logger.error(`API request failed with status ${response.status}: ${response.statusText}`);
109
+ return null;
110
+ }
111
+ const data = (await response.json());
112
+ return validateAndParse(data);
113
+ }
114
+ catch (error) {
115
+ logger.error(`Error fetching usage data: ${String(error)}`);
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Validate and parse API response
121
+ */
122
+ function validateAndParse(data) {
123
+ try {
124
+ if (typeof data !== 'object' || data === null) {
125
+ logger.error('Invalid API response format');
126
+ return null;
127
+ }
128
+ return data;
129
+ }
130
+ catch (error) {
131
+ logger.error(`Error parsing API response: ${String(error)}`);
132
+ return null;
133
+ }
134
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Event loading utilities for detailed usage analysis
3
+ * Fetches granular usage events for building daily/weekly reports
4
+ */
5
+ import type { CursorCredentials } from './_types';
6
+ export interface UsageEvent {
7
+ id: string;
8
+ timestamp: number;
9
+ model: string;
10
+ tokens: number;
11
+ inputTokens: number;
12
+ outputTokens: number;
13
+ cacheWriteTokens: number;
14
+ cacheReadTokens: number;
15
+ type: string;
16
+ kind: string;
17
+ cost: number;
18
+ maxMode: boolean;
19
+ }
20
+ export interface EventsResponse {
21
+ events: UsageEvent[];
22
+ total: number;
23
+ page: number;
24
+ pageSize: number;
25
+ }
26
+ /**
27
+ * Fetch usage events for a date range
28
+ */
29
+ export declare function fetchUsageEvents(credentials: CursorCredentials, startDate: Date, endDate: Date, pageSize?: number): Promise<UsageEvent[]>;
30
+ /**
31
+ * Group events by day
32
+ */
33
+ export declare function groupByDay(events: UsageEvent[]): Map<string, UsageEvent[]>;
34
+ /**
35
+ * Calculate daily statistics
36
+ */
37
+ export interface DailyStats {
38
+ date: string;
39
+ eventCount: number;
40
+ totalTokens: number;
41
+ inputTokens: number;
42
+ outputTokens: number;
43
+ totalCost: number;
44
+ models: Map<string, number>;
45
+ }
46
+ export declare function calculateDailyStats(groupedEvents: Map<string, UsageEvent[]>): DailyStats[];
47
+ /**
48
+ * Format daily stats for table display
49
+ */
50
+ export declare function formatDailyStatsTable(stats: DailyStats[]): string[][];
51
+ /**
52
+ * Group events by month
53
+ */
54
+ export declare function groupByMonth(events: UsageEvent[]): Map<string, UsageEvent[]>;
55
+ /**
56
+ * Calculate monthly statistics
57
+ */
58
+ export declare function calculateMonthlyStats(groupedEvents: Map<string, UsageEvent[]>): any[];
59
+ /**
60
+ * Format monthly stats for table display
61
+ */
62
+ export declare function formatMonthlyStatsTable(stats: any[]): string[][];
63
+ /**
64
+ * Calculate per-model breakdown
65
+ */
66
+ export declare function calculateModelBreakdown(events: UsageEvent[]): Map<string, any>;
67
+ /**
68
+ * Format model breakdown for display
69
+ */
70
+ export declare function formatModelBreakdownTable(breakdown: Map<string, any>, totalTokens: number, totalCost: number): string[][];
71
+ /**
72
+ * Group events by week
73
+ */
74
+ export declare function groupByWeek(events: UsageEvent[]): Map<string, UsageEvent[]>;
75
+ /**
76
+ * Calculate weekly statistics
77
+ */
78
+ export declare function calculateWeeklyStats(groupedEvents: Map<string, UsageEvent[]>): any[];
79
+ /**
80
+ * Format weekly stats for table display
81
+ */
82
+ export declare function formatWeeklyStatsTable(stats: any[]): string[][];