@tokscale/cli 1.0.24 → 1.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.
package/src/cursor.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import * as os from "node:os";
15
+ import { createHash } from "node:crypto";
15
16
  import { parse as parseCsv } from "csv-parse/sync";
16
17
 
17
18
  // ============================================================================
@@ -23,6 +24,13 @@ export interface CursorCredentials {
23
24
  userId?: string;
24
25
  createdAt: string;
25
26
  expiresAt?: string;
27
+ label?: string;
28
+ }
29
+
30
+ interface CursorCredentialsStoreV1 {
31
+ version: 1;
32
+ activeAccountId: string;
33
+ accounts: Record<string, CursorCredentials>;
26
34
  }
27
35
 
28
36
  export interface CursorUsageRow {
@@ -106,34 +114,395 @@ function migrateCursorFromOldPath(): void {
106
114
  }
107
115
  }
108
116
 
109
- export function saveCursorCredentials(credentials: CursorCredentials): void {
110
- ensureConfigDir();
111
- fs.writeFileSync(CURSOR_CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
112
- encoding: "utf-8",
113
- mode: 0o600,
114
- });
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 {}
115
129
  }
116
130
 
117
- export function loadCursorCredentials(): CursorCredentials | null {
118
- migrateCursorFromOldPath();
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 });
119
184
  try {
120
- if (!fs.existsSync(CURSOR_CREDENTIALS_FILE)) {
121
- return null;
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
122
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;
123
201
  const data = fs.readFileSync(CURSOR_CREDENTIALS_FILE, "utf-8");
124
- const parsed = JSON.parse(data);
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
+ };
125
236
 
126
- if (!parsed.sessionToken) {
127
- return null;
237
+ ensureConfigDir();
238
+ atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(migrated, null, 2), 0o600);
239
+ return migrated;
128
240
  }
129
241
 
130
- return parsed as CursorCredentials;
242
+ return null;
131
243
  } catch {
132
244
  return null;
133
245
  }
134
246
  }
135
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
+
136
504
  export function clearCursorCredentials(): boolean {
505
+ // Backward compatible: clears ALL accounts
137
506
  try {
138
507
  if (fs.existsSync(CURSOR_CREDENTIALS_FILE)) {
139
508
  fs.unlinkSync(CURSOR_CREDENTIALS_FILE);
@@ -145,8 +514,50 @@ export function clearCursorCredentials(): boolean {
145
514
  }
146
515
  }
147
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
+
148
558
  export function isCursorLoggedIn(): boolean {
149
- return loadCursorCredentials() !== null;
559
+ const store = loadCursorCredentialsStoreInternal();
560
+ return !!store && Object.keys(store.accounts).length > 0;
150
561
  }
151
562
 
152
563
  // ============================================================================
@@ -394,12 +805,12 @@ export function cursorRowsToMessages(rows: CursorUsageRow[]): CursorMessageWithT
394
805
  * Fetch and parse Cursor usage data
395
806
  * Requires valid credentials to be stored
396
807
  */
397
- export async function readCursorUsage(): Promise<{
808
+ export async function readCursorUsage(nameOrId?: string): Promise<{
398
809
  rows: CursorUsageRow[];
399
810
  byModel: CursorUsageData[];
400
811
  messages: CursorMessageWithTimestamp[];
401
812
  }> {
402
- const credentials = loadCursorCredentials();
813
+ const credentials = loadCursorCredentials(nameOrId);
403
814
  if (!credentials) {
404
815
  throw new Error("Cursor not authenticated. Run 'tokscale cursor login' first.");
405
816
  }
@@ -427,6 +838,12 @@ const OLD_CURSOR_CACHE_DIR = path.join(os.homedir(), ".tokscale", "cursor-cache"
427
838
  const CURSOR_CACHE_DIR = path.join(CONFIG_DIR, "cursor-cache");
428
839
  const CURSOR_CACHE_FILE = path.join(CURSOR_CACHE_DIR, "usage.csv");
429
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
+
430
847
  function ensureCacheDir(): void {
431
848
  if (!fs.existsSync(CURSOR_CACHE_DIR)) {
432
849
  fs.mkdirSync(CURSOR_CACHE_DIR, { recursive: true, mode: 0o700 });
@@ -461,19 +878,49 @@ function migrateCursorCacheFromOldPath(): void {
461
878
  */
462
879
  export async function syncCursorCache(): Promise<{ synced: boolean; rows: number; error?: string }> {
463
880
  migrateCursorCacheFromOldPath();
464
- const credentials = loadCursorCredentials();
465
- if (!credentials) {
466
- return { synced: false, rows: 0, error: "Not authenticated" };
467
- }
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" };
468
885
 
469
886
  try {
470
- const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
471
887
  ensureCacheDir();
472
- fs.writeFileSync(CURSOR_CACHE_FILE, csvText, { encoding: "utf-8", mode: 0o600 });
473
888
 
474
- // Count rows for feedback
475
- const rows = parseCursorCsv(csvText);
476
- return { synced: true, rows: rows.length };
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
+ };
477
924
  } catch (error) {
478
925
  return { synced: false, rows: 0, error: (error as Error).message };
479
926
  }
@@ -483,6 +930,8 @@ export async function syncCursorCache(): Promise<{ synced: boolean; rows: number
483
930
  * Get the cache file path
484
931
  */
485
932
  export function getCursorCachePath(): string {
933
+ // Ensure legacy cache is migrated before reporting paths
934
+ migrateCursorCacheFromOldPath();
486
935
  return CURSOR_CACHE_FILE;
487
936
  }
488
937
 
@@ -490,6 +939,7 @@ export function getCursorCachePath(): string {
490
939
  * Check if cache exists and when it was last updated
491
940
  */
492
941
  export function getCursorCacheStatus(): { exists: boolean; lastModified?: Date; path: string } {
942
+ migrateCursorCacheFromOldPath();
493
943
  const exists = fs.existsSync(CURSOR_CACHE_FILE);
494
944
  let lastModified: Date | undefined;
495
945
 
@@ -505,6 +955,17 @@ export function getCursorCacheStatus(): { exists: boolean; lastModified?: Date;
505
955
  return { exists, lastModified, path: CURSOR_CACHE_FILE };
506
956
  }
507
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
+
508
969
  export interface CursorUnifiedMessage {
509
970
  source: "cursor";
510
971
  modelId: string;
@@ -523,36 +984,61 @@ export interface CursorUnifiedMessage {
523
984
  }
524
985
 
525
986
  export function readCursorMessagesFromCache(): CursorUnifiedMessage[] {
526
- if (!fs.existsSync(CURSOR_CACHE_FILE)) {
987
+ migrateCursorCacheFromOldPath();
988
+ if (!fs.existsSync(CURSOR_CACHE_DIR)) {
527
989
  return [];
528
990
  }
529
991
 
992
+ let files: string[];
530
993
  try {
531
- const csvText = fs.readFileSync(CURSOR_CACHE_FILE, "utf-8");
532
- const rows = parseCursorCsv(csvText);
533
-
534
- return rows.map((row) => {
535
- const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
536
- const input = row.inputWithoutCacheWrite;
537
-
538
- return {
539
- source: "cursor" as const,
540
- modelId: row.model,
541
- providerId: inferProvider(row.model),
542
- sessionId: `cursor-${row.date}-${row.model}`,
543
- timestamp: row.timestamp,
544
- date: row.date,
545
- tokens: {
546
- input,
547
- output: row.outputTokens,
548
- cacheRead: row.cacheRead,
549
- cacheWrite,
550
- reasoning: 0,
551
- },
552
- cost: row.costToYou || row.apiCost,
553
- };
554
- });
994
+ files = fs
995
+ .readdirSync(CURSOR_CACHE_DIR)
996
+ .filter((f) => isCursorUsageCsvFilename(f));
555
997
  } catch {
556
998
  return [];
557
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;
558
1044
  }