@tokscale/cli 1.4.3 → 2.0.1

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.
Files changed (188) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +128 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +19 -26
  6. package/dist/auth.d.ts +0 -17
  7. package/dist/auth.d.ts.map +0 -1
  8. package/dist/auth.js +0 -162
  9. package/dist/auth.js.map +0 -1
  10. package/dist/cli.d.ts +0 -9
  11. package/dist/cli.d.ts.map +0 -1
  12. package/dist/cli.js +0 -1550
  13. package/dist/cli.js.map +0 -1
  14. package/dist/credentials.d.ts +0 -50
  15. package/dist/credentials.d.ts.map +0 -1
  16. package/dist/credentials.js +0 -151
  17. package/dist/credentials.js.map +0 -1
  18. package/dist/cursor.d.ts +0 -167
  19. package/dist/cursor.d.ts.map +0 -1
  20. package/dist/cursor.js +0 -906
  21. package/dist/cursor.js.map +0 -1
  22. package/dist/date-utils.d.ts +0 -10
  23. package/dist/date-utils.d.ts.map +0 -1
  24. package/dist/date-utils.js +0 -47
  25. package/dist/date-utils.js.map +0 -1
  26. package/dist/graph-types.d.ts +0 -142
  27. package/dist/graph-types.d.ts.map +0 -1
  28. package/dist/graph-types.js +0 -6
  29. package/dist/graph-types.js.map +0 -1
  30. package/dist/native-runner.d.ts +0 -11
  31. package/dist/native-runner.d.ts.map +0 -1
  32. package/dist/native-runner.js +0 -77
  33. package/dist/native-runner.js.map +0 -1
  34. package/dist/native.d.ts +0 -106
  35. package/dist/native.d.ts.map +0 -1
  36. package/dist/native.js +0 -302
  37. package/dist/native.js.map +0 -1
  38. package/dist/sessions/types.d.ts +0 -28
  39. package/dist/sessions/types.d.ts.map +0 -1
  40. package/dist/sessions/types.js +0 -27
  41. package/dist/sessions/types.js.map +0 -1
  42. package/dist/spinner.d.ts +0 -75
  43. package/dist/spinner.d.ts.map +0 -1
  44. package/dist/spinner.js +0 -203
  45. package/dist/spinner.js.map +0 -1
  46. package/dist/submit.d.ts +0 -23
  47. package/dist/submit.d.ts.map +0 -1
  48. package/dist/submit.js +0 -294
  49. package/dist/submit.js.map +0 -1
  50. package/dist/table.d.ts +0 -42
  51. package/dist/table.d.ts.map +0 -1
  52. package/dist/table.js +0 -181
  53. package/dist/table.js.map +0 -1
  54. package/dist/tui/App.d.ts +0 -4
  55. package/dist/tui/App.d.ts.map +0 -1
  56. package/dist/tui/App.js +0 -333
  57. package/dist/tui/App.js.map +0 -1
  58. package/dist/tui/components/BarChart.d.ts +0 -17
  59. package/dist/tui/components/BarChart.d.ts.map +0 -1
  60. package/dist/tui/components/BarChart.js +0 -146
  61. package/dist/tui/components/BarChart.js.map +0 -1
  62. package/dist/tui/components/DailyView.d.ts +0 -13
  63. package/dist/tui/components/DailyView.d.ts.map +0 -1
  64. package/dist/tui/components/DailyView.js +0 -86
  65. package/dist/tui/components/DailyView.js.map +0 -1
  66. package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
  67. package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
  68. package/dist/tui/components/DateBreakdownPanel.js +0 -36
  69. package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
  70. package/dist/tui/components/Footer.d.ts +0 -28
  71. package/dist/tui/components/Footer.d.ts.map +0 -1
  72. package/dist/tui/components/Footer.js +0 -130
  73. package/dist/tui/components/Footer.js.map +0 -1
  74. package/dist/tui/components/Header.d.ts +0 -9
  75. package/dist/tui/components/Header.d.ts.map +0 -1
  76. package/dist/tui/components/Header.js +0 -20
  77. package/dist/tui/components/Header.js.map +0 -1
  78. package/dist/tui/components/Legend.d.ts +0 -7
  79. package/dist/tui/components/Legend.d.ts.map +0 -1
  80. package/dist/tui/components/Legend.js +0 -16
  81. package/dist/tui/components/Legend.js.map +0 -1
  82. package/dist/tui/components/LoadingSpinner.d.ts +0 -8
  83. package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
  84. package/dist/tui/components/LoadingSpinner.js +0 -55
  85. package/dist/tui/components/LoadingSpinner.js.map +0 -1
  86. package/dist/tui/components/ModelRow.d.ts +0 -13
  87. package/dist/tui/components/ModelRow.d.ts.map +0 -1
  88. package/dist/tui/components/ModelRow.js +0 -15
  89. package/dist/tui/components/ModelRow.js.map +0 -1
  90. package/dist/tui/components/ModelView.d.ts +0 -13
  91. package/dist/tui/components/ModelView.d.ts.map +0 -1
  92. package/dist/tui/components/ModelView.js +0 -96
  93. package/dist/tui/components/ModelView.js.map +0 -1
  94. package/dist/tui/components/OverviewView.d.ts +0 -14
  95. package/dist/tui/components/OverviewView.d.ts.map +0 -1
  96. package/dist/tui/components/OverviewView.js +0 -65
  97. package/dist/tui/components/OverviewView.js.map +0 -1
  98. package/dist/tui/components/StatsView.d.ts +0 -14
  99. package/dist/tui/components/StatsView.d.ts.map +0 -1
  100. package/dist/tui/components/StatsView.js +0 -102
  101. package/dist/tui/components/StatsView.js.map +0 -1
  102. package/dist/tui/components/TokenBreakdown.d.ts +0 -14
  103. package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
  104. package/dist/tui/components/TokenBreakdown.js +0 -10
  105. package/dist/tui/components/TokenBreakdown.js.map +0 -1
  106. package/dist/tui/components/index.d.ts +0 -16
  107. package/dist/tui/components/index.d.ts.map +0 -1
  108. package/dist/tui/components/index.js +0 -13
  109. package/dist/tui/components/index.js.map +0 -1
  110. package/dist/tui/config/settings.d.ts +0 -15
  111. package/dist/tui/config/settings.d.ts.map +0 -1
  112. package/dist/tui/config/settings.js +0 -147
  113. package/dist/tui/config/settings.js.map +0 -1
  114. package/dist/tui/config/themes.d.ts +0 -15
  115. package/dist/tui/config/themes.d.ts.map +0 -1
  116. package/dist/tui/config/themes.js +0 -82
  117. package/dist/tui/config/themes.js.map +0 -1
  118. package/dist/tui/hooks/useData.d.ts +0 -19
  119. package/dist/tui/hooks/useData.d.ts.map +0 -1
  120. package/dist/tui/hooks/useData.js +0 -468
  121. package/dist/tui/hooks/useData.js.map +0 -1
  122. package/dist/tui/index.d.ts +0 -4
  123. package/dist/tui/index.d.ts.map +0 -1
  124. package/dist/tui/index.js +0 -36
  125. package/dist/tui/index.js.map +0 -1
  126. package/dist/tui/types/index.d.ts +0 -137
  127. package/dist/tui/types/index.d.ts.map +0 -1
  128. package/dist/tui/types/index.js +0 -26
  129. package/dist/tui/types/index.js.map +0 -1
  130. package/dist/tui/utils/cleanup.d.ts +0 -22
  131. package/dist/tui/utils/cleanup.d.ts.map +0 -1
  132. package/dist/tui/utils/cleanup.js +0 -59
  133. package/dist/tui/utils/cleanup.js.map +0 -1
  134. package/dist/tui/utils/colors.d.ts +0 -19
  135. package/dist/tui/utils/colors.d.ts.map +0 -1
  136. package/dist/tui/utils/colors.js +0 -71
  137. package/dist/tui/utils/colors.js.map +0 -1
  138. package/dist/tui/utils/format.d.ts +0 -7
  139. package/dist/tui/utils/format.d.ts.map +0 -1
  140. package/dist/tui/utils/format.js +0 -45
  141. package/dist/tui/utils/format.js.map +0 -1
  142. package/dist/tui/utils/responsive.d.ts +0 -5
  143. package/dist/tui/utils/responsive.d.ts.map +0 -1
  144. package/dist/tui/utils/responsive.js +0 -5
  145. package/dist/tui/utils/responsive.js.map +0 -1
  146. package/dist/wrapped.d.ts +0 -43
  147. package/dist/wrapped.d.ts.map +0 -1
  148. package/dist/wrapped.js +0 -719
  149. package/dist/wrapped.js.map +0 -1
  150. package/src/auth.ts +0 -211
  151. package/src/cli.ts +0 -1892
  152. package/src/credentials.ts +0 -176
  153. package/src/cursor.ts +0 -1044
  154. package/src/date-utils.ts +0 -51
  155. package/src/graph-types.ts +0 -175
  156. package/src/native-runner.js +0 -4
  157. package/src/native-runner.ts +0 -91
  158. package/src/native.ts +0 -633
  159. package/src/sessions/types.ts +0 -59
  160. package/src/spinner.ts +0 -283
  161. package/src/submit.ts +0 -360
  162. package/src/table.ts +0 -233
  163. package/src/tui/App.tsx +0 -453
  164. package/src/tui/components/BarChart.tsx +0 -205
  165. package/src/tui/components/DailyView.tsx +0 -132
  166. package/src/tui/components/DateBreakdownPanel.tsx +0 -79
  167. package/src/tui/components/Footer.tsx +0 -380
  168. package/src/tui/components/Header.tsx +0 -68
  169. package/src/tui/components/Legend.tsx +0 -39
  170. package/src/tui/components/LoadingSpinner.tsx +0 -81
  171. package/src/tui/components/ModelRow.tsx +0 -47
  172. package/src/tui/components/ModelView.tsx +0 -147
  173. package/src/tui/components/OverviewView.tsx +0 -121
  174. package/src/tui/components/StatsView.tsx +0 -249
  175. package/src/tui/components/TokenBreakdown.tsx +0 -46
  176. package/src/tui/components/index.ts +0 -15
  177. package/src/tui/config/settings.ts +0 -183
  178. package/src/tui/config/themes.ts +0 -115
  179. package/src/tui/hooks/useData.ts +0 -558
  180. package/src/tui/index.tsx +0 -44
  181. package/src/tui/opentui.d.ts +0 -166
  182. package/src/tui/types/index.ts +0 -173
  183. package/src/tui/utils/cleanup.ts +0 -65
  184. package/src/tui/utils/colors.ts +0 -78
  185. package/src/tui/utils/format.ts +0 -36
  186. package/src/tui/utils/responsive.ts +0 -8
  187. package/src/types.d.ts +0 -28
  188. package/src/wrapped.ts +0 -848
package/src/cursor.ts DELETED
@@ -1,1044 +0,0 @@
1
- /**
2
- * Cursor IDE API Client
3
- * Fetches usage data from Cursor's dashboard API via CSV export
4
- *
5
- * API Endpoint: https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens
6
- * Authentication: WorkosCursorSessionToken cookie
7
- *
8
- * CSV Format:
9
- * Date,Model,Input (w/ Cache Write),Input (w/o Cache Write),Cache Read,Output Tokens,Total Tokens,Cost,Cost to you
10
- */
11
-
12
- import * as fs from "node:fs";
13
- import * as path from "node:path";
14
- import * as os from "node:os";
15
- import { createHash } from "node:crypto";
16
- import { parse as parseCsv } from "csv-parse/sync";
17
-
18
- // ============================================================================
19
- // Types
20
- // ============================================================================
21
-
22
- export interface CursorCredentials {
23
- sessionToken: string;
24
- userId?: string;
25
- createdAt: string;
26
- expiresAt?: string;
27
- label?: string;
28
- }
29
-
30
- interface CursorCredentialsStoreV1 {
31
- version: 1;
32
- activeAccountId: string;
33
- accounts: Record<string, CursorCredentials>;
34
- }
35
-
36
- export interface CursorUsageRow {
37
- date: string; // YYYY-MM-DD
38
- timestamp: number; // Unix milliseconds
39
- model: string;
40
- inputWithCacheWrite: number;
41
- inputWithoutCacheWrite: number;
42
- cacheRead: number;
43
- outputTokens: number;
44
- totalTokens: number;
45
- apiCost: number; // in USD
46
- costToYou: number; // in USD
47
- }
48
-
49
- export interface CursorUsageData {
50
- source: "cursor";
51
- model: string;
52
- providerId: string;
53
- messageCount: number;
54
- input: number;
55
- output: number;
56
- cacheRead: number;
57
- cacheWrite: number;
58
- reasoning: number;
59
- cost: number;
60
- }
61
-
62
- export interface CursorMessageWithTimestamp {
63
- source: "cursor";
64
- model: string;
65
- providerId: string;
66
- timestamp: number;
67
- input: number;
68
- output: number;
69
- cacheRead: number;
70
- cacheWrite: number;
71
- reasoning: number;
72
- cost: number;
73
- }
74
-
75
- // ============================================================================
76
- // Credential Management
77
- // ============================================================================
78
-
79
- const OLD_CONFIG_DIR = path.join(os.homedir(), ".tokscale");
80
- const CONFIG_DIR = path.join(os.homedir(), ".config", "tokscale");
81
- const OLD_CURSOR_CREDENTIALS_FILE = path.join(OLD_CONFIG_DIR, "cursor-credentials.json");
82
- const CURSOR_CREDENTIALS_FILE = path.join(CONFIG_DIR, "cursor-credentials.json");
83
-
84
- function ensureConfigDir(): void {
85
- if (!fs.existsSync(CONFIG_DIR)) {
86
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
87
- }
88
- }
89
-
90
- /**
91
- * Migrate Cursor credentials and cache from old path to new XDG path
92
- */
93
- function migrateCursorFromOldPath(): void {
94
- try {
95
- // Migrate cursor credentials
96
- if (!fs.existsSync(CURSOR_CREDENTIALS_FILE) && fs.existsSync(OLD_CURSOR_CREDENTIALS_FILE)) {
97
- ensureConfigDir();
98
- fs.copyFileSync(OLD_CURSOR_CREDENTIALS_FILE, CURSOR_CREDENTIALS_FILE);
99
- fs.chmodSync(CURSOR_CREDENTIALS_FILE, 0o600);
100
- fs.unlinkSync(OLD_CURSOR_CREDENTIALS_FILE);
101
- }
102
-
103
- // Migrate cache directory (handled after CURSOR_CACHE_DIR is defined)
104
- // Cache migration happens in migrateCursorCacheFromOldPath()
105
-
106
- // Try to remove old config directory if empty
107
- try {
108
- fs.rmdirSync(OLD_CONFIG_DIR);
109
- } catch {
110
- // Directory not empty - ignore
111
- }
112
- } catch {
113
- // Migration failed - continue with normal operation
114
- }
115
- }
116
-
117
- export function ensureCursorMigration(): void {
118
- // Best-effort: never throw
119
- try {
120
- migrateCursorFromOldPath();
121
- } catch {}
122
- try {
123
- migrateCursorCacheFromOldPath();
124
- } catch {}
125
- try {
126
- // Triggers legacy schema -> v1 store migration if needed
127
- loadCursorCredentialsStoreInternal();
128
- } catch {}
129
- }
130
-
131
- function isStoreV1(data: unknown): data is CursorCredentialsStoreV1 {
132
- if (!data || typeof data !== "object") return false;
133
- const obj = data as Record<string, unknown>;
134
- return obj.version === 1 && typeof obj.activeAccountId === "string" && typeof obj.accounts === "object" && obj.accounts !== null;
135
- }
136
-
137
- function extractUserIdFromSessionToken(sessionToken: string): string | null {
138
- if (!sessionToken) return null;
139
- const token = sessionToken.trim();
140
- if (token.includes("%3A%3A")) {
141
- const userId = token.split("%3A%3A")[0]?.trim();
142
- return userId ? userId : null;
143
- }
144
- if (token.includes("::")) {
145
- const userId = token.split("::")[0]?.trim();
146
- return userId ? userId : null;
147
- }
148
- return null;
149
- }
150
-
151
- function sanitizeAccountIdForFilename(accountId: string): string {
152
- return accountId
153
- .trim()
154
- .toLowerCase()
155
- .replace(/[^a-z0-9._-]+/g, "-")
156
- .replace(/^-+|-+$/g, "")
157
- .slice(0, 80) || "account";
158
- }
159
-
160
- function isCursorUsageCsvFilename(fileName: string): boolean {
161
- if (fileName === "usage.csv") return true;
162
- if (!fileName.startsWith("usage.")) return false;
163
- if (!fileName.endsWith(".csv")) return false;
164
- // Exclude legacy backups (were previously written as usage.backup-<ts>.csv)
165
- if (fileName.startsWith("usage.backup")) return false;
166
-
167
- const stem = fileName.slice("usage.".length, -".csv".length);
168
- if (!stem) return false;
169
- return /^[a-z0-9._-]+$/i.test(stem);
170
- }
171
-
172
- function deriveAccountId(sessionToken: string): string {
173
- const userId = extractUserIdFromSessionToken(sessionToken);
174
- if (userId) return userId;
175
- const hash = createHash("sha256").update(sessionToken).digest("hex").slice(0, 12);
176
- return `anon-${hash}`;
177
- }
178
-
179
- function atomicWriteFile(filePath: string, data: string, mode: number): void {
180
- const dir = path.dirname(filePath);
181
- const base = path.basename(filePath);
182
- const tmp = path.join(dir, `.${base}.tmp-${process.pid}`);
183
- fs.writeFileSync(tmp, data, { encoding: "utf-8", mode });
184
- try {
185
- fs.renameSync(tmp, filePath);
186
- } catch {
187
- // Best-effort for platforms where rename over an existing file can fail.
188
- try {
189
- if (fs.existsSync(filePath)) fs.rmSync(filePath);
190
- } catch {
191
- // ignore
192
- }
193
- fs.renameSync(tmp, filePath);
194
- }
195
- }
196
-
197
- function loadCursorCredentialsStoreInternal(): CursorCredentialsStoreV1 | null {
198
- migrateCursorFromOldPath();
199
- try {
200
- if (!fs.existsSync(CURSOR_CREDENTIALS_FILE)) return null;
201
- const data = fs.readFileSync(CURSOR_CREDENTIALS_FILE, "utf-8");
202
- const parsed: unknown = JSON.parse(data);
203
-
204
- if (isStoreV1(parsed)) {
205
- const store = parsed;
206
- if (!store.activeAccountId || !store.accounts[store.activeAccountId]) {
207
- const firstId = Object.keys(store.accounts)[0];
208
- if (!firstId) return null;
209
- store.activeAccountId = firstId;
210
- ensureConfigDir();
211
- atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(store, null, 2), 0o600);
212
- }
213
- return store;
214
- }
215
-
216
- // Legacy single-account schema: { sessionToken, createdAt, ... }
217
- if (parsed && typeof parsed === "object") {
218
- const obj = parsed as Record<string, unknown>;
219
- const sessionToken = typeof obj.sessionToken === "string" ? obj.sessionToken : "";
220
- if (!sessionToken) return null;
221
-
222
- const accountId = deriveAccountId(sessionToken);
223
- const migrated: CursorCredentialsStoreV1 = {
224
- version: 1,
225
- activeAccountId: accountId,
226
- accounts: {
227
- [accountId]: {
228
- sessionToken,
229
- userId: typeof obj.userId === "string" ? obj.userId : extractUserIdFromSessionToken(sessionToken) || undefined,
230
- createdAt: typeof obj.createdAt === "string" ? obj.createdAt : new Date().toISOString(),
231
- expiresAt: typeof obj.expiresAt === "string" ? obj.expiresAt : undefined,
232
- label: typeof obj.label === "string" ? obj.label : undefined,
233
- },
234
- },
235
- };
236
-
237
- ensureConfigDir();
238
- atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(migrated, null, 2), 0o600);
239
- return migrated;
240
- }
241
-
242
- return null;
243
- } catch {
244
- return null;
245
- }
246
- }
247
-
248
- function saveCursorCredentialsStoreInternal(store: CursorCredentialsStoreV1): void {
249
- ensureConfigDir();
250
- atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(store, null, 2), 0o600);
251
- }
252
-
253
- function resolveAccountId(store: CursorCredentialsStoreV1, nameOrId: string): string | null {
254
- const needle = nameOrId.trim();
255
- if (!needle) return null;
256
- if (store.accounts[needle]) return needle;
257
-
258
- const needleLower = needle.toLowerCase();
259
- for (const [id, acct] of Object.entries(store.accounts)) {
260
- if (acct.label && acct.label.toLowerCase() === needleLower) return id;
261
- }
262
- return null;
263
- }
264
-
265
- export function listCursorAccounts(): Array<{ id: string; label?: string; userId?: string; createdAt: string; isActive: boolean }> {
266
- const store = loadCursorCredentialsStoreInternal();
267
- if (!store) return [];
268
-
269
- const accounts = Object.entries(store.accounts).map(([id, acct]) => ({
270
- id,
271
- label: acct.label,
272
- userId: acct.userId,
273
- createdAt: acct.createdAt,
274
- isActive: id === store.activeAccountId,
275
- }));
276
-
277
- accounts.sort((a, b) => {
278
- if (a.isActive !== b.isActive) return a.isActive ? -1 : 1;
279
- const la = (a.label || a.id).toLowerCase();
280
- const lb = (b.label || b.id).toLowerCase();
281
- return la.localeCompare(lb);
282
- });
283
-
284
- return accounts;
285
- }
286
-
287
- export function setActiveCursorAccount(nameOrId: string): { ok: boolean; error?: string } {
288
- const store = loadCursorCredentialsStoreInternal();
289
- if (!store) return { ok: false, error: "Not authenticated" };
290
- const resolved = resolveAccountId(store, nameOrId);
291
- if (!resolved) return { ok: false, error: `Account not found: ${nameOrId}` };
292
- const prev = store.activeAccountId;
293
- store.activeAccountId = resolved;
294
- saveCursorCredentialsStoreInternal(store);
295
-
296
- // Best-effort cache reconcile (avoid double-counting)
297
- try {
298
- migrateCursorCacheFromOldPath();
299
- ensureCacheDir();
300
-
301
- const archiveDir = path.join(CURSOR_CACHE_DIR, "archive");
302
- const ensureArchiveDir = (): void => {
303
- ensureCacheDir();
304
- if (!fs.existsSync(archiveDir)) {
305
- fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
306
- }
307
- };
308
- const archiveFile = (filePath: string, label: string): void => {
309
- ensureArchiveDir();
310
- const safeLabel = sanitizeAccountIdForFilename(label);
311
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
312
- const dest = path.join(archiveDir, `${safeLabel}-${ts}.csv`);
313
- fs.renameSync(filePath, dest);
314
- };
315
-
316
- // Move current active cache to previous account file (preserve any existing file by archiving).
317
- if (prev && fs.existsSync(CURSOR_CACHE_FILE)) {
318
- const prevFile = getCursorCacheFilePathForAccount(prev, false);
319
- if (fs.existsSync(prevFile)) {
320
- try {
321
- archiveFile(prevFile, `usage.${prev}.previous`);
322
- } catch {
323
- // ignore
324
- }
325
- }
326
- try {
327
- fs.renameSync(CURSOR_CACHE_FILE, prevFile);
328
- } catch {
329
- // ignore
330
- }
331
- }
332
-
333
- // Promote next account cache file into usage.csv.
334
- const nextFile = getCursorCacheFilePathForAccount(resolved, false);
335
- if (fs.existsSync(nextFile)) {
336
- if (fs.existsSync(CURSOR_CACHE_FILE)) {
337
- try {
338
- archiveFile(CURSOR_CACHE_FILE, `usage.active.pre-switch`);
339
- } catch {
340
- // ignore
341
- }
342
- }
343
- try {
344
- fs.renameSync(nextFile, CURSOR_CACHE_FILE);
345
- } catch {
346
- // ignore
347
- }
348
- }
349
-
350
- // If a per-account cache exists, it was promoted into usage.csv above.
351
- } catch {
352
- // ignore cache reconcile errors
353
- }
354
-
355
- return { ok: true };
356
- }
357
-
358
- export function saveCursorCredentials(credentials: CursorCredentials, options?: { label?: string; setActive?: boolean }): { accountId: string } {
359
- const sessionToken = credentials.sessionToken;
360
- const accountId = deriveAccountId(sessionToken);
361
- const store = loadCursorCredentialsStoreInternal() || {
362
- version: 1 as const,
363
- activeAccountId: accountId,
364
- accounts: {},
365
- };
366
-
367
- if (options?.label) {
368
- const needle = options.label.trim().toLowerCase();
369
- if (needle) {
370
- for (const [id, acct] of Object.entries(store.accounts)) {
371
- if (id === accountId) continue;
372
- if (acct.label && acct.label.trim().toLowerCase() === needle) {
373
- throw new Error(`Cursor account label already exists: ${options.label}`);
374
- }
375
- }
376
- }
377
- }
378
-
379
- const next: CursorCredentials = {
380
- ...credentials,
381
- userId: credentials.userId || extractUserIdFromSessionToken(sessionToken) || undefined,
382
- label: options?.label ?? credentials.label,
383
- };
384
-
385
- store.accounts[accountId] = next;
386
- if (options?.setActive !== false) {
387
- store.activeAccountId = accountId;
388
- }
389
- saveCursorCredentialsStoreInternal(store);
390
- return { accountId };
391
- }
392
-
393
- export function loadCursorCredentials(nameOrId?: string): CursorCredentials | null {
394
- const store = loadCursorCredentialsStoreInternal();
395
- if (!store) return null;
396
-
397
- if (nameOrId) {
398
- const resolved = resolveAccountId(store, nameOrId);
399
- return resolved ? store.accounts[resolved] : null;
400
- }
401
-
402
- return store.accounts[store.activeAccountId] || null;
403
- }
404
-
405
- export function loadCursorCredentialsStore(): CursorCredentialsStoreV1 | null {
406
- return loadCursorCredentialsStoreInternal();
407
- }
408
-
409
- // NOTE: implementation moved below to support cache archiving by default.
410
-
411
- export function removeCursorAccount(
412
- nameOrId: string,
413
- options?: { purgeCache?: boolean }
414
- ): { removed: boolean; error?: string } {
415
- const store = loadCursorCredentialsStoreInternal();
416
- if (!store) return { removed: false, error: "Not authenticated" };
417
-
418
- const resolved = resolveAccountId(store, nameOrId);
419
- if (!resolved) return { removed: false, error: `Account not found: ${nameOrId}` };
420
-
421
- const wasActive = resolved === store.activeAccountId;
422
-
423
- // Cache behavior:
424
- // - Default: keep history but remove from aggregation by archiving out of cursor-cache/.
425
- // - purgeCache: delete cache files.
426
- const CURSOR_CACHE_ARCHIVE_DIR = path.join(CURSOR_CACHE_DIR, "archive");
427
- const ensureCacheArchiveDir = (): void => {
428
- ensureCacheDir();
429
- if (!fs.existsSync(CURSOR_CACHE_ARCHIVE_DIR)) {
430
- fs.mkdirSync(CURSOR_CACHE_ARCHIVE_DIR, { recursive: true, mode: 0o700 });
431
- }
432
- };
433
- const archiveFile = (filePath: string, label: string): void => {
434
- ensureCacheArchiveDir();
435
- const safeLabel = sanitizeAccountIdForFilename(label);
436
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
437
- const dest = path.join(CURSOR_CACHE_ARCHIVE_DIR, `${safeLabel}-${ts}.csv`);
438
- fs.renameSync(filePath, dest);
439
- };
440
-
441
- try {
442
- migrateCursorCacheFromOldPath();
443
- if (fs.existsSync(CURSOR_CACHE_DIR)) {
444
- const perAccount = getCursorCacheFilePathForAccount(resolved, false);
445
- if (fs.existsSync(perAccount)) {
446
- if (options?.purgeCache) {
447
- fs.rmSync(perAccount);
448
- } else {
449
- archiveFile(perAccount, `usage.${resolved}`);
450
- }
451
- }
452
- if (wasActive && fs.existsSync(CURSOR_CACHE_FILE)) {
453
- if (options?.purgeCache) {
454
- fs.rmSync(CURSOR_CACHE_FILE);
455
- } else {
456
- archiveFile(CURSOR_CACHE_FILE, `usage.active.${resolved}`);
457
- }
458
- }
459
- }
460
- } catch {
461
- // ignore
462
- }
463
-
464
- delete store.accounts[resolved];
465
-
466
- const remaining = Object.keys(store.accounts);
467
- if (remaining.length === 0) {
468
- try {
469
- fs.unlinkSync(CURSOR_CREDENTIALS_FILE);
470
- } catch {}
471
- return { removed: true };
472
- }
473
-
474
- if (wasActive) {
475
- store.activeAccountId = remaining[0];
476
- }
477
-
478
- saveCursorCredentialsStoreInternal(store);
479
-
480
- if (wasActive) {
481
- // Best-effort: reconcile usage.csv for the new active account.
482
- try {
483
- migrateCursorCacheFromOldPath();
484
- ensureCacheDir();
485
- const nextId = store.activeAccountId;
486
- const nextFile = getCursorCacheFilePathForAccount(nextId, false);
487
- if (fs.existsSync(nextFile)) {
488
- if (fs.existsSync(CURSOR_CACHE_FILE)) {
489
- try {
490
- fs.rmSync(CURSOR_CACHE_FILE);
491
- } catch {}
492
- }
493
- fs.renameSync(nextFile, CURSOR_CACHE_FILE);
494
- }
495
- // If nextFile existed, it was promoted into usage.csv above.
496
- } catch {
497
- // ignore
498
- }
499
- }
500
-
501
- return { removed: true };
502
- }
503
-
504
- export function clearCursorCredentials(): boolean {
505
- // Backward compatible: clears ALL accounts
506
- try {
507
- if (fs.existsSync(CURSOR_CREDENTIALS_FILE)) {
508
- fs.unlinkSync(CURSOR_CREDENTIALS_FILE);
509
- return true;
510
- }
511
- return false;
512
- } catch {
513
- return false;
514
- }
515
- }
516
-
517
- export function clearCursorCredentialsAndCache(options?: { purgeCache?: boolean }): boolean {
518
- const cleared = clearCursorCredentials();
519
- if (!cleared) return false;
520
-
521
- try {
522
- migrateCursorCacheFromOldPath();
523
- if (!fs.existsSync(CURSOR_CACHE_DIR)) return true;
524
-
525
- const archiveDir = path.join(CURSOR_CACHE_DIR, "archive");
526
- const ensureArchiveDir = (): void => {
527
- ensureCacheDir();
528
- if (!fs.existsSync(archiveDir)) {
529
- fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
530
- }
531
- };
532
- const archiveFile = (filePath: string, label: string): void => {
533
- ensureArchiveDir();
534
- const safeLabel = sanitizeAccountIdForFilename(label);
535
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
536
- const dest = path.join(archiveDir, `${safeLabel}-${ts}.csv`);
537
- fs.renameSync(filePath, dest);
538
- };
539
-
540
- for (const f of fs.readdirSync(CURSOR_CACHE_DIR)) {
541
- if (!f.startsWith("usage") || !f.endsWith(".csv")) continue;
542
- const filePath = path.join(CURSOR_CACHE_DIR, f);
543
- try {
544
- if (options?.purgeCache) {
545
- fs.rmSync(filePath);
546
- } else {
547
- archiveFile(filePath, `usage.all.${f}`);
548
- }
549
- } catch {}
550
- }
551
- } catch {
552
- // ignore
553
- }
554
-
555
- return true;
556
- }
557
-
558
- export function isCursorLoggedIn(): boolean {
559
- const store = loadCursorCredentialsStoreInternal();
560
- return !!store && Object.keys(store.accounts).length > 0;
561
- }
562
-
563
- // ============================================================================
564
- // API Client
565
- // ============================================================================
566
-
567
- const CURSOR_API_BASE = "https://cursor.com";
568
- const USAGE_CSV_ENDPOINT = `${CURSOR_API_BASE}/api/dashboard/export-usage-events-csv?strategy=tokens`;
569
- const USAGE_SUMMARY_ENDPOINT = `${CURSOR_API_BASE}/api/usage-summary`;
570
-
571
- /**
572
- * Build HTTP headers for Cursor API requests
573
- */
574
- function buildCursorHeaders(sessionToken: string): Record<string, string> {
575
- return {
576
- Accept: "*/*",
577
- "Accept-Language": "en-US,en;q=0.9",
578
- Cookie: `WorkosCursorSessionToken=${sessionToken}`,
579
- Referer: "https://www.cursor.com/settings",
580
- "User-Agent":
581
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
582
- };
583
- }
584
-
585
- /**
586
- * Validate Cursor session token by hitting the usage-summary endpoint
587
- */
588
- export async function validateCursorSession(
589
- sessionToken: string
590
- ): Promise<{ valid: boolean; membershipType?: string; error?: string }> {
591
- try {
592
- const response = await fetch(USAGE_SUMMARY_ENDPOINT, {
593
- method: "GET",
594
- headers: buildCursorHeaders(sessionToken),
595
- });
596
-
597
- if (response.status === 401 || response.status === 403) {
598
- return { valid: false, error: "Session token expired or invalid" };
599
- }
600
-
601
- if (!response.ok) {
602
- return { valid: false, error: `API returned status ${response.status}` };
603
- }
604
-
605
- const data = await response.json();
606
-
607
- // Check for required fields that indicate valid auth
608
- if (data.billingCycleStart && data.billingCycleEnd) {
609
- return { valid: true, membershipType: data.membershipType };
610
- }
611
-
612
- return { valid: false, error: "Invalid response format" };
613
- } catch (error) {
614
- return { valid: false, error: (error as Error).message };
615
- }
616
- }
617
-
618
- /**
619
- * Fetch usage CSV from Cursor API
620
- */
621
- export async function fetchCursorUsageCsv(sessionToken: string): Promise<string> {
622
- const response = await fetch(USAGE_CSV_ENDPOINT, {
623
- method: "GET",
624
- headers: buildCursorHeaders(sessionToken),
625
- });
626
-
627
- if (response.status === 401 || response.status === 403) {
628
- throw new Error("Cursor session expired. Please run 'tokscale cursor login' to re-authenticate.");
629
- }
630
-
631
- if (!response.ok) {
632
- throw new Error(`Cursor API returned status ${response.status}`);
633
- }
634
-
635
- const text = await response.text();
636
-
637
- // Validate it's actually CSV (handle both old and new formats)
638
- // Old: "Date,Model,..."
639
- // New: "Date,Kind,Model,..."
640
- if (!text.startsWith("Date,")) {
641
- throw new Error("Invalid response from Cursor API - expected CSV format");
642
- }
643
-
644
- return text;
645
- }
646
-
647
- // ============================================================================
648
- // CSV Parsing
649
- // ============================================================================
650
-
651
- /**
652
- * Parse cost string (e.g., "$0.50" or "0.50") to number
653
- */
654
- function parseCost(costStr: string): number {
655
- if (!costStr) return 0;
656
- const cleaned = costStr.replace(/[$,]/g, "").trim();
657
- const value = parseFloat(cleaned);
658
- return isNaN(value) ? 0 : value;
659
- }
660
-
661
- /**
662
- * Infer provider from model name
663
- */
664
- function inferProvider(model: string): string {
665
- const lowerModel = model.toLowerCase();
666
-
667
- if (lowerModel.includes("claude") || lowerModel.includes("sonnet") || lowerModel.includes("opus") || lowerModel.includes("haiku")) {
668
- return "anthropic";
669
- }
670
- if (lowerModel.includes("gpt") || lowerModel.includes("o1") || lowerModel.includes("o3")) {
671
- return "openai";
672
- }
673
- if (lowerModel.includes("gemini")) {
674
- return "google";
675
- }
676
- if (lowerModel.includes("deepseek")) {
677
- return "deepseek";
678
- }
679
- if (lowerModel.includes("llama") || lowerModel.includes("mixtral")) {
680
- return "meta";
681
- }
682
-
683
- return "cursor"; // Default provider
684
- }
685
-
686
- /**
687
- * Parse Cursor usage CSV into structured rows
688
- */
689
- export function parseCursorCsv(csvText: string): CursorUsageRow[] {
690
- try {
691
- const records: Array<Record<string, string>> = parseCsv(csvText, {
692
- columns: true,
693
- skip_empty_lines: true,
694
- trim: true,
695
- relax_column_count: true,
696
- });
697
-
698
- return records
699
- .filter((record) => record["Date"] && record["Model"])
700
- .map((record) => {
701
- const dateStr = record["Date"] || "";
702
- const date = new Date(dateStr);
703
- const isValidDate = !isNaN(date.getTime());
704
- const dateOnly = isValidDate
705
- ? date.toISOString().slice(0, 10)
706
- : dateStr.length >= 10
707
- ? dateStr.slice(0, 10)
708
- : dateStr;
709
-
710
- return {
711
- date: dateOnly,
712
- timestamp: isValidDate ? date.getTime() : 0,
713
- model: (record["Model"] || "").trim(),
714
- inputWithCacheWrite: parseInt(record["Input (w/ Cache Write)"] || "0", 10),
715
- inputWithoutCacheWrite: parseInt(record["Input (w/o Cache Write)"] || "0", 10),
716
- cacheRead: parseInt(record["Cache Read"] || "0", 10),
717
- outputTokens: parseInt(record["Output Tokens"] || "0", 10),
718
- totalTokens: parseInt(record["Total Tokens"] || "0", 10),
719
- apiCost: parseCost(record["Cost"] || record["API Cost"] || "0"),
720
- costToYou: parseCost(record["Cost to you"] || "0"),
721
- };
722
- });
723
- } catch (error) {
724
- throw new Error(`Failed to parse Cursor CSV: ${(error as Error).message}`);
725
- }
726
- }
727
-
728
- // ============================================================================
729
- // Data Aggregation (for table display)
730
- // ============================================================================
731
-
732
- /**
733
- * Aggregate Cursor usage by model
734
- */
735
- export function aggregateCursorByModel(rows: CursorUsageRow[]): CursorUsageData[] {
736
- const modelMap = new Map<string, CursorUsageData>();
737
-
738
- for (const row of rows) {
739
- const key = row.model;
740
- const existing = modelMap.get(key);
741
-
742
- // Cache write = inputWithCacheWrite - inputWithoutCacheWrite (tokens written to cache)
743
- const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
744
- // Input tokens (without cache) = inputWithoutCacheWrite
745
- const input = row.inputWithoutCacheWrite;
746
-
747
- if (existing) {
748
- existing.messageCount += 1;
749
- existing.input += input;
750
- existing.output += row.outputTokens;
751
- existing.cacheRead += row.cacheRead;
752
- existing.cacheWrite += cacheWrite;
753
- existing.cost += row.costToYou || row.apiCost;
754
- } else {
755
- modelMap.set(key, {
756
- source: "cursor",
757
- model: row.model,
758
- providerId: inferProvider(row.model),
759
- messageCount: 1,
760
- input,
761
- output: row.outputTokens,
762
- cacheRead: row.cacheRead,
763
- cacheWrite,
764
- reasoning: 0, // Cursor doesn't expose reasoning tokens
765
- cost: row.costToYou || row.apiCost,
766
- });
767
- }
768
- }
769
-
770
- return Array.from(modelMap.values()).sort((a, b) => b.cost - a.cost);
771
- }
772
-
773
- // ============================================================================
774
- // Data Conversion (for graph/native module)
775
- // ============================================================================
776
-
777
- /**
778
- * Convert Cursor CSV rows to timestamped messages for graph generation
779
- */
780
- export function cursorRowsToMessages(rows: CursorUsageRow[]): CursorMessageWithTimestamp[] {
781
- return rows.map((row) => {
782
- const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
783
- const input = row.inputWithoutCacheWrite;
784
-
785
- return {
786
- source: "cursor" as const,
787
- model: row.model,
788
- providerId: inferProvider(row.model),
789
- timestamp: row.timestamp,
790
- input,
791
- output: row.outputTokens,
792
- cacheRead: row.cacheRead,
793
- cacheWrite,
794
- reasoning: 0,
795
- cost: row.costToYou || row.apiCost,
796
- };
797
- });
798
- }
799
-
800
- // ============================================================================
801
- // High-Level API
802
- // ============================================================================
803
-
804
- /**
805
- * Fetch and parse Cursor usage data
806
- * Requires valid credentials to be stored
807
- */
808
- export async function readCursorUsage(nameOrId?: string): Promise<{
809
- rows: CursorUsageRow[];
810
- byModel: CursorUsageData[];
811
- messages: CursorMessageWithTimestamp[];
812
- }> {
813
- const credentials = loadCursorCredentials(nameOrId);
814
- if (!credentials) {
815
- throw new Error("Cursor not authenticated. Run 'tokscale cursor login' first.");
816
- }
817
-
818
- const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
819
- const rows = parseCursorCsv(csvText);
820
- const byModel = aggregateCursorByModel(rows);
821
- const messages = cursorRowsToMessages(rows);
822
-
823
- return { rows, byModel, messages };
824
- }
825
-
826
- /**
827
- * Get Cursor credentials file path (for debugging)
828
- */
829
- export function getCursorCredentialsPath(): string {
830
- return CURSOR_CREDENTIALS_FILE;
831
- }
832
-
833
- // ============================================================================
834
- // Cache Management (for Rust integration)
835
- // ============================================================================
836
-
837
- const OLD_CURSOR_CACHE_DIR = path.join(os.homedir(), ".tokscale", "cursor-cache");
838
- const CURSOR_CACHE_DIR = path.join(CONFIG_DIR, "cursor-cache");
839
- const CURSOR_CACHE_FILE = path.join(CURSOR_CACHE_DIR, "usage.csv");
840
-
841
- function getCursorCacheFilePathForAccount(accountId: string, isActive: boolean): string {
842
- if (isActive) return CURSOR_CACHE_FILE;
843
- const safe = sanitizeAccountIdForFilename(accountId);
844
- return path.join(CURSOR_CACHE_DIR, `usage.${safe}.csv`);
845
- }
846
-
847
- function ensureCacheDir(): void {
848
- if (!fs.existsSync(CURSOR_CACHE_DIR)) {
849
- fs.mkdirSync(CURSOR_CACHE_DIR, { recursive: true, mode: 0o700 });
850
- }
851
- }
852
-
853
- /**
854
- * Migrate cursor cache from old path to new XDG path
855
- */
856
- function migrateCursorCacheFromOldPath(): void {
857
- try {
858
- if (!fs.existsSync(CURSOR_CACHE_DIR) && fs.existsSync(OLD_CURSOR_CACHE_DIR)) {
859
- ensureCacheDir();
860
- fs.cpSync(OLD_CURSOR_CACHE_DIR, CURSOR_CACHE_DIR, { recursive: true });
861
- fs.rmSync(OLD_CURSOR_CACHE_DIR, { recursive: true });
862
- }
863
-
864
- // Try to remove old config directory if empty
865
- try {
866
- fs.rmdirSync(OLD_CONFIG_DIR);
867
- } catch {
868
- // Directory not empty - ignore
869
- }
870
- } catch {
871
- // Migration failed - continue with normal operation
872
- }
873
- }
874
-
875
- /**
876
- * Sync Cursor usage data from API to local cache
877
- * This downloads the CSV and saves it for the Rust module to parse
878
- */
879
- export async function syncCursorCache(): Promise<{ synced: boolean; rows: number; error?: string }> {
880
- migrateCursorCacheFromOldPath();
881
- const store = loadCursorCredentialsStoreInternal();
882
- if (!store) return { synced: false, rows: 0, error: "Not authenticated" };
883
- const accounts = Object.entries(store.accounts);
884
- if (accounts.length === 0) return { synced: false, rows: 0, error: "Not authenticated" };
885
-
886
- try {
887
- ensureCacheDir();
888
-
889
- // Ensure we don't double-count active account (usage.csv + usage.<active>.csv)
890
- const activeId = store.activeAccountId;
891
- if (activeId) {
892
- const dup = getCursorCacheFilePathForAccount(activeId, false);
893
- if (fs.existsSync(dup)) {
894
- try { fs.rmSync(dup); } catch {}
895
- }
896
- }
897
-
898
- let totalRows = 0;
899
- let successCount = 0;
900
- const errors: string[] = [];
901
-
902
- for (const [accountId, credentials] of accounts) {
903
- const isActive = accountId === store.activeAccountId;
904
- try {
905
- const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
906
- const filePath = getCursorCacheFilePathForAccount(accountId, isActive);
907
- atomicWriteFile(filePath, csvText, 0o600);
908
- totalRows += parseCursorCsv(csvText).length;
909
- successCount += 1;
910
- } catch (e) {
911
- errors.push(`${accountId}: ${(e as Error).message}`);
912
- }
913
- }
914
-
915
- if (successCount === 0) {
916
- return { synced: false, rows: 0, error: errors[0] || "Cursor sync failed" };
917
- }
918
-
919
- return {
920
- synced: true,
921
- rows: totalRows,
922
- error: errors.length > 0 ? `Some accounts failed to sync (${errors.length}/${accounts.length})` : undefined,
923
- };
924
- } catch (error) {
925
- return { synced: false, rows: 0, error: (error as Error).message };
926
- }
927
- }
928
-
929
- /**
930
- * Get the cache file path
931
- */
932
- export function getCursorCachePath(): string {
933
- // Ensure legacy cache is migrated before reporting paths
934
- migrateCursorCacheFromOldPath();
935
- return CURSOR_CACHE_FILE;
936
- }
937
-
938
- /**
939
- * Check if cache exists and when it was last updated
940
- */
941
- export function getCursorCacheStatus(): { exists: boolean; lastModified?: Date; path: string } {
942
- migrateCursorCacheFromOldPath();
943
- const exists = fs.existsSync(CURSOR_CACHE_FILE);
944
- let lastModified: Date | undefined;
945
-
946
- if (exists) {
947
- try {
948
- const stats = fs.statSync(CURSOR_CACHE_FILE);
949
- lastModified = stats.mtime;
950
- } catch {
951
- // Ignore stat errors
952
- }
953
- }
954
-
955
- return { exists, lastModified, path: CURSOR_CACHE_FILE };
956
- }
957
-
958
- export function hasCursorUsageCache(): boolean {
959
- migrateCursorCacheFromOldPath();
960
- try {
961
- if (!fs.existsSync(CURSOR_CACHE_DIR)) return false;
962
- const files = fs.readdirSync(CURSOR_CACHE_DIR);
963
- return files.some((f) => isCursorUsageCsvFilename(f));
964
- } catch {
965
- return false;
966
- }
967
- }
968
-
969
- export interface CursorUnifiedMessage {
970
- source: "cursor";
971
- modelId: string;
972
- providerId: string;
973
- sessionId: string;
974
- timestamp: number;
975
- date: string;
976
- tokens: {
977
- input: number;
978
- output: number;
979
- cacheRead: number;
980
- cacheWrite: number;
981
- reasoning: number;
982
- };
983
- cost: number;
984
- }
985
-
986
- export function readCursorMessagesFromCache(): CursorUnifiedMessage[] {
987
- migrateCursorCacheFromOldPath();
988
- if (!fs.existsSync(CURSOR_CACHE_DIR)) {
989
- return [];
990
- }
991
-
992
- let files: string[];
993
- try {
994
- files = fs
995
- .readdirSync(CURSOR_CACHE_DIR)
996
- .filter((f) => isCursorUsageCsvFilename(f));
997
- } catch {
998
- return [];
999
- }
1000
-
1001
- const store = loadCursorCredentialsStoreInternal();
1002
- const activeId = store?.activeAccountId;
1003
-
1004
- const all: CursorUnifiedMessage[] = [];
1005
- for (const file of files) {
1006
- const filePath = path.join(CURSOR_CACHE_DIR, file);
1007
- let accountId = "unknown";
1008
- if (file === "usage.csv") {
1009
- accountId = activeId || "active";
1010
- } else if (file.startsWith("usage.") && file.endsWith(".csv")) {
1011
- accountId = file.slice("usage.".length, -".csv".length);
1012
- }
1013
-
1014
- try {
1015
- const csvText = fs.readFileSync(filePath, "utf-8");
1016
- const rows = parseCursorCsv(csvText);
1017
-
1018
- for (const row of rows) {
1019
- const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
1020
- const input = row.inputWithoutCacheWrite;
1021
- all.push({
1022
- source: "cursor" as const,
1023
- modelId: row.model,
1024
- providerId: inferProvider(row.model),
1025
- sessionId: `cursor-${accountId}-${row.date}-${row.model}`,
1026
- timestamp: row.timestamp,
1027
- date: row.date,
1028
- tokens: {
1029
- input,
1030
- output: row.outputTokens,
1031
- cacheRead: row.cacheRead,
1032
- cacheWrite,
1033
- reasoning: 0,
1034
- },
1035
- cost: row.costToYou || row.apiCost,
1036
- });
1037
- }
1038
- } catch {
1039
- // ignore file
1040
- }
1041
- }
1042
-
1043
- return all;
1044
- }