@tokscale/cli 1.0.16 → 1.0.18

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.
Files changed (108) hide show
  1. package/dist/cli.js +220 -96
  2. package/dist/cli.js.map +1 -1
  3. package/dist/graph-types.d.ts +1 -1
  4. package/dist/graph-types.d.ts.map +1 -1
  5. package/dist/native-runner.js +5 -5
  6. package/dist/native-runner.js.map +1 -1
  7. package/dist/native.d.ts +9 -30
  8. package/dist/native.d.ts.map +1 -1
  9. package/dist/native.js +18 -134
  10. package/dist/native.js.map +1 -1
  11. package/dist/sessions/types.d.ts +1 -1
  12. package/dist/sessions/types.d.ts.map +1 -1
  13. package/dist/submit.d.ts +2 -0
  14. package/dist/submit.d.ts.map +1 -1
  15. package/dist/submit.js +32 -16
  16. package/dist/submit.js.map +1 -1
  17. package/dist/tui/App.d.ts.map +1 -1
  18. package/dist/tui/App.js +13 -6
  19. package/dist/tui/App.js.map +1 -1
  20. package/dist/tui/components/DailyView.d.ts.map +1 -1
  21. package/dist/tui/components/DailyView.js +25 -8
  22. package/dist/tui/components/DailyView.js.map +1 -1
  23. package/dist/tui/components/DateBreakdownPanel.js +2 -2
  24. package/dist/tui/components/DateBreakdownPanel.js.map +1 -1
  25. package/dist/tui/components/Footer.d.ts.map +1 -1
  26. package/dist/tui/components/Footer.js +2 -3
  27. package/dist/tui/components/Footer.js.map +1 -1
  28. package/dist/tui/components/LoadingSpinner.d.ts.map +1 -1
  29. package/dist/tui/components/LoadingSpinner.js +1 -2
  30. package/dist/tui/components/LoadingSpinner.js.map +1 -1
  31. package/dist/tui/components/ModelView.js +2 -2
  32. package/dist/tui/components/ModelView.js.map +1 -1
  33. package/dist/tui/config/settings.d.ts +4 -4
  34. package/dist/tui/config/settings.d.ts.map +1 -1
  35. package/dist/tui/config/settings.js +11 -4
  36. package/dist/tui/config/settings.js.map +1 -1
  37. package/dist/tui/hooks/useData.d.ts.map +1 -1
  38. package/dist/tui/hooks/useData.js +29 -42
  39. package/dist/tui/hooks/useData.js.map +1 -1
  40. package/dist/tui/types/index.d.ts +2 -2
  41. package/dist/tui/types/index.d.ts.map +1 -1
  42. package/dist/tui/types/index.js +3 -1
  43. package/dist/tui/types/index.js.map +1 -1
  44. package/dist/tui/utils/colors.d.ts +1 -0
  45. package/dist/tui/utils/colors.d.ts.map +1 -1
  46. package/dist/tui/utils/colors.js +7 -0
  47. package/dist/tui/utils/colors.js.map +1 -1
  48. package/dist/wrapped.d.ts.map +1 -1
  49. package/dist/wrapped.js +35 -53
  50. package/dist/wrapped.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/cli.ts +240 -103
  53. package/src/graph-types.ts +1 -1
  54. package/src/native-runner.ts +5 -5
  55. package/src/native.ts +35 -200
  56. package/src/sessions/types.ts +1 -1
  57. package/src/submit.ts +36 -22
  58. package/src/tui/App.tsx +9 -6
  59. package/src/tui/components/DailyView.tsx +29 -11
  60. package/src/tui/components/DateBreakdownPanel.tsx +2 -2
  61. package/src/tui/components/Footer.tsx +7 -2
  62. package/src/tui/components/LoadingSpinner.tsx +1 -2
  63. package/src/tui/components/ModelView.tsx +2 -2
  64. package/src/tui/config/settings.ts +18 -9
  65. package/src/tui/hooks/useData.ts +36 -47
  66. package/src/tui/types/index.ts +5 -4
  67. package/src/tui/utils/colors.ts +7 -0
  68. package/src/wrapped.ts +39 -59
  69. package/dist/graph.d.ts +0 -29
  70. package/dist/graph.d.ts.map +0 -1
  71. package/dist/graph.js +0 -383
  72. package/dist/graph.js.map +0 -1
  73. package/dist/pricing.d.ts +0 -58
  74. package/dist/pricing.d.ts.map +0 -1
  75. package/dist/pricing.js +0 -232
  76. package/dist/pricing.js.map +0 -1
  77. package/dist/sessions/claudecode.d.ts +0 -8
  78. package/dist/sessions/claudecode.d.ts.map +0 -1
  79. package/dist/sessions/claudecode.js +0 -84
  80. package/dist/sessions/claudecode.js.map +0 -1
  81. package/dist/sessions/codex.d.ts +0 -8
  82. package/dist/sessions/codex.d.ts.map +0 -1
  83. package/dist/sessions/codex.js +0 -158
  84. package/dist/sessions/codex.js.map +0 -1
  85. package/dist/sessions/gemini.d.ts +0 -8
  86. package/dist/sessions/gemini.d.ts.map +0 -1
  87. package/dist/sessions/gemini.js +0 -66
  88. package/dist/sessions/gemini.js.map +0 -1
  89. package/dist/sessions/index.d.ts +0 -32
  90. package/dist/sessions/index.d.ts.map +0 -1
  91. package/dist/sessions/index.js +0 -96
  92. package/dist/sessions/index.js.map +0 -1
  93. package/dist/sessions/opencode.d.ts +0 -9
  94. package/dist/sessions/opencode.d.ts.map +0 -1
  95. package/dist/sessions/opencode.js +0 -69
  96. package/dist/sessions/opencode.js.map +0 -1
  97. package/dist/sessions/reports.d.ts +0 -58
  98. package/dist/sessions/reports.d.ts.map +0 -1
  99. package/dist/sessions/reports.js +0 -337
  100. package/dist/sessions/reports.js.map +0 -1
  101. package/src/graph.ts +0 -485
  102. package/src/pricing.ts +0 -309
  103. package/src/sessions/claudecode.ts +0 -119
  104. package/src/sessions/codex.ts +0 -227
  105. package/src/sessions/gemini.ts +0 -108
  106. package/src/sessions/index.ts +0 -126
  107. package/src/sessions/opencode.ts +0 -117
  108. package/src/sessions/reports.ts +0 -475
package/src/cli.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * All heavy computation is done in the native Rust module.
7
7
  */
8
8
 
9
- import { Command } from "commander";
9
+ import { Command, Option } from "commander";
10
10
  import { createRequire } from "module";
11
11
  const require = createRequire(import.meta.url);
12
12
  const pkg = require("../package.json") as { version: string };
@@ -14,7 +14,7 @@ import pc from "picocolors";
14
14
  import { login, logout, whoami } from "./auth.js";
15
15
  import { submit } from "./submit.js";
16
16
  import { generateWrapped } from "./wrapped.js";
17
- import { PricingFetcher } from "./pricing.js";
17
+
18
18
  import {
19
19
  loadCursorCredentials,
20
20
  saveCursorCredentials,
@@ -48,6 +48,7 @@ import * as fs from "node:fs";
48
48
  import { performance } from "node:perf_hooks";
49
49
  import type { SourceType } from "./graph-types.js";
50
50
  import type { TUIOptions, TabType } from "./tui/types/index.js";
51
+ import { loadSettings } from "./tui/config/settings.js";
51
52
 
52
53
  type LaunchTUIFunction = (options?: TUIOptions) => Promise<void>;
53
54
 
@@ -106,6 +107,8 @@ interface FilterOptions {
106
107
  codex?: boolean;
107
108
  gemini?: boolean;
108
109
  cursor?: boolean;
110
+ amp?: boolean;
111
+ droid?: boolean;
109
112
  }
110
113
 
111
114
  interface DateFilterOptions {
@@ -204,7 +207,7 @@ async function main() {
204
207
 
205
208
  program
206
209
  .name("tokscale")
207
- .description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, and Cursor")
210
+ .description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, Cursor, and Amp")
208
211
  .version(pkg.version);
209
212
 
210
213
  program
@@ -217,6 +220,8 @@ async function main() {
217
220
  .option("--codex", "Show only Codex CLI usage")
218
221
  .option("--gemini", "Show only Gemini CLI usage")
219
222
  .option("--cursor", "Show only Cursor IDE usage")
223
+ .option("--amp", "Show only Amp usage")
224
+ .option("--droid", "Show only Factory Droid usage")
220
225
  .option("--today", "Show only today's usage")
221
226
  .option("--week", "Show last 7 days")
222
227
  .option("--month", "Show current month")
@@ -224,18 +229,19 @@ async function main() {
224
229
  .option("--until <date>", "End date (YYYY-MM-DD)")
225
230
  .option("--year <year>", "Filter to specific year")
226
231
  .option("--benchmark", "Show processing time")
232
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
227
233
  .action(async (options) => {
228
234
  if (options.json) {
229
235
  await outputJsonReport("monthly", options);
230
236
  } else if (options.light) {
231
- await showMonthlyReport(options);
237
+ await showMonthlyReport(options, { spinner: options.spinner });
232
238
  } else {
233
239
  const launchTUI = await tryLoadTUI();
234
240
  if (launchTUI) {
235
241
  await launchTUI(buildTUIOptions(options, "daily"));
236
242
  } else {
237
243
  showTUIUnavailableMessage();
238
- await showMonthlyReport(options);
244
+ await showMonthlyReport(options, { spinner: options.spinner });
239
245
  }
240
246
  }
241
247
  });
@@ -250,6 +256,8 @@ async function main() {
250
256
  .option("--codex", "Show only Codex CLI usage")
251
257
  .option("--gemini", "Show only Gemini CLI usage")
252
258
  .option("--cursor", "Show only Cursor IDE usage")
259
+ .option("--amp", "Show only Amp usage")
260
+ .option("--droid", "Show only Factory Droid usage")
253
261
  .option("--today", "Show only today's usage")
254
262
  .option("--week", "Show last 7 days")
255
263
  .option("--month", "Show current month")
@@ -257,18 +265,19 @@ async function main() {
257
265
  .option("--until <date>", "End date (YYYY-MM-DD)")
258
266
  .option("--year <year>", "Filter to specific year")
259
267
  .option("--benchmark", "Show processing time")
268
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
260
269
  .action(async (options) => {
261
270
  if (options.json) {
262
271
  await outputJsonReport("models", options);
263
272
  } else if (options.light) {
264
- await showModelReport(options);
273
+ await showModelReport(options, { spinner: options.spinner });
265
274
  } else {
266
275
  const launchTUI = await tryLoadTUI();
267
276
  if (launchTUI) {
268
277
  await launchTUI(buildTUIOptions(options, "model"));
269
278
  } else {
270
279
  showTUIUnavailableMessage();
271
- await showModelReport(options);
280
+ await showModelReport(options, { spinner: options.spinner });
272
281
  }
273
282
  }
274
283
  });
@@ -282,6 +291,8 @@ async function main() {
282
291
  .option("--codex", "Include only Codex CLI data")
283
292
  .option("--gemini", "Include only Gemini CLI data")
284
293
  .option("--cursor", "Include only Cursor IDE data")
294
+ .option("--amp", "Include only Amp data")
295
+ .option("--droid", "Include only Factory Droid data")
285
296
  .option("--today", "Show only today's usage")
286
297
  .option("--week", "Show last 7 days")
287
298
  .option("--month", "Show current month")
@@ -289,6 +300,7 @@ async function main() {
289
300
  .option("--until <date>", "End date (YYYY-MM-DD)")
290
301
  .option("--year <year>", "Filter to specific year")
291
302
  .option("--benchmark", "Show processing time")
303
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
292
304
  .action(async (options) => {
293
305
  await handleGraphCommand(options);
294
306
  });
@@ -303,10 +315,13 @@ async function main() {
303
315
  .option("--codex", "Include only Codex CLI data")
304
316
  .option("--gemini", "Include only Gemini CLI data")
305
317
  .option("--cursor", "Include only Cursor IDE data")
318
+ .option("--amp", "Include only Amp data")
319
+ .option("--droid", "Include only Factory Droid data")
306
320
  .option("--no-spinner", "Disable loading spinner (for scripting)")
307
321
  .option("--short", "Display total tokens in abbreviated format (e.g., 7.14B)")
308
- .option("--agents", "Show Top OpenCode Agents instead of Top Clients")
309
- .option("--pin-sisyphus", "Pin Sisyphus and Planner-Sisyphus at top of agents list")
322
+ .addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
323
+ .addOption(new Option("--clients", "Show Top Clients instead of Top OpenCode Agents").conflicts("agents"))
324
+ .option("--disable-pinned", "Disable pinning of Sisyphus agents in rankings")
310
325
  .action(async (options) => {
311
326
  await handleWrappedCommand(options);
312
327
  });
@@ -344,6 +359,8 @@ async function main() {
344
359
  .option("--codex", "Include only Codex CLI data")
345
360
  .option("--gemini", "Include only Gemini CLI data")
346
361
  .option("--cursor", "Include only Cursor IDE data")
362
+ .option("--amp", "Include only Amp data")
363
+ .option("--droid", "Include only Factory Droid data")
347
364
  .option("--since <date>", "Start date (YYYY-MM-DD)")
348
365
  .option("--until <date>", "End date (YYYY-MM-DD)")
349
366
  .option("--year <year>", "Filter to specific year")
@@ -355,6 +372,8 @@ async function main() {
355
372
  codex: options.codex,
356
373
  gemini: options.gemini,
357
374
  cursor: options.cursor,
375
+ amp: options.amp,
376
+ droid: options.droid,
358
377
  since: options.since,
359
378
  until: options.until,
360
379
  year: options.year,
@@ -374,6 +393,8 @@ async function main() {
374
393
  .option("--codex", "Show only Codex CLI usage")
375
394
  .option("--gemini", "Show only Gemini CLI usage")
376
395
  .option("--cursor", "Show only Cursor IDE usage")
396
+ .option("--amp", "Show only Amp usage")
397
+ .option("--droid", "Show only Factory Droid usage")
377
398
  .option("--today", "Show only today's usage")
378
399
  .option("--week", "Show last 7 days")
379
400
  .option("--month", "Show current month")
@@ -390,9 +411,15 @@ async function main() {
390
411
  }
391
412
  });
392
413
 
393
- // =========================================================================
394
- // Cursor IDE Authentication Commands
395
- // =========================================================================
414
+ program
415
+ .command("pricing <model-id>")
416
+ .description("Look up pricing for a model")
417
+ .option("--json", "Output as JSON")
418
+ .option("--provider <source>", "Force pricing source: 'litellm' or 'openrouter'")
419
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
420
+ .action(async (modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) => {
421
+ await handlePricingCommand(modelId, options);
422
+ });
396
423
 
397
424
  const cursorCommand = program
398
425
  .command("cursor")
@@ -425,7 +452,7 @@ async function main() {
425
452
  // Global flags should go to main program
426
453
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
427
454
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
428
- const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'help'];
455
+ const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
429
456
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
430
457
 
431
458
  if (isKnownCommand || isGlobalFlag) {
@@ -442,6 +469,7 @@ async function main() {
442
469
  .option("--codex", "Show only Codex CLI usage")
443
470
  .option("--gemini", "Show only Gemini CLI usage")
444
471
  .option("--cursor", "Show only Cursor IDE usage")
472
+ .option("--amp", "Show only Amp usage")
445
473
  .option("--today", "Show only today's usage")
446
474
  .option("--week", "Show last 7 days")
447
475
  .option("--month", "Show current month")
@@ -449,27 +477,28 @@ async function main() {
449
477
  .option("--until <date>", "End date (YYYY-MM-DD)")
450
478
  .option("--year <year>", "Filter to specific year")
451
479
  .option("--benchmark", "Show processing time")
480
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
452
481
  .parse();
453
482
 
454
483
  const opts = defaultProgram.opts();
455
484
  if (opts.json) {
456
485
  await outputJsonReport("models", opts);
457
486
  } else if (opts.light) {
458
- await showModelReport(opts);
487
+ await showModelReport(opts, { spinner: opts.spinner });
459
488
  } else {
460
489
  const launchTUI = await tryLoadTUI();
461
490
  if (launchTUI) {
462
491
  await launchTUI(buildTUIOptions(opts));
463
492
  } else {
464
493
  showTUIUnavailableMessage();
465
- await showModelReport(opts);
494
+ await showModelReport(opts, { spinner: opts.spinner });
466
495
  }
467
496
  }
468
497
  }
469
498
  }
470
499
 
471
500
  function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
472
- const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor;
501
+ const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid;
473
502
  if (!hasFilter) return undefined; // All sources
474
503
 
475
504
  const sources: SourceType[] = [];
@@ -478,21 +507,14 @@ function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
478
507
  if (options.codex) sources.push("codex");
479
508
  if (options.gemini) sources.push("gemini");
480
509
  if (options.cursor) sources.push("cursor");
510
+ if (options.amp) sources.push("amp");
511
+ if (options.droid) sources.push("droid");
481
512
  return sources;
482
513
  }
483
514
 
484
- function logNativeStatus(): void {
485
- if (!isNativeAvailable()) {
486
- console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
487
- console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
488
- }
489
- }
490
515
 
491
- async function fetchPricingData(): Promise<PricingFetcher> {
492
- const fetcher = new PricingFetcher();
493
- await fetcher.fetchPricing();
494
- return fetcher;
495
- }
516
+
517
+
496
518
 
497
519
  /**
498
520
  * Sync Cursor usage data from API to local cache.
@@ -514,31 +536,19 @@ async function syncCursorData(): Promise<CursorSyncResult> {
514
536
  }
515
537
 
516
538
  interface LoadedDataSources {
517
- fetcher: PricingFetcher;
518
539
  cursorSync: CursorSyncResult;
519
540
  localMessages: ParsedMessages | null;
520
541
  }
521
542
 
522
- /**
523
- * Load all data sources in parallel (two-phase optimization):
524
- * - Cursor API sync (network)
525
- * - Pricing fetch (network)
526
- * - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
527
- *
528
- * This overlaps network I/O with local file parsing for better performance.
529
- */
530
543
  async function loadDataSourcesParallel(
531
544
  localSources: SourceType[],
532
- dateFilters: { since?: string; until?: string; year?: string }
545
+ dateFilters: { since?: string; until?: string; year?: string },
546
+ onPhase?: (phase: string) => void
533
547
  ): Promise<LoadedDataSources> {
534
- // Skip local parsing if no local sources requested (e.g., cursor-only mode)
535
548
  const shouldParseLocal = localSources.length > 0;
536
549
 
537
- // Use Promise.allSettled for graceful degradation
538
- const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
550
+ const [cursorResult, localResult] = await Promise.allSettled([
539
551
  syncCursorData(),
540
- fetchPricingData(),
541
- // Parse local sources in parallel (excludes Cursor) - skip if empty
542
552
  shouldParseLocal
543
553
  ? parseLocalSourcesAsync({
544
554
  sources: localSources.filter(s => s !== 'cursor'),
@@ -549,25 +559,18 @@ async function loadDataSourcesParallel(
549
559
  : Promise.resolve(null),
550
560
  ]);
551
561
 
552
- // Handle partial failures gracefully
553
562
  const cursorSync: CursorSyncResult = cursorResult.status === 'fulfilled'
554
563
  ? cursorResult.value
555
564
  : { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
556
565
 
557
- const fetcher: PricingFetcher = pricingResult.status === 'fulfilled'
558
- ? pricingResult.value
559
- : new PricingFetcher(); // Empty pricing → costs = 0
560
-
561
566
  const localMessages: ParsedMessages | null = localResult.status === 'fulfilled'
562
567
  ? localResult.value
563
568
  : null;
564
569
 
565
- return { fetcher, cursorSync, localMessages };
570
+ return { cursorSync, localMessages };
566
571
  }
567
572
 
568
- async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
569
- logNativeStatus();
570
-
573
+ async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
571
574
  const dateFilters = getDateFilters(options);
572
575
  const enabledSources = getEnabledSources(options);
573
576
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
@@ -594,47 +597,53 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
594
597
  }
595
598
  console.log();
596
599
 
597
- // Start spinner for loading phase
598
- const spinner = createSpinner({ color: "cyan" });
599
- spinner.start(pc.gray("Loading data sources..."));
600
+ const useSpinner = extraOptions?.spinner !== false;
601
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
600
602
 
601
- // Filter out cursor for local parsing (it's synced separately via network)
602
- const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
603
+ const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
603
604
  .filter(s => s !== 'cursor');
604
605
 
605
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
606
- // If cursor-only, skip local parsing entirely
607
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(
606
+ spinner?.start(pc.gray("Scanning session data..."));
607
+
608
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(
608
609
  onlyCursor ? [] : localSources,
609
- dateFilters
610
+ dateFilters,
611
+ (phase) => spinner?.update(phase)
610
612
  );
611
613
 
612
614
  if (!localMessages && !onlyCursor) {
613
- spinner.error('Failed to parse local session files');
615
+ if (spinner) {
616
+ spinner.error('Failed to parse local session files');
617
+ } else {
618
+ console.error('Failed to parse local session files');
619
+ }
614
620
  process.exit(1);
615
621
  }
616
622
 
617
- spinner.update(pc.gray("Finalizing report..."));
623
+ spinner?.update(pc.gray("Finalizing report..."));
618
624
  const startTime = performance.now();
619
625
 
620
626
  let report: ModelReport;
621
627
  try {
622
- const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
628
+ const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
623
629
  report = await finalizeReportAsync({
624
630
  localMessages: localMessages || emptyMessages,
625
- pricing: fetcher.toPricingEntries(),
626
631
  includeCursor: includeCursor && cursorSync.synced,
627
632
  since: dateFilters.since,
628
633
  until: dateFilters.until,
629
634
  year: dateFilters.year,
630
635
  });
631
636
  } catch (e) {
632
- spinner.error(`Error: ${(e as Error).message}`);
637
+ if (spinner) {
638
+ spinner.error(`Error: ${(e as Error).message}`);
639
+ } else {
640
+ console.error(`Error: ${(e as Error).message}`);
641
+ }
633
642
  process.exit(1);
634
643
  }
635
644
 
636
645
  const processingTime = performance.now() - startTime;
637
- spinner.stop();
646
+ spinner?.stop();
638
647
 
639
648
  if (report.entries.length === 0) {
640
649
  if (onlyCursor && !cursorSync.synced) {
@@ -648,8 +657,13 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
648
657
 
649
658
  // Create table
650
659
  const table = createUsageTable("Source/Model");
660
+
661
+ const settings = loadSettings();
662
+ const filteredEntries = settings.includeUnusedModels
663
+ ? report.entries
664
+ : report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
651
665
 
652
- for (const entry of report.entries) {
666
+ for (const entry of filteredEntries) {
653
667
  const sourceLabel = getSourceLabel(entry.source);
654
668
  const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
655
669
  table.push(
@@ -701,9 +715,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
701
715
  console.log();
702
716
  }
703
717
 
704
- async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
705
- logNativeStatus();
706
-
718
+ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
707
719
  const dateRange = getDateRangeLabel(options);
708
720
  const title = dateRange
709
721
  ? `Monthly Token Usage Report (${dateRange})`
@@ -715,45 +727,55 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
715
727
  }
716
728
  console.log();
717
729
 
718
- // Start spinner for loading phase
719
- const spinner = createSpinner({ color: "cyan" });
720
- spinner.start(pc.gray("Loading data sources..."));
730
+ const useSpinner = extraOptions?.spinner !== false;
731
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
721
732
 
722
733
  const dateFilters = getDateFilters(options);
723
734
  const enabledSources = getEnabledSources(options);
724
- // Filter out cursor for local parsing (it's synced separately via network)
725
- const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
735
+ const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
726
736
  .filter(s => s !== 'cursor');
727
737
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
728
738
 
729
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
730
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters);
739
+ spinner?.start(pc.gray("Scanning session data..."));
740
+
741
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(
742
+ localSources,
743
+ dateFilters,
744
+ (phase) => spinner?.update(phase)
745
+ );
731
746
 
732
747
  if (!localMessages) {
733
- spinner.error('Failed to parse local session files');
748
+ if (spinner) {
749
+ spinner.error('Failed to parse local session files');
750
+ } else {
751
+ console.error('Failed to parse local session files');
752
+ }
734
753
  process.exit(1);
735
754
  }
736
755
 
737
- spinner.update(pc.gray("Finalizing report..."));
756
+ spinner?.update(pc.gray("Finalizing report..."));
738
757
  const startTime = performance.now();
739
758
 
740
759
  let report: MonthlyReport;
741
760
  try {
742
761
  report = await finalizeMonthlyReportAsync({
743
762
  localMessages,
744
- pricing: fetcher.toPricingEntries(),
745
763
  includeCursor: includeCursor && cursorSync.synced,
746
764
  since: dateFilters.since,
747
765
  until: dateFilters.until,
748
766
  year: dateFilters.year,
749
767
  });
750
768
  } catch (e) {
751
- spinner.error(`Error: ${(e as Error).message}`);
769
+ if (spinner) {
770
+ spinner.error(`Error: ${(e as Error).message}`);
771
+ } else {
772
+ console.error(`Error: ${(e as Error).message}`);
773
+ }
752
774
  process.exit(1);
753
775
  }
754
776
 
755
777
  const processingTime = performance.now() - startTime;
756
- spinner.stop();
778
+ spinner?.stop();
757
779
 
758
780
  if (report.entries.length === 0) {
759
781
  console.log(pc.yellow(" No usage data found.\n"));
@@ -763,7 +785,12 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
763
785
  // Create table
764
786
  const table = createUsageTable("Month");
765
787
 
766
- for (const entry of report.entries) {
788
+ const settings = loadSettings();
789
+ const filteredEntries = settings.includeUnusedModels
790
+ ? report.entries
791
+ : report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
792
+
793
+ for (const entry of filteredEntries) {
767
794
  table.push(
768
795
  formatUsageRow(
769
796
  entry.month,
@@ -810,16 +837,14 @@ async function outputJsonReport(
810
837
  reportType: JsonReportType,
811
838
  options: FilterOptions & DateFilterOptions
812
839
  ) {
813
- logNativeStatus();
814
-
815
840
  const dateFilters = getDateFilters(options);
816
841
  const enabledSources = getEnabledSources(options);
817
842
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
818
843
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
819
- const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
844
+ const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
820
845
  .filter(s => s !== 'cursor');
821
846
 
822
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(
847
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(
823
848
  onlyCursor ? [] : localSources,
824
849
  dateFilters
825
850
  );
@@ -829,12 +854,11 @@ async function outputJsonReport(
829
854
  process.exit(1);
830
855
  }
831
856
 
832
- const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
857
+ const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
833
858
 
834
859
  if (reportType === "models") {
835
860
  const report = await finalizeReportAsync({
836
861
  localMessages: localMessages || emptyMessages,
837
- pricing: fetcher.toPricingEntries(),
838
862
  includeCursor: includeCursor && cursorSync.synced,
839
863
  since: dateFilters.since,
840
864
  until: dateFilters.until,
@@ -844,7 +868,6 @@ async function outputJsonReport(
844
868
  } else {
845
869
  const report = await finalizeMonthlyReportAsync({
846
870
  localMessages: localMessages || emptyMessages,
847
- pricing: fetcher.toPricingEntries(),
848
871
  includeCursor: includeCursor && cursorSync.synced,
849
872
  since: dateFilters.since,
850
873
  until: dateFilters.until,
@@ -857,24 +880,26 @@ async function outputJsonReport(
857
880
  interface GraphCommandOptions extends FilterOptions, DateFilterOptions {
858
881
  output?: string;
859
882
  benchmark?: boolean;
883
+ spinner?: boolean;
860
884
  }
861
885
 
862
886
  async function handleGraphCommand(options: GraphCommandOptions) {
863
- logNativeStatus();
864
-
865
- // Start spinner for loading phase (only if outputting to file, not stdout)
866
- const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
867
- spinner?.start(pc.gray("Loading data sources..."));
887
+ const useSpinner = options.output && options.spinner !== false;
888
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
868
889
 
869
890
  const dateFilters = getDateFilters(options);
870
891
  const enabledSources = getEnabledSources(options);
871
- // Filter out cursor for local parsing (it's synced separately via network)
872
- const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
892
+ const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
873
893
  .filter(s => s !== 'cursor');
874
894
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
875
895
 
876
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
877
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters);
896
+ spinner?.start(pc.gray("Scanning session data..."));
897
+
898
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(
899
+ localSources,
900
+ dateFilters,
901
+ (phase) => spinner?.update(phase)
902
+ );
878
903
 
879
904
  if (!localMessages) {
880
905
  spinner?.error('Failed to parse local session files');
@@ -886,7 +911,6 @@ async function handleGraphCommand(options: GraphCommandOptions) {
886
911
 
887
912
  const data = await finalizeGraphAsync({
888
913
  localMessages,
889
- pricing: fetcher.toPricingEntries(),
890
914
  includeCursor: includeCursor && cursorSync.synced,
891
915
  since: dateFilters.since,
892
916
  until: dateFilters.until,
@@ -929,7 +953,8 @@ interface WrappedCommandOptions extends FilterOptions {
929
953
  spinner?: boolean;
930
954
  short?: boolean;
931
955
  agents?: boolean;
932
- pinSisyphus?: boolean;
956
+ clients?: boolean;
957
+ disablePinned?: boolean;
933
958
  }
934
959
 
935
960
  async function handleWrappedCommand(options: WrappedCommandOptions) {
@@ -946,8 +971,8 @@ async function handleWrappedCommand(options: WrappedCommandOptions) {
946
971
  year,
947
972
  sources: enabledSources,
948
973
  short: options.short,
949
- includeAgents: options.agents,
950
- pinSisyphus: options.pinSisyphus,
974
+ includeAgents: !options.clients,
975
+ pinSisyphus: !options.disablePinned,
951
976
  });
952
977
 
953
978
  spinner?.stop();
@@ -966,6 +991,114 @@ async function handleWrappedCommand(options: WrappedCommandOptions) {
966
991
  }
967
992
  }
968
993
 
994
+ async function handlePricingCommand(modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) {
995
+ const validProviders = ["litellm", "openrouter"];
996
+ if (options.provider && !validProviders.includes(options.provider.toLowerCase())) {
997
+ console.log(pc.red(`\n Invalid provider: ${options.provider}`));
998
+ console.log(pc.gray(` Valid providers: ${validProviders.join(", ")}\n`));
999
+ process.exit(1);
1000
+ }
1001
+
1002
+ const useSpinner = options.spinner !== false;
1003
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
1004
+ const providerLabel = options.provider ? ` from ${options.provider}` : "";
1005
+ spinner?.start(pc.gray(`Fetching pricing data${providerLabel}...`));
1006
+
1007
+ let core: typeof import("@tokscale/core");
1008
+ try {
1009
+ const mod = await import("@tokscale/core");
1010
+ core = (mod.default ?? mod) as typeof import("@tokscale/core");
1011
+ } catch (importErr) {
1012
+ spinner?.stop();
1013
+ const errorMsg = (importErr as Error).message || "Unknown error";
1014
+ if (options.json) {
1015
+ console.log(JSON.stringify({ error: "Native module not available", details: errorMsg }, null, 2));
1016
+ } else {
1017
+ console.log(pc.red(`\n Native module not available: ${errorMsg}`));
1018
+ console.log(pc.gray(" Run 'bun run build:core' to build the native module.\n"));
1019
+ }
1020
+ process.exit(1);
1021
+ }
1022
+
1023
+ try {
1024
+ const provider = options.provider?.toLowerCase() || undefined;
1025
+ const nativeResult = await core.lookupPricing(modelId, provider);
1026
+ spinner?.stop();
1027
+
1028
+ const result = {
1029
+ matchedKey: nativeResult.matchedKey,
1030
+ source: nativeResult.source as "litellm" | "openrouter",
1031
+ pricing: {
1032
+ input_cost_per_token: nativeResult.pricing.inputCostPerToken,
1033
+ output_cost_per_token: nativeResult.pricing.outputCostPerToken,
1034
+ cache_read_input_token_cost: nativeResult.pricing.cacheReadInputTokenCost,
1035
+ cache_creation_input_token_cost: nativeResult.pricing.cacheCreationInputTokenCost,
1036
+ },
1037
+ };
1038
+
1039
+ if (options.json) {
1040
+ console.log(JSON.stringify({
1041
+ modelId,
1042
+ matchedKey: result.matchedKey,
1043
+ source: result.source,
1044
+ pricing: {
1045
+ inputCostPerToken: result.pricing.input_cost_per_token ?? 0,
1046
+ outputCostPerToken: result.pricing.output_cost_per_token ?? 0,
1047
+ cacheReadInputTokenCost: result.pricing.cache_read_input_token_cost,
1048
+ cacheCreationInputTokenCost: result.pricing.cache_creation_input_token_cost,
1049
+ },
1050
+ }, null, 2));
1051
+ } else {
1052
+ const sourceLabel = result.source.toLowerCase() === "litellm" ? pc.blue("LiteLLM") : pc.magenta("OpenRouter");
1053
+ const inputCost = result.pricing.input_cost_per_token ?? 0;
1054
+ const outputCost = result.pricing.output_cost_per_token ?? 0;
1055
+ const cacheReadCost = result.pricing.cache_read_input_token_cost;
1056
+ const cacheWriteCost = result.pricing.cache_creation_input_token_cost;
1057
+
1058
+ console.log(pc.cyan(`\n Pricing for: ${pc.white(modelId)}`));
1059
+ console.log(pc.gray(` Matched key: ${result.matchedKey}`));
1060
+ console.log(pc.gray(` Source: `) + sourceLabel);
1061
+ console.log();
1062
+ console.log(pc.white(` Input: `) + formatPricePerMillion(inputCost));
1063
+ console.log(pc.white(` Output: `) + formatPricePerMillion(outputCost));
1064
+ if (cacheReadCost !== undefined) {
1065
+ console.log(pc.white(` Cache Read: `) + formatPricePerMillion(cacheReadCost));
1066
+ }
1067
+ if (cacheWriteCost !== undefined) {
1068
+ console.log(pc.white(` Cache Write: `) + formatPricePerMillion(cacheWriteCost));
1069
+ }
1070
+ console.log();
1071
+ }
1072
+ } catch (err) {
1073
+ spinner?.stop();
1074
+ const errorMsg = (err as Error).message || "Unknown error";
1075
+
1076
+ // Check if this is a "model not found" error from Rust or a different error
1077
+ const isModelNotFound = errorMsg.toLowerCase().includes("not found") ||
1078
+ errorMsg.toLowerCase().includes("no pricing");
1079
+
1080
+ if (options.json) {
1081
+ if (isModelNotFound) {
1082
+ console.log(JSON.stringify({ error: "Model not found", modelId }, null, 2));
1083
+ } else {
1084
+ console.log(JSON.stringify({ error: errorMsg, modelId }, null, 2));
1085
+ }
1086
+ } else {
1087
+ if (isModelNotFound) {
1088
+ console.log(pc.red(`\n Model not found: ${modelId}\n`));
1089
+ } else {
1090
+ console.log(pc.red(`\n Error looking up pricing: ${errorMsg}\n`));
1091
+ }
1092
+ }
1093
+ process.exit(1);
1094
+ }
1095
+ }
1096
+
1097
+ function formatPricePerMillion(costPerToken: number): string {
1098
+ const perMillion = costPerToken * 1_000_000;
1099
+ return pc.green(`$${perMillion.toFixed(2)}`) + pc.gray(" / 1M tokens");
1100
+ }
1101
+
969
1102
  function getSourceLabel(source: string): string {
970
1103
  switch (source) {
971
1104
  case "opencode":
@@ -978,6 +1111,10 @@ function getSourceLabel(source: string): string {
978
1111
  return "Gemini";
979
1112
  case "cursor":
980
1113
  return "Cursor";
1114
+ case "amp":
1115
+ return "Amp";
1116
+ case "droid":
1117
+ return "Droid";
981
1118
  default:
982
1119
  return source;
983
1120
  }