@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 +409 -42
- package/dist/cli.js.map +1 -1
- package/dist/cursor.d.ts +38 -3
- package/dist/cursor.d.ts.map +1 -1
- package/dist/cursor.js +520 -46
- package/dist/cursor.js.map +1 -1
- package/dist/native.d.ts +1 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +1 -1
- package/dist/native.js.map +1 -1
- package/dist/submit.d.ts.map +1 -1
- package/dist/submit.js +8 -4
- package/dist/submit.js.map +1 -1
- package/dist/tui/hooks/useData.d.ts.map +1 -1
- package/dist/tui/hooks/useData.js +9 -4
- package/dist/tui/hooks/useData.js.map +1 -1
- package/dist/wrapped.d.ts.map +1 -1
- package/dist/wrapped.js +8 -4
- package/dist/wrapped.js.map +1 -1
- package/package.json +2 -5
- package/src/cli.ts +499 -44
- package/src/cursor.ts +537 -51
- package/src/native.ts +2 -1
- package/src/submit.ts +9 -4
- package/src/tui/hooks/useData.ts +10 -4
- package/src/wrapped.ts +9 -4
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,
|
|
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
|
-
.
|
|
349
|
-
|
|
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
|
-
.
|
|
355
|
-
|
|
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
|
-
.
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
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}`));
|