@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/dist/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * All heavy computation is done in the native Rust module.
7
7
  */
8
- import { Command } from "commander";
8
+ import { Command, Option } from "commander";
9
9
  import { createRequire } from "module";
10
10
  const require = createRequire(import.meta.url);
11
11
  const pkg = require("../package.json");
@@ -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,10 +241,13 @@ 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
- .option("--agents", "Show Top OpenCode Agents instead of Top Clients")
238
- .option("--pin-sisyphus", "Pin Sisyphus and Planner-Sisyphus at top of agents list")
248
+ .addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
249
+ .addOption(new Option("--clients", "Show Top Clients instead of Top OpenCode Agents").conflicts("agents"))
250
+ .option("--disable-pinned", "Disable pinning of Sisyphus agents in rankings")
239
251
  .action(async (options) => {
240
252
  await handleWrappedCommand(options);
241
253
  });
@@ -268,6 +280,8 @@ async function main() {
268
280
  .option("--codex", "Include only Codex CLI data")
269
281
  .option("--gemini", "Include only Gemini CLI data")
270
282
  .option("--cursor", "Include only Cursor IDE data")
283
+ .option("--amp", "Include only Amp data")
284
+ .option("--droid", "Include only Factory Droid data")
271
285
  .option("--since <date>", "Start date (YYYY-MM-DD)")
272
286
  .option("--until <date>", "End date (YYYY-MM-DD)")
273
287
  .option("--year <year>", "Filter to specific year")
@@ -279,6 +293,8 @@ async function main() {
279
293
  codex: options.codex,
280
294
  gemini: options.gemini,
281
295
  cursor: options.cursor,
296
+ amp: options.amp,
297
+ droid: options.droid,
282
298
  since: options.since,
283
299
  until: options.until,
284
300
  year: options.year,
@@ -296,6 +312,8 @@ async function main() {
296
312
  .option("--codex", "Show only Codex CLI usage")
297
313
  .option("--gemini", "Show only Gemini CLI usage")
298
314
  .option("--cursor", "Show only Cursor IDE usage")
315
+ .option("--amp", "Show only Amp usage")
316
+ .option("--droid", "Show only Factory Droid usage")
299
317
  .option("--today", "Show only today's usage")
300
318
  .option("--week", "Show last 7 days")
301
319
  .option("--month", "Show current month")
@@ -312,9 +330,15 @@ async function main() {
312
330
  process.exit(1);
313
331
  }
314
332
  });
315
- // =========================================================================
316
- // Cursor IDE Authentication Commands
317
- // =========================================================================
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
+ });
318
342
  const cursorCommand = program
319
343
  .command("cursor")
320
344
  .description("Cursor IDE integration commands");
@@ -342,7 +366,7 @@ async function main() {
342
366
  // Global flags should go to main program
343
367
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
344
368
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
345
- 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'];
346
370
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
347
371
  if (isKnownCommand || isGlobalFlag) {
348
372
  // Run the specified subcommand or show full help/version
@@ -359,6 +383,7 @@ async function main() {
359
383
  .option("--codex", "Show only Codex CLI usage")
360
384
  .option("--gemini", "Show only Gemini CLI usage")
361
385
  .option("--cursor", "Show only Cursor IDE usage")
386
+ .option("--amp", "Show only Amp usage")
362
387
  .option("--today", "Show only today's usage")
363
388
  .option("--week", "Show last 7 days")
364
389
  .option("--month", "Show current month")
@@ -366,13 +391,14 @@ async function main() {
366
391
  .option("--until <date>", "End date (YYYY-MM-DD)")
367
392
  .option("--year <year>", "Filter to specific year")
368
393
  .option("--benchmark", "Show processing time")
394
+ .option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
369
395
  .parse();
370
396
  const opts = defaultProgram.opts();
371
397
  if (opts.json) {
372
398
  await outputJsonReport("models", opts);
373
399
  }
374
400
  else if (opts.light) {
375
- await showModelReport(opts);
401
+ await showModelReport(opts, { spinner: opts.spinner });
376
402
  }
377
403
  else {
378
404
  const launchTUI = await tryLoadTUI();
@@ -381,13 +407,13 @@ async function main() {
381
407
  }
382
408
  else {
383
409
  showTUIUnavailableMessage();
384
- await showModelReport(opts);
410
+ await showModelReport(opts, { spinner: opts.spinner });
385
411
  }
386
412
  }
387
413
  }
388
414
  }
389
415
  function getEnabledSources(options) {
390
- 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;
391
417
  if (!hasFilter)
392
418
  return undefined; // All sources
393
419
  const sources = [];
@@ -401,19 +427,12 @@ function getEnabledSources(options) {
401
427
  sources.push("gemini");
402
428
  if (options.cursor)
403
429
  sources.push("cursor");
430
+ if (options.amp)
431
+ sources.push("amp");
432
+ if (options.droid)
433
+ sources.push("droid");
404
434
  return sources;
405
435
  }
406
- function logNativeStatus() {
407
- if (!isNativeAvailable()) {
408
- console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
409
- console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
410
- }
411
- }
412
- async function fetchPricingData() {
413
- const fetcher = new PricingFetcher();
414
- await fetcher.fetchPricing();
415
- return fetcher;
416
- }
417
436
  /**
418
437
  * Sync Cursor usage data from API to local cache.
419
438
  * Only attempts sync if user is authenticated with Cursor.
@@ -431,22 +450,10 @@ async function syncCursorData() {
431
450
  error: result.error,
432
451
  };
433
452
  }
434
- /**
435
- * Load all data sources in parallel (two-phase optimization):
436
- * - Cursor API sync (network)
437
- * - Pricing fetch (network)
438
- * - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
439
- *
440
- * This overlaps network I/O with local file parsing for better performance.
441
- */
442
- async function loadDataSourcesParallel(localSources, dateFilters) {
443
- // Skip local parsing if no local sources requested (e.g., cursor-only mode)
453
+ async function loadDataSourcesParallel(localSources, dateFilters, onPhase) {
444
454
  const shouldParseLocal = localSources.length > 0;
445
- // Use Promise.allSettled for graceful degradation
446
- const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
455
+ const [cursorResult, localResult] = await Promise.allSettled([
447
456
  syncCursorData(),
448
- fetchPricingData(),
449
- // Parse local sources in parallel (excludes Cursor) - skip if empty
450
457
  shouldParseLocal
451
458
  ? parseLocalSourcesAsync({
452
459
  sources: localSources.filter(s => s !== 'cursor'),
@@ -456,20 +463,15 @@ async function loadDataSourcesParallel(localSources, dateFilters) {
456
463
  })
457
464
  : Promise.resolve(null),
458
465
  ]);
459
- // Handle partial failures gracefully
460
466
  const cursorSync = cursorResult.status === 'fulfilled'
461
467
  ? cursorResult.value
462
468
  : { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
463
- const fetcher = pricingResult.status === 'fulfilled'
464
- ? pricingResult.value
465
- : new PricingFetcher(); // Empty pricing → costs = 0
466
469
  const localMessages = localResult.status === 'fulfilled'
467
470
  ? localResult.value
468
471
  : null;
469
- return { fetcher, cursorSync, localMessages };
472
+ return { cursorSync, localMessages };
470
473
  }
471
- async function showModelReport(options) {
472
- logNativeStatus();
474
+ async function showModelReport(options, extraOptions) {
473
475
  const dateFilters = getDateFilters(options);
474
476
  const enabledSources = getEnabledSources(options);
475
477
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
@@ -492,27 +494,28 @@ async function showModelReport(options) {
492
494
  console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
493
495
  }
494
496
  console.log();
495
- // Start spinner for loading phase
496
- const spinner = createSpinner({ color: "cyan" });
497
- spinner.start(pc.gray("Loading data sources..."));
498
- // Filter out cursor for local parsing (it's synced separately via network)
499
- 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'])
500
500
  .filter(s => s !== 'cursor');
501
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
502
- // If cursor-only, skip local parsing entirely
503
- 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));
504
503
  if (!localMessages && !onlyCursor) {
505
- 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
+ }
506
510
  process.exit(1);
507
511
  }
508
- spinner.update(pc.gray("Finalizing report..."));
512
+ spinner?.update(pc.gray("Finalizing report..."));
509
513
  const startTime = performance.now();
510
514
  let report;
511
515
  try {
512
- 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 };
513
517
  report = await finalizeReportAsync({
514
518
  localMessages: localMessages || emptyMessages,
515
- pricing: fetcher.toPricingEntries(),
516
519
  includeCursor: includeCursor && cursorSync.synced,
517
520
  since: dateFilters.since,
518
521
  until: dateFilters.until,
@@ -520,11 +523,16 @@ async function showModelReport(options) {
520
523
  });
521
524
  }
522
525
  catch (e) {
523
- spinner.error(`Error: ${e.message}`);
526
+ if (spinner) {
527
+ spinner.error(`Error: ${e.message}`);
528
+ }
529
+ else {
530
+ console.error(`Error: ${e.message}`);
531
+ }
524
532
  process.exit(1);
525
533
  }
526
534
  const processingTime = performance.now() - startTime;
527
- spinner.stop();
535
+ spinner?.stop();
528
536
  if (report.entries.length === 0) {
529
537
  if (onlyCursor && !cursorSync.synced) {
530
538
  console.log(pc.yellow(" No Cursor data available."));
@@ -537,7 +545,11 @@ async function showModelReport(options) {
537
545
  }
538
546
  // Create table
539
547
  const table = createUsageTable("Source/Model");
540
- 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) {
541
553
  const sourceLabel = getSourceLabel(entry.source);
542
554
  const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
543
555
  table.push(formatUsageRow(modelDisplay, [entry.model], entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
@@ -562,8 +574,7 @@ async function showModelReport(options) {
562
574
  }
563
575
  console.log();
564
576
  }
565
- async function showMonthlyReport(options) {
566
- logNativeStatus();
577
+ async function showMonthlyReport(options, extraOptions) {
567
578
  const dateRange = getDateRangeLabel(options);
568
579
  const title = dateRange
569
580
  ? `Monthly Token Usage Report (${dateRange})`
@@ -573,28 +584,30 @@ async function showMonthlyReport(options) {
573
584
  console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
574
585
  }
575
586
  console.log();
576
- // Start spinner for loading phase
577
- const spinner = createSpinner({ color: "cyan" });
578
- spinner.start(pc.gray("Loading data sources..."));
587
+ const useSpinner = extraOptions?.spinner !== false;
588
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
579
589
  const dateFilters = getDateFilters(options);
580
590
  const enabledSources = getEnabledSources(options);
581
- // Filter out cursor for local parsing (it's synced separately via network)
582
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
591
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
583
592
  .filter(s => s !== 'cursor');
584
593
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
585
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
586
- 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));
587
596
  if (!localMessages) {
588
- 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
+ }
589
603
  process.exit(1);
590
604
  }
591
- spinner.update(pc.gray("Finalizing report..."));
605
+ spinner?.update(pc.gray("Finalizing report..."));
592
606
  const startTime = performance.now();
593
607
  let report;
594
608
  try {
595
609
  report = await finalizeMonthlyReportAsync({
596
610
  localMessages,
597
- pricing: fetcher.toPricingEntries(),
598
611
  includeCursor: includeCursor && cursorSync.synced,
599
612
  since: dateFilters.since,
600
613
  until: dateFilters.until,
@@ -602,18 +615,27 @@ async function showMonthlyReport(options) {
602
615
  });
603
616
  }
604
617
  catch (e) {
605
- spinner.error(`Error: ${e.message}`);
618
+ if (spinner) {
619
+ spinner.error(`Error: ${e.message}`);
620
+ }
621
+ else {
622
+ console.error(`Error: ${e.message}`);
623
+ }
606
624
  process.exit(1);
607
625
  }
608
626
  const processingTime = performance.now() - startTime;
609
- spinner.stop();
627
+ spinner?.stop();
610
628
  if (report.entries.length === 0) {
611
629
  console.log(pc.yellow(" No usage data found.\n"));
612
630
  return;
613
631
  }
614
632
  // Create table
615
633
  const table = createUsageTable("Month");
616
- 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) {
617
639
  table.push(formatUsageRow(entry.month, entry.models, entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
618
640
  }
619
641
  // Add totals row
@@ -638,23 +660,21 @@ async function showMonthlyReport(options) {
638
660
  console.log();
639
661
  }
640
662
  async function outputJsonReport(reportType, options) {
641
- logNativeStatus();
642
663
  const dateFilters = getDateFilters(options);
643
664
  const enabledSources = getEnabledSources(options);
644
665
  const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
645
666
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
646
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
667
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
647
668
  .filter(s => s !== 'cursor');
648
- const { fetcher, cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
669
+ const { cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
649
670
  if (!localMessages && !onlyCursor) {
650
671
  console.error(JSON.stringify({ error: "Failed to parse local session files" }));
651
672
  process.exit(1);
652
673
  }
653
- 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 };
654
675
  if (reportType === "models") {
655
676
  const report = await finalizeReportAsync({
656
677
  localMessages: localMessages || emptyMessages,
657
- pricing: fetcher.toPricingEntries(),
658
678
  includeCursor: includeCursor && cursorSync.synced,
659
679
  since: dateFilters.since,
660
680
  until: dateFilters.until,
@@ -665,7 +685,6 @@ async function outputJsonReport(reportType, options) {
665
685
  else {
666
686
  const report = await finalizeMonthlyReportAsync({
667
687
  localMessages: localMessages || emptyMessages,
668
- pricing: fetcher.toPricingEntries(),
669
688
  includeCursor: includeCursor && cursorSync.synced,
670
689
  since: dateFilters.since,
671
690
  until: dateFilters.until,
@@ -675,18 +694,15 @@ async function outputJsonReport(reportType, options) {
675
694
  }
676
695
  }
677
696
  async function handleGraphCommand(options) {
678
- logNativeStatus();
679
- // Start spinner for loading phase (only if outputting to file, not stdout)
680
- const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
681
- spinner?.start(pc.gray("Loading data sources..."));
697
+ const useSpinner = options.output && options.spinner !== false;
698
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
682
699
  const dateFilters = getDateFilters(options);
683
700
  const enabledSources = getEnabledSources(options);
684
- // Filter out cursor for local parsing (it's synced separately via network)
685
- const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
701
+ const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
686
702
  .filter(s => s !== 'cursor');
687
703
  const includeCursor = !enabledSources || enabledSources.includes('cursor');
688
- // Two-phase parallel loading: network (Cursor + pricing) overlaps with local file parsing
689
- 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));
690
706
  if (!localMessages) {
691
707
  spinner?.error('Failed to parse local session files');
692
708
  process.exit(1);
@@ -695,7 +711,6 @@ async function handleGraphCommand(options) {
695
711
  const startTime = performance.now();
696
712
  const data = await finalizeGraphAsync({
697
713
  localMessages,
698
- pricing: fetcher.toPricingEntries(),
699
714
  includeCursor: includeCursor && cursorSync.synced,
700
715
  since: dateFilters.since,
701
716
  until: dateFilters.until,
@@ -739,8 +754,8 @@ async function handleWrappedCommand(options) {
739
754
  year,
740
755
  sources: enabledSources,
741
756
  short: options.short,
742
- includeAgents: options.agents,
743
- pinSisyphus: options.pinSisyphus,
757
+ includeAgents: !options.clients,
758
+ pinSisyphus: !options.disablePinned,
744
759
  });
745
760
  spinner?.stop();
746
761
  console.log(pc.green(`\n ✓ Your Tokscale Wrapped image is ready!`));
@@ -759,6 +774,111 @@ async function handleWrappedCommand(options) {
759
774
  process.exit(1);
760
775
  }
761
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
+ }
762
882
  function getSourceLabel(source) {
763
883
  switch (source) {
764
884
  case "opencode":
@@ -771,6 +891,10 @@ function getSourceLabel(source) {
771
891
  return "Gemini";
772
892
  case "cursor":
773
893
  return "Cursor";
894
+ case "amp":
895
+ return "Amp";
896
+ case "droid":
897
+ return "Droid";
774
898
  default:
775
899
  return source;
776
900
  }