@tokscale/cli 1.0.17 → 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 +214 -91
  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 +20 -48
  50. package/dist/wrapped.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/cli.ts +232 -97
  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 +21 -54
  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/dist/cli.js CHANGED
@@ -13,13 +13,13 @@ 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 { PricingFetcher } from "./pricing.js";
17
16
  import { loadCursorCredentials, saveCursorCredentials, clearCursorCredentials, validateCursorSession, readCursorUsage, getCursorCredentialsPath, syncCursorCache, } from "./cursor.js";
18
17
  import { createUsageTable, formatUsageRow, formatTotalsRow, formatNumber, formatCurrency, formatModelName, } from "./table.js";
19
- import { isNativeAvailable, getNativeVersion, parseLocalSourcesAsync, finalizeReportAsync, finalizeMonthlyReportAsync, finalizeGraphAsync, } from "./native.js";
18
+ import { getNativeVersion, parseLocalSourcesAsync, finalizeReportAsync, finalizeMonthlyReportAsync, finalizeGraphAsync, } from "./native.js";
20
19
  import { createSpinner } from "./spinner.js";
21
20
  import * as fs from "node:fs";
22
21
  import { performance } from "node:perf_hooks";
22
+ import { loadSettings } from "./tui/config/settings.js";
23
23
  let cachedTUILoader = null;
24
24
  let tuiLoadAttempted = false;
25
25
  async function tryLoadTUI() {
@@ -131,7 +131,7 @@ async function main() {
131
131
  const program = new Command();
132
132
  program
133
133
  .name("tokscale")
134
- .description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, and Cursor")
134
+ .description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, Cursor, and Amp")
135
135
  .version(pkg.version);
136
136
  program
137
137
  .command("monthly")
@@ -143,6 +143,8 @@ async function main() {
143
143
  .option("--codex", "Show only Codex CLI usage")
144
144
  .option("--gemini", "Show only Gemini CLI usage")
145
145
  .option("--cursor", "Show only Cursor IDE usage")
146
+ .option("--amp", "Show only Amp usage")
147
+ .option("--droid", "Show only Factory Droid usage")
146
148
  .option("--today", "Show only today's usage")
147
149
  .option("--week", "Show last 7 days")
148
150
  .option("--month", "Show current month")
@@ -150,12 +152,13 @@ async function main() {
150
152
  .option("--until <date>", "End date (YYYY-MM-DD)")
151
153
  .option("--year <year>", "Filter to specific year")
152
154
  .option("--benchmark", "Show processing time")
155
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
153
156
  .action(async (options) => {
154
157
  if (options.json) {
155
158
  await outputJsonReport("monthly", options);
156
159
  }
157
160
  else if (options.light) {
158
- await showMonthlyReport(options);
161
+ await showMonthlyReport(options, { spinner: options.spinner });
159
162
  }
160
163
  else {
161
164
  const launchTUI = await tryLoadTUI();
@@ -164,7 +167,7 @@ async function main() {
164
167
  }
165
168
  else {
166
169
  showTUIUnavailableMessage();
167
- await showMonthlyReport(options);
170
+ await showMonthlyReport(options, { spinner: options.spinner });
168
171
  }
169
172
  }
170
173
  });
@@ -178,6 +181,8 @@ async function main() {
178
181
  .option("--codex", "Show only Codex CLI usage")
179
182
  .option("--gemini", "Show only Gemini CLI usage")
180
183
  .option("--cursor", "Show only Cursor IDE usage")
184
+ .option("--amp", "Show only Amp usage")
185
+ .option("--droid", "Show only Factory Droid usage")
181
186
  .option("--today", "Show only today's usage")
182
187
  .option("--week", "Show last 7 days")
183
188
  .option("--month", "Show current month")
@@ -185,12 +190,13 @@ async function main() {
185
190
  .option("--until <date>", "End date (YYYY-MM-DD)")
186
191
  .option("--year <year>", "Filter to specific year")
187
192
  .option("--benchmark", "Show processing time")
193
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
188
194
  .action(async (options) => {
189
195
  if (options.json) {
190
196
  await outputJsonReport("models", options);
191
197
  }
192
198
  else if (options.light) {
193
- await showModelReport(options);
199
+ await showModelReport(options, { spinner: options.spinner });
194
200
  }
195
201
  else {
196
202
  const launchTUI = await tryLoadTUI();
@@ -199,7 +205,7 @@ async function main() {
199
205
  }
200
206
  else {
201
207
  showTUIUnavailableMessage();
202
- await showModelReport(options);
208
+ await showModelReport(options, { spinner: options.spinner });
203
209
  }
204
210
  }
205
211
  });
@@ -212,6 +218,8 @@ async function main() {
212
218
  .option("--codex", "Include only Codex CLI data")
213
219
  .option("--gemini", "Include only Gemini CLI data")
214
220
  .option("--cursor", "Include only Cursor IDE data")
221
+ .option("--amp", "Include only Amp data")
222
+ .option("--droid", "Include only Factory Droid data")
215
223
  .option("--today", "Show only today's usage")
216
224
  .option("--week", "Show last 7 days")
217
225
  .option("--month", "Show current month")
@@ -219,6 +227,7 @@ async function main() {
219
227
  .option("--until <date>", "End date (YYYY-MM-DD)")
220
228
  .option("--year <year>", "Filter to specific year")
221
229
  .option("--benchmark", "Show processing time")
230
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
222
231
  .action(async (options) => {
223
232
  await handleGraphCommand(options);
224
233
  });
@@ -232,6 +241,8 @@ async function main() {
232
241
  .option("--codex", "Include only Codex CLI data")
233
242
  .option("--gemini", "Include only Gemini CLI data")
234
243
  .option("--cursor", "Include only Cursor IDE data")
244
+ .option("--amp", "Include only Amp data")
245
+ .option("--droid", "Include only Factory Droid data")
235
246
  .option("--no-spinner", "Disable loading spinner (for scripting)")
236
247
  .option("--short", "Display total tokens in abbreviated format (e.g., 7.14B)")
237
248
  .addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
@@ -269,6 +280,8 @@ async function main() {
269
280
  .option("--codex", "Include only Codex CLI data")
270
281
  .option("--gemini", "Include only Gemini CLI data")
271
282
  .option("--cursor", "Include only Cursor IDE data")
283
+ .option("--amp", "Include only Amp data")
284
+ .option("--droid", "Include only Factory Droid data")
272
285
  .option("--since <date>", "Start date (YYYY-MM-DD)")
273
286
  .option("--until <date>", "End date (YYYY-MM-DD)")
274
287
  .option("--year <year>", "Filter to specific year")
@@ -280,6 +293,8 @@ async function main() {
280
293
  codex: options.codex,
281
294
  gemini: options.gemini,
282
295
  cursor: options.cursor,
296
+ amp: options.amp,
297
+ droid: options.droid,
283
298
  since: options.since,
284
299
  until: options.until,
285
300
  year: options.year,
@@ -297,6 +312,8 @@ async function main() {
297
312
  .option("--codex", "Show only Codex CLI usage")
298
313
  .option("--gemini", "Show only Gemini CLI usage")
299
314
  .option("--cursor", "Show only Cursor IDE usage")
315
+ .option("--amp", "Show only Amp usage")
316
+ .option("--droid", "Show only Factory Droid usage")
300
317
  .option("--today", "Show only today's usage")
301
318
  .option("--week", "Show last 7 days")
302
319
  .option("--month", "Show current month")
@@ -313,9 +330,15 @@ async function main() {
313
330
  process.exit(1);
314
331
  }
315
332
  });
316
- // =========================================================================
317
- // Cursor IDE Authentication Commands
318
- // =========================================================================
333
+ program
334
+ .command("pricing <model-id>")
335
+ .description("Look up pricing for a model")
336
+ .option("--json", "Output as JSON")
337
+ .option("--provider <source>", "Force pricing source: 'litellm' or 'openrouter'")
338
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
339
+ .action(async (modelId, options) => {
340
+ await handlePricingCommand(modelId, options);
341
+ });
319
342
  const cursorCommand = program
320
343
  .command("cursor")
321
344
  .description("Cursor IDE integration commands");
@@ -343,7 +366,7 @@ async function main() {
343
366
  // Global flags should go to main program
344
367
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
345
368
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
346
- const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'help'];
369
+ const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
347
370
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
348
371
  if (isKnownCommand || isGlobalFlag) {
349
372
  // Run the specified subcommand or show full help/version
@@ -360,6 +383,7 @@ async function main() {
360
383
  .option("--codex", "Show only Codex CLI usage")
361
384
  .option("--gemini", "Show only Gemini CLI usage")
362
385
  .option("--cursor", "Show only Cursor IDE usage")
386
+ .option("--amp", "Show only Amp usage")
363
387
  .option("--today", "Show only today's usage")
364
388
  .option("--week", "Show last 7 days")
365
389
  .option("--month", "Show current month")
@@ -367,13 +391,14 @@ async function main() {
367
391
  .option("--until <date>", "End date (YYYY-MM-DD)")
368
392
  .option("--year <year>", "Filter to specific year")
369
393
  .option("--benchmark", "Show processing time")
394
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
370
395
  .parse();
371
396
  const opts = defaultProgram.opts();
372
397
  if (opts.json) {
373
398
  await outputJsonReport("models", opts);
374
399
  }
375
400
  else if (opts.light) {
376
- await showModelReport(opts);
401
+ await showModelReport(opts, { spinner: opts.spinner });
377
402
  }
378
403
  else {
379
404
  const launchTUI = await tryLoadTUI();
@@ -382,13 +407,13 @@ async function main() {
382
407
  }
383
408
  else {
384
409
  showTUIUnavailableMessage();
385
- await showModelReport(opts);
410
+ await showModelReport(opts, { spinner: opts.spinner });
386
411
  }
387
412
  }
388
413
  }
389
414
  }
390
415
  function getEnabledSources(options) {
391
- const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor;
416
+ const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid;
392
417
  if (!hasFilter)
393
418
  return undefined; // All sources
394
419
  const sources = [];
@@ -402,19 +427,12 @@ function getEnabledSources(options) {
402
427
  sources.push("gemini");
403
428
  if (options.cursor)
404
429
  sources.push("cursor");
430
+ if (options.amp)
431
+ sources.push("amp");
432
+ if (options.droid)
433
+ sources.push("droid");
405
434
  return sources;
406
435
  }
407
- function logNativeStatus() {
408
- if (!isNativeAvailable()) {
409
- console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
410
- console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
411
- }
412
- }
413
- async function fetchPricingData() {
414
- const fetcher = new PricingFetcher();
415
- await fetcher.fetchPricing();
416
- return fetcher;
417
- }
418
436
  /**
419
437
  * Sync Cursor usage data from API to local cache.
420
438
  * Only attempts sync if user is authenticated with Cursor.
@@ -432,22 +450,10 @@ async function syncCursorData() {
432
450
  error: result.error,
433
451
  };
434
452
  }
435
- /**
436
- * Load all data sources in parallel (two-phase optimization):
437
- * - Cursor API sync (network)
438
- * - Pricing fetch (network)
439
- * - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
440
- *
441
- * This overlaps network I/O with local file parsing for better performance.
442
- */
443
- async function loadDataSourcesParallel(localSources, dateFilters) {
444
- // Skip local parsing if no local sources requested (e.g., cursor-only mode)
453
+ async function loadDataSourcesParallel(localSources, dateFilters, onPhase) {
445
454
  const shouldParseLocal = localSources.length > 0;
446
- // Use Promise.allSettled for graceful degradation
447
- const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
455
+ const [cursorResult, localResult] = await Promise.allSettled([
448
456
  syncCursorData(),
449
- fetchPricingData(),
450
- // Parse local sources in parallel (excludes Cursor) - skip if empty
451
457
  shouldParseLocal
452
458
  ? parseLocalSourcesAsync({
453
459
  sources: localSources.filter(s => s !== 'cursor'),
@@ -457,20 +463,15 @@ async function loadDataSourcesParallel(localSources, dateFilters) {
457
463
  })
458
464
  : Promise.resolve(null),
459
465
  ]);
460
- // Handle partial failures gracefully
461
466
  const cursorSync = cursorResult.status === 'fulfilled'
462
467
  ? cursorResult.value
463
468
  : { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
464
- const fetcher = pricingResult.status === 'fulfilled'
465
- ? pricingResult.value
466
- : new PricingFetcher(); // Empty pricing → costs = 0
467
469
  const localMessages = localResult.status === 'fulfilled'
468
470
  ? localResult.value
469
471
  : null;
470
- return { fetcher, cursorSync, localMessages };
472
+ return { cursorSync, localMessages };
471
473
  }
472
- async function showModelReport(options) {
473
- logNativeStatus();
474
+ async function showModelReport(options, extraOptions) {
474
475
  const dateFilters = getDateFilters(options);
475
476
  const enabledSources = getEnabledSources(options);
476
477
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
@@ -493,27 +494,28 @@ async function showModelReport(options) {
493
494
  console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
494
495
  }
495
496
  console.log();
496
- // Start spinner for loading phase
497
- const spinner = createSpinner({ color: "cyan" });
498
- spinner.start(pc.gray("Loading data sources..."));
499
- // Filter out cursor for local parsing (it's synced separately via network)
500
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
497
+ const useSpinner = extraOptions?.spinner !== false;
498
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
499
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
501
500
  .filter(s => s !== 'cursor');
502
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
503
- // If cursor-only, skip local parsing entirely
504
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
501
+ spinner?.start(pc.gray("Scanning session data..."));
502
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters, (phase) => spinner?.update(phase));
505
503
  if (!localMessages && !onlyCursor) {
506
- spinner.error('Failed to parse local session files');
504
+ if (spinner) {
505
+ spinner.error('Failed to parse local session files');
506
+ }
507
+ else {
508
+ console.error('Failed to parse local session files');
509
+ }
507
510
  process.exit(1);
508
511
  }
509
- spinner.update(pc.gray("Finalizing report..."));
512
+ spinner?.update(pc.gray("Finalizing report..."));
510
513
  const startTime = performance.now();
511
514
  let report;
512
515
  try {
513
- const emptyMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
516
+ const emptyMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
514
517
  report = await finalizeReportAsync({
515
518
  localMessages: localMessages || emptyMessages,
516
- pricing: fetcher.toPricingEntries(),
517
519
  includeCursor: includeCursor && cursorSync.synced,
518
520
  since: dateFilters.since,
519
521
  until: dateFilters.until,
@@ -521,11 +523,16 @@ async function showModelReport(options) {
521
523
  });
522
524
  }
523
525
  catch (e) {
524
- spinner.error(`Error: ${e.message}`);
526
+ if (spinner) {
527
+ spinner.error(`Error: ${e.message}`);
528
+ }
529
+ else {
530
+ console.error(`Error: ${e.message}`);
531
+ }
525
532
  process.exit(1);
526
533
  }
527
534
  const processingTime = performance.now() - startTime;
528
- spinner.stop();
535
+ spinner?.stop();
529
536
  if (report.entries.length === 0) {
530
537
  if (onlyCursor && !cursorSync.synced) {
531
538
  console.log(pc.yellow(" No Cursor data available."));
@@ -538,7 +545,11 @@ async function showModelReport(options) {
538
545
  }
539
546
  // Create table
540
547
  const table = createUsageTable("Source/Model");
541
- for (const entry of report.entries) {
548
+ const settings = loadSettings();
549
+ const filteredEntries = settings.includeUnusedModels
550
+ ? report.entries
551
+ : report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
552
+ for (const entry of filteredEntries) {
542
553
  const sourceLabel = getSourceLabel(entry.source);
543
554
  const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
544
555
  table.push(formatUsageRow(modelDisplay, [entry.model], entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
@@ -563,8 +574,7 @@ async function showModelReport(options) {
563
574
  }
564
575
  console.log();
565
576
  }
566
- async function showMonthlyReport(options) {
567
- logNativeStatus();
577
+ async function showMonthlyReport(options, extraOptions) {
568
578
  const dateRange = getDateRangeLabel(options);
569
579
  const title = dateRange
570
580
  ? `Monthly Token Usage Report (${dateRange})`
@@ -574,28 +584,30 @@ async function showMonthlyReport(options) {
574
584
  console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
575
585
  }
576
586
  console.log();
577
- // Start spinner for loading phase
578
- const spinner = createSpinner({ color: "cyan" });
579
- spinner.start(pc.gray("Loading data sources..."));
587
+ const useSpinner = extraOptions?.spinner !== false;
588
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
580
589
  const dateFilters = getDateFilters(options);
581
590
  const enabledSources = getEnabledSources(options);
582
- // Filter out cursor for local parsing (it's synced separately via network)
583
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
591
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
584
592
  .filter(s => s !== 'cursor');
585
593
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
586
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
587
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters);
594
+ spinner?.start(pc.gray("Scanning session data..."));
595
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters, (phase) => spinner?.update(phase));
588
596
  if (!localMessages) {
589
- spinner.error('Failed to parse local session files');
597
+ if (spinner) {
598
+ spinner.error('Failed to parse local session files');
599
+ }
600
+ else {
601
+ console.error('Failed to parse local session files');
602
+ }
590
603
  process.exit(1);
591
604
  }
592
- spinner.update(pc.gray("Finalizing report..."));
605
+ spinner?.update(pc.gray("Finalizing report..."));
593
606
  const startTime = performance.now();
594
607
  let report;
595
608
  try {
596
609
  report = await finalizeMonthlyReportAsync({
597
610
  localMessages,
598
- pricing: fetcher.toPricingEntries(),
599
611
  includeCursor: includeCursor && cursorSync.synced,
600
612
  since: dateFilters.since,
601
613
  until: dateFilters.until,
@@ -603,18 +615,27 @@ async function showMonthlyReport(options) {
603
615
  });
604
616
  }
605
617
  catch (e) {
606
- spinner.error(`Error: ${e.message}`);
618
+ if (spinner) {
619
+ spinner.error(`Error: ${e.message}`);
620
+ }
621
+ else {
622
+ console.error(`Error: ${e.message}`);
623
+ }
607
624
  process.exit(1);
608
625
  }
609
626
  const processingTime = performance.now() - startTime;
610
- spinner.stop();
627
+ spinner?.stop();
611
628
  if (report.entries.length === 0) {
612
629
  console.log(pc.yellow(" No usage data found.\n"));
613
630
  return;
614
631
  }
615
632
  // Create table
616
633
  const table = createUsageTable("Month");
617
- for (const entry of report.entries) {
634
+ const settings = loadSettings();
635
+ const filteredEntries = settings.includeUnusedModels
636
+ ? report.entries
637
+ : report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
638
+ for (const entry of filteredEntries) {
618
639
  table.push(formatUsageRow(entry.month, entry.models, entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
619
640
  }
620
641
  // Add totals row
@@ -639,23 +660,21 @@ async function showMonthlyReport(options) {
639
660
  console.log();
640
661
  }
641
662
  async function outputJsonReport(reportType, options) {
642
- logNativeStatus();
643
663
  const dateFilters = getDateFilters(options);
644
664
  const enabledSources = getEnabledSources(options);
645
665
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
646
666
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
647
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
667
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
648
668
  .filter(s => s !== 'cursor');
649
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
669
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
650
670
  if (!localMessages && !onlyCursor) {
651
671
  console.error(JSON.stringify({ error: "Failed to parse local session files" }));
652
672
  process.exit(1);
653
673
  }
654
- const emptyMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
674
+ const emptyMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
655
675
  if (reportType === "models") {
656
676
  const report = await finalizeReportAsync({
657
677
  localMessages: localMessages || emptyMessages,
658
- pricing: fetcher.toPricingEntries(),
659
678
  includeCursor: includeCursor && cursorSync.synced,
660
679
  since: dateFilters.since,
661
680
  until: dateFilters.until,
@@ -666,7 +685,6 @@ async function outputJsonReport(reportType, options) {
666
685
  else {
667
686
  const report = await finalizeMonthlyReportAsync({
668
687
  localMessages: localMessages || emptyMessages,
669
- pricing: fetcher.toPricingEntries(),
670
688
  includeCursor: includeCursor && cursorSync.synced,
671
689
  since: dateFilters.since,
672
690
  until: dateFilters.until,
@@ -676,18 +694,15 @@ async function outputJsonReport(reportType, options) {
676
694
  }
677
695
  }
678
696
  async function handleGraphCommand(options) {
679
- logNativeStatus();
680
- // Start spinner for loading phase (only if outputting to file, not stdout)
681
- const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
682
- spinner?.start(pc.gray("Loading data sources..."));
697
+ const useSpinner = options.output && options.spinner !== false;
698
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
683
699
  const dateFilters = getDateFilters(options);
684
700
  const enabledSources = getEnabledSources(options);
685
- // Filter out cursor for local parsing (it's synced separately via network)
686
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
701
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
687
702
  .filter(s => s !== 'cursor');
688
703
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
689
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
690
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters);
704
+ spinner?.start(pc.gray("Scanning session data..."));
705
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters, (phase) => spinner?.update(phase));
691
706
  if (!localMessages) {
692
707
  spinner?.error('Failed to parse local session files');
693
708
  process.exit(1);
@@ -696,7 +711,6 @@ async function handleGraphCommand(options) {
696
711
  const startTime = performance.now();
697
712
  const data = await finalizeGraphAsync({
698
713
  localMessages,
699
- pricing: fetcher.toPricingEntries(),
700
714
  includeCursor: includeCursor && cursorSync.synced,
701
715
  since: dateFilters.since,
702
716
  until: dateFilters.until,
@@ -760,6 +774,111 @@ async function handleWrappedCommand(options) {
760
774
  process.exit(1);
761
775
  }
762
776
  }
777
+ async function handlePricingCommand(modelId, options) {
778
+ const validProviders = ["litellm", "openrouter"];
779
+ if (options.provider && !validProviders.includes(options.provider.toLowerCase())) {
780
+ console.log(pc.red(`\n Invalid provider: ${options.provider}`));
781
+ console.log(pc.gray(` Valid providers: ${validProviders.join(", ")}\n`));
782
+ process.exit(1);
783
+ }
784
+ const useSpinner = options.spinner !== false;
785
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
786
+ const providerLabel = options.provider ? ` from ${options.provider}` : "";
787
+ spinner?.start(pc.gray(`Fetching pricing data${providerLabel}...`));
788
+ let core;
789
+ try {
790
+ const mod = await import("@tokscale/core");
791
+ core = (mod.default ?? mod);
792
+ }
793
+ catch (importErr) {
794
+ spinner?.stop();
795
+ const errorMsg = importErr.message || "Unknown error";
796
+ if (options.json) {
797
+ console.log(JSON.stringify({ error: "Native module not available", details: errorMsg }, null, 2));
798
+ }
799
+ else {
800
+ console.log(pc.red(`\n Native module not available: ${errorMsg}`));
801
+ console.log(pc.gray(" Run 'bun run build:core' to build the native module.\n"));
802
+ }
803
+ process.exit(1);
804
+ }
805
+ try {
806
+ const provider = options.provider?.toLowerCase() || undefined;
807
+ const nativeResult = await core.lookupPricing(modelId, provider);
808
+ spinner?.stop();
809
+ const result = {
810
+ matchedKey: nativeResult.matchedKey,
811
+ source: nativeResult.source,
812
+ pricing: {
813
+ input_cost_per_token: nativeResult.pricing.inputCostPerToken,
814
+ output_cost_per_token: nativeResult.pricing.outputCostPerToken,
815
+ cache_read_input_token_cost: nativeResult.pricing.cacheReadInputTokenCost,
816
+ cache_creation_input_token_cost: nativeResult.pricing.cacheCreationInputTokenCost,
817
+ },
818
+ };
819
+ if (options.json) {
820
+ console.log(JSON.stringify({
821
+ modelId,
822
+ matchedKey: result.matchedKey,
823
+ source: result.source,
824
+ pricing: {
825
+ inputCostPerToken: result.pricing.input_cost_per_token ?? 0,
826
+ outputCostPerToken: result.pricing.output_cost_per_token ?? 0,
827
+ cacheReadInputTokenCost: result.pricing.cache_read_input_token_cost,
828
+ cacheCreationInputTokenCost: result.pricing.cache_creation_input_token_cost,
829
+ },
830
+ }, null, 2));
831
+ }
832
+ else {
833
+ const sourceLabel = result.source.toLowerCase() === "litellm" ? pc.blue("LiteLLM") : pc.magenta("OpenRouter");
834
+ const inputCost = result.pricing.input_cost_per_token ?? 0;
835
+ const outputCost = result.pricing.output_cost_per_token ?? 0;
836
+ const cacheReadCost = result.pricing.cache_read_input_token_cost;
837
+ const cacheWriteCost = result.pricing.cache_creation_input_token_cost;
838
+ console.log(pc.cyan(`\n Pricing for: ${pc.white(modelId)}`));
839
+ console.log(pc.gray(` Matched key: ${result.matchedKey}`));
840
+ console.log(pc.gray(` Source: `) + sourceLabel);
841
+ console.log();
842
+ console.log(pc.white(` Input: `) + formatPricePerMillion(inputCost));
843
+ console.log(pc.white(` Output: `) + formatPricePerMillion(outputCost));
844
+ if (cacheReadCost !== undefined) {
845
+ console.log(pc.white(` Cache Read: `) + formatPricePerMillion(cacheReadCost));
846
+ }
847
+ if (cacheWriteCost !== undefined) {
848
+ console.log(pc.white(` Cache Write: `) + formatPricePerMillion(cacheWriteCost));
849
+ }
850
+ console.log();
851
+ }
852
+ }
853
+ catch (err) {
854
+ spinner?.stop();
855
+ const errorMsg = err.message || "Unknown error";
856
+ // Check if this is a "model not found" error from Rust or a different error
857
+ const isModelNotFound = errorMsg.toLowerCase().includes("not found") ||
858
+ errorMsg.toLowerCase().includes("no pricing");
859
+ if (options.json) {
860
+ if (isModelNotFound) {
861
+ console.log(JSON.stringify({ error: "Model not found", modelId }, null, 2));
862
+ }
863
+ else {
864
+ console.log(JSON.stringify({ error: errorMsg, modelId }, null, 2));
865
+ }
866
+ }
867
+ else {
868
+ if (isModelNotFound) {
869
+ console.log(pc.red(`\n Model not found: ${modelId}\n`));
870
+ }
871
+ else {
872
+ console.log(pc.red(`\n Error looking up pricing: ${errorMsg}\n`));
873
+ }
874
+ }
875
+ process.exit(1);
876
+ }
877
+ }
878
+ function formatPricePerMillion(costPerToken) {
879
+ const perMillion = costPerToken * 1_000_000;
880
+ return pc.green(`$${perMillion.toFixed(2)}`) + pc.gray(" / 1M tokens");
881
+ }
763
882
  function getSourceLabel(source) {
764
883
  switch (source) {
765
884
  case "opencode":
@@ -772,6 +891,10 @@ function getSourceLabel(source) {
772
891
  return "Gemini";
773
892
  case "cursor":
774
893
  return "Cursor";
894
+ case "amp":
895
+ return "Amp";
896
+ case "droid":
897
+ return "Droid";
775
898
  default:
776
899
  return source;
777
900
  }