@tokscale/cli 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -13,6 +13,7 @@ const pkg = require("../package.json") as { version: string };
13
13
  import pc from "picocolors";
14
14
  import { login, logout, whoami } from "./auth.js";
15
15
  import { submit } from "./submit.js";
16
+ import { generateWrapped } from "./wrapped.js";
16
17
  import { PricingFetcher } from "./pricing.js";
17
18
  import {
18
19
  loadCursorCredentials,
@@ -203,7 +204,7 @@ async function main() {
203
204
 
204
205
  program
205
206
  .name("tokscale")
206
- .description("Token Usage Leaderboard CLI - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, and Cursor")
207
+ .description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, and Cursor")
207
208
  .version(pkg.version);
208
209
 
209
210
  program
@@ -292,9 +293,21 @@ async function main() {
292
293
  await handleGraphCommand(options);
293
294
  });
294
295
 
295
- // =========================================================================
296
- // Authentication Commands
297
- // =========================================================================
296
+ program
297
+ .command("wrapped")
298
+ .description("Generate 2025 Wrapped shareable image")
299
+ .option("--output <file>", "Output file path (default: tokscale-2025-wrapped.png)")
300
+ .option("--year <year>", "Year to generate wrapped for (default: current year)")
301
+ .option("--opencode", "Include only OpenCode data")
302
+ .option("--claude", "Include only Claude Code data")
303
+ .option("--codex", "Include only Codex CLI data")
304
+ .option("--gemini", "Include only Gemini CLI data")
305
+ .option("--cursor", "Include only Cursor IDE data")
306
+ .option("--no-spinner", "Disable loading spinner (for scripting)")
307
+ .option("--short", "Display total tokens in abbreviated format (e.g., 7.14B)")
308
+ .action(async (options) => {
309
+ await handleWrappedCommand(options);
310
+ });
298
311
 
299
312
  program
300
313
  .command("login")
@@ -410,7 +423,7 @@ async function main() {
410
423
  // Global flags should go to main program
411
424
  const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
412
425
  const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
413
- const knownCommands = ['monthly', 'models', 'graph', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'help'];
426
+ const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'help'];
414
427
  const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
415
428
 
416
429
  if (isKnownCommand || isGlobalFlag) {
@@ -908,6 +921,43 @@ async function handleGraphCommand(options: GraphCommandOptions) {
908
921
  }
909
922
  }
910
923
 
924
+ interface WrappedCommandOptions extends FilterOptions {
925
+ output?: string;
926
+ year?: string;
927
+ spinner?: boolean; // --no-spinner sets this to false
928
+ short?: boolean;
929
+ }
930
+
931
+ async function handleWrappedCommand(options: WrappedCommandOptions) {
932
+ const useSpinner = options.spinner !== false;
933
+ const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
934
+ spinner?.start(pc.gray("Generating your 2025 Wrapped..."));
935
+
936
+ try {
937
+ const enabledSources = getEnabledSources(options);
938
+ const outputPath = await generateWrapped({
939
+ output: options.output,
940
+ year: options.year || "2025",
941
+ sources: enabledSources,
942
+ short: options.short,
943
+ });
944
+
945
+ spinner?.stop();
946
+ console.log(pc.green(`\n ✓ Your Tokscale Wrapped image is ready!`));
947
+ console.log(pc.white(` ${outputPath}`));
948
+ console.log();
949
+ console.log(pc.gray(" Share it on Twitter/X with #TokscaleWrapped"));
950
+ console.log();
951
+ } catch (error) {
952
+ if (spinner) {
953
+ spinner.error(`Failed to generate wrapped: ${(error as Error).message}`);
954
+ } else {
955
+ console.error(pc.red(`Failed to generate wrapped: ${(error as Error).message}`));
956
+ }
957
+ process.exit(1);
958
+ }
959
+ }
960
+
911
961
  function getSourceLabel(source: string): string {
912
962
  switch (source) {
913
963
  case "opencode":
@@ -54,9 +54,16 @@ export function BarChart(props: BarChartProps) {
54
54
  });
55
55
 
56
56
  const chartWidth = () => Math.max(width() - 8, 20);
57
- const barWidth = () => Math.max(1, Math.floor(chartWidth() / Math.min(data().length, 52)));
58
- const visibleBars = () => Math.min(data().length, Math.floor(chartWidth() / barWidth()));
59
- const visibleData = createMemo(() => data().slice(-visibleBars()));
57
+
58
+ const getBarWidth = (index: number, total: number) => {
59
+ const cw = chartWidth();
60
+ if (total === 0) return 1;
61
+ const start = Math.floor((index * cw) / total);
62
+ const end = Math.floor(((index + 1) * cw) / total);
63
+ return Math.max(1, end - start);
64
+ };
65
+
66
+ const visibleData = () => data();
60
67
 
61
68
  const sortedModelsMap = createMemo(() => {
62
69
  const vd = visibleData();
@@ -99,7 +106,7 @@ export function BarChart(props: BarChartProps) {
99
106
  return labels;
100
107
  });
101
108
 
102
- const axisWidth = () => Math.min(chartWidth(), visibleBars() * barWidth());
109
+ const axisWidth = () => chartWidth();
103
110
  const labelPadding = () => {
104
111
  const labels = dateLabels();
105
112
  return labels.length > 0 ? Math.floor(axisWidth() / labels.length) : 0;
@@ -107,13 +114,13 @@ export function BarChart(props: BarChartProps) {
107
114
 
108
115
  const chartTitle = () => isVeryNarrowTerminal() ? "Tokens" : "Tokens per Day";
109
116
 
110
- const getBarContent = (point: ChartDataPoint, row: number): { char: string; color: string } => {
117
+ const getBarContent = (point: ChartDataPoint, row: number, barIndex: number): { char: string; color: string } => {
111
118
  const mt = maxTotal();
112
119
  const sh = safeHeight();
113
120
  const rowThreshold = ((row + 1) / sh) * mt;
114
121
  const prevThreshold = (row / sh) * mt;
115
122
  const thresholdDiff = rowThreshold - prevThreshold;
116
- const bw = barWidth();
123
+ const bw = getBarWidth(barIndex, visibleData().length);
117
124
 
118
125
  if (point.total <= prevThreshold) {
119
126
  return { char: getRepeatedString(" ", bw), color: "dim" };
@@ -169,8 +176,8 @@ export function BarChart(props: BarChartProps) {
169
176
  <box flexDirection="row">
170
177
  <text dim>{yLabel}│</text>
171
178
  <For each={visibleData()}>
172
- {(point) => {
173
- const bar = getBarContent(point, row);
179
+ {(point, barIndex) => {
180
+ const bar = getBarContent(point, row, barIndex());
174
181
  return bar.color === "dim"
175
182
  ? <text dim>{bar.char}</text>
176
183
  : <text fg={bar.color}>{bar.char}</text>;
@@ -6,7 +6,13 @@ import type { TUIData, SortType } from "../hooks/useData.js";
6
6
  import { formatCost } from "../utils/format.js";
7
7
  import { isNarrow, isVeryNarrow } from "../utils/responsive.js";
8
8
 
9
- const CHART_MAX_DAYS = 90;
9
+ const CHART_MAX_DAYS = 60;
10
+
11
+ function getDateDaysAgo(days: number): string {
12
+ const date = new Date();
13
+ date.setDate(date.getDate() - days);
14
+ return date.toISOString().split("T")[0];
15
+ }
10
16
 
11
17
  interface OverviewViewProps {
12
18
  data: TUIData;
@@ -27,7 +33,10 @@ export function OverviewView(props: OverviewViewProps) {
27
33
  const isNarrowTerminal = () => isNarrow(props.width);
28
34
  const isVeryNarrowTerminal = () => isVeryNarrow(props.width);
29
35
 
30
- const recentChartData = createMemo(() => props.data.chartData.slice(-CHART_MAX_DAYS));
36
+ const recentChartData = createMemo(() => {
37
+ const cutoffDate = getDateDaysAgo(CHART_MAX_DAYS);
38
+ return props.data.chartData.filter(d => d.date >= cutoffDate);
39
+ });
31
40
 
32
41
  const legendModelLimit = () => isVeryNarrowTerminal() ? 3 : 5;
33
42
  const topModelsForLegend = () => props.data.topModels.slice(0, legendModelLimit()).map(m => m.modelId);
@@ -66,7 +75,7 @@ export function OverviewView(props: OverviewViewProps) {
66
75
  return (
67
76
  <box flexDirection="column" gap={1}>
68
77
  <box flexDirection="column">
69
- <BarChart data={recentChartData()} width={props.width - 4} height={chartHeight()} />
78
+ <BarChart data={recentChartData()} width={props.width - 2} height={chartHeight()} />
70
79
  <Legend models={topModelsForLegend()} width={props.width} />
71
80
  </box>
72
81
 
@@ -95,7 +95,7 @@ export const colorPalettes: Record<ColorPaletteName, GraphColorPalette> = {
95
95
  },
96
96
  };
97
97
 
98
- export const DEFAULT_PALETTE: ColorPaletteName = "green";
98
+ export const DEFAULT_PALETTE: ColorPaletteName = "blue";
99
99
 
100
100
  export const getPaletteNames = (): ColorPaletteName[] =>
101
101
  Object.keys(colorPalettes) as ColorPaletteName[];