@tokscale/cli 1.0.24 → 1.1.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.
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,
@@ -44,7 +51,11 @@ import {
44
51
  type ParsedMessages,
45
52
  } from "./native.js";
46
53
  import { createSpinner } from "./spinner.js";
54
+ import { spawn } from "node:child_process";
55
+ import { randomUUID } from "node:crypto";
47
56
  import * as fs from "node:fs";
57
+ import * as os from "node:os";
58
+ import * as path from "node:path";
48
59
  import { performance } from "node:perf_hooks";
49
60
  import type { SourceType } from "./graph-types.js";
50
61
  import type { TUIOptions, TabType } from "./tui/types/index.js";
@@ -186,6 +197,217 @@ function getDateRangeLabel(options: DateFilterOptions): string | null {
186
197
  return null;
187
198
  }
188
199
 
200
+ function getHeadlessRoots(homeDir: string): string[] {
201
+ const override = process.env.TOKSCALE_HEADLESS_DIR;
202
+ if (override && override.trim()) {
203
+ return [override];
204
+ }
205
+
206
+ const roots = [
207
+ path.join(homeDir, ".config", "tokscale", "headless"),
208
+ path.join(homeDir, "Library", "Application Support", "tokscale", "headless"),
209
+ ];
210
+
211
+ return Array.from(new Set(roots));
212
+ }
213
+
214
+ function describePath(targetPath: string): string {
215
+ return fs.existsSync(targetPath) ? targetPath : `${targetPath} (missing)`;
216
+ }
217
+
218
+ type HeadlessFormat = "json" | "jsonl";
219
+ type HeadlessSource = "codex";
220
+
221
+ const HEADLESS_SOURCES: HeadlessSource[] = ["codex"];
222
+
223
+ function normalizeHeadlessSource(source: string): HeadlessSource | null {
224
+ const normalized = source.toLowerCase();
225
+ return HEADLESS_SOURCES.includes(normalized as HeadlessSource)
226
+ ? (normalized as HeadlessSource)
227
+ : null;
228
+ }
229
+
230
+ function resolveHeadlessFormat(
231
+ source: HeadlessSource,
232
+ args: string[],
233
+ override?: string
234
+ ): HeadlessFormat {
235
+ if (override === "json" || override === "jsonl") {
236
+ return override;
237
+ }
238
+
239
+ return "jsonl";
240
+ }
241
+
242
+ function applyHeadlessDefaults(
243
+ source: HeadlessSource,
244
+ args: string[],
245
+ format: HeadlessFormat,
246
+ autoFlags: boolean
247
+ ): string[] {
248
+ if (!autoFlags) return args;
249
+
250
+ const updated = [...args];
251
+
252
+ if (source === "codex" && !updated.includes("--json")) {
253
+ updated.push("--json");
254
+ }
255
+
256
+ return updated;
257
+ }
258
+
259
+ function buildHeadlessOutputPath(
260
+ headlessRoots: string[],
261
+ source: HeadlessSource,
262
+ format: HeadlessFormat,
263
+ outputPath?: string
264
+ ): string {
265
+ if (outputPath) {
266
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
267
+ return outputPath;
268
+ }
269
+
270
+ const root = headlessRoots[0] || path.join(os.homedir(), ".config", "tokscale", "headless");
271
+ const dir = path.join(root, source);
272
+ fs.mkdirSync(dir, { recursive: true });
273
+
274
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
275
+ const id = randomUUID().replace(/-/g, "").slice(0, 8);
276
+ const filename = `${source}-${stamp}-${id}.${format}`;
277
+ return path.join(dir, filename);
278
+ }
279
+
280
+ function printHeadlessHelp(): void {
281
+ console.log("\n Usage: tokscale headless codex [args...]");
282
+ console.log(" Options:");
283
+ console.log(" --format <json|jsonl> Override output format");
284
+ console.log(" --output <file> Write captured output to file");
285
+ console.log(" --no-auto-flags Do not auto-add JSON output flags");
286
+ console.log("\n Examples:");
287
+ console.log(" tokscale headless codex exec -m gpt-5");
288
+ console.log();
289
+ }
290
+
291
+ async function runHeadlessCapture(argv: string[]): Promise<void> {
292
+ const sourceArg = argv[1];
293
+ if (!sourceArg || sourceArg === "--help" || sourceArg === "-h") {
294
+ printHeadlessHelp();
295
+ return;
296
+ }
297
+
298
+ const source = normalizeHeadlessSource(sourceArg);
299
+ if (!source) {
300
+ console.error(`\n Error: Unknown headless source '${sourceArg}'.`);
301
+ printHeadlessHelp();
302
+ process.exit(1);
303
+ }
304
+
305
+ const rawArgs = argv.slice(2);
306
+ let outputPath: string | undefined;
307
+ let formatOverride: HeadlessFormat | undefined;
308
+ let autoFlags = true;
309
+ const cmdArgs: string[] = [];
310
+
311
+ for (let i = 0; i < rawArgs.length; i += 1) {
312
+ const arg = rawArgs[i];
313
+ if (arg === "--") continue;
314
+ if ((arg === "--help" || arg === "-h") && cmdArgs.length === 0) {
315
+ printHeadlessHelp();
316
+ return;
317
+ }
318
+ if (arg === "--output") {
319
+ const value = rawArgs[i + 1];
320
+ if (!value) {
321
+ console.error("\n Error: --output requires a file path.");
322
+ process.exit(1);
323
+ }
324
+ outputPath = value;
325
+ i += 1;
326
+ continue;
327
+ }
328
+ if (arg === "--format") {
329
+ const format = rawArgs[i + 1];
330
+ if (!format) {
331
+ console.error("\n Error: --format requires a value (json or jsonl).");
332
+ process.exit(1);
333
+ }
334
+ if (format !== "json" && format !== "jsonl") {
335
+ console.error(`\n Error: Invalid format '${format}'. Use json or jsonl.`);
336
+ process.exit(1);
337
+ }
338
+ formatOverride = format as HeadlessFormat;
339
+ i += 1;
340
+ continue;
341
+ }
342
+ if (arg === "--no-auto-flags") {
343
+ autoFlags = false;
344
+ continue;
345
+ }
346
+ cmdArgs.push(arg);
347
+ }
348
+
349
+ if (cmdArgs.length === 0) {
350
+ console.error("\n Error: Missing CLI arguments to execute.");
351
+ printHeadlessHelp();
352
+ process.exit(1);
353
+ }
354
+
355
+ const format = resolveHeadlessFormat(source, cmdArgs, formatOverride);
356
+ const finalArgs = applyHeadlessDefaults(source, cmdArgs, format, autoFlags);
357
+ const headlessRoots = getHeadlessRoots(os.homedir());
358
+ const output = buildHeadlessOutputPath(headlessRoots, source, format, outputPath);
359
+
360
+ console.log(pc.cyan("\n Headless capture"));
361
+ console.log(pc.gray(` source: ${source}`));
362
+ console.log(pc.gray(` output: ${output}`));
363
+ console.log();
364
+
365
+ const proc = spawn(source, finalArgs, {
366
+ stdio: ["inherit", "pipe", "inherit"],
367
+ });
368
+
369
+ if (!proc.stdout) {
370
+ console.error("\n Error: Failed to capture stdout from command.");
371
+ process.exit(1);
372
+ }
373
+
374
+ const outputStream = fs.createWriteStream(output, { encoding: "utf-8" });
375
+ const outputFinished = new Promise<void>((resolve, reject) => {
376
+ outputStream.on("finish", () => resolve());
377
+ outputStream.on("error", reject);
378
+ });
379
+ proc.stdout.pipe(outputStream);
380
+ let exitCode: number;
381
+ try {
382
+ exitCode = await new Promise<number>((resolve, reject) => {
383
+ proc.on("error", reject);
384
+ proc.on("close", (code) => resolve(code ?? 1));
385
+ });
386
+ } catch (err) {
387
+ outputStream.destroy();
388
+ const message = err instanceof Error ? err.message : String(err);
389
+ console.error(`\n Error: Failed to run '${source}': ${message}`);
390
+ process.exit(1);
391
+ }
392
+
393
+ outputStream.end();
394
+
395
+ try {
396
+ await outputFinished;
397
+ } catch (err) {
398
+ const message = err instanceof Error ? err.message : String(err);
399
+ console.error(`\n Error: Failed to write headless output: ${message}`);
400
+ process.exit(1);
401
+ }
402
+
403
+ if (exitCode !== 0) {
404
+ process.exit(exitCode);
405
+ }
406
+
407
+ console.log(pc.green(` Saved headless output to ${output}`));
408
+ console.log();
409
+ }
410
+
189
411
  function buildTUIOptions(
190
412
  options: FilterOptions & DateFilterOptions,
191
413
  initialTab?: TabType
@@ -282,6 +504,142 @@ async function main() {
282
504
  }
283
505
  });
284
506
 
507
+ program
508
+ .command("sources")
509
+ .description("Show local scan locations and Codex headless paths")
510
+ .option("--json", "Output as JSON (for scripting)")
511
+ .action(async (options) => {
512
+ const homeDir = os.homedir();
513
+ const headlessRoots = getHeadlessRoots(homeDir);
514
+
515
+ const claudeSessions = path.join(homeDir, ".claude", "projects");
516
+ const codexHome = process.env.CODEX_HOME || path.join(homeDir, ".codex");
517
+ const codexSessions = path.join(codexHome, "sessions");
518
+ const geminiSessions = path.join(homeDir, ".gemini", "tmp");
519
+
520
+ let localMessages: ParsedMessages | null = null;
521
+ try {
522
+ localMessages = await parseLocalSourcesAsync({
523
+ homeDir,
524
+ sources: ["claude", "codex", "gemini"],
525
+ });
526
+ } catch (e) {
527
+ console.error(`Error: ${(e as Error).message}`);
528
+ process.exit(1);
529
+ }
530
+
531
+ const headlessCounts = {
532
+ codex: 0,
533
+ };
534
+
535
+ for (const message of localMessages.messages) {
536
+ if (message.agent === "headless" && message.source === "codex") {
537
+ headlessCounts.codex += 1;
538
+ }
539
+ }
540
+
541
+ const sourceRows: Array<{
542
+ source: "claude" | "codex" | "gemini";
543
+ label: string;
544
+ sessionsPath: string;
545
+ messageCount: number;
546
+ headlessSupported: boolean;
547
+ headlessPaths: string[];
548
+ headlessMessageCount: number;
549
+ }> = [
550
+ {
551
+ source: "claude",
552
+ label: "Claude Code",
553
+ sessionsPath: claudeSessions,
554
+ messageCount: localMessages.claudeCount,
555
+ headlessSupported: false,
556
+ headlessPaths: [],
557
+ headlessMessageCount: 0,
558
+ },
559
+ {
560
+ source: "codex",
561
+ label: "Codex CLI",
562
+ sessionsPath: codexSessions,
563
+ headlessPaths: headlessRoots.map((root) => path.join(root, "codex")),
564
+ messageCount: localMessages.codexCount,
565
+ headlessMessageCount: headlessCounts.codex,
566
+ headlessSupported: true,
567
+ },
568
+ {
569
+ source: "gemini",
570
+ label: "Gemini CLI",
571
+ sessionsPath: geminiSessions,
572
+ messageCount: localMessages.geminiCount,
573
+ headlessSupported: false,
574
+ headlessPaths: [],
575
+ headlessMessageCount: 0,
576
+ },
577
+ ];
578
+
579
+ if (options.json) {
580
+ const payload = {
581
+ headlessRoots,
582
+ sources: sourceRows.map((row) => ({
583
+ source: row.source,
584
+ label: row.label,
585
+ sessionsPath: row.sessionsPath,
586
+ sessionsPathExists: fs.existsSync(row.sessionsPath),
587
+ messageCount: row.messageCount,
588
+ headlessSupported: row.headlessSupported,
589
+ headlessPaths: row.headlessSupported
590
+ ? row.headlessPaths.map((headlessPath) => ({
591
+ path: headlessPath,
592
+ exists: fs.existsSync(headlessPath),
593
+ }))
594
+ : [],
595
+ headlessMessageCount: row.headlessSupported ? row.headlessMessageCount : 0,
596
+ })),
597
+ note: "Headless capture is supported for Codex CLI only.",
598
+ };
599
+ console.log(JSON.stringify(payload, null, 2));
600
+ return;
601
+ }
602
+
603
+ console.log(pc.cyan("\n Local sources & Codex headless capture"));
604
+ console.log(pc.gray(` Headless roots: ${headlessRoots.join(", ")}`));
605
+ console.log();
606
+
607
+ for (const row of sourceRows) {
608
+ console.log(pc.white(` ${row.label}`));
609
+ console.log(pc.gray(` sessions: ${describePath(row.sessionsPath)}`));
610
+ if (row.headlessSupported) {
611
+ console.log(
612
+ pc.gray(
613
+ ` headless: ${row.headlessPaths.map(describePath).join(", ")}`
614
+ )
615
+ );
616
+ console.log(
617
+ pc.gray(
618
+ ` messages: ${formatNumber(row.messageCount)} (headless: ${formatNumber(
619
+ row.headlessMessageCount
620
+ )})`
621
+ )
622
+ );
623
+ } else {
624
+ console.log(pc.gray(` messages: ${formatNumber(row.messageCount)}`));
625
+ }
626
+ console.log();
627
+ }
628
+
629
+ console.log(
630
+ pc.gray(
631
+ " Note: Headless capture is supported for Codex CLI only."
632
+ )
633
+ );
634
+ console.log();
635
+ });
636
+
637
+ program
638
+ .command("headless")
639
+ .description("Run a CLI in headless mode and capture stdout")
640
+ .argument("<source>", "Source CLI to capture (currently only 'codex' is supported)")
641
+ .argument("[args...]", "Arguments passed to the CLI");
642
+
285
643
  program
286
644
  .command("graph")
287
645
  .description("Export contribution graph data as JSON")
@@ -428,31 +786,82 @@ async function main() {
428
786
  cursorCommand
429
787
  .command("login")
430
788
  .description("Login to Cursor (paste your session token)")
431
- .action(async () => {
432
- await cursorLogin();
789
+ .option("--name <name>", "Label for this Cursor account (e.g., work, personal)")
790
+ .action(async (options: { name?: string }) => {
791
+ ensureCursorMigration();
792
+ await cursorLogin(options);
433
793
  });
434
794
 
435
795
  cursorCommand
436
796
  .command("logout")
437
- .description("Logout from Cursor")
438
- .action(async () => {
439
- await cursorLogout();
797
+ .description("Logout from a Cursor account")
798
+ .option("--name <name>", "Account label or id")
799
+ .option("--all", "Logout from all Cursor accounts")
800
+ .option("--purge-cache", "Also delete cached Cursor usage for the logged-out account(s)")
801
+ .action(async (options: { name?: string; all?: boolean; purgeCache?: boolean }) => {
802
+ ensureCursorMigration();
803
+ await cursorLogout(options);
440
804
  });
441
805
 
442
806
  cursorCommand
443
807
  .command("status")
444
808
  .description("Check Cursor authentication status")
445
- .action(async () => {
446
- await cursorStatus();
809
+ .option("--name <name>", "Account label or id")
810
+ .action(async (options: { name?: string }) => {
811
+ ensureCursorMigration();
812
+ await cursorStatus(options);
813
+ });
814
+
815
+ cursorCommand
816
+ .command("accounts")
817
+ .description("List saved Cursor accounts")
818
+ .option("--json", "Output as JSON")
819
+ .action(async (options: { json?: boolean }) => {
820
+ ensureCursorMigration();
821
+ const accounts = listCursorAccounts();
822
+ if (options.json) {
823
+ console.log(JSON.stringify({ accounts }, null, 2));
824
+ return;
825
+ }
826
+
827
+ if (accounts.length === 0) {
828
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
829
+ return;
830
+ }
831
+
832
+ console.log(pc.cyan("\n Cursor IDE - Accounts\n"));
833
+ for (const acct of accounts) {
834
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
835
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
836
+ }
837
+ console.log();
838
+ });
839
+
840
+ cursorCommand
841
+ .command("switch")
842
+ .description("Switch active Cursor account")
843
+ .argument("<name>", "Account label or id")
844
+ .action(async (name: string) => {
845
+ ensureCursorMigration();
846
+ const result = setActiveCursorAccount(name);
847
+ if (!result.ok) {
848
+ console.log(pc.red(`\n Error: ${result.error}\n`));
849
+ process.exit(1);
850
+ }
851
+ console.log(pc.green(`\n Active Cursor account set to ${pc.bold(name)}\n`));
447
852
  });
448
853
 
449
854
  // Check if a subcommand was provided
450
855
  const args = process.argv.slice(2);
856
+ if (args[0] === "headless") {
857
+ await runHeadlessCapture(args);
858
+ return;
859
+ }
451
860
  const firstArg = args[0] || '';
452
861
  // Global flags should go to main program
453
862
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
454
863
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
455
- const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
864
+ const knownCommands = ['monthly', 'models', 'sources', 'headless', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
456
865
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
457
866
 
458
867
  if (isKnownCommand || isGlobalFlag) {
@@ -521,8 +930,7 @@ function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
521
930
  * Only attempts sync if user is authenticated with Cursor.
522
931
  */
523
932
  async function syncCursorData(): Promise<CursorSyncResult> {
524
- const credentials = loadCursorCredentials();
525
- if (!credentials) {
933
+ if (!isCursorLoggedIn()) {
526
934
  return { attempted: false, synced: false, rows: 0 };
527
935
  }
528
936
 
@@ -578,8 +986,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
578
986
 
579
987
  // Check cursor auth early if cursor-only mode
580
988
  if (onlyCursor) {
581
- const credentials = loadCursorCredentials();
582
- if (!credentials) {
989
+ if (!isCursorLoggedIn() && !hasCursorUsageCache()) {
583
990
  console.log(pc.red("\n Error: Cursor authentication required."));
584
991
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate with Cursor.\n"));
585
992
  process.exit(1);
@@ -610,6 +1017,11 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
610
1017
  dateFilters,
611
1018
  (phase) => spinner?.update(phase)
612
1019
  );
1020
+
1021
+ if (includeCursor && cursorSync.attempted && cursorSync.error) {
1022
+ // Don't block report generation; just warn about partial Cursor sync.
1023
+ console.log(pc.yellow(` Cursor sync warning: ${cursorSync.error}`));
1024
+ }
613
1025
 
614
1026
  if (!localMessages && !onlyCursor) {
615
1027
  if (spinner) {
@@ -628,7 +1040,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
628
1040
  const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
629
1041
  report = await finalizeReportAsync({
630
1042
  localMessages: localMessages || emptyMessages,
631
- includeCursor: includeCursor && cursorSync.synced,
1043
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
632
1044
  since: dateFilters.since,
633
1045
  until: dateFilters.until,
634
1046
  year: dateFilters.year,
@@ -760,7 +1172,7 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
760
1172
  try {
761
1173
  report = await finalizeMonthlyReportAsync({
762
1174
  localMessages,
763
- includeCursor: includeCursor && cursorSync.synced,
1175
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
764
1176
  since: dateFilters.since,
765
1177
  until: dateFilters.until,
766
1178
  year: dateFilters.year,
@@ -859,7 +1271,7 @@ async function outputJsonReport(
859
1271
  if (reportType === "models") {
860
1272
  const report = await finalizeReportAsync({
861
1273
  localMessages: localMessages || emptyMessages,
862
- includeCursor: includeCursor && cursorSync.synced,
1274
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
863
1275
  since: dateFilters.since,
864
1276
  until: dateFilters.until,
865
1277
  year: dateFilters.year,
@@ -868,7 +1280,7 @@ async function outputJsonReport(
868
1280
  } else {
869
1281
  const report = await finalizeMonthlyReportAsync({
870
1282
  localMessages: localMessages || emptyMessages,
871
- includeCursor: includeCursor && cursorSync.synced,
1283
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
872
1284
  since: dateFilters.since,
873
1285
  until: dateFilters.until,
874
1286
  year: dateFilters.year,
@@ -911,7 +1323,7 @@ async function handleGraphCommand(options: GraphCommandOptions) {
911
1323
 
912
1324
  const data = await finalizeGraphAsync({
913
1325
  localMessages,
914
- includeCursor: includeCursor && cursorSync.synced,
1326
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
915
1327
  since: dateFilters.since,
916
1328
  until: dateFilters.until,
917
1329
  year: dateFilters.year,
@@ -1124,14 +1536,7 @@ function getSourceLabel(source: string): string {
1124
1536
  // Cursor IDE Authentication
1125
1537
  // =============================================================================
1126
1538
 
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
-
1539
+ async function cursorLogin(options: { name?: string } = {}): Promise<void> {
1135
1540
  console.log(pc.cyan("\n Cursor IDE - Login\n"));
1136
1541
  console.log(pc.white(" To get your session token:"));
1137
1542
  console.log(pc.gray(" 1. Open https://www.cursor.com/settings in your browser"));
@@ -1169,47 +1574,97 @@ async function cursorLogin(): Promise<void> {
1169
1574
  return;
1170
1575
  }
1171
1576
 
1172
- // Save credentials
1173
- saveCursorCredentials({
1174
- sessionToken: token,
1175
- createdAt: new Date().toISOString(),
1176
- });
1577
+ // Save credentials (multi-account)
1578
+ let savedAccountId: string;
1579
+ try {
1580
+ const saved = saveCursorCredentials(
1581
+ {
1582
+ sessionToken: token,
1583
+ createdAt: new Date().toISOString(),
1584
+ },
1585
+ { label: options.name }
1586
+ );
1587
+ savedAccountId = saved.accountId;
1588
+ } catch (e) {
1589
+ console.log(pc.red(`\n Failed to save credentials: ${(e as Error).message}\n`));
1590
+ return;
1591
+ }
1177
1592
 
1178
1593
  console.log(pc.green("\n Success! Logged in to Cursor."));
1594
+ if (options.name) {
1595
+ console.log(pc.gray(` Account: ${options.name} (${savedAccountId})`));
1596
+ } else {
1597
+ console.log(pc.gray(` Account ID: ${savedAccountId}`));
1598
+ }
1179
1599
  if (validation.membershipType) {
1180
1600
  console.log(pc.gray(` Membership: ${validation.membershipType}`));
1181
1601
  }
1182
1602
  console.log(pc.gray(" Your usage data will now be included in reports.\n"));
1183
1603
  }
1184
1604
 
1185
- async function cursorLogout(): Promise<void> {
1186
- const credentials = loadCursorCredentials();
1187
-
1188
- if (!credentials) {
1605
+ async function cursorLogout(options: { name?: string; all?: boolean; purgeCache?: boolean } = {}): Promise<void> {
1606
+ if (!isCursorLoggedIn()) {
1189
1607
  console.log(pc.yellow("\n Not logged in to Cursor.\n"));
1190
1608
  return;
1191
1609
  }
1192
1610
 
1193
- const cleared = clearCursorCredentials();
1194
-
1195
- if (cleared) {
1196
- console.log(pc.green("\n Logged out from Cursor.\n"));
1197
- } else {
1611
+ if (options.all) {
1612
+ const cleared = options.purgeCache ? clearCursorCredentialsAndCache({ purgeCache: true }) : clearCursorCredentialsAndCache();
1613
+ if (cleared) {
1614
+ console.log(pc.green("\n Logged out from all Cursor accounts.\n"));
1615
+ return;
1616
+ }
1198
1617
  console.error(pc.red("\n Failed to clear Cursor credentials.\n"));
1199
1618
  process.exit(1);
1200
1619
  }
1201
- }
1202
1620
 
1203
- async function cursorStatus(): Promise<void> {
1204
- const credentials = loadCursorCredentials();
1621
+ const target = options.name || listCursorAccounts().find((a) => a.isActive)?.id;
1622
+ if (!target) {
1623
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
1624
+ return;
1625
+ }
1205
1626
 
1206
- if (!credentials) {
1627
+ const removed = removeCursorAccount(target, { purgeCache: options.purgeCache });
1628
+ if (!removed.removed) {
1629
+ console.error(pc.red(`\n Failed to log out: ${removed.error}\n`));
1630
+ process.exit(1);
1631
+ }
1632
+
1633
+ if (options.purgeCache) {
1634
+ console.log(pc.green(`\n Logged out from Cursor account (cache purged): ${pc.bold(target)}\n`));
1635
+ } else {
1636
+ console.log(pc.green(`\n Logged out from Cursor account (history archived): ${pc.bold(target)}\n`));
1637
+ }
1638
+ }
1639
+
1640
+ async function cursorStatus(options: { name?: string } = {}): Promise<void> {
1641
+ if (!isCursorLoggedIn()) {
1207
1642
  console.log(pc.yellow("\n Not logged in to Cursor."));
1208
1643
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate.\n"));
1209
1644
  return;
1210
1645
  }
1211
1646
 
1647
+ const accounts = listCursorAccounts();
1648
+ const target = options.name
1649
+ ? options.name
1650
+ : accounts.find((a) => a.isActive)?.id;
1651
+
1652
+ const credentials = target ? loadCursorCredentials(target) : null;
1653
+ if (!credentials) {
1654
+ console.log(pc.red("\n Error: Cursor account not found."));
1655
+ console.log(pc.gray(" Run 'tokscale cursor accounts' to list saved accounts.\n"));
1656
+ process.exit(1);
1657
+ }
1658
+
1212
1659
  console.log(pc.cyan("\n Cursor IDE - Status\n"));
1660
+ if (accounts.length > 0) {
1661
+ console.log(pc.white(" Accounts:"));
1662
+ for (const acct of accounts) {
1663
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
1664
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
1665
+ }
1666
+ console.log();
1667
+ }
1213
1668
  console.log(pc.gray(" Checking session validity..."));
1214
1669
 
1215
1670
  const validation = await validateCursorSession(credentials.sessionToken);
@@ -1223,7 +1678,7 @@ async function cursorStatus(): Promise<void> {
1223
1678
 
1224
1679
  // Try to fetch usage to show summary
1225
1680
  try {
1226
- const usage = await readCursorUsage();
1681
+ const usage = await readCursorUsage(target);
1227
1682
  const totalCost = usage.byModel.reduce((sum, m) => sum + m.cost, 0);
1228
1683
  console.log(pc.gray(` Models used: ${usage.byModel.length}`));
1229
1684
  console.log(pc.gray(` Total usage events: ${usage.rows.length}`));