@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokscale/cli",
3
- "version": "1.0.24",
3
+ "version": "1.1.0",
4
4
  "description": "A high-performance CLI tool and visualization dashboard for tracking AI coding assistant token usage and costs across multiple platforms.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,23 +29,20 @@
29
29
  "dependencies": {
30
30
  "@napi-rs/canvas": "^0.1.68",
31
31
  "@opentui/core": "0.1.60",
32
- "@opentui/react": "^0.1.60",
33
32
  "@opentui/solid": "^0.1.60",
34
33
  "@resvg/resvg-js": "^2.6.2",
35
- "@tokscale/core": "1.0.24",
34
+ "@tokscale/core": "1.1.0",
36
35
  "cli-table3": "^0.6.5",
37
36
  "clipboardy": "^5.0.2",
38
37
  "commander": "^14.0.2",
39
38
  "csv-parse": "^5.6.0",
40
39
  "date-fns": "^4.1.0",
41
40
  "picocolors": "^1.1.1",
42
- "react": "^19.2.3",
43
41
  "solid-js": "1.9.9",
44
42
  "string-width": "^8.1.0"
45
43
  },
46
44
  "devDependencies": {
47
45
  "@types/node": "^20.0.0",
48
- "@types/react": "^19.2.7",
49
46
  "typescript": "^5.0.0"
50
47
  },
51
48
  "engines": {
package/src/cli.ts CHANGED
@@ -16,9 +16,16 @@ import { submit } from "./submit.js";
16
16
  import { generateWrapped } from "./wrapped.js";
17
17
 
18
18
  import {
19
+ ensureCursorMigration,
19
20
  loadCursorCredentials,
20
21
  saveCursorCredentials,
21
22
  clearCursorCredentials,
23
+ clearCursorCredentialsAndCache,
24
+ isCursorLoggedIn,
25
+ hasCursorUsageCache,
26
+ listCursorAccounts,
27
+ setActiveCursorAccount,
28
+ removeCursorAccount,
22
29
  validateCursorSession,
23
30
  readCursorUsage,
24
31
  getCursorCredentialsPath,
@@ -428,22 +435,69 @@ async function main() {
428
435
  cursorCommand
429
436
  .command("login")
430
437
  .description("Login to Cursor (paste your session token)")
431
- .action(async () => {
432
- await cursorLogin();
438
+ .option("--name <name>", "Label for this Cursor account (e.g., work, personal)")
439
+ .action(async (options: { name?: string }) => {
440
+ ensureCursorMigration();
441
+ await cursorLogin(options);
433
442
  });
434
443
 
435
444
  cursorCommand
436
445
  .command("logout")
437
- .description("Logout from Cursor")
438
- .action(async () => {
439
- await cursorLogout();
446
+ .description("Logout from a Cursor account")
447
+ .option("--name <name>", "Account label or id")
448
+ .option("--all", "Logout from all Cursor accounts")
449
+ .option("--purge-cache", "Also delete cached Cursor usage for the logged-out account(s)")
450
+ .action(async (options: { name?: string; all?: boolean; purgeCache?: boolean }) => {
451
+ ensureCursorMigration();
452
+ await cursorLogout(options);
440
453
  });
441
454
 
442
455
  cursorCommand
443
456
  .command("status")
444
457
  .description("Check Cursor authentication status")
445
- .action(async () => {
446
- await cursorStatus();
458
+ .option("--name <name>", "Account label or id")
459
+ .action(async (options: { name?: string }) => {
460
+ ensureCursorMigration();
461
+ await cursorStatus(options);
462
+ });
463
+
464
+ cursorCommand
465
+ .command("accounts")
466
+ .description("List saved Cursor accounts")
467
+ .option("--json", "Output as JSON")
468
+ .action(async (options: { json?: boolean }) => {
469
+ ensureCursorMigration();
470
+ const accounts = listCursorAccounts();
471
+ if (options.json) {
472
+ console.log(JSON.stringify({ accounts }, null, 2));
473
+ return;
474
+ }
475
+
476
+ if (accounts.length === 0) {
477
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
478
+ return;
479
+ }
480
+
481
+ console.log(pc.cyan("\n Cursor IDE - Accounts\n"));
482
+ for (const acct of accounts) {
483
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
484
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
485
+ }
486
+ console.log();
487
+ });
488
+
489
+ cursorCommand
490
+ .command("switch")
491
+ .description("Switch active Cursor account")
492
+ .argument("<name>", "Account label or id")
493
+ .action(async (name: string) => {
494
+ ensureCursorMigration();
495
+ const result = setActiveCursorAccount(name);
496
+ if (!result.ok) {
497
+ console.log(pc.red(`\n Error: ${result.error}\n`));
498
+ process.exit(1);
499
+ }
500
+ console.log(pc.green(`\n Active Cursor account set to ${pc.bold(name)}\n`));
447
501
  });
448
502
 
449
503
  // Check if a subcommand was provided
@@ -521,8 +575,7 @@ function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
521
575
  * Only attempts sync if user is authenticated with Cursor.
522
576
  */
523
577
  async function syncCursorData(): Promise<CursorSyncResult> {
524
- const credentials = loadCursorCredentials();
525
- if (!credentials) {
578
+ if (!isCursorLoggedIn()) {
526
579
  return { attempted: false, synced: false, rows: 0 };
527
580
  }
528
581
 
@@ -578,8 +631,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
578
631
 
579
632
  // Check cursor auth early if cursor-only mode
580
633
  if (onlyCursor) {
581
- const credentials = loadCursorCredentials();
582
- if (!credentials) {
634
+ if (!isCursorLoggedIn() && !hasCursorUsageCache()) {
583
635
  console.log(pc.red("\n Error: Cursor authentication required."));
584
636
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate with Cursor.\n"));
585
637
  process.exit(1);
@@ -610,6 +662,11 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
610
662
  dateFilters,
611
663
  (phase) => spinner?.update(phase)
612
664
  );
665
+
666
+ if (includeCursor && cursorSync.attempted && cursorSync.error) {
667
+ // Don't block report generation; just warn about partial Cursor sync.
668
+ console.log(pc.yellow(` Cursor sync warning: ${cursorSync.error}`));
669
+ }
613
670
 
614
671
  if (!localMessages && !onlyCursor) {
615
672
  if (spinner) {
@@ -628,7 +685,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
628
685
  const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
629
686
  report = await finalizeReportAsync({
630
687
  localMessages: localMessages || emptyMessages,
631
- includeCursor: includeCursor && cursorSync.synced,
688
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
632
689
  since: dateFilters.since,
633
690
  until: dateFilters.until,
634
691
  year: dateFilters.year,
@@ -760,7 +817,7 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
760
817
  try {
761
818
  report = await finalizeMonthlyReportAsync({
762
819
  localMessages,
763
- includeCursor: includeCursor && cursorSync.synced,
820
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
764
821
  since: dateFilters.since,
765
822
  until: dateFilters.until,
766
823
  year: dateFilters.year,
@@ -859,7 +916,7 @@ async function outputJsonReport(
859
916
  if (reportType === "models") {
860
917
  const report = await finalizeReportAsync({
861
918
  localMessages: localMessages || emptyMessages,
862
- includeCursor: includeCursor && cursorSync.synced,
919
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
863
920
  since: dateFilters.since,
864
921
  until: dateFilters.until,
865
922
  year: dateFilters.year,
@@ -868,7 +925,7 @@ async function outputJsonReport(
868
925
  } else {
869
926
  const report = await finalizeMonthlyReportAsync({
870
927
  localMessages: localMessages || emptyMessages,
871
- includeCursor: includeCursor && cursorSync.synced,
928
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
872
929
  since: dateFilters.since,
873
930
  until: dateFilters.until,
874
931
  year: dateFilters.year,
@@ -911,7 +968,7 @@ async function handleGraphCommand(options: GraphCommandOptions) {
911
968
 
912
969
  const data = await finalizeGraphAsync({
913
970
  localMessages,
914
- includeCursor: includeCursor && cursorSync.synced,
971
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
915
972
  since: dateFilters.since,
916
973
  until: dateFilters.until,
917
974
  year: dateFilters.year,
@@ -1124,14 +1181,7 @@ function getSourceLabel(source: string): string {
1124
1181
  // Cursor IDE Authentication
1125
1182
  // =============================================================================
1126
1183
 
1127
- async function cursorLogin(): Promise<void> {
1128
- const credentials = loadCursorCredentials();
1129
- if (credentials) {
1130
- console.log(pc.yellow("\n Already logged in to Cursor."));
1131
- console.log(pc.gray(" Run 'tokscale cursor logout' to sign out first.\n"));
1132
- return;
1133
- }
1134
-
1184
+ async function cursorLogin(options: { name?: string } = {}): Promise<void> {
1135
1185
  console.log(pc.cyan("\n Cursor IDE - Login\n"));
1136
1186
  console.log(pc.white(" To get your session token:"));
1137
1187
  console.log(pc.gray(" 1. Open https://www.cursor.com/settings in your browser"));
@@ -1169,47 +1219,97 @@ async function cursorLogin(): Promise<void> {
1169
1219
  return;
1170
1220
  }
1171
1221
 
1172
- // Save credentials
1173
- saveCursorCredentials({
1174
- sessionToken: token,
1175
- createdAt: new Date().toISOString(),
1176
- });
1222
+ // Save credentials (multi-account)
1223
+ let savedAccountId: string;
1224
+ try {
1225
+ const saved = saveCursorCredentials(
1226
+ {
1227
+ sessionToken: token,
1228
+ createdAt: new Date().toISOString(),
1229
+ },
1230
+ { label: options.name }
1231
+ );
1232
+ savedAccountId = saved.accountId;
1233
+ } catch (e) {
1234
+ console.log(pc.red(`\n Failed to save credentials: ${(e as Error).message}\n`));
1235
+ return;
1236
+ }
1177
1237
 
1178
1238
  console.log(pc.green("\n Success! Logged in to Cursor."));
1239
+ if (options.name) {
1240
+ console.log(pc.gray(` Account: ${options.name} (${savedAccountId})`));
1241
+ } else {
1242
+ console.log(pc.gray(` Account ID: ${savedAccountId}`));
1243
+ }
1179
1244
  if (validation.membershipType) {
1180
1245
  console.log(pc.gray(` Membership: ${validation.membershipType}`));
1181
1246
  }
1182
1247
  console.log(pc.gray(" Your usage data will now be included in reports.\n"));
1183
1248
  }
1184
1249
 
1185
- async function cursorLogout(): Promise<void> {
1186
- const credentials = loadCursorCredentials();
1187
-
1188
- if (!credentials) {
1250
+ async function cursorLogout(options: { name?: string; all?: boolean; purgeCache?: boolean } = {}): Promise<void> {
1251
+ if (!isCursorLoggedIn()) {
1189
1252
  console.log(pc.yellow("\n Not logged in to Cursor.\n"));
1190
1253
  return;
1191
1254
  }
1192
1255
 
1193
- const cleared = clearCursorCredentials();
1194
-
1195
- if (cleared) {
1196
- console.log(pc.green("\n Logged out from Cursor.\n"));
1197
- } else {
1256
+ if (options.all) {
1257
+ const cleared = options.purgeCache ? clearCursorCredentialsAndCache({ purgeCache: true }) : clearCursorCredentialsAndCache();
1258
+ if (cleared) {
1259
+ console.log(pc.green("\n Logged out from all Cursor accounts.\n"));
1260
+ return;
1261
+ }
1198
1262
  console.error(pc.red("\n Failed to clear Cursor credentials.\n"));
1199
1263
  process.exit(1);
1200
1264
  }
1201
- }
1202
1265
 
1203
- async function cursorStatus(): Promise<void> {
1204
- const credentials = loadCursorCredentials();
1266
+ const target = options.name || listCursorAccounts().find((a) => a.isActive)?.id;
1267
+ if (!target) {
1268
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
1269
+ return;
1270
+ }
1205
1271
 
1206
- if (!credentials) {
1272
+ const removed = removeCursorAccount(target, { purgeCache: options.purgeCache });
1273
+ if (!removed.removed) {
1274
+ console.error(pc.red(`\n Failed to log out: ${removed.error}\n`));
1275
+ process.exit(1);
1276
+ }
1277
+
1278
+ if (options.purgeCache) {
1279
+ console.log(pc.green(`\n Logged out from Cursor account (cache purged): ${pc.bold(target)}\n`));
1280
+ } else {
1281
+ console.log(pc.green(`\n Logged out from Cursor account (history archived): ${pc.bold(target)}\n`));
1282
+ }
1283
+ }
1284
+
1285
+ async function cursorStatus(options: { name?: string } = {}): Promise<void> {
1286
+ if (!isCursorLoggedIn()) {
1207
1287
  console.log(pc.yellow("\n Not logged in to Cursor."));
1208
1288
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate.\n"));
1209
1289
  return;
1210
1290
  }
1211
1291
 
1292
+ const accounts = listCursorAccounts();
1293
+ const target = options.name
1294
+ ? options.name
1295
+ : accounts.find((a) => a.isActive)?.id;
1296
+
1297
+ const credentials = target ? loadCursorCredentials(target) : null;
1298
+ if (!credentials) {
1299
+ console.log(pc.red("\n Error: Cursor account not found."));
1300
+ console.log(pc.gray(" Run 'tokscale cursor accounts' to list saved accounts.\n"));
1301
+ process.exit(1);
1302
+ }
1303
+
1212
1304
  console.log(pc.cyan("\n Cursor IDE - Status\n"));
1305
+ if (accounts.length > 0) {
1306
+ console.log(pc.white(" Accounts:"));
1307
+ for (const acct of accounts) {
1308
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
1309
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
1310
+ }
1311
+ console.log();
1312
+ }
1213
1313
  console.log(pc.gray(" Checking session validity..."));
1214
1314
 
1215
1315
  const validation = await validateCursorSession(credentials.sessionToken);
@@ -1223,7 +1323,7 @@ async function cursorStatus(): Promise<void> {
1223
1323
 
1224
1324
  // Try to fetch usage to show summary
1225
1325
  try {
1226
- const usage = await readCursorUsage();
1326
+ const usage = await readCursorUsage(target);
1227
1327
  const totalCost = usage.byModel.reduce((sum, m) => sum + m.cost, 0);
1228
1328
  console.log(pc.gray(` Models used: ${usage.byModel.length}`));
1229
1329
  console.log(pc.gray(` Total usage events: ${usage.rows.length}`));