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