@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/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
|
-
.
|
|
432
|
-
|
|
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
|
-
.
|
|
439
|
-
|
|
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
|
-
.
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
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}`));
|