@tokscale/cli 1.4.3 → 2.0.0
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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -26
- package/dist/auth.d.ts +0 -17
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -162
- package/dist/auth.js.map +0 -1
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -1550
- package/dist/cli.js.map +0 -1
- package/dist/credentials.d.ts +0 -50
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js +0 -151
- package/dist/credentials.js.map +0 -1
- package/dist/cursor.d.ts +0 -167
- package/dist/cursor.d.ts.map +0 -1
- package/dist/cursor.js +0 -906
- package/dist/cursor.js.map +0 -1
- package/dist/date-utils.d.ts +0 -10
- package/dist/date-utils.d.ts.map +0 -1
- package/dist/date-utils.js +0 -47
- package/dist/date-utils.js.map +0 -1
- package/dist/graph-types.d.ts +0 -142
- package/dist/graph-types.d.ts.map +0 -1
- package/dist/graph-types.js +0 -6
- package/dist/graph-types.js.map +0 -1
- package/dist/native-runner.d.ts +0 -11
- package/dist/native-runner.d.ts.map +0 -1
- package/dist/native-runner.js +0 -77
- package/dist/native-runner.js.map +0 -1
- package/dist/native.d.ts +0 -106
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -302
- package/dist/native.js.map +0 -1
- package/dist/sessions/types.d.ts +0 -28
- package/dist/sessions/types.d.ts.map +0 -1
- package/dist/sessions/types.js +0 -27
- package/dist/sessions/types.js.map +0 -1
- package/dist/spinner.d.ts +0 -75
- package/dist/spinner.d.ts.map +0 -1
- package/dist/spinner.js +0 -203
- package/dist/spinner.js.map +0 -1
- package/dist/submit.d.ts +0 -23
- package/dist/submit.d.ts.map +0 -1
- package/dist/submit.js +0 -294
- package/dist/submit.js.map +0 -1
- package/dist/table.d.ts +0 -42
- package/dist/table.d.ts.map +0 -1
- package/dist/table.js +0 -181
- package/dist/table.js.map +0 -1
- package/dist/tui/App.d.ts +0 -4
- package/dist/tui/App.d.ts.map +0 -1
- package/dist/tui/App.js +0 -333
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/components/BarChart.d.ts +0 -17
- package/dist/tui/components/BarChart.d.ts.map +0 -1
- package/dist/tui/components/BarChart.js +0 -146
- package/dist/tui/components/BarChart.js.map +0 -1
- package/dist/tui/components/DailyView.d.ts +0 -13
- package/dist/tui/components/DailyView.d.ts.map +0 -1
- package/dist/tui/components/DailyView.js +0 -86
- package/dist/tui/components/DailyView.js.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
- package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.js +0 -36
- package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
- package/dist/tui/components/Footer.d.ts +0 -28
- package/dist/tui/components/Footer.d.ts.map +0 -1
- package/dist/tui/components/Footer.js +0 -130
- package/dist/tui/components/Footer.js.map +0 -1
- package/dist/tui/components/Header.d.ts +0 -9
- package/dist/tui/components/Header.d.ts.map +0 -1
- package/dist/tui/components/Header.js +0 -20
- package/dist/tui/components/Header.js.map +0 -1
- package/dist/tui/components/Legend.d.ts +0 -7
- package/dist/tui/components/Legend.d.ts.map +0 -1
- package/dist/tui/components/Legend.js +0 -16
- package/dist/tui/components/Legend.js.map +0 -1
- package/dist/tui/components/LoadingSpinner.d.ts +0 -8
- package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
- package/dist/tui/components/LoadingSpinner.js +0 -55
- package/dist/tui/components/LoadingSpinner.js.map +0 -1
- package/dist/tui/components/ModelRow.d.ts +0 -13
- package/dist/tui/components/ModelRow.d.ts.map +0 -1
- package/dist/tui/components/ModelRow.js +0 -15
- package/dist/tui/components/ModelRow.js.map +0 -1
- package/dist/tui/components/ModelView.d.ts +0 -13
- package/dist/tui/components/ModelView.d.ts.map +0 -1
- package/dist/tui/components/ModelView.js +0 -96
- package/dist/tui/components/ModelView.js.map +0 -1
- package/dist/tui/components/OverviewView.d.ts +0 -14
- package/dist/tui/components/OverviewView.d.ts.map +0 -1
- package/dist/tui/components/OverviewView.js +0 -65
- package/dist/tui/components/OverviewView.js.map +0 -1
- package/dist/tui/components/StatsView.d.ts +0 -14
- package/dist/tui/components/StatsView.d.ts.map +0 -1
- package/dist/tui/components/StatsView.js +0 -102
- package/dist/tui/components/StatsView.js.map +0 -1
- package/dist/tui/components/TokenBreakdown.d.ts +0 -14
- package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
- package/dist/tui/components/TokenBreakdown.js +0 -10
- package/dist/tui/components/TokenBreakdown.js.map +0 -1
- package/dist/tui/components/index.d.ts +0 -16
- package/dist/tui/components/index.d.ts.map +0 -1
- package/dist/tui/components/index.js +0 -13
- package/dist/tui/components/index.js.map +0 -1
- package/dist/tui/config/settings.d.ts +0 -15
- package/dist/tui/config/settings.d.ts.map +0 -1
- package/dist/tui/config/settings.js +0 -147
- package/dist/tui/config/settings.js.map +0 -1
- package/dist/tui/config/themes.d.ts +0 -15
- package/dist/tui/config/themes.d.ts.map +0 -1
- package/dist/tui/config/themes.js +0 -82
- package/dist/tui/config/themes.js.map +0 -1
- package/dist/tui/hooks/useData.d.ts +0 -19
- package/dist/tui/hooks/useData.d.ts.map +0 -1
- package/dist/tui/hooks/useData.js +0 -468
- package/dist/tui/hooks/useData.js.map +0 -1
- package/dist/tui/index.d.ts +0 -4
- package/dist/tui/index.d.ts.map +0 -1
- package/dist/tui/index.js +0 -36
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/types/index.d.ts +0 -137
- package/dist/tui/types/index.d.ts.map +0 -1
- package/dist/tui/types/index.js +0 -26
- package/dist/tui/types/index.js.map +0 -1
- package/dist/tui/utils/cleanup.d.ts +0 -22
- package/dist/tui/utils/cleanup.d.ts.map +0 -1
- package/dist/tui/utils/cleanup.js +0 -59
- package/dist/tui/utils/cleanup.js.map +0 -1
- package/dist/tui/utils/colors.d.ts +0 -19
- package/dist/tui/utils/colors.d.ts.map +0 -1
- package/dist/tui/utils/colors.js +0 -71
- package/dist/tui/utils/colors.js.map +0 -1
- package/dist/tui/utils/format.d.ts +0 -7
- package/dist/tui/utils/format.d.ts.map +0 -1
- package/dist/tui/utils/format.js +0 -45
- package/dist/tui/utils/format.js.map +0 -1
- package/dist/tui/utils/responsive.d.ts +0 -5
- package/dist/tui/utils/responsive.d.ts.map +0 -1
- package/dist/tui/utils/responsive.js +0 -5
- package/dist/tui/utils/responsive.js.map +0 -1
- package/dist/wrapped.d.ts +0 -43
- package/dist/wrapped.d.ts.map +0 -1
- package/dist/wrapped.js +0 -719
- package/dist/wrapped.js.map +0 -1
- package/src/auth.ts +0 -211
- package/src/cli.ts +0 -1892
- package/src/credentials.ts +0 -176
- package/src/cursor.ts +0 -1044
- package/src/date-utils.ts +0 -51
- package/src/graph-types.ts +0 -175
- package/src/native-runner.js +0 -4
- package/src/native-runner.ts +0 -91
- package/src/native.ts +0 -633
- package/src/sessions/types.ts +0 -59
- package/src/spinner.ts +0 -283
- package/src/submit.ts +0 -360
- package/src/table.ts +0 -233
- package/src/tui/App.tsx +0 -453
- package/src/tui/components/BarChart.tsx +0 -205
- package/src/tui/components/DailyView.tsx +0 -132
- package/src/tui/components/DateBreakdownPanel.tsx +0 -79
- package/src/tui/components/Footer.tsx +0 -380
- package/src/tui/components/Header.tsx +0 -68
- package/src/tui/components/Legend.tsx +0 -39
- package/src/tui/components/LoadingSpinner.tsx +0 -81
- package/src/tui/components/ModelRow.tsx +0 -47
- package/src/tui/components/ModelView.tsx +0 -147
- package/src/tui/components/OverviewView.tsx +0 -121
- package/src/tui/components/StatsView.tsx +0 -249
- package/src/tui/components/TokenBreakdown.tsx +0 -46
- package/src/tui/components/index.ts +0 -15
- package/src/tui/config/settings.ts +0 -183
- package/src/tui/config/themes.ts +0 -115
- package/src/tui/hooks/useData.ts +0 -558
- package/src/tui/index.tsx +0 -44
- package/src/tui/opentui.d.ts +0 -166
- package/src/tui/types/index.ts +0 -173
- package/src/tui/utils/cleanup.ts +0 -65
- package/src/tui/utils/colors.ts +0 -78
- package/src/tui/utils/format.ts +0 -36
- package/src/tui/utils/responsive.ts +0 -8
- package/src/types.d.ts +0 -28
- package/src/wrapped.ts +0 -848
package/src/cli.ts
DELETED
|
@@ -1,1892 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Tokscale CLI
|
|
4
|
-
* Display OpenCode, Claude Code, Codex, Gemini, and Cursor usage with dynamic width tables
|
|
5
|
-
*
|
|
6
|
-
* All heavy computation is done in the native Rust module.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Command, Option } from "commander";
|
|
10
|
-
import { createRequire } from "module";
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
const pkg = require("../package.json") as { version: string };
|
|
13
|
-
import pc from "picocolors";
|
|
14
|
-
import { login, logout, whoami } from "./auth.js";
|
|
15
|
-
import { submit } from "./submit.js";
|
|
16
|
-
import { generateWrapped } from "./wrapped.js";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
ensureCursorMigration,
|
|
20
|
-
loadCursorCredentials,
|
|
21
|
-
saveCursorCredentials,
|
|
22
|
-
clearCursorCredentials,
|
|
23
|
-
clearCursorCredentialsAndCache,
|
|
24
|
-
isCursorLoggedIn,
|
|
25
|
-
hasCursorUsageCache,
|
|
26
|
-
listCursorAccounts,
|
|
27
|
-
setActiveCursorAccount,
|
|
28
|
-
removeCursorAccount,
|
|
29
|
-
validateCursorSession,
|
|
30
|
-
readCursorUsage,
|
|
31
|
-
getCursorCredentialsPath,
|
|
32
|
-
syncCursorCache,
|
|
33
|
-
} from "./cursor.js";
|
|
34
|
-
import {
|
|
35
|
-
createUsageTable,
|
|
36
|
-
formatUsageRow,
|
|
37
|
-
formatTotalsRow,
|
|
38
|
-
formatNumber,
|
|
39
|
-
formatCurrency,
|
|
40
|
-
formatModelName,
|
|
41
|
-
} from "./table.js";
|
|
42
|
-
import {
|
|
43
|
-
isNativeAvailable,
|
|
44
|
-
getNativeVersion,
|
|
45
|
-
parseLocalSourcesAsync,
|
|
46
|
-
finalizeReportAsync,
|
|
47
|
-
finalizeMonthlyReportAsync,
|
|
48
|
-
finalizeGraphAsync,
|
|
49
|
-
type ModelReport,
|
|
50
|
-
type MonthlyReport,
|
|
51
|
-
type ParsedMessages,
|
|
52
|
-
} from "./native.js";
|
|
53
|
-
import { createSpinner } from "./spinner.js";
|
|
54
|
-
import { spawn } from "node:child_process";
|
|
55
|
-
import { randomUUID } from "node:crypto";
|
|
56
|
-
import * as fs from "node:fs";
|
|
57
|
-
import * as os from "node:os";
|
|
58
|
-
import * as path from "node:path";
|
|
59
|
-
import { performance } from "node:perf_hooks";
|
|
60
|
-
import type { SourceType } from "./graph-types.js";
|
|
61
|
-
import type { TUIOptions, TabType } from "./tui/types/index.js";
|
|
62
|
-
import { loadSettings } from "./tui/config/settings.js";
|
|
63
|
-
import { formatDateLocal, parseDateStringToLocal, getStartOfDayTimestamp, getEndOfDayTimestamp, validateTimestampMs } from "./date-utils.js";
|
|
64
|
-
|
|
65
|
-
type LaunchTUIFunction = (options?: TUIOptions) => Promise<void>;
|
|
66
|
-
|
|
67
|
-
let cachedTUILoader: LaunchTUIFunction | null = null;
|
|
68
|
-
let tuiLoadAttempted = false;
|
|
69
|
-
|
|
70
|
-
async function tryLoadTUI(): Promise<LaunchTUIFunction | null> {
|
|
71
|
-
if (tuiLoadAttempted) return cachedTUILoader;
|
|
72
|
-
tuiLoadAttempted = true;
|
|
73
|
-
|
|
74
|
-
const isBun = typeof (globalThis as Record<string, unknown>).Bun !== "undefined";
|
|
75
|
-
|
|
76
|
-
if (!isBun) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
// Load OpenTUI preload to register Babel transform for TSX
|
|
82
|
-
// This is needed for both dev mode (via bunfig.toml) and production
|
|
83
|
-
// Use variable to prevent TypeScript from analyzing the module
|
|
84
|
-
const preloadModule = "@opentui/solid/preload";
|
|
85
|
-
await import(preloadModule);
|
|
86
|
-
|
|
87
|
-
// Always load from source TSX - OpenTUI only works with Bun + preload
|
|
88
|
-
// Calculate path to src/tui/index.tsx regardless of whether running from src or dist
|
|
89
|
-
const currentPath = new URL(".", import.meta.url).pathname;
|
|
90
|
-
const isFromDist = currentPath.includes("/dist/");
|
|
91
|
-
const tuiPath = isFromDist
|
|
92
|
-
? new URL("../src/tui/index.tsx", import.meta.url).href
|
|
93
|
-
: new URL("./tui/index.tsx", import.meta.url).href;
|
|
94
|
-
const tuiModule = await import(tuiPath) as { launchTUI: LaunchTUIFunction };
|
|
95
|
-
cachedTUILoader = tuiModule.launchTUI;
|
|
96
|
-
return cachedTUILoader;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (process.env.DEBUG) {
|
|
99
|
-
console.error("TUI load error:", error);
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function showTUIUnavailableMessage(): void {
|
|
106
|
-
console.log(pc.yellow("\n TUI mode requires Bun runtime."));
|
|
107
|
-
console.log(pc.gray(" OpenTUI's native modules are not compatible with Node.js."));
|
|
108
|
-
console.log();
|
|
109
|
-
console.log(pc.white(" Options:"));
|
|
110
|
-
console.log(pc.gray(" • Use 'bunx tokscale' instead of 'npx tokscale'"));
|
|
111
|
-
// console.log(pc.gray(" • Use '--light' flag for legacy CLI table output"));
|
|
112
|
-
console.log(pc.gray(" • Use '--json' flag for JSON output"));
|
|
113
|
-
console.log();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface FilterOptions {
|
|
117
|
-
opencode?: boolean;
|
|
118
|
-
claude?: boolean;
|
|
119
|
-
codex?: boolean;
|
|
120
|
-
gemini?: boolean;
|
|
121
|
-
cursor?: boolean;
|
|
122
|
-
amp?: boolean;
|
|
123
|
-
droid?: boolean;
|
|
124
|
-
openclaw?: boolean;
|
|
125
|
-
pi?: boolean;
|
|
126
|
-
kimi?: boolean;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface DateFilterOptions {
|
|
130
|
-
since?: string;
|
|
131
|
-
until?: string;
|
|
132
|
-
year?: string;
|
|
133
|
-
today?: boolean;
|
|
134
|
-
week?: boolean;
|
|
135
|
-
month?: boolean;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface CursorSyncResult {
|
|
139
|
-
/** Whether a sync was attempted (true if credentials exist) */
|
|
140
|
-
attempted: boolean;
|
|
141
|
-
/** Whether the sync succeeded */
|
|
142
|
-
synced: boolean;
|
|
143
|
-
/** Number of usage events fetched */
|
|
144
|
-
rows: number;
|
|
145
|
-
/** Error message if sync failed */
|
|
146
|
-
error?: string;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
interface DateFilters {
|
|
152
|
-
since?: string;
|
|
153
|
-
until?: string;
|
|
154
|
-
year?: string;
|
|
155
|
-
sinceTs?: number;
|
|
156
|
-
untilTs?: number;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getDateFilters(options: DateFilterOptions): DateFilters {
|
|
160
|
-
const today = new Date();
|
|
161
|
-
|
|
162
|
-
if (options.today) {
|
|
163
|
-
let sinceTs = getStartOfDayTimestamp(today);
|
|
164
|
-
let untilTs = getEndOfDayTimestamp(today);
|
|
165
|
-
sinceTs = validateTimestampMs(sinceTs, '--today (since)');
|
|
166
|
-
untilTs = validateTimestampMs(untilTs, '--today (until)');
|
|
167
|
-
return {
|
|
168
|
-
sinceTs,
|
|
169
|
-
untilTs,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (options.week) {
|
|
174
|
-
const weekAgo = new Date(today);
|
|
175
|
-
weekAgo.setDate(weekAgo.getDate() - 6);
|
|
176
|
-
let sinceTs = getStartOfDayTimestamp(weekAgo);
|
|
177
|
-
let untilTs = getEndOfDayTimestamp(today);
|
|
178
|
-
sinceTs = validateTimestampMs(sinceTs, '--week (since)');
|
|
179
|
-
untilTs = validateTimestampMs(untilTs, '--week (until)');
|
|
180
|
-
return {
|
|
181
|
-
sinceTs,
|
|
182
|
-
untilTs,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (options.month) {
|
|
187
|
-
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
188
|
-
let sinceTs = getStartOfDayTimestamp(startOfMonth);
|
|
189
|
-
let untilTs = getEndOfDayTimestamp(today);
|
|
190
|
-
sinceTs = validateTimestampMs(sinceTs, '--month (since)');
|
|
191
|
-
untilTs = validateTimestampMs(untilTs, '--month (until)');
|
|
192
|
-
return {
|
|
193
|
-
sinceTs,
|
|
194
|
-
untilTs,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (options.since || options.until) {
|
|
199
|
-
let sinceTs: number | undefined;
|
|
200
|
-
let untilTs: number | undefined;
|
|
201
|
-
|
|
202
|
-
if (options.since) {
|
|
203
|
-
const sinceDate = parseDateStringToLocal(options.since);
|
|
204
|
-
if (!sinceDate) {
|
|
205
|
-
console.error(pc.red(`\n Error: Invalid --since date '${options.since}'. Use YYYY-MM-DD format with valid date.\n`));
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
sinceTs = getStartOfDayTimestamp(sinceDate);
|
|
209
|
-
sinceTs = validateTimestampMs(sinceTs, '--since');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (options.until) {
|
|
213
|
-
const untilDate = parseDateStringToLocal(options.until);
|
|
214
|
-
if (!untilDate) {
|
|
215
|
-
console.error(pc.red(`\n Error: Invalid --until date '${options.until}'. Use YYYY-MM-DD format with valid date.\n`));
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
untilTs = getEndOfDayTimestamp(untilDate);
|
|
219
|
-
untilTs = validateTimestampMs(untilTs, '--until');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (sinceTs !== undefined && untilTs !== undefined && sinceTs > untilTs) {
|
|
223
|
-
console.error(pc.red(`\n Error: --since date must be before --until date.\n`));
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
since: options.since,
|
|
229
|
-
until: options.until,
|
|
230
|
-
year: options.year,
|
|
231
|
-
sinceTs,
|
|
232
|
-
untilTs,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
year: options.year,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getDateRangeLabel(options: DateFilterOptions): string | null {
|
|
242
|
-
if (options.today) return "Today";
|
|
243
|
-
if (options.week) return "Last 7 days";
|
|
244
|
-
if (options.month) {
|
|
245
|
-
const today = new Date();
|
|
246
|
-
return today.toLocaleString("en-US", { month: "long", year: "numeric" } as Intl.DateTimeFormatOptions);
|
|
247
|
-
}
|
|
248
|
-
if (options.year) return options.year;
|
|
249
|
-
if (options.since || options.until) {
|
|
250
|
-
const parts: string[] = [];
|
|
251
|
-
if (options.since) parts.push(`from ${options.since}`);
|
|
252
|
-
if (options.until) parts.push(`to ${options.until}`);
|
|
253
|
-
return parts.join(" ");
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getHeadlessRoots(homeDir: string): string[] {
|
|
259
|
-
const override = process.env.TOKSCALE_HEADLESS_DIR;
|
|
260
|
-
if (override && override.trim()) {
|
|
261
|
-
return [override];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const roots = [
|
|
265
|
-
path.join(homeDir, ".config", "tokscale", "headless"),
|
|
266
|
-
path.join(homeDir, "Library", "Application Support", "tokscale", "headless"),
|
|
267
|
-
];
|
|
268
|
-
|
|
269
|
-
return Array.from(new Set(roots));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function describePath(targetPath: string): string {
|
|
273
|
-
return fs.existsSync(targetPath) ? targetPath : `${targetPath} (missing)`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
type HeadlessFormat = "json" | "jsonl";
|
|
277
|
-
type HeadlessSource = "codex";
|
|
278
|
-
|
|
279
|
-
const HEADLESS_SOURCES: HeadlessSource[] = ["codex"];
|
|
280
|
-
|
|
281
|
-
function normalizeHeadlessSource(source: string): HeadlessSource | null {
|
|
282
|
-
const normalized = source.toLowerCase();
|
|
283
|
-
return HEADLESS_SOURCES.includes(normalized as HeadlessSource)
|
|
284
|
-
? (normalized as HeadlessSource)
|
|
285
|
-
: null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function resolveHeadlessFormat(
|
|
289
|
-
source: HeadlessSource,
|
|
290
|
-
args: string[],
|
|
291
|
-
override?: string
|
|
292
|
-
): HeadlessFormat {
|
|
293
|
-
if (override === "json" || override === "jsonl") {
|
|
294
|
-
return override;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return "jsonl";
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function applyHeadlessDefaults(
|
|
301
|
-
source: HeadlessSource,
|
|
302
|
-
args: string[],
|
|
303
|
-
format: HeadlessFormat,
|
|
304
|
-
autoFlags: boolean
|
|
305
|
-
): string[] {
|
|
306
|
-
if (!autoFlags) return args;
|
|
307
|
-
|
|
308
|
-
const updated = [...args];
|
|
309
|
-
|
|
310
|
-
if (source === "codex" && !updated.includes("--json")) {
|
|
311
|
-
updated.push("--json");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return updated;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function buildHeadlessOutputPath(
|
|
318
|
-
headlessRoots: string[],
|
|
319
|
-
source: HeadlessSource,
|
|
320
|
-
format: HeadlessFormat,
|
|
321
|
-
outputPath?: string
|
|
322
|
-
): string {
|
|
323
|
-
if (outputPath) {
|
|
324
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
325
|
-
return outputPath;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const root = headlessRoots[0] || path.join(os.homedir(), ".config", "tokscale", "headless");
|
|
329
|
-
const dir = path.join(root, source);
|
|
330
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
331
|
-
|
|
332
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
333
|
-
const id = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
334
|
-
const filename = `${source}-${stamp}-${id}.${format}`;
|
|
335
|
-
return path.join(dir, filename);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function printHeadlessHelp(): void {
|
|
339
|
-
console.log("\n Usage: tokscale headless codex [args...]");
|
|
340
|
-
console.log(" Options:");
|
|
341
|
-
console.log(" --format <json|jsonl> Override output format");
|
|
342
|
-
console.log(" --output <file> Write captured output to file");
|
|
343
|
-
console.log(" --no-auto-flags Do not auto-add JSON output flags");
|
|
344
|
-
console.log("\n Examples:");
|
|
345
|
-
console.log(" tokscale headless codex exec -m gpt-5");
|
|
346
|
-
console.log();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function runHeadlessCapture(argv: string[]): Promise<void> {
|
|
350
|
-
const sourceArg = argv[1];
|
|
351
|
-
if (!sourceArg || sourceArg === "--help" || sourceArg === "-h") {
|
|
352
|
-
printHeadlessHelp();
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const source = normalizeHeadlessSource(sourceArg);
|
|
357
|
-
if (!source) {
|
|
358
|
-
console.error(`\n Error: Unknown headless source '${sourceArg}'.`);
|
|
359
|
-
printHeadlessHelp();
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const rawArgs = argv.slice(2);
|
|
364
|
-
let outputPath: string | undefined;
|
|
365
|
-
let formatOverride: HeadlessFormat | undefined;
|
|
366
|
-
let autoFlags = true;
|
|
367
|
-
const cmdArgs: string[] = [];
|
|
368
|
-
|
|
369
|
-
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
370
|
-
const arg = rawArgs[i];
|
|
371
|
-
if (arg === "--") continue;
|
|
372
|
-
if ((arg === "--help" || arg === "-h") && cmdArgs.length === 0) {
|
|
373
|
-
printHeadlessHelp();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (arg === "--output") {
|
|
377
|
-
const value = rawArgs[i + 1];
|
|
378
|
-
if (!value) {
|
|
379
|
-
console.error("\n Error: --output requires a file path.");
|
|
380
|
-
process.exit(1);
|
|
381
|
-
}
|
|
382
|
-
outputPath = value;
|
|
383
|
-
i += 1;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (arg === "--format") {
|
|
387
|
-
const format = rawArgs[i + 1];
|
|
388
|
-
if (!format) {
|
|
389
|
-
console.error("\n Error: --format requires a value (json or jsonl).");
|
|
390
|
-
process.exit(1);
|
|
391
|
-
}
|
|
392
|
-
if (format !== "json" && format !== "jsonl") {
|
|
393
|
-
console.error(`\n Error: Invalid format '${format}'. Use json or jsonl.`);
|
|
394
|
-
process.exit(1);
|
|
395
|
-
}
|
|
396
|
-
formatOverride = format as HeadlessFormat;
|
|
397
|
-
i += 1;
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
if (arg === "--no-auto-flags") {
|
|
401
|
-
autoFlags = false;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
cmdArgs.push(arg);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (cmdArgs.length === 0) {
|
|
408
|
-
console.error("\n Error: Missing CLI arguments to execute.");
|
|
409
|
-
printHeadlessHelp();
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const format = resolveHeadlessFormat(source, cmdArgs, formatOverride);
|
|
414
|
-
const finalArgs = applyHeadlessDefaults(source, cmdArgs, format, autoFlags);
|
|
415
|
-
const headlessRoots = getHeadlessRoots(os.homedir());
|
|
416
|
-
const output = buildHeadlessOutputPath(headlessRoots, source, format, outputPath);
|
|
417
|
-
|
|
418
|
-
console.log(pc.cyan("\n Headless capture"));
|
|
419
|
-
console.log(pc.gray(` source: ${source}`));
|
|
420
|
-
console.log(pc.gray(` output: ${output}`));
|
|
421
|
-
console.log();
|
|
422
|
-
|
|
423
|
-
const proc = spawn(source, finalArgs, {
|
|
424
|
-
stdio: ["inherit", "pipe", "inherit"],
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
if (!proc.stdout) {
|
|
428
|
-
console.error("\n Error: Failed to capture stdout from command.");
|
|
429
|
-
process.exit(1);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const outputStream = fs.createWriteStream(output, { encoding: "utf-8" });
|
|
433
|
-
const outputFinished = new Promise<void>((resolve, reject) => {
|
|
434
|
-
outputStream.on("finish", () => resolve());
|
|
435
|
-
outputStream.on("error", reject);
|
|
436
|
-
});
|
|
437
|
-
proc.stdout.pipe(outputStream);
|
|
438
|
-
let exitCode: number;
|
|
439
|
-
try {
|
|
440
|
-
exitCode = await new Promise<number>((resolve, reject) => {
|
|
441
|
-
proc.on("error", reject);
|
|
442
|
-
proc.on("close", (code) => resolve(code ?? 1));
|
|
443
|
-
});
|
|
444
|
-
} catch (err) {
|
|
445
|
-
outputStream.destroy();
|
|
446
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
447
|
-
console.error(`\n Error: Failed to run '${source}': ${message}`);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
outputStream.end();
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
await outputFinished;
|
|
455
|
-
} catch (err) {
|
|
456
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
457
|
-
console.error(`\n Error: Failed to write headless output: ${message}`);
|
|
458
|
-
process.exit(1);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (exitCode !== 0) {
|
|
462
|
-
process.exit(exitCode);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
console.log(pc.green(` Saved headless output to ${output}`));
|
|
466
|
-
console.log();
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function buildTUIOptions(
|
|
470
|
-
options: FilterOptions & DateFilterOptions,
|
|
471
|
-
initialTab?: TabType
|
|
472
|
-
): TUIOptions {
|
|
473
|
-
const dateFilters = getDateFilters(options);
|
|
474
|
-
const enabledSources = getEnabledSources(options);
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
initialTab,
|
|
478
|
-
enabledSources: enabledSources as TUIOptions["enabledSources"],
|
|
479
|
-
since: dateFilters.since,
|
|
480
|
-
until: dateFilters.until,
|
|
481
|
-
year: dateFilters.year,
|
|
482
|
-
sinceTs: dateFilters.sinceTs,
|
|
483
|
-
untilTs: dateFilters.untilTs,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async function main() {
|
|
488
|
-
const program = new Command();
|
|
489
|
-
|
|
490
|
-
program
|
|
491
|
-
.name("tokscale")
|
|
492
|
-
.description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, Cursor, and Amp")
|
|
493
|
-
.version(pkg.version);
|
|
494
|
-
|
|
495
|
-
program
|
|
496
|
-
.command("monthly")
|
|
497
|
-
.description("Show monthly usage report (launches TUI by default)")
|
|
498
|
-
.option("--light", "Use legacy CLI table output instead of TUI")
|
|
499
|
-
.option("--json", "Output as JSON (for scripting)")
|
|
500
|
-
.option("--opencode", "Show only OpenCode usage")
|
|
501
|
-
.option("--claude", "Show only Claude Code usage")
|
|
502
|
-
.option("--codex", "Show only Codex CLI usage")
|
|
503
|
-
.option("--gemini", "Show only Gemini CLI usage")
|
|
504
|
-
.option("--cursor", "Show only Cursor IDE usage")
|
|
505
|
-
.option("--amp", "Show only Amp usage")
|
|
506
|
-
.option("--droid", "Show only Factory Droid usage")
|
|
507
|
-
.option("--openclaw", "Show only OpenClaw usage")
|
|
508
|
-
.option("--pi", "Show only Pi usage")
|
|
509
|
-
.option("--kimi", "Show only Kimi CLI usage")
|
|
510
|
-
.option("--today", "Show only today's usage")
|
|
511
|
-
.option("--week", "Show last 7 days")
|
|
512
|
-
.option("--month", "Show current month")
|
|
513
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
514
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
515
|
-
.option("--year <year>", "Filter to specific year")
|
|
516
|
-
.option("--benchmark", "Show processing time")
|
|
517
|
-
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
518
|
-
.action(async (options) => {
|
|
519
|
-
if (options.json) {
|
|
520
|
-
await outputJsonReport("monthly", options);
|
|
521
|
-
} else if (options.light) {
|
|
522
|
-
await showMonthlyReport(options, { spinner: options.spinner });
|
|
523
|
-
} else {
|
|
524
|
-
const launchTUI = await tryLoadTUI();
|
|
525
|
-
if (launchTUI) {
|
|
526
|
-
await launchTUI(buildTUIOptions(options, "daily"));
|
|
527
|
-
} else {
|
|
528
|
-
showTUIUnavailableMessage();
|
|
529
|
-
await showMonthlyReport(options, { spinner: options.spinner });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
program
|
|
535
|
-
.command("models")
|
|
536
|
-
.description("Show usage breakdown by model (launches TUI by default)")
|
|
537
|
-
.option("--light", "Use legacy CLI table output instead of TUI")
|
|
538
|
-
.option("--json", "Output as JSON (for scripting)")
|
|
539
|
-
.option("--opencode", "Show only OpenCode usage")
|
|
540
|
-
.option("--claude", "Show only Claude Code usage")
|
|
541
|
-
.option("--codex", "Show only Codex CLI usage")
|
|
542
|
-
.option("--gemini", "Show only Gemini CLI usage")
|
|
543
|
-
.option("--cursor", "Show only Cursor IDE usage")
|
|
544
|
-
.option("--amp", "Show only Amp usage")
|
|
545
|
-
.option("--droid", "Show only Factory Droid usage")
|
|
546
|
-
.option("--openclaw", "Show only OpenClaw usage")
|
|
547
|
-
.option("--pi", "Show only Pi usage")
|
|
548
|
-
.option("--kimi", "Show only Kimi CLI usage")
|
|
549
|
-
.option("--today", "Show only today's usage")
|
|
550
|
-
.option("--week", "Show last 7 days")
|
|
551
|
-
.option("--month", "Show current month")
|
|
552
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
553
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
554
|
-
.option("--year <year>", "Filter to specific year")
|
|
555
|
-
.option("--benchmark", "Show processing time")
|
|
556
|
-
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
557
|
-
.action(async (options) => {
|
|
558
|
-
if (options.json) {
|
|
559
|
-
await outputJsonReport("models", options);
|
|
560
|
-
} else if (options.light) {
|
|
561
|
-
await showModelReport(options, { spinner: options.spinner });
|
|
562
|
-
} else {
|
|
563
|
-
const launchTUI = await tryLoadTUI();
|
|
564
|
-
if (launchTUI) {
|
|
565
|
-
await launchTUI(buildTUIOptions(options, "model"));
|
|
566
|
-
} else {
|
|
567
|
-
showTUIUnavailableMessage();
|
|
568
|
-
await showModelReport(options, { spinner: options.spinner });
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
program
|
|
574
|
-
.command("sources")
|
|
575
|
-
.description("Show local scan locations and session counts")
|
|
576
|
-
.option("--json", "Output as JSON (for scripting)")
|
|
577
|
-
.action(async (options) => {
|
|
578
|
-
const homeDir = os.homedir();
|
|
579
|
-
const headlessRoots = getHeadlessRoots(homeDir);
|
|
580
|
-
|
|
581
|
-
// Define all session paths
|
|
582
|
-
const opencodeSessions = path.join(homeDir, ".local", "share", "opencode", "storage", "message");
|
|
583
|
-
const claudeSessions = path.join(homeDir, ".claude", "projects");
|
|
584
|
-
const codexHome = process.env.CODEX_HOME || path.join(homeDir, ".codex");
|
|
585
|
-
const codexSessions = path.join(codexHome, "sessions");
|
|
586
|
-
const geminiSessions = path.join(homeDir, ".gemini", "tmp");
|
|
587
|
-
const ampSessions = path.join(homeDir, ".local", "share", "amp", "threads");
|
|
588
|
-
const droidSessions = path.join(homeDir, ".factory", "sessions");
|
|
589
|
-
const openclawSessions = path.join(homeDir, ".openclaw", "agents");
|
|
590
|
-
const openclawLegacyPaths = [
|
|
591
|
-
path.join(homeDir, ".clawdbot", "agents"),
|
|
592
|
-
path.join(homeDir, ".moltbot", "agents"),
|
|
593
|
-
path.join(homeDir, ".moldbot", "agents"),
|
|
594
|
-
];
|
|
595
|
-
const piSessions = path.join(homeDir, ".pi", "agent", "sessions");
|
|
596
|
-
const kimiSessions = path.join(homeDir, ".kimi", "sessions");
|
|
597
|
-
|
|
598
|
-
let localMessages: ParsedMessages | null = null;
|
|
599
|
-
try {
|
|
600
|
-
localMessages = await parseLocalSourcesAsync({
|
|
601
|
-
homeDir,
|
|
602
|
-
sources: ["opencode", "claude", "codex", "gemini", "amp", "droid", "openclaw", "pi", "kimi"],
|
|
603
|
-
});
|
|
604
|
-
} catch (e) {
|
|
605
|
-
console.error(`Error: ${(e as Error).message}`);
|
|
606
|
-
process.exit(1);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const headlessCounts = {
|
|
610
|
-
codex: 0,
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
for (const message of localMessages.messages) {
|
|
614
|
-
if (message.agent === "headless" && message.source === "codex") {
|
|
615
|
-
headlessCounts.codex += 1;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const sourceRows: Array<{
|
|
620
|
-
source: SourceType;
|
|
621
|
-
label: string;
|
|
622
|
-
sessionsPath: string;
|
|
623
|
-
legacyPaths?: string[];
|
|
624
|
-
messageCount: number;
|
|
625
|
-
headlessSupported: boolean;
|
|
626
|
-
headlessPaths: string[];
|
|
627
|
-
headlessMessageCount: number;
|
|
628
|
-
}> = [
|
|
629
|
-
{
|
|
630
|
-
source: "opencode",
|
|
631
|
-
label: "OpenCode",
|
|
632
|
-
sessionsPath: opencodeSessions,
|
|
633
|
-
messageCount: localMessages.opencodeCount,
|
|
634
|
-
headlessSupported: false,
|
|
635
|
-
headlessPaths: [],
|
|
636
|
-
headlessMessageCount: 0,
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
source: "claude",
|
|
640
|
-
label: "Claude Code",
|
|
641
|
-
sessionsPath: claudeSessions,
|
|
642
|
-
messageCount: localMessages.claudeCount,
|
|
643
|
-
headlessSupported: false,
|
|
644
|
-
headlessPaths: [],
|
|
645
|
-
headlessMessageCount: 0,
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
source: "codex",
|
|
649
|
-
label: "Codex CLI",
|
|
650
|
-
sessionsPath: codexSessions,
|
|
651
|
-
headlessPaths: headlessRoots.map((root) => path.join(root, "codex")),
|
|
652
|
-
messageCount: localMessages.codexCount,
|
|
653
|
-
headlessMessageCount: headlessCounts.codex,
|
|
654
|
-
headlessSupported: true,
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
source: "gemini",
|
|
658
|
-
label: "Gemini CLI",
|
|
659
|
-
sessionsPath: geminiSessions,
|
|
660
|
-
messageCount: localMessages.geminiCount,
|
|
661
|
-
headlessSupported: false,
|
|
662
|
-
headlessPaths: [],
|
|
663
|
-
headlessMessageCount: 0,
|
|
664
|
-
},
|
|
665
|
-
{
|
|
666
|
-
source: "cursor",
|
|
667
|
-
label: "Cursor IDE",
|
|
668
|
-
sessionsPath: path.join(homeDir, ".config", "tokscale", "cursor-cache"),
|
|
669
|
-
messageCount: 0, // Cursor uses API sync, not local sessions
|
|
670
|
-
headlessSupported: false,
|
|
671
|
-
headlessPaths: [],
|
|
672
|
-
headlessMessageCount: 0,
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
source: "amp",
|
|
676
|
-
label: "Amp",
|
|
677
|
-
sessionsPath: ampSessions,
|
|
678
|
-
messageCount: localMessages.ampCount,
|
|
679
|
-
headlessSupported: false,
|
|
680
|
-
headlessPaths: [],
|
|
681
|
-
headlessMessageCount: 0,
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
source: "droid",
|
|
685
|
-
label: "Droid",
|
|
686
|
-
sessionsPath: droidSessions,
|
|
687
|
-
messageCount: localMessages.droidCount,
|
|
688
|
-
headlessSupported: false,
|
|
689
|
-
headlessPaths: [],
|
|
690
|
-
headlessMessageCount: 0,
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
source: "openclaw",
|
|
694
|
-
label: "OpenClaw",
|
|
695
|
-
sessionsPath: openclawSessions,
|
|
696
|
-
legacyPaths: openclawLegacyPaths,
|
|
697
|
-
messageCount: localMessages.openclawCount,
|
|
698
|
-
headlessSupported: false,
|
|
699
|
-
headlessPaths: [],
|
|
700
|
-
headlessMessageCount: 0,
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
source: "pi",
|
|
704
|
-
label: "Pi",
|
|
705
|
-
sessionsPath: piSessions,
|
|
706
|
-
messageCount: localMessages.piCount,
|
|
707
|
-
headlessSupported: false,
|
|
708
|
-
headlessPaths: [],
|
|
709
|
-
headlessMessageCount: 0,
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
source: "kimi",
|
|
713
|
-
label: "Kimi CLI",
|
|
714
|
-
sessionsPath: kimiSessions,
|
|
715
|
-
messageCount: localMessages.kimiCount,
|
|
716
|
-
headlessSupported: false,
|
|
717
|
-
headlessPaths: [],
|
|
718
|
-
headlessMessageCount: 0,
|
|
719
|
-
},
|
|
720
|
-
];
|
|
721
|
-
|
|
722
|
-
if (options.json) {
|
|
723
|
-
const payload = {
|
|
724
|
-
headlessRoots,
|
|
725
|
-
sources: sourceRows.map((row) => ({
|
|
726
|
-
source: row.source,
|
|
727
|
-
label: row.label,
|
|
728
|
-
sessionsPath: row.sessionsPath,
|
|
729
|
-
sessionsPathExists: fs.existsSync(row.sessionsPath),
|
|
730
|
-
legacyPaths: row.legacyPaths
|
|
731
|
-
? row.legacyPaths.map((legacyPath) => ({
|
|
732
|
-
path: legacyPath,
|
|
733
|
-
exists: fs.existsSync(legacyPath),
|
|
734
|
-
}))
|
|
735
|
-
: [],
|
|
736
|
-
messageCount: row.messageCount,
|
|
737
|
-
headlessSupported: row.headlessSupported,
|
|
738
|
-
headlessPaths: row.headlessSupported
|
|
739
|
-
? row.headlessPaths.map((headlessPath) => ({
|
|
740
|
-
path: headlessPath,
|
|
741
|
-
exists: fs.existsSync(headlessPath),
|
|
742
|
-
}))
|
|
743
|
-
: [],
|
|
744
|
-
headlessMessageCount: row.headlessSupported ? row.headlessMessageCount : 0,
|
|
745
|
-
})),
|
|
746
|
-
note: "Headless capture is supported for Codex CLI only.",
|
|
747
|
-
};
|
|
748
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
console.log(pc.cyan("\n Local sources & session counts"));
|
|
753
|
-
console.log(pc.gray(` Headless roots: ${headlessRoots.join(", ")}`));
|
|
754
|
-
console.log();
|
|
755
|
-
|
|
756
|
-
for (const row of sourceRows) {
|
|
757
|
-
console.log(pc.white(` ${row.label}`));
|
|
758
|
-
console.log(pc.gray(` sessions: ${describePath(row.sessionsPath)}`));
|
|
759
|
-
if (row.legacyPaths && row.legacyPaths.length > 0) {
|
|
760
|
-
console.log(
|
|
761
|
-
pc.gray(` legacy: ${row.legacyPaths.map(describePath).join(", ")}`)
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
if (row.headlessSupported) {
|
|
765
|
-
console.log(
|
|
766
|
-
pc.gray(
|
|
767
|
-
` headless: ${row.headlessPaths.map(describePath).join(", ")}`
|
|
768
|
-
)
|
|
769
|
-
);
|
|
770
|
-
console.log(
|
|
771
|
-
pc.gray(
|
|
772
|
-
` messages: ${formatNumber(row.messageCount)} (headless: ${formatNumber(
|
|
773
|
-
row.headlessMessageCount
|
|
774
|
-
)})`
|
|
775
|
-
)
|
|
776
|
-
);
|
|
777
|
-
} else {
|
|
778
|
-
console.log(pc.gray(` messages: ${formatNumber(row.messageCount)}`));
|
|
779
|
-
}
|
|
780
|
-
console.log();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
console.log(
|
|
784
|
-
pc.gray(
|
|
785
|
-
" Note: Headless capture is supported for Codex CLI only."
|
|
786
|
-
)
|
|
787
|
-
);
|
|
788
|
-
console.log();
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
program
|
|
792
|
-
.command("headless")
|
|
793
|
-
.description("Run a CLI in headless mode and capture stdout")
|
|
794
|
-
.argument("<source>", "Source CLI to capture (currently only 'codex' is supported)")
|
|
795
|
-
.argument("[args...]", "Arguments passed to the CLI");
|
|
796
|
-
|
|
797
|
-
program
|
|
798
|
-
.command("graph")
|
|
799
|
-
.description("Export contribution graph data as JSON")
|
|
800
|
-
.option("--output <file>", "Write to file instead of stdout")
|
|
801
|
-
.option("--opencode", "Include only OpenCode data")
|
|
802
|
-
.option("--claude", "Include only Claude Code data")
|
|
803
|
-
.option("--codex", "Include only Codex CLI data")
|
|
804
|
-
.option("--gemini", "Include only Gemini CLI data")
|
|
805
|
-
.option("--cursor", "Include only Cursor IDE data")
|
|
806
|
-
.option("--amp", "Include only Amp data")
|
|
807
|
-
.option("--droid", "Include only Factory Droid data")
|
|
808
|
-
.option("--openclaw", "Include only OpenClaw data")
|
|
809
|
-
.option("--pi", "Include only Pi data")
|
|
810
|
-
.option("--kimi", "Include only Kimi CLI data")
|
|
811
|
-
.option("--today", "Show only today's usage")
|
|
812
|
-
.option("--week", "Show last 7 days")
|
|
813
|
-
.option("--month", "Show current month")
|
|
814
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
815
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
816
|
-
.option("--year <year>", "Filter to specific year")
|
|
817
|
-
.option("--benchmark", "Show processing time")
|
|
818
|
-
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
819
|
-
.action(async (options) => {
|
|
820
|
-
await handleGraphCommand(options);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
program
|
|
824
|
-
.command("wrapped")
|
|
825
|
-
.description("Generate Wrapped shareable image")
|
|
826
|
-
.option("--output <file>", "Output file path (default: tokscale-<year>-wrapped.png)")
|
|
827
|
-
.option("--year <year>", "Year to generate (default: current year)")
|
|
828
|
-
.option("--opencode", "Include only OpenCode data")
|
|
829
|
-
.option("--claude", "Include only Claude Code data")
|
|
830
|
-
.option("--codex", "Include only Codex CLI data")
|
|
831
|
-
.option("--gemini", "Include only Gemini CLI data")
|
|
832
|
-
.option("--cursor", "Include only Cursor IDE data")
|
|
833
|
-
.option("--amp", "Include only Amp data")
|
|
834
|
-
.option("--droid", "Include only Factory Droid data")
|
|
835
|
-
.option("--openclaw", "Include only OpenClaw data")
|
|
836
|
-
.option("--pi", "Include only Pi data")
|
|
837
|
-
.option("--kimi", "Include only Kimi CLI data")
|
|
838
|
-
.option("--no-spinner", "Disable loading spinner (for scripting)")
|
|
839
|
-
.option("--short", "Display total tokens in abbreviated format (e.g., 7.14B)")
|
|
840
|
-
.addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
|
|
841
|
-
.addOption(new Option("--clients", "Show Top Clients instead of Top OpenCode Agents").conflicts("agents"))
|
|
842
|
-
.option("--disable-pinned", "Disable pinning of Sisyphus agents in rankings")
|
|
843
|
-
.action(async (options) => {
|
|
844
|
-
await handleWrappedCommand(options);
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
program
|
|
848
|
-
.command("login")
|
|
849
|
-
.description("Login to Tokscale (opens browser for GitHub auth)")
|
|
850
|
-
.action(async () => {
|
|
851
|
-
await login();
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
program
|
|
855
|
-
.command("logout")
|
|
856
|
-
.description("Logout from Tokscale")
|
|
857
|
-
.action(async () => {
|
|
858
|
-
await logout();
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
program
|
|
862
|
-
.command("whoami")
|
|
863
|
-
.description("Show current logged in user")
|
|
864
|
-
.action(async () => {
|
|
865
|
-
await whoami();
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
// =========================================================================
|
|
869
|
-
// Submit Command
|
|
870
|
-
// =========================================================================
|
|
871
|
-
|
|
872
|
-
program
|
|
873
|
-
.command("submit")
|
|
874
|
-
.description("Submit your usage data to Tokscale")
|
|
875
|
-
.option("--opencode", "Include only OpenCode data")
|
|
876
|
-
.option("--claude", "Include only Claude Code data")
|
|
877
|
-
.option("--codex", "Include only Codex CLI data")
|
|
878
|
-
.option("--gemini", "Include only Gemini CLI data")
|
|
879
|
-
.option("--cursor", "Include only Cursor IDE data")
|
|
880
|
-
.option("--amp", "Include only Amp data")
|
|
881
|
-
.option("--droid", "Include only Factory Droid data")
|
|
882
|
-
.option("--openclaw", "Include only OpenClaw data")
|
|
883
|
-
.option("--pi", "Include only Pi data")
|
|
884
|
-
.option("--kimi", "Include only Kimi CLI data")
|
|
885
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
886
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
887
|
-
.option("--year <year>", "Filter to specific year")
|
|
888
|
-
.option("--dry-run", "Show what would be submitted without actually submitting")
|
|
889
|
-
.action(async (options) => {
|
|
890
|
-
await submit({
|
|
891
|
-
opencode: options.opencode,
|
|
892
|
-
claude: options.claude,
|
|
893
|
-
codex: options.codex,
|
|
894
|
-
gemini: options.gemini,
|
|
895
|
-
cursor: options.cursor,
|
|
896
|
-
amp: options.amp,
|
|
897
|
-
droid: options.droid,
|
|
898
|
-
openclaw: options.openclaw,
|
|
899
|
-
pi: options.pi,
|
|
900
|
-
kimi: options.kimi,
|
|
901
|
-
since: options.since,
|
|
902
|
-
until: options.until,
|
|
903
|
-
year: options.year,
|
|
904
|
-
dryRun: options.dryRun,
|
|
905
|
-
});
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
// =========================================================================
|
|
909
|
-
// Interactive TUI Command
|
|
910
|
-
// =========================================================================
|
|
911
|
-
|
|
912
|
-
program
|
|
913
|
-
.command("tui")
|
|
914
|
-
.description("Launch interactive terminal UI")
|
|
915
|
-
.option("--opencode", "Show only OpenCode usage")
|
|
916
|
-
.option("--claude", "Show only Claude Code usage")
|
|
917
|
-
.option("--codex", "Show only Codex CLI usage")
|
|
918
|
-
.option("--gemini", "Show only Gemini CLI usage")
|
|
919
|
-
.option("--cursor", "Show only Cursor IDE usage")
|
|
920
|
-
.option("--amp", "Show only Amp usage")
|
|
921
|
-
.option("--droid", "Show only Factory Droid usage")
|
|
922
|
-
.option("--openclaw", "Show only OpenClaw usage")
|
|
923
|
-
.option("--pi", "Show only Pi usage")
|
|
924
|
-
.option("--kimi", "Show only Kimi CLI usage")
|
|
925
|
-
.option("--today", "Show only today's usage")
|
|
926
|
-
.option("--week", "Show last 7 days")
|
|
927
|
-
.option("--month", "Show current month")
|
|
928
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
929
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
930
|
-
.option("--year <year>", "Filter to specific year")
|
|
931
|
-
.action(async (options) => {
|
|
932
|
-
const launchTUI = await tryLoadTUI();
|
|
933
|
-
if (launchTUI) {
|
|
934
|
-
await launchTUI(buildTUIOptions(options));
|
|
935
|
-
} else {
|
|
936
|
-
showTUIUnavailableMessage();
|
|
937
|
-
process.exit(1);
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
program
|
|
942
|
-
.command("pricing <model-id>")
|
|
943
|
-
.description("Look up pricing for a model")
|
|
944
|
-
.option("--json", "Output as JSON")
|
|
945
|
-
.option("--provider <source>", "Force pricing source: 'litellm' or 'openrouter'")
|
|
946
|
-
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
947
|
-
.action(async (modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) => {
|
|
948
|
-
await handlePricingCommand(modelId, options);
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
const cursorCommand = program
|
|
952
|
-
.command("cursor")
|
|
953
|
-
.description("Cursor IDE integration commands");
|
|
954
|
-
|
|
955
|
-
cursorCommand
|
|
956
|
-
.command("login")
|
|
957
|
-
.description("Login to Cursor (paste your session token)")
|
|
958
|
-
.option("--name <name>", "Label for this Cursor account (e.g., work, personal)")
|
|
959
|
-
.action(async (options: { name?: string }) => {
|
|
960
|
-
ensureCursorMigration();
|
|
961
|
-
await cursorLogin(options);
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
cursorCommand
|
|
965
|
-
.command("logout")
|
|
966
|
-
.description("Logout from a Cursor account")
|
|
967
|
-
.option("--name <name>", "Account label or id")
|
|
968
|
-
.option("--all", "Logout from all Cursor accounts")
|
|
969
|
-
.option("--purge-cache", "Also delete cached Cursor usage for the logged-out account(s)")
|
|
970
|
-
.action(async (options: { name?: string; all?: boolean; purgeCache?: boolean }) => {
|
|
971
|
-
ensureCursorMigration();
|
|
972
|
-
await cursorLogout(options);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
cursorCommand
|
|
976
|
-
.command("status")
|
|
977
|
-
.description("Check Cursor authentication status")
|
|
978
|
-
.option("--name <name>", "Account label or id")
|
|
979
|
-
.action(async (options: { name?: string }) => {
|
|
980
|
-
ensureCursorMigration();
|
|
981
|
-
await cursorStatus(options);
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
cursorCommand
|
|
985
|
-
.command("accounts")
|
|
986
|
-
.description("List saved Cursor accounts")
|
|
987
|
-
.option("--json", "Output as JSON")
|
|
988
|
-
.action(async (options: { json?: boolean }) => {
|
|
989
|
-
ensureCursorMigration();
|
|
990
|
-
const accounts = listCursorAccounts();
|
|
991
|
-
if (options.json) {
|
|
992
|
-
console.log(JSON.stringify({ accounts }, null, 2));
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (accounts.length === 0) {
|
|
997
|
-
console.log(pc.yellow("\n No saved Cursor accounts.\n"));
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
console.log(pc.cyan("\n Cursor IDE - Accounts\n"));
|
|
1002
|
-
for (const acct of accounts) {
|
|
1003
|
-
const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
|
|
1004
|
-
console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
|
|
1005
|
-
}
|
|
1006
|
-
console.log();
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
cursorCommand
|
|
1010
|
-
.command("switch")
|
|
1011
|
-
.description("Switch active Cursor account")
|
|
1012
|
-
.argument("<name>", "Account label or id")
|
|
1013
|
-
.action(async (name: string) => {
|
|
1014
|
-
ensureCursorMigration();
|
|
1015
|
-
const result = setActiveCursorAccount(name);
|
|
1016
|
-
if (!result.ok) {
|
|
1017
|
-
console.log(pc.red(`\n Error: ${result.error}\n`));
|
|
1018
|
-
process.exit(1);
|
|
1019
|
-
}
|
|
1020
|
-
console.log(pc.green(`\n Active Cursor account set to ${pc.bold(name)}\n`));
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
// Check if a subcommand was provided
|
|
1024
|
-
const args = process.argv.slice(2);
|
|
1025
|
-
if (args[0] === "headless") {
|
|
1026
|
-
await runHeadlessCapture(args);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
const firstArg = args[0] || '';
|
|
1030
|
-
// Global flags should go to main program
|
|
1031
|
-
const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
|
|
1032
|
-
const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
|
|
1033
|
-
const knownCommands = ['monthly', 'models', 'sources', 'headless', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
|
|
1034
|
-
const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
|
|
1035
|
-
|
|
1036
|
-
if (isKnownCommand || isGlobalFlag) {
|
|
1037
|
-
// Run the specified subcommand or show full help/version
|
|
1038
|
-
await program.parseAsync();
|
|
1039
|
-
} else {
|
|
1040
|
-
// No subcommand - launch TUI by default, or legacy CLI with --light, or JSON with --json
|
|
1041
|
-
const defaultProgram = new Command();
|
|
1042
|
-
defaultProgram
|
|
1043
|
-
.option("--light", "Use legacy CLI table output instead of TUI")
|
|
1044
|
-
.option("--json", "Output as JSON (for scripting)")
|
|
1045
|
-
.option("--opencode", "Show only OpenCode usage")
|
|
1046
|
-
.option("--claude", "Show only Claude Code usage")
|
|
1047
|
-
.option("--codex", "Show only Codex CLI usage")
|
|
1048
|
-
.option("--gemini", "Show only Gemini CLI usage")
|
|
1049
|
-
.option("--cursor", "Show only Cursor IDE usage")
|
|
1050
|
-
.option("--amp", "Show only Amp usage")
|
|
1051
|
-
.option("--droid", "Show only Factory Droid usage")
|
|
1052
|
-
.option("--openclaw", "Show only OpenClaw usage")
|
|
1053
|
-
.option("--pi", "Show only Pi usage")
|
|
1054
|
-
.option("--kimi", "Show only Kimi CLI usage")
|
|
1055
|
-
.option("--today", "Show only today's usage")
|
|
1056
|
-
.option("--week", "Show last 7 days")
|
|
1057
|
-
.option("--month", "Show current month")
|
|
1058
|
-
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
1059
|
-
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
1060
|
-
.option("--year <year>", "Filter to specific year")
|
|
1061
|
-
.option("--benchmark", "Show processing time")
|
|
1062
|
-
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
1063
|
-
.parse();
|
|
1064
|
-
|
|
1065
|
-
const opts = defaultProgram.opts();
|
|
1066
|
-
if (opts.json) {
|
|
1067
|
-
await outputJsonReport("models", opts);
|
|
1068
|
-
} else if (opts.light) {
|
|
1069
|
-
await showModelReport(opts, { spinner: opts.spinner });
|
|
1070
|
-
} else {
|
|
1071
|
-
const launchTUI = await tryLoadTUI();
|
|
1072
|
-
if (launchTUI) {
|
|
1073
|
-
await launchTUI(buildTUIOptions(opts));
|
|
1074
|
-
} else {
|
|
1075
|
-
showTUIUnavailableMessage();
|
|
1076
|
-
await showModelReport(opts, { spinner: opts.spinner });
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
|
|
1083
|
-
const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid || options.openclaw || options.pi || options.kimi;
|
|
1084
|
-
if (!hasFilter) return undefined; // All sources
|
|
1085
|
-
|
|
1086
|
-
const sources: SourceType[] = [];
|
|
1087
|
-
if (options.opencode) sources.push("opencode");
|
|
1088
|
-
if (options.claude) sources.push("claude");
|
|
1089
|
-
if (options.codex) sources.push("codex");
|
|
1090
|
-
if (options.gemini) sources.push("gemini");
|
|
1091
|
-
if (options.cursor) sources.push("cursor");
|
|
1092
|
-
if (options.amp) sources.push("amp");
|
|
1093
|
-
if (options.droid) sources.push("droid");
|
|
1094
|
-
if (options.openclaw) sources.push("openclaw");
|
|
1095
|
-
if (options.pi) sources.push("pi");
|
|
1096
|
-
if (options.kimi) sources.push("kimi");
|
|
1097
|
-
return sources;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
/**
|
|
1105
|
-
* Sync Cursor usage data from API to local cache.
|
|
1106
|
-
* Only attempts sync if user is authenticated with Cursor.
|
|
1107
|
-
*/
|
|
1108
|
-
async function syncCursorData(): Promise<CursorSyncResult> {
|
|
1109
|
-
if (!isCursorLoggedIn()) {
|
|
1110
|
-
return { attempted: false, synced: false, rows: 0 };
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const result = await syncCursorCache();
|
|
1114
|
-
return {
|
|
1115
|
-
attempted: true,
|
|
1116
|
-
synced: result.synced,
|
|
1117
|
-
rows: result.rows,
|
|
1118
|
-
error: result.error,
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
interface LoadedDataSources {
|
|
1123
|
-
cursorSync: CursorSyncResult;
|
|
1124
|
-
localMessages: ParsedMessages | null;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
async function loadDataSourcesParallel(
|
|
1128
|
-
localSources: SourceType[],
|
|
1129
|
-
dateFilters: DateFilters,
|
|
1130
|
-
onPhase?: (phase: string) => void
|
|
1131
|
-
): Promise<LoadedDataSources> {
|
|
1132
|
-
const shouldParseLocal = localSources.length > 0;
|
|
1133
|
-
|
|
1134
|
-
const [cursorResult, localResult] = await Promise.allSettled([
|
|
1135
|
-
syncCursorData(),
|
|
1136
|
-
shouldParseLocal
|
|
1137
|
-
? parseLocalSourcesAsync({
|
|
1138
|
-
sources: localSources.filter(s => s !== 'cursor'),
|
|
1139
|
-
since: dateFilters.since,
|
|
1140
|
-
until: dateFilters.until,
|
|
1141
|
-
year: dateFilters.year,
|
|
1142
|
-
sinceTs: dateFilters.sinceTs,
|
|
1143
|
-
untilTs: dateFilters.untilTs,
|
|
1144
|
-
})
|
|
1145
|
-
: Promise.resolve(null),
|
|
1146
|
-
]);
|
|
1147
|
-
|
|
1148
|
-
const cursorSync: CursorSyncResult = cursorResult.status === 'fulfilled'
|
|
1149
|
-
? cursorResult.value
|
|
1150
|
-
: { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
|
|
1151
|
-
|
|
1152
|
-
const localMessages: ParsedMessages | null = localResult.status === 'fulfilled'
|
|
1153
|
-
? localResult.value
|
|
1154
|
-
: null;
|
|
1155
|
-
|
|
1156
|
-
return { cursorSync, localMessages };
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
1160
|
-
const dateFilters = getDateFilters(options);
|
|
1161
|
-
const enabledSources = getEnabledSources(options);
|
|
1162
|
-
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
1163
|
-
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
1164
|
-
|
|
1165
|
-
// Check cursor auth early if cursor-only mode
|
|
1166
|
-
if (onlyCursor) {
|
|
1167
|
-
if (!isCursorLoggedIn() && !hasCursorUsageCache()) {
|
|
1168
|
-
console.log(pc.red("\n Error: Cursor authentication required."));
|
|
1169
|
-
console.log(pc.gray(" Run 'tokscale cursor login' to authenticate with Cursor.\n"));
|
|
1170
|
-
process.exit(1);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const dateRange = getDateRangeLabel(options);
|
|
1175
|
-
const title = dateRange
|
|
1176
|
-
? `Token Usage Report by Model (${dateRange})`
|
|
1177
|
-
: "Token Usage Report by Model";
|
|
1178
|
-
|
|
1179
|
-
console.log(pc.cyan(`\n ${title}`));
|
|
1180
|
-
if (options.benchmark) {
|
|
1181
|
-
console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
|
|
1182
|
-
}
|
|
1183
|
-
console.log();
|
|
1184
|
-
|
|
1185
|
-
const useSpinner = extraOptions?.spinner !== false;
|
|
1186
|
-
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1187
|
-
|
|
1188
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid', 'openclaw', 'pi', 'kimi'])
|
|
1189
|
-
.filter(s => s !== 'cursor');
|
|
1190
|
-
|
|
1191
|
-
spinner?.start(pc.gray("Scanning session data..."));
|
|
1192
|
-
|
|
1193
|
-
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
1194
|
-
onlyCursor ? [] : localSources,
|
|
1195
|
-
dateFilters,
|
|
1196
|
-
(phase) => spinner?.update(phase)
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
if (includeCursor && cursorSync.attempted && cursorSync.error) {
|
|
1200
|
-
// Don't block report generation; just warn about partial Cursor sync.
|
|
1201
|
-
console.log(pc.yellow(` Cursor sync warning: ${cursorSync.error}`));
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
if (!localMessages && !onlyCursor) {
|
|
1205
|
-
if (spinner) {
|
|
1206
|
-
spinner.error('Failed to parse local session files');
|
|
1207
|
-
} else {
|
|
1208
|
-
console.error('Failed to parse local session files');
|
|
1209
|
-
}
|
|
1210
|
-
process.exit(1);
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
spinner?.update(pc.gray("Finalizing report..."));
|
|
1214
|
-
const startTime = performance.now();
|
|
1215
|
-
|
|
1216
|
-
let report: ModelReport;
|
|
1217
|
-
try {
|
|
1218
|
-
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, piCount: 0, kimiCount: 0, processingTimeMs: 0 };
|
|
1219
|
-
report = await finalizeReportAsync({
|
|
1220
|
-
localMessages: localMessages || emptyMessages,
|
|
1221
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
1222
|
-
since: dateFilters.since,
|
|
1223
|
-
until: dateFilters.until,
|
|
1224
|
-
year: dateFilters.year,
|
|
1225
|
-
sinceTs: dateFilters.sinceTs,
|
|
1226
|
-
untilTs: dateFilters.untilTs,
|
|
1227
|
-
});
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
if (spinner) {
|
|
1230
|
-
spinner.error(`Error: ${(e as Error).message}`);
|
|
1231
|
-
} else {
|
|
1232
|
-
console.error(`Error: ${(e as Error).message}`);
|
|
1233
|
-
}
|
|
1234
|
-
process.exit(1);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const processingTime = performance.now() - startTime;
|
|
1238
|
-
spinner?.stop();
|
|
1239
|
-
|
|
1240
|
-
if (report.entries.length === 0) {
|
|
1241
|
-
if (onlyCursor && !cursorSync.synced) {
|
|
1242
|
-
console.log(pc.yellow(" No Cursor data available."));
|
|
1243
|
-
console.log(pc.gray(" Run 'tokscale cursor login' to authenticate with Cursor.\n"));
|
|
1244
|
-
} else {
|
|
1245
|
-
console.log(pc.yellow(" No usage data found.\n"));
|
|
1246
|
-
}
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// Create table
|
|
1251
|
-
const table = createUsageTable("Source/Model");
|
|
1252
|
-
|
|
1253
|
-
const settings = loadSettings();
|
|
1254
|
-
const filteredEntries = settings.includeUnusedModels
|
|
1255
|
-
? report.entries
|
|
1256
|
-
: report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
|
|
1257
|
-
|
|
1258
|
-
for (const entry of filteredEntries) {
|
|
1259
|
-
const sourceLabel = getSourceLabel(entry.source);
|
|
1260
|
-
const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
|
|
1261
|
-
table.push(
|
|
1262
|
-
formatUsageRow(
|
|
1263
|
-
modelDisplay,
|
|
1264
|
-
[entry.model],
|
|
1265
|
-
entry.input,
|
|
1266
|
-
entry.output,
|
|
1267
|
-
entry.cacheWrite,
|
|
1268
|
-
entry.cacheRead,
|
|
1269
|
-
entry.cost
|
|
1270
|
-
)
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// Add totals row
|
|
1275
|
-
table.push(
|
|
1276
|
-
formatTotalsRow(
|
|
1277
|
-
report.totalInput,
|
|
1278
|
-
report.totalOutput,
|
|
1279
|
-
report.totalCacheWrite,
|
|
1280
|
-
report.totalCacheRead,
|
|
1281
|
-
report.totalCost
|
|
1282
|
-
)
|
|
1283
|
-
);
|
|
1284
|
-
|
|
1285
|
-
console.log(table.toString());
|
|
1286
|
-
|
|
1287
|
-
// Summary stats
|
|
1288
|
-
console.log(
|
|
1289
|
-
pc.gray(
|
|
1290
|
-
`\n Total: ${formatNumber(report.totalMessages)} messages, ` +
|
|
1291
|
-
`${formatNumber(report.totalInput + report.totalOutput + report.totalCacheRead + report.totalCacheWrite)} tokens, ` +
|
|
1292
|
-
`${pc.green(formatCurrency(report.totalCost))}`
|
|
1293
|
-
)
|
|
1294
|
-
);
|
|
1295
|
-
|
|
1296
|
-
if (options.benchmark) {
|
|
1297
|
-
console.log(pc.gray(` Processing time: ${processingTime.toFixed(0)}ms (Rust) + ${report.processingTimeMs}ms (parsing)`));
|
|
1298
|
-
if (cursorSync.attempted) {
|
|
1299
|
-
if (cursorSync.synced) {
|
|
1300
|
-
console.log(pc.gray(` Cursor: ${cursorSync.rows} usage events synced (full lifetime data)`));
|
|
1301
|
-
} else {
|
|
1302
|
-
console.log(pc.yellow(` Cursor: sync failed - ${cursorSync.error}`));
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
console.log();
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
1311
|
-
const dateRange = getDateRangeLabel(options);
|
|
1312
|
-
const title = dateRange
|
|
1313
|
-
? `Monthly Token Usage Report (${dateRange})`
|
|
1314
|
-
: "Monthly Token Usage Report";
|
|
1315
|
-
|
|
1316
|
-
console.log(pc.cyan(`\n ${title}`));
|
|
1317
|
-
if (options.benchmark) {
|
|
1318
|
-
console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
|
|
1319
|
-
}
|
|
1320
|
-
console.log();
|
|
1321
|
-
|
|
1322
|
-
const useSpinner = extraOptions?.spinner !== false;
|
|
1323
|
-
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1324
|
-
|
|
1325
|
-
const dateFilters = getDateFilters(options);
|
|
1326
|
-
const enabledSources = getEnabledSources(options);
|
|
1327
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid', 'openclaw', 'pi', 'kimi'])
|
|
1328
|
-
.filter(s => s !== 'cursor');
|
|
1329
|
-
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
1330
|
-
|
|
1331
|
-
spinner?.start(pc.gray("Scanning session data..."));
|
|
1332
|
-
|
|
1333
|
-
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
1334
|
-
localSources,
|
|
1335
|
-
dateFilters,
|
|
1336
|
-
(phase) => spinner?.update(phase)
|
|
1337
|
-
);
|
|
1338
|
-
|
|
1339
|
-
if (!localMessages) {
|
|
1340
|
-
if (spinner) {
|
|
1341
|
-
spinner.error('Failed to parse local session files');
|
|
1342
|
-
} else {
|
|
1343
|
-
console.error('Failed to parse local session files');
|
|
1344
|
-
}
|
|
1345
|
-
process.exit(1);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
spinner?.update(pc.gray("Finalizing report..."));
|
|
1349
|
-
const startTime = performance.now();
|
|
1350
|
-
|
|
1351
|
-
let report: MonthlyReport;
|
|
1352
|
-
try {
|
|
1353
|
-
report = await finalizeMonthlyReportAsync({
|
|
1354
|
-
localMessages,
|
|
1355
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
1356
|
-
since: dateFilters.since,
|
|
1357
|
-
until: dateFilters.until,
|
|
1358
|
-
year: dateFilters.year,
|
|
1359
|
-
sinceTs: dateFilters.sinceTs,
|
|
1360
|
-
untilTs: dateFilters.untilTs,
|
|
1361
|
-
});
|
|
1362
|
-
} catch (e) {
|
|
1363
|
-
if (spinner) {
|
|
1364
|
-
spinner.error(`Error: ${(e as Error).message}`);
|
|
1365
|
-
} else {
|
|
1366
|
-
console.error(`Error: ${(e as Error).message}`);
|
|
1367
|
-
}
|
|
1368
|
-
process.exit(1);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
const processingTime = performance.now() - startTime;
|
|
1372
|
-
spinner?.stop();
|
|
1373
|
-
|
|
1374
|
-
if (report.entries.length === 0) {
|
|
1375
|
-
console.log(pc.yellow(" No usage data found.\n"));
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// Create table
|
|
1380
|
-
const table = createUsageTable("Month");
|
|
1381
|
-
|
|
1382
|
-
const settings = loadSettings();
|
|
1383
|
-
const filteredEntries = settings.includeUnusedModels
|
|
1384
|
-
? report.entries
|
|
1385
|
-
: report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
|
|
1386
|
-
|
|
1387
|
-
for (const entry of filteredEntries) {
|
|
1388
|
-
table.push(
|
|
1389
|
-
formatUsageRow(
|
|
1390
|
-
entry.month,
|
|
1391
|
-
entry.models,
|
|
1392
|
-
entry.input,
|
|
1393
|
-
entry.output,
|
|
1394
|
-
entry.cacheWrite,
|
|
1395
|
-
entry.cacheRead,
|
|
1396
|
-
entry.cost
|
|
1397
|
-
)
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Add totals row
|
|
1402
|
-
const totalInput = report.entries.reduce((sum, e) => sum + e.input, 0);
|
|
1403
|
-
const totalOutput = report.entries.reduce((sum, e) => sum + e.output, 0);
|
|
1404
|
-
const totalCacheRead = report.entries.reduce((sum, e) => sum + e.cacheRead, 0);
|
|
1405
|
-
const totalCacheWrite = report.entries.reduce((sum, e) => sum + e.cacheWrite, 0);
|
|
1406
|
-
|
|
1407
|
-
table.push(
|
|
1408
|
-
formatTotalsRow(totalInput, totalOutput, totalCacheWrite, totalCacheRead, report.totalCost)
|
|
1409
|
-
);
|
|
1410
|
-
|
|
1411
|
-
console.log(table.toString());
|
|
1412
|
-
console.log(pc.gray(`\n Total Cost: ${pc.green(formatCurrency(report.totalCost))}`));
|
|
1413
|
-
|
|
1414
|
-
if (options.benchmark) {
|
|
1415
|
-
console.log(pc.gray(` Processing time: ${processingTime.toFixed(0)}ms (Rust) + ${report.processingTimeMs}ms (parsing)`));
|
|
1416
|
-
if (cursorSync.attempted) {
|
|
1417
|
-
if (cursorSync.synced) {
|
|
1418
|
-
console.log(pc.gray(` Cursor: ${cursorSync.rows} usage events synced (full lifetime data)`));
|
|
1419
|
-
} else {
|
|
1420
|
-
console.log(pc.yellow(` Cursor: sync failed - ${cursorSync.error}`));
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
console.log();
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
type JsonReportType = "models" | "monthly";
|
|
1429
|
-
|
|
1430
|
-
async function outputJsonReport(
|
|
1431
|
-
reportType: JsonReportType,
|
|
1432
|
-
options: FilterOptions & DateFilterOptions
|
|
1433
|
-
) {
|
|
1434
|
-
const dateFilters = getDateFilters(options);
|
|
1435
|
-
const enabledSources = getEnabledSources(options);
|
|
1436
|
-
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
1437
|
-
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
1438
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid', 'openclaw', 'pi', 'kimi'])
|
|
1439
|
-
.filter(s => s !== 'cursor');
|
|
1440
|
-
|
|
1441
|
-
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
1442
|
-
onlyCursor ? [] : localSources,
|
|
1443
|
-
dateFilters
|
|
1444
|
-
);
|
|
1445
|
-
|
|
1446
|
-
if (!localMessages && !onlyCursor) {
|
|
1447
|
-
console.error(JSON.stringify({ error: "Failed to parse local session files" }));
|
|
1448
|
-
process.exit(1);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, piCount: 0, kimiCount: 0, processingTimeMs: 0 };
|
|
1452
|
-
|
|
1453
|
-
if (reportType === "models") {
|
|
1454
|
-
const report = await finalizeReportAsync({
|
|
1455
|
-
localMessages: localMessages || emptyMessages,
|
|
1456
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
1457
|
-
since: dateFilters.since,
|
|
1458
|
-
until: dateFilters.until,
|
|
1459
|
-
year: dateFilters.year,
|
|
1460
|
-
sinceTs: dateFilters.sinceTs,
|
|
1461
|
-
untilTs: dateFilters.untilTs,
|
|
1462
|
-
});
|
|
1463
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1464
|
-
} else {
|
|
1465
|
-
const report = await finalizeMonthlyReportAsync({
|
|
1466
|
-
localMessages: localMessages || emptyMessages,
|
|
1467
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
1468
|
-
since: dateFilters.since,
|
|
1469
|
-
until: dateFilters.until,
|
|
1470
|
-
year: dateFilters.year,
|
|
1471
|
-
sinceTs: dateFilters.sinceTs,
|
|
1472
|
-
untilTs: dateFilters.untilTs,
|
|
1473
|
-
});
|
|
1474
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
interface GraphCommandOptions extends FilterOptions, DateFilterOptions {
|
|
1479
|
-
output?: string;
|
|
1480
|
-
benchmark?: boolean;
|
|
1481
|
-
spinner?: boolean;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
async function handleGraphCommand(options: GraphCommandOptions) {
|
|
1485
|
-
const useSpinner = options.output && options.spinner !== false;
|
|
1486
|
-
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1487
|
-
|
|
1488
|
-
const dateFilters = getDateFilters(options);
|
|
1489
|
-
const enabledSources = getEnabledSources(options);
|
|
1490
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid', 'openclaw', 'pi', 'kimi'])
|
|
1491
|
-
.filter(s => s !== 'cursor');
|
|
1492
|
-
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
1493
|
-
|
|
1494
|
-
spinner?.start(pc.gray("Scanning session data..."));
|
|
1495
|
-
|
|
1496
|
-
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
1497
|
-
localSources,
|
|
1498
|
-
dateFilters,
|
|
1499
|
-
(phase) => spinner?.update(phase)
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
if (!localMessages) {
|
|
1503
|
-
spinner?.error('Failed to parse local session files');
|
|
1504
|
-
process.exit(1);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
spinner?.update(pc.gray("Generating graph data..."));
|
|
1508
|
-
const startTime = performance.now();
|
|
1509
|
-
|
|
1510
|
-
const data = await finalizeGraphAsync({
|
|
1511
|
-
localMessages,
|
|
1512
|
-
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
1513
|
-
since: dateFilters.since,
|
|
1514
|
-
until: dateFilters.until,
|
|
1515
|
-
year: dateFilters.year,
|
|
1516
|
-
sinceTs: dateFilters.sinceTs,
|
|
1517
|
-
untilTs: dateFilters.untilTs,
|
|
1518
|
-
});
|
|
1519
|
-
|
|
1520
|
-
const processingTime = performance.now() - startTime;
|
|
1521
|
-
spinner?.stop();
|
|
1522
|
-
|
|
1523
|
-
const jsonOutput = JSON.stringify(data, null, 2);
|
|
1524
|
-
|
|
1525
|
-
// Output to file or stdout
|
|
1526
|
-
if (options.output) {
|
|
1527
|
-
fs.writeFileSync(options.output, jsonOutput, "utf-8");
|
|
1528
|
-
console.error(pc.green(`✓ Graph data written to ${options.output}`));
|
|
1529
|
-
console.error(
|
|
1530
|
-
pc.gray(
|
|
1531
|
-
` ${data.contributions.length} days, ${data.summary.sources.length} sources, ${data.summary.models.length} models`
|
|
1532
|
-
)
|
|
1533
|
-
);
|
|
1534
|
-
console.error(pc.gray(` Total: ${formatCurrency(data.summary.totalCost)}`));
|
|
1535
|
-
if (options.benchmark) {
|
|
1536
|
-
console.error(pc.gray(` Processing time: ${processingTime.toFixed(0)}ms (Rust native)`));
|
|
1537
|
-
if (cursorSync.attempted) {
|
|
1538
|
-
if (cursorSync.synced) {
|
|
1539
|
-
console.error(pc.gray(` Cursor: ${cursorSync.rows} usage events synced (full lifetime data)`));
|
|
1540
|
-
} else {
|
|
1541
|
-
console.error(pc.yellow(` Cursor: sync failed - ${cursorSync.error}`));
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
} else {
|
|
1546
|
-
console.log(jsonOutput);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
interface WrappedCommandOptions extends FilterOptions {
|
|
1551
|
-
output?: string;
|
|
1552
|
-
year?: string;
|
|
1553
|
-
spinner?: boolean;
|
|
1554
|
-
short?: boolean;
|
|
1555
|
-
agents?: boolean;
|
|
1556
|
-
clients?: boolean;
|
|
1557
|
-
disablePinned?: boolean;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
async function handleWrappedCommand(options: WrappedCommandOptions) {
|
|
1561
|
-
const useSpinner = options.spinner !== false;
|
|
1562
|
-
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1563
|
-
const currentYear = new Date().getFullYear().toString();
|
|
1564
|
-
const year = options.year || currentYear;
|
|
1565
|
-
spinner?.start(pc.gray(`Generating your ${year} Wrapped...`));
|
|
1566
|
-
|
|
1567
|
-
try {
|
|
1568
|
-
const enabledSources = getEnabledSources(options);
|
|
1569
|
-
const outputPath = await generateWrapped({
|
|
1570
|
-
output: options.output,
|
|
1571
|
-
year,
|
|
1572
|
-
sources: enabledSources,
|
|
1573
|
-
short: options.short,
|
|
1574
|
-
includeAgents: !options.clients,
|
|
1575
|
-
pinSisyphus: !options.disablePinned,
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
spinner?.stop();
|
|
1579
|
-
console.log(pc.green(`\n ✓ Your Tokscale Wrapped image is ready!`));
|
|
1580
|
-
console.log(pc.white(` ${outputPath}`));
|
|
1581
|
-
console.log();
|
|
1582
|
-
console.log(pc.gray(" Share it on Twitter/X with #TokscaleWrapped"));
|
|
1583
|
-
console.log();
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
if (spinner) {
|
|
1586
|
-
spinner.error(`Failed to generate wrapped: ${(error as Error).message}`);
|
|
1587
|
-
} else {
|
|
1588
|
-
console.error(pc.red(`Failed to generate wrapped: ${(error as Error).message}`));
|
|
1589
|
-
}
|
|
1590
|
-
process.exit(1);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
async function handlePricingCommand(modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) {
|
|
1595
|
-
const validProviders = ["litellm", "openrouter"];
|
|
1596
|
-
if (options.provider && !validProviders.includes(options.provider.toLowerCase())) {
|
|
1597
|
-
console.log(pc.red(`\n Invalid provider: ${options.provider}`));
|
|
1598
|
-
console.log(pc.gray(` Valid providers: ${validProviders.join(", ")}\n`));
|
|
1599
|
-
process.exit(1);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const useSpinner = options.spinner !== false;
|
|
1603
|
-
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1604
|
-
const providerLabel = options.provider ? ` from ${options.provider}` : "";
|
|
1605
|
-
spinner?.start(pc.gray(`Fetching pricing data${providerLabel}...`));
|
|
1606
|
-
|
|
1607
|
-
let core: typeof import("@tokscale/core");
|
|
1608
|
-
try {
|
|
1609
|
-
const mod = await import("@tokscale/core");
|
|
1610
|
-
core = (mod.default ?? mod) as typeof import("@tokscale/core");
|
|
1611
|
-
} catch (importErr) {
|
|
1612
|
-
spinner?.stop();
|
|
1613
|
-
const errorMsg = (importErr as Error).message || "Unknown error";
|
|
1614
|
-
if (options.json) {
|
|
1615
|
-
console.log(JSON.stringify({ error: "Native module not available", details: errorMsg }, null, 2));
|
|
1616
|
-
} else {
|
|
1617
|
-
console.log(pc.red(`\n Native module not available: ${errorMsg}`));
|
|
1618
|
-
console.log(pc.gray(" Run 'bun run build:core' to build the native module.\n"));
|
|
1619
|
-
}
|
|
1620
|
-
process.exit(1);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
try {
|
|
1624
|
-
const provider = options.provider?.toLowerCase() || undefined;
|
|
1625
|
-
const nativeResult = await core.lookupPricing(modelId, provider);
|
|
1626
|
-
spinner?.stop();
|
|
1627
|
-
|
|
1628
|
-
const result = {
|
|
1629
|
-
matchedKey: nativeResult.matchedKey,
|
|
1630
|
-
source: nativeResult.source as "litellm" | "openrouter",
|
|
1631
|
-
pricing: {
|
|
1632
|
-
input_cost_per_token: nativeResult.pricing.inputCostPerToken,
|
|
1633
|
-
output_cost_per_token: nativeResult.pricing.outputCostPerToken,
|
|
1634
|
-
cache_read_input_token_cost: nativeResult.pricing.cacheReadInputTokenCost,
|
|
1635
|
-
cache_creation_input_token_cost: nativeResult.pricing.cacheCreationInputTokenCost,
|
|
1636
|
-
},
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
if (options.json) {
|
|
1640
|
-
console.log(JSON.stringify({
|
|
1641
|
-
modelId,
|
|
1642
|
-
matchedKey: result.matchedKey,
|
|
1643
|
-
source: result.source,
|
|
1644
|
-
pricing: {
|
|
1645
|
-
inputCostPerToken: result.pricing.input_cost_per_token ?? 0,
|
|
1646
|
-
outputCostPerToken: result.pricing.output_cost_per_token ?? 0,
|
|
1647
|
-
cacheReadInputTokenCost: result.pricing.cache_read_input_token_cost,
|
|
1648
|
-
cacheCreationInputTokenCost: result.pricing.cache_creation_input_token_cost,
|
|
1649
|
-
},
|
|
1650
|
-
}, null, 2));
|
|
1651
|
-
} else {
|
|
1652
|
-
const sourceLower = result.source.toLowerCase();
|
|
1653
|
-
const sourceLabel = sourceLower === "litellm" ? pc.blue("LiteLLM") : sourceLower === "cursor" ? pc.yellow("Cursor") : pc.magenta("OpenRouter");
|
|
1654
|
-
const inputCost = result.pricing.input_cost_per_token ?? 0;
|
|
1655
|
-
const outputCost = result.pricing.output_cost_per_token ?? 0;
|
|
1656
|
-
const cacheReadCost = result.pricing.cache_read_input_token_cost;
|
|
1657
|
-
const cacheWriteCost = result.pricing.cache_creation_input_token_cost;
|
|
1658
|
-
|
|
1659
|
-
console.log(pc.cyan(`\n Pricing for: ${pc.white(modelId)}`));
|
|
1660
|
-
console.log(pc.gray(` Matched key: ${result.matchedKey}`));
|
|
1661
|
-
console.log(pc.gray(` Source: `) + sourceLabel);
|
|
1662
|
-
console.log();
|
|
1663
|
-
console.log(pc.white(` Input: `) + formatPricePerMillion(inputCost));
|
|
1664
|
-
console.log(pc.white(` Output: `) + formatPricePerMillion(outputCost));
|
|
1665
|
-
if (cacheReadCost !== undefined) {
|
|
1666
|
-
console.log(pc.white(` Cache Read: `) + formatPricePerMillion(cacheReadCost));
|
|
1667
|
-
}
|
|
1668
|
-
if (cacheWriteCost !== undefined) {
|
|
1669
|
-
console.log(pc.white(` Cache Write: `) + formatPricePerMillion(cacheWriteCost));
|
|
1670
|
-
}
|
|
1671
|
-
console.log();
|
|
1672
|
-
}
|
|
1673
|
-
} catch (err) {
|
|
1674
|
-
spinner?.stop();
|
|
1675
|
-
const errorMsg = (err as Error).message || "Unknown error";
|
|
1676
|
-
|
|
1677
|
-
// Check if this is a "model not found" error from Rust or a different error
|
|
1678
|
-
const isModelNotFound = errorMsg.toLowerCase().includes("not found") ||
|
|
1679
|
-
errorMsg.toLowerCase().includes("no pricing");
|
|
1680
|
-
|
|
1681
|
-
if (options.json) {
|
|
1682
|
-
if (isModelNotFound) {
|
|
1683
|
-
console.log(JSON.stringify({ error: "Model not found", modelId }, null, 2));
|
|
1684
|
-
} else {
|
|
1685
|
-
console.log(JSON.stringify({ error: errorMsg, modelId }, null, 2));
|
|
1686
|
-
}
|
|
1687
|
-
} else {
|
|
1688
|
-
if (isModelNotFound) {
|
|
1689
|
-
console.log(pc.red(`\n Model not found: ${modelId}\n`));
|
|
1690
|
-
} else {
|
|
1691
|
-
console.log(pc.red(`\n Error looking up pricing: ${errorMsg}\n`));
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
process.exit(1);
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function formatPricePerMillion(costPerToken: number): string {
|
|
1699
|
-
const perMillion = costPerToken * 1_000_000;
|
|
1700
|
-
return pc.green(`$${perMillion.toFixed(2)}`) + pc.gray(" / 1M tokens");
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
function getSourceLabel(source: string): string {
|
|
1704
|
-
switch (source) {
|
|
1705
|
-
case "opencode":
|
|
1706
|
-
return "OpenCode";
|
|
1707
|
-
case "claude":
|
|
1708
|
-
return "Claude";
|
|
1709
|
-
case "codex":
|
|
1710
|
-
return "Codex";
|
|
1711
|
-
case "gemini":
|
|
1712
|
-
return "Gemini";
|
|
1713
|
-
case "cursor":
|
|
1714
|
-
return "Cursor";
|
|
1715
|
-
case "amp":
|
|
1716
|
-
return "Amp";
|
|
1717
|
-
case "droid":
|
|
1718
|
-
return "Droid";
|
|
1719
|
-
case "openclaw":
|
|
1720
|
-
return "OpenClaw";
|
|
1721
|
-
case "pi":
|
|
1722
|
-
return "Pi";
|
|
1723
|
-
case "kimi":
|
|
1724
|
-
return "Kimi";
|
|
1725
|
-
default:
|
|
1726
|
-
return source;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// =============================================================================
|
|
1731
|
-
// Cursor IDE Authentication
|
|
1732
|
-
// =============================================================================
|
|
1733
|
-
|
|
1734
|
-
async function cursorLogin(options: { name?: string } = {}): Promise<void> {
|
|
1735
|
-
console.log(pc.cyan("\n Cursor IDE - Login\n"));
|
|
1736
|
-
console.log(pc.white(" To get your session token:"));
|
|
1737
|
-
console.log(pc.gray(" 1. Open https://www.cursor.com/settings in your browser"));
|
|
1738
|
-
console.log(pc.gray(" 2. Open Developer Tools (F12) > Network tab"));
|
|
1739
|
-
console.log(pc.gray(" 3. Find any request to cursor.com/api"));
|
|
1740
|
-
console.log(pc.gray(" 4. Copy the 'WorkosCursorSessionToken' cookie value"));
|
|
1741
|
-
console.log();
|
|
1742
|
-
|
|
1743
|
-
// Read token from stdin
|
|
1744
|
-
const readline = await import("node:readline");
|
|
1745
|
-
const rl = readline.createInterface({
|
|
1746
|
-
input: process.stdin,
|
|
1747
|
-
output: process.stdout,
|
|
1748
|
-
});
|
|
1749
|
-
|
|
1750
|
-
const token = await new Promise<string>((resolve) => {
|
|
1751
|
-
rl.question(pc.white(" Paste your session token: "), (answer) => {
|
|
1752
|
-
rl.close();
|
|
1753
|
-
resolve(answer.trim());
|
|
1754
|
-
});
|
|
1755
|
-
});
|
|
1756
|
-
|
|
1757
|
-
if (!token) {
|
|
1758
|
-
console.log(pc.red("\n No token provided. Login cancelled.\n"));
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
// Validate the token
|
|
1763
|
-
console.log(pc.gray("\n Validating token..."));
|
|
1764
|
-
const validation = await validateCursorSession(token);
|
|
1765
|
-
|
|
1766
|
-
if (!validation.valid) {
|
|
1767
|
-
console.log(pc.red(`\n Invalid token: ${validation.error}`));
|
|
1768
|
-
console.log(pc.gray(" Please try again with a valid session token.\n"));
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
// Save credentials (multi-account)
|
|
1773
|
-
let savedAccountId: string;
|
|
1774
|
-
try {
|
|
1775
|
-
const saved = saveCursorCredentials(
|
|
1776
|
-
{
|
|
1777
|
-
sessionToken: token,
|
|
1778
|
-
createdAt: new Date().toISOString(),
|
|
1779
|
-
},
|
|
1780
|
-
{ label: options.name }
|
|
1781
|
-
);
|
|
1782
|
-
savedAccountId = saved.accountId;
|
|
1783
|
-
} catch (e) {
|
|
1784
|
-
console.log(pc.red(`\n Failed to save credentials: ${(e as Error).message}\n`));
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
console.log(pc.green("\n Success! Logged in to Cursor."));
|
|
1789
|
-
if (options.name) {
|
|
1790
|
-
console.log(pc.gray(` Account: ${options.name} (${savedAccountId})`));
|
|
1791
|
-
} else {
|
|
1792
|
-
console.log(pc.gray(` Account ID: ${savedAccountId}`));
|
|
1793
|
-
}
|
|
1794
|
-
if (validation.membershipType) {
|
|
1795
|
-
console.log(pc.gray(` Membership: ${validation.membershipType}`));
|
|
1796
|
-
}
|
|
1797
|
-
console.log(pc.gray(" Your usage data will now be included in reports.\n"));
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
async function cursorLogout(options: { name?: string; all?: boolean; purgeCache?: boolean } = {}): Promise<void> {
|
|
1801
|
-
if (!isCursorLoggedIn()) {
|
|
1802
|
-
console.log(pc.yellow("\n Not logged in to Cursor.\n"));
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
if (options.all) {
|
|
1807
|
-
const cleared = options.purgeCache ? clearCursorCredentialsAndCache({ purgeCache: true }) : clearCursorCredentialsAndCache();
|
|
1808
|
-
if (cleared) {
|
|
1809
|
-
console.log(pc.green("\n Logged out from all Cursor accounts.\n"));
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
console.error(pc.red("\n Failed to clear Cursor credentials.\n"));
|
|
1813
|
-
process.exit(1);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
const target = options.name || listCursorAccounts().find((a) => a.isActive)?.id;
|
|
1817
|
-
if (!target) {
|
|
1818
|
-
console.log(pc.yellow("\n No saved Cursor accounts.\n"));
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
const removed = removeCursorAccount(target, { purgeCache: options.purgeCache });
|
|
1823
|
-
if (!removed.removed) {
|
|
1824
|
-
console.error(pc.red(`\n Failed to log out: ${removed.error}\n`));
|
|
1825
|
-
process.exit(1);
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
if (options.purgeCache) {
|
|
1829
|
-
console.log(pc.green(`\n Logged out from Cursor account (cache purged): ${pc.bold(target)}\n`));
|
|
1830
|
-
} else {
|
|
1831
|
-
console.log(pc.green(`\n Logged out from Cursor account (history archived): ${pc.bold(target)}\n`));
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
async function cursorStatus(options: { name?: string } = {}): Promise<void> {
|
|
1836
|
-
if (!isCursorLoggedIn()) {
|
|
1837
|
-
console.log(pc.yellow("\n Not logged in to Cursor."));
|
|
1838
|
-
console.log(pc.gray(" Run 'tokscale cursor login' to authenticate.\n"));
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
const accounts = listCursorAccounts();
|
|
1843
|
-
const target = options.name
|
|
1844
|
-
? options.name
|
|
1845
|
-
: accounts.find((a) => a.isActive)?.id;
|
|
1846
|
-
|
|
1847
|
-
const credentials = target ? loadCursorCredentials(target) : null;
|
|
1848
|
-
if (!credentials) {
|
|
1849
|
-
console.log(pc.red("\n Error: Cursor account not found."));
|
|
1850
|
-
console.log(pc.gray(" Run 'tokscale cursor accounts' to list saved accounts.\n"));
|
|
1851
|
-
process.exit(1);
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
console.log(pc.cyan("\n Cursor IDE - Status\n"));
|
|
1855
|
-
if (accounts.length > 0) {
|
|
1856
|
-
console.log(pc.white(" Accounts:"));
|
|
1857
|
-
for (const acct of accounts) {
|
|
1858
|
-
const name = acct.label ? `${acct.label} ${pc.gray(`(${acct.id})`)}` : acct.id;
|
|
1859
|
-
console.log(` ${acct.isActive ? pc.green("*") : pc.gray("-")} ${name}`);
|
|
1860
|
-
}
|
|
1861
|
-
console.log();
|
|
1862
|
-
}
|
|
1863
|
-
console.log(pc.gray(" Checking session validity..."));
|
|
1864
|
-
|
|
1865
|
-
const validation = await validateCursorSession(credentials.sessionToken);
|
|
1866
|
-
|
|
1867
|
-
if (validation.valid) {
|
|
1868
|
-
console.log(pc.green(" ✓ Session is valid"));
|
|
1869
|
-
if (validation.membershipType) {
|
|
1870
|
-
console.log(pc.white(` Membership: ${validation.membershipType}`));
|
|
1871
|
-
}
|
|
1872
|
-
console.log(pc.gray(` Logged in: ${new Date(credentials.createdAt).toLocaleDateString()}`));
|
|
1873
|
-
|
|
1874
|
-
// Try to fetch usage to show summary
|
|
1875
|
-
try {
|
|
1876
|
-
const usage = await readCursorUsage(target);
|
|
1877
|
-
const totalCost = usage.byModel.reduce((sum, m) => sum + m.cost, 0);
|
|
1878
|
-
console.log(pc.gray(` Models used: ${usage.byModel.length}`));
|
|
1879
|
-
console.log(pc.gray(` Total usage events: ${usage.rows.length}`));
|
|
1880
|
-
console.log(pc.gray(` Total cost: $${totalCost.toFixed(2)}`));
|
|
1881
|
-
} catch (e) {
|
|
1882
|
-
// Ignore fetch errors for status check
|
|
1883
|
-
}
|
|
1884
|
-
} else {
|
|
1885
|
-
console.log(pc.red(` ✗ Session invalid: ${validation.error}`));
|
|
1886
|
-
console.log(pc.gray(" Run 'tokscale cursor login' to re-authenticate."));
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
console.log(pc.gray(`\n Credentials: ${getCursorCredentialsPath()}\n`));
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
main().catch(console.error);
|