@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/dist/cli.js +47 -5
- package/dist/cli.js.map +1 -1
- package/dist/tui/components/BarChart.d.ts.map +1 -1
- package/dist/tui/components/BarChart.js +14 -8
- package/dist/tui/components/BarChart.js.map +1 -1
- package/dist/tui/components/OverviewView.d.ts.map +1 -1
- package/dist/tui/components/OverviewView.js +11 -3
- package/dist/tui/components/OverviewView.js.map +1 -1
- package/dist/tui/config/themes.d.ts.map +1 -1
- package/dist/tui/config/themes.js +1 -1
- package/dist/tui/config/themes.js.map +1 -1
- package/dist/wrapped.d.ts +35 -0
- package/dist/wrapped.d.ts.map +1 -0
- package/dist/wrapped.js +569 -0
- package/dist/wrapped.js.map +1 -0
- package/package.json +4 -2
- package/src/cli.ts +55 -5
- package/src/tui/components/BarChart.tsx +15 -8
- package/src/tui/components/OverviewView.tsx +12 -3
- package/src/tui/config/themes.ts +1 -1
- package/src/wrapped.ts +673 -0
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("
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
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 = () =>
|
|
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 =
|
|
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 =
|
|
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(() =>
|
|
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 -
|
|
78
|
+
<BarChart data={recentChartData()} width={props.width - 2} height={chartHeight()} />
|
|
70
79
|
<Legend models={topModelsForLegend()} width={props.width} />
|
|
71
80
|
</box>
|
|
72
81
|
|
package/src/tui/config/themes.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const colorPalettes: Record<ColorPaletteName, GraphColorPalette> = {
|
|
|
95
95
|
},
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
export const DEFAULT_PALETTE: ColorPaletteName = "
|
|
98
|
+
export const DEFAULT_PALETTE: ColorPaletteName = "blue";
|
|
99
99
|
|
|
100
100
|
export const getPaletteNames = (): ColorPaletteName[] =>
|
|
101
101
|
Object.keys(colorPalettes) as ColorPaletteName[];
|