@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/dist/cli.js CHANGED
@@ -13,11 +13,15 @@ import pc from "picocolors";
13
13
  import { login, logout, whoami } from "./auth.js";
14
14
  import { submit } from "./submit.js";
15
15
  import { generateWrapped } from "./wrapped.js";
16
- import { loadCursorCredentials, saveCursorCredentials, clearCursorCredentials, validateCursorSession, readCursorUsage, getCursorCredentialsPath, syncCursorCache, } from "./cursor.js";
16
+ import { ensureCursorMigration, loadCursorCredentials, saveCursorCredentials, clearCursorCredentialsAndCache, isCursorLoggedIn, hasCursorUsageCache, listCursorAccounts, setActiveCursorAccount, removeCursorAccount, validateCursorSession, readCursorUsage, getCursorCredentialsPath, syncCursorCache, } from "./cursor.js";
17
17
  import { createUsageTable, formatUsageRow, formatTotalsRow, formatNumber, formatCurrency, formatModelName, } from "./table.js";
18
18
  import { getNativeVersion, parseLocalSourcesAsync, finalizeReportAsync, finalizeMonthlyReportAsync, finalizeGraphAsync, } from "./native.js";
19
19
  import { createSpinner } from "./spinner.js";
20
+ import { spawn } from "node:child_process";
21
+ import { randomUUID } from "node:crypto";
20
22
  import * as fs from "node:fs";
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
21
25
  import { performance } from "node:perf_hooks";
22
26
  import { loadSettings } from "./tui/config/settings.js";
23
27
  let cachedTUILoader = null;
@@ -116,6 +120,174 @@ function getDateRangeLabel(options) {
116
120
  }
117
121
  return null;
118
122
  }
123
+ function getHeadlessRoots(homeDir) {
124
+ const override = process.env.TOKSCALE_HEADLESS_DIR;
125
+ if (override && override.trim()) {
126
+ return [override];
127
+ }
128
+ const roots = [
129
+ path.join(homeDir, ".config", "tokscale", "headless"),
130
+ path.join(homeDir, "Library", "Application Support", "tokscale", "headless"),
131
+ ];
132
+ return Array.from(new Set(roots));
133
+ }
134
+ function describePath(targetPath) {
135
+ return fs.existsSync(targetPath) ? targetPath : `${targetPath} (missing)`;
136
+ }
137
+ const HEADLESS_SOURCES = ["codex"];
138
+ function normalizeHeadlessSource(source) {
139
+ const normalized = source.toLowerCase();
140
+ return HEADLESS_SOURCES.includes(normalized)
141
+ ? normalized
142
+ : null;
143
+ }
144
+ function resolveHeadlessFormat(source, args, override) {
145
+ if (override === "json" || override === "jsonl") {
146
+ return override;
147
+ }
148
+ return "jsonl";
149
+ }
150
+ function applyHeadlessDefaults(source, args, format, autoFlags) {
151
+ if (!autoFlags)
152
+ return args;
153
+ const updated = [...args];
154
+ if (source === "codex" && !updated.includes("--json")) {
155
+ updated.push("--json");
156
+ }
157
+ return updated;
158
+ }
159
+ function buildHeadlessOutputPath(headlessRoots, source, format, outputPath) {
160
+ if (outputPath) {
161
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
162
+ return outputPath;
163
+ }
164
+ const root = headlessRoots[0] || path.join(os.homedir(), ".config", "tokscale", "headless");
165
+ const dir = path.join(root, source);
166
+ fs.mkdirSync(dir, { recursive: true });
167
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
168
+ const id = randomUUID().replace(/-/g, "").slice(0, 8);
169
+ const filename = `${source}-${stamp}-${id}.${format}`;
170
+ return path.join(dir, filename);
171
+ }
172
+ function printHeadlessHelp() {
173
+ console.log("\n Usage: tokscale headless codex [args...]");
174
+ console.log(" Options:");
175
+ console.log(" --format <json|jsonl> Override output format");
176
+ console.log(" --output <file> Write captured output to file");
177
+ console.log(" --no-auto-flags Do not auto-add JSON output flags");
178
+ console.log("\n Examples:");
179
+ console.log(" tokscale headless codex exec -m gpt-5");
180
+ console.log();
181
+ }
182
+ async function runHeadlessCapture(argv) {
183
+ const sourceArg = argv[1];
184
+ if (!sourceArg || sourceArg === "--help" || sourceArg === "-h") {
185
+ printHeadlessHelp();
186
+ return;
187
+ }
188
+ const source = normalizeHeadlessSource(sourceArg);
189
+ if (!source) {
190
+ console.error(`\n Error: Unknown headless source '${sourceArg}'.`);
191
+ printHeadlessHelp();
192
+ process.exit(1);
193
+ }
194
+ const rawArgs = argv.slice(2);
195
+ let outputPath;
196
+ let formatOverride;
197
+ let autoFlags = true;
198
+ const cmdArgs = [];
199
+ for (let i = 0; i < rawArgs.length; i += 1) {
200
+ const arg = rawArgs[i];
201
+ if (arg === "--")
202
+ continue;
203
+ if ((arg === "--help" || arg === "-h") && cmdArgs.length === 0) {
204
+ printHeadlessHelp();
205
+ return;
206
+ }
207
+ if (arg === "--output") {
208
+ const value = rawArgs[i + 1];
209
+ if (!value) {
210
+ console.error("\n Error: --output requires a file path.");
211
+ process.exit(1);
212
+ }
213
+ outputPath = value;
214
+ i += 1;
215
+ continue;
216
+ }
217
+ if (arg === "--format") {
218
+ const format = rawArgs[i + 1];
219
+ if (!format) {
220
+ console.error("\n Error: --format requires a value (json or jsonl).");
221
+ process.exit(1);
222
+ }
223
+ if (format !== "json" && format !== "jsonl") {
224
+ console.error(`\n Error: Invalid format '${format}'. Use json or jsonl.`);
225
+ process.exit(1);
226
+ }
227
+ formatOverride = format;
228
+ i += 1;
229
+ continue;
230
+ }
231
+ if (arg === "--no-auto-flags") {
232
+ autoFlags = false;
233
+ continue;
234
+ }
235
+ cmdArgs.push(arg);
236
+ }
237
+ if (cmdArgs.length === 0) {
238
+ console.error("\n Error: Missing CLI arguments to execute.");
239
+ printHeadlessHelp();
240
+ process.exit(1);
241
+ }
242
+ const format = resolveHeadlessFormat(source, cmdArgs, formatOverride);
243
+ const finalArgs = applyHeadlessDefaults(source, cmdArgs, format, autoFlags);
244
+ const headlessRoots = getHeadlessRoots(os.homedir());
245
+ const output = buildHeadlessOutputPath(headlessRoots, source, format, outputPath);
246
+ console.log(pc.cyan("\n Headless capture"));
247
+ console.log(pc.gray(` source: ${source}`));
248
+ console.log(pc.gray(` output: ${output}`));
249
+ console.log();
250
+ const proc = spawn(source, finalArgs, {
251
+ stdio: ["inherit", "pipe", "inherit"],
252
+ });
253
+ if (!proc.stdout) {
254
+ console.error("\n Error: Failed to capture stdout from command.");
255
+ process.exit(1);
256
+ }
257
+ const outputStream = fs.createWriteStream(output, { encoding: "utf-8" });
258
+ const outputFinished = new Promise((resolve, reject) => {
259
+ outputStream.on("finish", () => resolve());
260
+ outputStream.on("error", reject);
261
+ });
262
+ proc.stdout.pipe(outputStream);
263
+ let exitCode;
264
+ try {
265
+ exitCode = await new Promise((resolve, reject) => {
266
+ proc.on("error", reject);
267
+ proc.on("close", (code) => resolve(code ?? 1));
268
+ });
269
+ }
270
+ catch (err) {
271
+ outputStream.destroy();
272
+ const message = err instanceof Error ? err.message : String(err);
273
+ console.error(`\n Error: Failed to run '${source}': ${message}`);
274
+ process.exit(1);
275
+ }
276
+ outputStream.end();
277
+ try {
278
+ await outputFinished;
279
+ }
280
+ catch (err) {
281
+ const message = err instanceof Error ? err.message : String(err);
282
+ console.error(`\n Error: Failed to write headless output: ${message}`);
283
+ process.exit(1);
284
+ }
285
+ if (exitCode !== 0) {
286
+ process.exit(exitCode);
287
+ }
288
+ console.log(pc.green(` Saved headless output to ${output}`));
289
+ console.log();
290
+ }
119
291
  function buildTUIOptions(options, initialTab) {
120
292
  const dateFilters = getDateFilters(options);
121
293
  const enabledSources = getEnabledSources(options);
@@ -209,6 +381,111 @@ async function main() {
209
381
  }
210
382
  }
211
383
  });
384
+ program
385
+ .command("sources")
386
+ .description("Show local scan locations and Codex headless paths")
387
+ .option("--json", "Output as JSON (for scripting)")
388
+ .action(async (options) => {
389
+ const homeDir = os.homedir();
390
+ const headlessRoots = getHeadlessRoots(homeDir);
391
+ const claudeSessions = path.join(homeDir, ".claude", "projects");
392
+ const codexHome = process.env.CODEX_HOME || path.join(homeDir, ".codex");
393
+ const codexSessions = path.join(codexHome, "sessions");
394
+ const geminiSessions = path.join(homeDir, ".gemini", "tmp");
395
+ let localMessages = null;
396
+ try {
397
+ localMessages = await parseLocalSourcesAsync({
398
+ homeDir,
399
+ sources: ["claude", "codex", "gemini"],
400
+ });
401
+ }
402
+ catch (e) {
403
+ console.error(`Error: ${e.message}`);
404
+ process.exit(1);
405
+ }
406
+ const headlessCounts = {
407
+ codex: 0,
408
+ };
409
+ for (const message of localMessages.messages) {
410
+ if (message.agent === "headless" && message.source === "codex") {
411
+ headlessCounts.codex += 1;
412
+ }
413
+ }
414
+ const sourceRows = [
415
+ {
416
+ source: "claude",
417
+ label: "Claude Code",
418
+ sessionsPath: claudeSessions,
419
+ messageCount: localMessages.claudeCount,
420
+ headlessSupported: false,
421
+ headlessPaths: [],
422
+ headlessMessageCount: 0,
423
+ },
424
+ {
425
+ source: "codex",
426
+ label: "Codex CLI",
427
+ sessionsPath: codexSessions,
428
+ headlessPaths: headlessRoots.map((root) => path.join(root, "codex")),
429
+ messageCount: localMessages.codexCount,
430
+ headlessMessageCount: headlessCounts.codex,
431
+ headlessSupported: true,
432
+ },
433
+ {
434
+ source: "gemini",
435
+ label: "Gemini CLI",
436
+ sessionsPath: geminiSessions,
437
+ messageCount: localMessages.geminiCount,
438
+ headlessSupported: false,
439
+ headlessPaths: [],
440
+ headlessMessageCount: 0,
441
+ },
442
+ ];
443
+ if (options.json) {
444
+ const payload = {
445
+ headlessRoots,
446
+ sources: sourceRows.map((row) => ({
447
+ source: row.source,
448
+ label: row.label,
449
+ sessionsPath: row.sessionsPath,
450
+ sessionsPathExists: fs.existsSync(row.sessionsPath),
451
+ messageCount: row.messageCount,
452
+ headlessSupported: row.headlessSupported,
453
+ headlessPaths: row.headlessSupported
454
+ ? row.headlessPaths.map((headlessPath) => ({
455
+ path: headlessPath,
456
+ exists: fs.existsSync(headlessPath),
457
+ }))
458
+ : [],
459
+ headlessMessageCount: row.headlessSupported ? row.headlessMessageCount : 0,
460
+ })),
461
+ note: "Headless capture is supported for Codex CLI only.",
462
+ };
463
+ console.log(JSON.stringify(payload, null, 2));
464
+ return;
465
+ }
466
+ console.log(pc.cyan("\n Local sources & Codex headless capture"));
467
+ console.log(pc.gray(` Headless roots: ${headlessRoots.join(", ")}`));
468
+ console.log();
469
+ for (const row of sourceRows) {
470
+ console.log(pc.white(` ${row.label}`));
471
+ console.log(pc.gray(` sessions: ${describePath(row.sessionsPath)}`));
472
+ if (row.headlessSupported) {
473
+ console.log(pc.gray(` headless: ${row.headlessPaths.map(describePath).join(", ")}`));
474
+ console.log(pc.gray(` messages: ${formatNumber(row.messageCount)} (headless: ${formatNumber(row.headlessMessageCount)})`));
475
+ }
476
+ else {
477
+ console.log(pc.gray(` messages: ${formatNumber(row.messageCount)}`));
478
+ }
479
+ console.log();
480
+ }
481
+ console.log(pc.gray(" Note: Headless capture is supported for Codex CLI only."));
482
+ console.log();
483
+ });
484
+ program
485
+ .command("headless")
486
+ .description("Run a CLI in headless mode and capture stdout")
487
+ .argument("<source>", "Source CLI to capture (currently only 'codex' is supported)")
488
+ .argument("[args...]", "Arguments passed to the CLI");
212
489
  program
213
490
  .command("graph")
214
491
  .description("Export contribution graph data as JSON")
@@ -345,28 +622,75 @@ async function main() {
345
622
  cursorCommand
346
623
  .command("login")
347
624
  .description("Login to Cursor (paste your session token)")
348
- .action(async () => {
349
- await cursorLogin();
625
+ .option("--name <name>", "Label for this Cursor account (e.g., work, personal)")
626
+ .action(async (options) => {
627
+ ensureCursorMigration();
628
+ await cursorLogin(options);
350
629
  });
351
630
  cursorCommand
352
631
  .command("logout")
353
- .description("Logout from Cursor")
354
- .action(async () => {
355
- await cursorLogout();
632
+ .description("Logout from a Cursor account")
633
+ .option("--name <name>", "Account label or id")
634
+ .option("--all", "Logout from all Cursor accounts")
635
+ .option("--purge-cache", "Also delete cached Cursor usage for the logged-out account(s)")
636
+ .action(async (options) => {
637
+ ensureCursorMigration();
638
+ await cursorLogout(options);
356
639
  });
357
640
  cursorCommand
358
641
  .command("status")
359
642
  .description("Check Cursor authentication status")
360
- .action(async () => {
361
- await cursorStatus();
643
+ .option("--name <name>", "Account label or id")
644
+ .action(async (options) => {
645
+ ensureCursorMigration();
646
+ await cursorStatus(options);
647
+ });
648
+ cursorCommand
649
+ .command("accounts")
650
+ .description("List saved Cursor accounts")
651
+ .option("--json", "Output as JSON")
652
+ .action(async (options) => {
653
+ ensureCursorMigration();
654
+ const accounts = listCursorAccounts();
655
+ if (options.json) {
656
+ console.log(JSON.stringify({ accounts }, null, 2));
657
+ return;
658
+ }
659
+ if (accounts.length === 0) {
660
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
661
+ return;
662
+ }
663
+ console.log(pc.cyan("\n Cursor IDE - Accounts\n"));
664
+ for (const acct of accounts) {
665
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
666
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
667
+ }
668
+ console.log();
669
+ });
670
+ cursorCommand
671
+ .command("switch")
672
+ .description("Switch active Cursor account")
673
+ .argument("<name>", "Account label or id")
674
+ .action(async (name) => {
675
+ ensureCursorMigration();
676
+ const result = setActiveCursorAccount(name);
677
+ if (!result.ok) {
678
+ console.log(pc.red(`\n Error: ${result.error}\n`));
679
+ process.exit(1);
680
+ }
681
+ console.log(pc.green(`\n Active Cursor account set to ${pc.bold(name)}\n`));
362
682
  });
363
683
  // Check if a subcommand was provided
364
684
  const args = process.argv.slice(2);
685
+ if (args[0] === "headless") {
686
+ await runHeadlessCapture(args);
687
+ return;
688
+ }
365
689
  const firstArg = args[0] || '';
366
690
  // Global flags should go to main program
367
691
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
368
692
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
369
- const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
693
+ const knownCommands = ['monthly', 'models', 'sources', 'headless', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
370
694
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
371
695
  if (isKnownCommand || isGlobalFlag) {
372
696
  // Run the specified subcommand or show full help/version
@@ -438,8 +762,7 @@ function getEnabledSources(options) {
438
762
  * Only attempts sync if user is authenticated with Cursor.
439
763
  */
440
764
  async function syncCursorData() {
441
- const credentials = loadCursorCredentials();
442
- if (!credentials) {
765
+ if (!isCursorLoggedIn()) {
443
766
  return { attempted: false, synced: false, rows: 0 };
444
767
  }
445
768
  const result = await syncCursorCache();
@@ -478,8 +801,7 @@ async function showModelReport(options, extraOptions) {
478
801
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
479
802
  // Check cursor auth early if cursor-only mode
480
803
  if (onlyCursor) {
481
- const credentials = loadCursorCredentials();
482
- if (!credentials) {
804
+ if (!isCursorLoggedIn() && !hasCursorUsageCache()) {
483
805
  console.log(pc.red("\n Error: Cursor authentication required."));
484
806
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate with Cursor.\n"));
485
807
  process.exit(1);
@@ -500,6 +822,10 @@ async function showModelReport(options, extraOptions) {
500
822
  .filter(s => s !== 'cursor');
501
823
  spinner?.start(pc.gray("Scanning session data..."));
502
824
  const { cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters, (phase) => spinner?.update(phase));
825
+ if (includeCursor && cursorSync.attempted && cursorSync.error) {
826
+ // Don't block report generation; just warn about partial Cursor sync.
827
+ console.log(pc.yellow(` Cursor sync warning: ${cursorSync.error}`));
828
+ }
503
829
  if (!localMessages && !onlyCursor) {
504
830
  if (spinner) {
505
831
  spinner.error('Failed to parse local session files');
@@ -516,7 +842,7 @@ async function showModelReport(options, extraOptions) {
516
842
  const emptyMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
517
843
  report = await finalizeReportAsync({
518
844
  localMessages: localMessages || emptyMessages,
519
- includeCursor: includeCursor && cursorSync.synced,
845
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
520
846
  since: dateFilters.since,
521
847
  until: dateFilters.until,
522
848
  year: dateFilters.year,
@@ -608,7 +934,7 @@ async function showMonthlyReport(options, extraOptions) {
608
934
  try {
609
935
  report = await finalizeMonthlyReportAsync({
610
936
  localMessages,
611
- includeCursor: includeCursor && cursorSync.synced,
937
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
612
938
  since: dateFilters.since,
613
939
  until: dateFilters.until,
614
940
  year: dateFilters.year,
@@ -675,7 +1001,7 @@ async function outputJsonReport(reportType, options) {
675
1001
  if (reportType === "models") {
676
1002
  const report = await finalizeReportAsync({
677
1003
  localMessages: localMessages || emptyMessages,
678
- includeCursor: includeCursor && cursorSync.synced,
1004
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
679
1005
  since: dateFilters.since,
680
1006
  until: dateFilters.until,
681
1007
  year: dateFilters.year,
@@ -685,7 +1011,7 @@ async function outputJsonReport(reportType, options) {
685
1011
  else {
686
1012
  const report = await finalizeMonthlyReportAsync({
687
1013
  localMessages: localMessages || emptyMessages,
688
- includeCursor: includeCursor && cursorSync.synced,
1014
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
689
1015
  since: dateFilters.since,
690
1016
  until: dateFilters.until,
691
1017
  year: dateFilters.year,
@@ -711,7 +1037,7 @@ async function handleGraphCommand(options) {
711
1037
  const startTime = performance.now();
712
1038
  const data = await finalizeGraphAsync({
713
1039
  localMessages,
714
- includeCursor: includeCursor && cursorSync.synced,
1040
+ includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
715
1041
  since: dateFilters.since,
716
1042
  until: dateFilters.until,
717
1043
  year: dateFilters.year,
@@ -902,13 +1228,7 @@ function getSourceLabel(source) {
902
1228
  // =============================================================================
903
1229
  // Cursor IDE Authentication
904
1230
  // =============================================================================
905
- async function cursorLogin() {
906
- const credentials = loadCursorCredentials();
907
- if (credentials) {
908
- console.log(pc.yellow("\n Already logged in to Cursor."));
909
- console.log(pc.gray(" Run 'tokscale cursor logout' to sign out first.\n"));
910
- return;
911
- }
1231
+ async function cursorLogin(options = {}) {
912
1232
  console.log(pc.cyan("\n Cursor IDE - Login\n"));
913
1233
  console.log(pc.white(" To get your session token:"));
914
1234
  console.log(pc.gray(" 1. Open https://www.cursor.com/settings in your browser"));
@@ -940,40 +1260,87 @@ async function cursorLogin() {
940
1260
  console.log(pc.gray(" Please try again with a valid session token.\n"));
941
1261
  return;
942
1262
  }
943
- // Save credentials
944
- saveCursorCredentials({
945
- sessionToken: token,
946
- createdAt: new Date().toISOString(),
947
- });
1263
+ // Save credentials (multi-account)
1264
+ let savedAccountId;
1265
+ try {
1266
+ const saved = saveCursorCredentials({
1267
+ sessionToken: token,
1268
+ createdAt: new Date().toISOString(),
1269
+ }, { label: options.name });
1270
+ savedAccountId = saved.accountId;
1271
+ }
1272
+ catch (e) {
1273
+ console.log(pc.red(`\n Failed to save credentials: ${e.message}\n`));
1274
+ return;
1275
+ }
948
1276
  console.log(pc.green("\n Success! Logged in to Cursor."));
1277
+ if (options.name) {
1278
+ console.log(pc.gray(` Account: ${options.name} (${savedAccountId})`));
1279
+ }
1280
+ else {
1281
+ console.log(pc.gray(` Account ID: ${savedAccountId}`));
1282
+ }
949
1283
  if (validation.membershipType) {
950
1284
  console.log(pc.gray(` Membership: ${validation.membershipType}`));
951
1285
  }
952
1286
  console.log(pc.gray(" Your usage data will now be included in reports.\n"));
953
1287
  }
954
- async function cursorLogout() {
955
- const credentials = loadCursorCredentials();
956
- if (!credentials) {
1288
+ async function cursorLogout(options = {}) {
1289
+ if (!isCursorLoggedIn()) {
957
1290
  console.log(pc.yellow("\n Not logged in to Cursor.\n"));
958
1291
  return;
959
1292
  }
960
- const cleared = clearCursorCredentials();
961
- if (cleared) {
962
- console.log(pc.green("\n Logged out from Cursor.\n"));
963
- }
964
- else {
1293
+ if (options.all) {
1294
+ const cleared = options.purgeCache ? clearCursorCredentialsAndCache({ purgeCache: true }) : clearCursorCredentialsAndCache();
1295
+ if (cleared) {
1296
+ console.log(pc.green("\n Logged out from all Cursor accounts.\n"));
1297
+ return;
1298
+ }
965
1299
  console.error(pc.red("\n Failed to clear Cursor credentials.\n"));
966
1300
  process.exit(1);
967
1301
  }
1302
+ const target = options.name || listCursorAccounts().find((a) => a.isActive)?.id;
1303
+ if (!target) {
1304
+ console.log(pc.yellow("\n No saved Cursor accounts.\n"));
1305
+ return;
1306
+ }
1307
+ const removed = removeCursorAccount(target, { purgeCache: options.purgeCache });
1308
+ if (!removed.removed) {
1309
+ console.error(pc.red(`\n Failed to log out: ${removed.error}\n`));
1310
+ process.exit(1);
1311
+ }
1312
+ if (options.purgeCache) {
1313
+ console.log(pc.green(`\n Logged out from Cursor account (cache purged): ${pc.bold(target)}\n`));
1314
+ }
1315
+ else {
1316
+ console.log(pc.green(`\n Logged out from Cursor account (history archived): ${pc.bold(target)}\n`));
1317
+ }
968
1318
  }
969
- async function cursorStatus() {
970
- const credentials = loadCursorCredentials();
971
- if (!credentials) {
1319
+ async function cursorStatus(options = {}) {
1320
+ if (!isCursorLoggedIn()) {
972
1321
  console.log(pc.yellow("\n Not logged in to Cursor."));
973
1322
  console.log(pc.gray(" Run 'tokscale cursor login' to authenticate.\n"));
974
1323
  return;
975
1324
  }
1325
+ const accounts = listCursorAccounts();
1326
+ const target = options.name
1327
+ ? options.name
1328
+ : accounts.find((a) => a.isActive)?.id;
1329
+ const credentials = target ? loadCursorCredentials(target) : null;
1330
+ if (!credentials) {
1331
+ console.log(pc.red("\n Error: Cursor account not found."));
1332
+ console.log(pc.gray(" Run 'tokscale cursor accounts' to list saved accounts.\n"));
1333
+ process.exit(1);
1334
+ }
976
1335
  console.log(pc.cyan("\n Cursor IDE - Status\n"));
1336
+ if (accounts.length > 0) {
1337
+ console.log(pc.white(" Accounts:"));
1338
+ for (const acct of accounts) {
1339
+ const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
1340
+ console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
1341
+ }
1342
+ console.log();
1343
+ }
977
1344
  console.log(pc.gray(" Checking session validity..."));
978
1345
  const validation = await validateCursorSession(credentials.sessionToken);
979
1346
  if (validation.valid) {
@@ -984,7 +1351,7 @@ async function cursorStatus() {
984
1351
  console.log(pc.gray(` Logged in: ${new Date(credentials.createdAt).toLocaleDateString()}`));
985
1352
  // Try to fetch usage to show summary
986
1353
  try {
987
- const usage = await readCursorUsage();
1354
+ const usage = await readCursorUsage(target);
988
1355
  const totalCost = usage.byModel.reduce((sum, m) => sum + m.cost, 0);
989
1356
  console.log(pc.gray(` Models used: ${usage.byModel.length}`));
990
1357
  console.log(pc.gray(` Total usage events: ${usage.rows.length}`));