@tokscale/cli 1.0.17 → 1.0.19
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 +214 -91
- 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.d.ts +1 -2
- package/dist/native-runner.d.ts.map +1 -1
- package/dist/native-runner.js +11 -39
- 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 +31 -138
- 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 +14 -7
- 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 +20 -48
- package/dist/wrapped.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +232 -97
- package/src/graph-types.ts +1 -1
- package/src/native-runner.js +4 -0
- package/src/native-runner.ts +12 -42
- package/src/native.ts +47 -207
- package/src/sessions/types.ts +1 -1
- package/src/submit.ts +36 -22
- package/src/tui/App.tsx +10 -7
- 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 +21 -54
- 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/src/cli.ts
CHANGED
|
@@ -14,7 +14,7 @@ import pc from "picocolors";
|
|
|
14
14
|
import { login, logout, whoami } from "./auth.js";
|
|
15
15
|
import { submit } from "./submit.js";
|
|
16
16
|
import { generateWrapped } from "./wrapped.js";
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
import {
|
|
19
19
|
loadCursorCredentials,
|
|
20
20
|
saveCursorCredentials,
|
|
@@ -48,6 +48,7 @@ import * as fs from "node:fs";
|
|
|
48
48
|
import { performance } from "node:perf_hooks";
|
|
49
49
|
import type { SourceType } from "./graph-types.js";
|
|
50
50
|
import type { TUIOptions, TabType } from "./tui/types/index.js";
|
|
51
|
+
import { loadSettings } from "./tui/config/settings.js";
|
|
51
52
|
|
|
52
53
|
type LaunchTUIFunction = (options?: TUIOptions) => Promise<void>;
|
|
53
54
|
|
|
@@ -106,6 +107,8 @@ interface FilterOptions {
|
|
|
106
107
|
codex?: boolean;
|
|
107
108
|
gemini?: boolean;
|
|
108
109
|
cursor?: boolean;
|
|
110
|
+
amp?: boolean;
|
|
111
|
+
droid?: boolean;
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
interface DateFilterOptions {
|
|
@@ -204,7 +207,7 @@ async function main() {
|
|
|
204
207
|
|
|
205
208
|
program
|
|
206
209
|
.name("tokscale")
|
|
207
|
-
.description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, and
|
|
210
|
+
.description("Tokscale - Track AI coding costs across OpenCode, Claude Code, Codex, Gemini, Cursor, and Amp")
|
|
208
211
|
.version(pkg.version);
|
|
209
212
|
|
|
210
213
|
program
|
|
@@ -217,6 +220,8 @@ async function main() {
|
|
|
217
220
|
.option("--codex", "Show only Codex CLI usage")
|
|
218
221
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
219
222
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
223
|
+
.option("--amp", "Show only Amp usage")
|
|
224
|
+
.option("--droid", "Show only Factory Droid usage")
|
|
220
225
|
.option("--today", "Show only today's usage")
|
|
221
226
|
.option("--week", "Show last 7 days")
|
|
222
227
|
.option("--month", "Show current month")
|
|
@@ -224,18 +229,19 @@ async function main() {
|
|
|
224
229
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
225
230
|
.option("--year <year>", "Filter to specific year")
|
|
226
231
|
.option("--benchmark", "Show processing time")
|
|
232
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
227
233
|
.action(async (options) => {
|
|
228
234
|
if (options.json) {
|
|
229
235
|
await outputJsonReport("monthly", options);
|
|
230
236
|
} else if (options.light) {
|
|
231
|
-
await showMonthlyReport(options);
|
|
237
|
+
await showMonthlyReport(options, { spinner: options.spinner });
|
|
232
238
|
} else {
|
|
233
239
|
const launchTUI = await tryLoadTUI();
|
|
234
240
|
if (launchTUI) {
|
|
235
241
|
await launchTUI(buildTUIOptions(options, "daily"));
|
|
236
242
|
} else {
|
|
237
243
|
showTUIUnavailableMessage();
|
|
238
|
-
await showMonthlyReport(options);
|
|
244
|
+
await showMonthlyReport(options, { spinner: options.spinner });
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
});
|
|
@@ -250,6 +256,8 @@ async function main() {
|
|
|
250
256
|
.option("--codex", "Show only Codex CLI usage")
|
|
251
257
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
252
258
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
259
|
+
.option("--amp", "Show only Amp usage")
|
|
260
|
+
.option("--droid", "Show only Factory Droid usage")
|
|
253
261
|
.option("--today", "Show only today's usage")
|
|
254
262
|
.option("--week", "Show last 7 days")
|
|
255
263
|
.option("--month", "Show current month")
|
|
@@ -257,18 +265,19 @@ async function main() {
|
|
|
257
265
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
258
266
|
.option("--year <year>", "Filter to specific year")
|
|
259
267
|
.option("--benchmark", "Show processing time")
|
|
268
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
260
269
|
.action(async (options) => {
|
|
261
270
|
if (options.json) {
|
|
262
271
|
await outputJsonReport("models", options);
|
|
263
272
|
} else if (options.light) {
|
|
264
|
-
await showModelReport(options);
|
|
273
|
+
await showModelReport(options, { spinner: options.spinner });
|
|
265
274
|
} else {
|
|
266
275
|
const launchTUI = await tryLoadTUI();
|
|
267
276
|
if (launchTUI) {
|
|
268
277
|
await launchTUI(buildTUIOptions(options, "model"));
|
|
269
278
|
} else {
|
|
270
279
|
showTUIUnavailableMessage();
|
|
271
|
-
await showModelReport(options);
|
|
280
|
+
await showModelReport(options, { spinner: options.spinner });
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
283
|
});
|
|
@@ -282,6 +291,8 @@ async function main() {
|
|
|
282
291
|
.option("--codex", "Include only Codex CLI data")
|
|
283
292
|
.option("--gemini", "Include only Gemini CLI data")
|
|
284
293
|
.option("--cursor", "Include only Cursor IDE data")
|
|
294
|
+
.option("--amp", "Include only Amp data")
|
|
295
|
+
.option("--droid", "Include only Factory Droid data")
|
|
285
296
|
.option("--today", "Show only today's usage")
|
|
286
297
|
.option("--week", "Show last 7 days")
|
|
287
298
|
.option("--month", "Show current month")
|
|
@@ -289,6 +300,7 @@ async function main() {
|
|
|
289
300
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
290
301
|
.option("--year <year>", "Filter to specific year")
|
|
291
302
|
.option("--benchmark", "Show processing time")
|
|
303
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
292
304
|
.action(async (options) => {
|
|
293
305
|
await handleGraphCommand(options);
|
|
294
306
|
});
|
|
@@ -303,6 +315,8 @@ async function main() {
|
|
|
303
315
|
.option("--codex", "Include only Codex CLI data")
|
|
304
316
|
.option("--gemini", "Include only Gemini CLI data")
|
|
305
317
|
.option("--cursor", "Include only Cursor IDE data")
|
|
318
|
+
.option("--amp", "Include only Amp data")
|
|
319
|
+
.option("--droid", "Include only Factory Droid data")
|
|
306
320
|
.option("--no-spinner", "Disable loading spinner (for scripting)")
|
|
307
321
|
.option("--short", "Display total tokens in abbreviated format (e.g., 7.14B)")
|
|
308
322
|
.addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
|
|
@@ -345,6 +359,8 @@ async function main() {
|
|
|
345
359
|
.option("--codex", "Include only Codex CLI data")
|
|
346
360
|
.option("--gemini", "Include only Gemini CLI data")
|
|
347
361
|
.option("--cursor", "Include only Cursor IDE data")
|
|
362
|
+
.option("--amp", "Include only Amp data")
|
|
363
|
+
.option("--droid", "Include only Factory Droid data")
|
|
348
364
|
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
349
365
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
350
366
|
.option("--year <year>", "Filter to specific year")
|
|
@@ -356,6 +372,8 @@ async function main() {
|
|
|
356
372
|
codex: options.codex,
|
|
357
373
|
gemini: options.gemini,
|
|
358
374
|
cursor: options.cursor,
|
|
375
|
+
amp: options.amp,
|
|
376
|
+
droid: options.droid,
|
|
359
377
|
since: options.since,
|
|
360
378
|
until: options.until,
|
|
361
379
|
year: options.year,
|
|
@@ -375,6 +393,8 @@ async function main() {
|
|
|
375
393
|
.option("--codex", "Show only Codex CLI usage")
|
|
376
394
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
377
395
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
396
|
+
.option("--amp", "Show only Amp usage")
|
|
397
|
+
.option("--droid", "Show only Factory Droid usage")
|
|
378
398
|
.option("--today", "Show only today's usage")
|
|
379
399
|
.option("--week", "Show last 7 days")
|
|
380
400
|
.option("--month", "Show current month")
|
|
@@ -391,9 +411,15 @@ async function main() {
|
|
|
391
411
|
}
|
|
392
412
|
});
|
|
393
413
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
414
|
+
program
|
|
415
|
+
.command("pricing <model-id>")
|
|
416
|
+
.description("Look up pricing for a model")
|
|
417
|
+
.option("--json", "Output as JSON")
|
|
418
|
+
.option("--provider <source>", "Force pricing source: 'litellm' or 'openrouter'")
|
|
419
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
420
|
+
.action(async (modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) => {
|
|
421
|
+
await handlePricingCommand(modelId, options);
|
|
422
|
+
});
|
|
397
423
|
|
|
398
424
|
const cursorCommand = program
|
|
399
425
|
.command("cursor")
|
|
@@ -426,7 +452,7 @@ async function main() {
|
|
|
426
452
|
// Global flags should go to main program
|
|
427
453
|
const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
|
|
428
454
|
const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
|
|
429
|
-
const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'help'];
|
|
455
|
+
const knownCommands = ['monthly', 'models', 'graph', 'wrapped', 'login', 'logout', 'whoami', 'submit', 'cursor', 'tui', 'pricing', 'help'];
|
|
430
456
|
const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
|
|
431
457
|
|
|
432
458
|
if (isKnownCommand || isGlobalFlag) {
|
|
@@ -443,6 +469,7 @@ async function main() {
|
|
|
443
469
|
.option("--codex", "Show only Codex CLI usage")
|
|
444
470
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
445
471
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
472
|
+
.option("--amp", "Show only Amp usage")
|
|
446
473
|
.option("--today", "Show only today's usage")
|
|
447
474
|
.option("--week", "Show last 7 days")
|
|
448
475
|
.option("--month", "Show current month")
|
|
@@ -450,27 +477,28 @@ async function main() {
|
|
|
450
477
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
451
478
|
.option("--year <year>", "Filter to specific year")
|
|
452
479
|
.option("--benchmark", "Show processing time")
|
|
480
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
453
481
|
.parse();
|
|
454
482
|
|
|
455
483
|
const opts = defaultProgram.opts();
|
|
456
484
|
if (opts.json) {
|
|
457
485
|
await outputJsonReport("models", opts);
|
|
458
486
|
} else if (opts.light) {
|
|
459
|
-
await showModelReport(opts);
|
|
487
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
460
488
|
} else {
|
|
461
489
|
const launchTUI = await tryLoadTUI();
|
|
462
490
|
if (launchTUI) {
|
|
463
491
|
await launchTUI(buildTUIOptions(opts));
|
|
464
492
|
} else {
|
|
465
493
|
showTUIUnavailableMessage();
|
|
466
|
-
await showModelReport(opts);
|
|
494
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
467
495
|
}
|
|
468
496
|
}
|
|
469
497
|
}
|
|
470
498
|
}
|
|
471
499
|
|
|
472
500
|
function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
|
|
473
|
-
const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor;
|
|
501
|
+
const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid;
|
|
474
502
|
if (!hasFilter) return undefined; // All sources
|
|
475
503
|
|
|
476
504
|
const sources: SourceType[] = [];
|
|
@@ -479,21 +507,14 @@ function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
|
|
|
479
507
|
if (options.codex) sources.push("codex");
|
|
480
508
|
if (options.gemini) sources.push("gemini");
|
|
481
509
|
if (options.cursor) sources.push("cursor");
|
|
510
|
+
if (options.amp) sources.push("amp");
|
|
511
|
+
if (options.droid) sources.push("droid");
|
|
482
512
|
return sources;
|
|
483
513
|
}
|
|
484
514
|
|
|
485
|
-
function logNativeStatus(): void {
|
|
486
|
-
if (!isNativeAvailable()) {
|
|
487
|
-
console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
|
|
488
|
-
console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
515
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
await fetcher.fetchPricing();
|
|
495
|
-
return fetcher;
|
|
496
|
-
}
|
|
516
|
+
|
|
517
|
+
|
|
497
518
|
|
|
498
519
|
/**
|
|
499
520
|
* Sync Cursor usage data from API to local cache.
|
|
@@ -515,31 +536,19 @@ async function syncCursorData(): Promise<CursorSyncResult> {
|
|
|
515
536
|
}
|
|
516
537
|
|
|
517
538
|
interface LoadedDataSources {
|
|
518
|
-
fetcher: PricingFetcher;
|
|
519
539
|
cursorSync: CursorSyncResult;
|
|
520
540
|
localMessages: ParsedMessages | null;
|
|
521
541
|
}
|
|
522
542
|
|
|
523
|
-
/**
|
|
524
|
-
* Load all data sources in parallel (two-phase optimization):
|
|
525
|
-
* - Cursor API sync (network)
|
|
526
|
-
* - Pricing fetch (network)
|
|
527
|
-
* - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
|
|
528
|
-
*
|
|
529
|
-
* This overlaps network I/O with local file parsing for better performance.
|
|
530
|
-
*/
|
|
531
543
|
async function loadDataSourcesParallel(
|
|
532
544
|
localSources: SourceType[],
|
|
533
|
-
dateFilters: { since?: string; until?: string; year?: string }
|
|
545
|
+
dateFilters: { since?: string; until?: string; year?: string },
|
|
546
|
+
onPhase?: (phase: string) => void
|
|
534
547
|
): Promise<LoadedDataSources> {
|
|
535
|
-
// Skip local parsing if no local sources requested (e.g., cursor-only mode)
|
|
536
548
|
const shouldParseLocal = localSources.length > 0;
|
|
537
549
|
|
|
538
|
-
|
|
539
|
-
const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
|
|
550
|
+
const [cursorResult, localResult] = await Promise.allSettled([
|
|
540
551
|
syncCursorData(),
|
|
541
|
-
fetchPricingData(),
|
|
542
|
-
// Parse local sources in parallel (excludes Cursor) - skip if empty
|
|
543
552
|
shouldParseLocal
|
|
544
553
|
? parseLocalSourcesAsync({
|
|
545
554
|
sources: localSources.filter(s => s !== 'cursor'),
|
|
@@ -550,25 +559,18 @@ async function loadDataSourcesParallel(
|
|
|
550
559
|
: Promise.resolve(null),
|
|
551
560
|
]);
|
|
552
561
|
|
|
553
|
-
// Handle partial failures gracefully
|
|
554
562
|
const cursorSync: CursorSyncResult = cursorResult.status === 'fulfilled'
|
|
555
563
|
? cursorResult.value
|
|
556
564
|
: { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
|
|
557
565
|
|
|
558
|
-
const fetcher: PricingFetcher = pricingResult.status === 'fulfilled'
|
|
559
|
-
? pricingResult.value
|
|
560
|
-
: new PricingFetcher(); // Empty pricing → costs = 0
|
|
561
|
-
|
|
562
566
|
const localMessages: ParsedMessages | null = localResult.status === 'fulfilled'
|
|
563
567
|
? localResult.value
|
|
564
568
|
: null;
|
|
565
569
|
|
|
566
|
-
return {
|
|
570
|
+
return { cursorSync, localMessages };
|
|
567
571
|
}
|
|
568
572
|
|
|
569
|
-
async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
|
|
570
|
-
logNativeStatus();
|
|
571
|
-
|
|
573
|
+
async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
572
574
|
const dateFilters = getDateFilters(options);
|
|
573
575
|
const enabledSources = getEnabledSources(options);
|
|
574
576
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
@@ -595,47 +597,53 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
595
597
|
}
|
|
596
598
|
console.log();
|
|
597
599
|
|
|
598
|
-
|
|
599
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
600
|
-
spinner.start(pc.gray("Loading data sources..."));
|
|
600
|
+
const useSpinner = extraOptions?.spinner !== false;
|
|
601
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
601
602
|
|
|
602
|
-
|
|
603
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
603
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
604
604
|
.filter(s => s !== 'cursor');
|
|
605
605
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const {
|
|
606
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
607
|
+
|
|
608
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
609
609
|
onlyCursor ? [] : localSources,
|
|
610
|
-
dateFilters
|
|
610
|
+
dateFilters,
|
|
611
|
+
(phase) => spinner?.update(phase)
|
|
611
612
|
);
|
|
612
613
|
|
|
613
614
|
if (!localMessages && !onlyCursor) {
|
|
614
|
-
spinner
|
|
615
|
+
if (spinner) {
|
|
616
|
+
spinner.error('Failed to parse local session files');
|
|
617
|
+
} else {
|
|
618
|
+
console.error('Failed to parse local session files');
|
|
619
|
+
}
|
|
615
620
|
process.exit(1);
|
|
616
621
|
}
|
|
617
622
|
|
|
618
|
-
spinner
|
|
623
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
619
624
|
const startTime = performance.now();
|
|
620
625
|
|
|
621
626
|
let report: ModelReport;
|
|
622
627
|
try {
|
|
623
|
-
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
|
|
628
|
+
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
|
|
624
629
|
report = await finalizeReportAsync({
|
|
625
630
|
localMessages: localMessages || emptyMessages,
|
|
626
|
-
pricing: fetcher.toPricingEntries(),
|
|
627
631
|
includeCursor: includeCursor && cursorSync.synced,
|
|
628
632
|
since: dateFilters.since,
|
|
629
633
|
until: dateFilters.until,
|
|
630
634
|
year: dateFilters.year,
|
|
631
635
|
});
|
|
632
636
|
} catch (e) {
|
|
633
|
-
spinner
|
|
637
|
+
if (spinner) {
|
|
638
|
+
spinner.error(`Error: ${(e as Error).message}`);
|
|
639
|
+
} else {
|
|
640
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
641
|
+
}
|
|
634
642
|
process.exit(1);
|
|
635
643
|
}
|
|
636
644
|
|
|
637
645
|
const processingTime = performance.now() - startTime;
|
|
638
|
-
spinner
|
|
646
|
+
spinner?.stop();
|
|
639
647
|
|
|
640
648
|
if (report.entries.length === 0) {
|
|
641
649
|
if (onlyCursor && !cursorSync.synced) {
|
|
@@ -649,8 +657,13 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
649
657
|
|
|
650
658
|
// Create table
|
|
651
659
|
const table = createUsageTable("Source/Model");
|
|
660
|
+
|
|
661
|
+
const settings = loadSettings();
|
|
662
|
+
const filteredEntries = settings.includeUnusedModels
|
|
663
|
+
? report.entries
|
|
664
|
+
: report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
|
|
652
665
|
|
|
653
|
-
for (const entry of
|
|
666
|
+
for (const entry of filteredEntries) {
|
|
654
667
|
const sourceLabel = getSourceLabel(entry.source);
|
|
655
668
|
const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
|
|
656
669
|
table.push(
|
|
@@ -702,9 +715,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
702
715
|
console.log();
|
|
703
716
|
}
|
|
704
717
|
|
|
705
|
-
async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
|
|
706
|
-
logNativeStatus();
|
|
707
|
-
|
|
718
|
+
async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
708
719
|
const dateRange = getDateRangeLabel(options);
|
|
709
720
|
const title = dateRange
|
|
710
721
|
? `Monthly Token Usage Report (${dateRange})`
|
|
@@ -716,45 +727,55 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
|
|
|
716
727
|
}
|
|
717
728
|
console.log();
|
|
718
729
|
|
|
719
|
-
|
|
720
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
721
|
-
spinner.start(pc.gray("Loading data sources..."));
|
|
730
|
+
const useSpinner = extraOptions?.spinner !== false;
|
|
731
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
722
732
|
|
|
723
733
|
const dateFilters = getDateFilters(options);
|
|
724
734
|
const enabledSources = getEnabledSources(options);
|
|
725
|
-
|
|
726
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
735
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
727
736
|
.filter(s => s !== 'cursor');
|
|
728
737
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
729
738
|
|
|
730
|
-
|
|
731
|
-
|
|
739
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
740
|
+
|
|
741
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
742
|
+
localSources,
|
|
743
|
+
dateFilters,
|
|
744
|
+
(phase) => spinner?.update(phase)
|
|
745
|
+
);
|
|
732
746
|
|
|
733
747
|
if (!localMessages) {
|
|
734
|
-
spinner
|
|
748
|
+
if (spinner) {
|
|
749
|
+
spinner.error('Failed to parse local session files');
|
|
750
|
+
} else {
|
|
751
|
+
console.error('Failed to parse local session files');
|
|
752
|
+
}
|
|
735
753
|
process.exit(1);
|
|
736
754
|
}
|
|
737
755
|
|
|
738
|
-
spinner
|
|
756
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
739
757
|
const startTime = performance.now();
|
|
740
758
|
|
|
741
759
|
let report: MonthlyReport;
|
|
742
760
|
try {
|
|
743
761
|
report = await finalizeMonthlyReportAsync({
|
|
744
762
|
localMessages,
|
|
745
|
-
pricing: fetcher.toPricingEntries(),
|
|
746
763
|
includeCursor: includeCursor && cursorSync.synced,
|
|
747
764
|
since: dateFilters.since,
|
|
748
765
|
until: dateFilters.until,
|
|
749
766
|
year: dateFilters.year,
|
|
750
767
|
});
|
|
751
768
|
} catch (e) {
|
|
752
|
-
spinner
|
|
769
|
+
if (spinner) {
|
|
770
|
+
spinner.error(`Error: ${(e as Error).message}`);
|
|
771
|
+
} else {
|
|
772
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
773
|
+
}
|
|
753
774
|
process.exit(1);
|
|
754
775
|
}
|
|
755
776
|
|
|
756
777
|
const processingTime = performance.now() - startTime;
|
|
757
|
-
spinner
|
|
778
|
+
spinner?.stop();
|
|
758
779
|
|
|
759
780
|
if (report.entries.length === 0) {
|
|
760
781
|
console.log(pc.yellow(" No usage data found.\n"));
|
|
@@ -764,7 +785,12 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
|
|
|
764
785
|
// Create table
|
|
765
786
|
const table = createUsageTable("Month");
|
|
766
787
|
|
|
767
|
-
|
|
788
|
+
const settings = loadSettings();
|
|
789
|
+
const filteredEntries = settings.includeUnusedModels
|
|
790
|
+
? report.entries
|
|
791
|
+
: report.entries.filter(e => e.input + e.output + e.cacheRead + e.cacheWrite > 0);
|
|
792
|
+
|
|
793
|
+
for (const entry of filteredEntries) {
|
|
768
794
|
table.push(
|
|
769
795
|
formatUsageRow(
|
|
770
796
|
entry.month,
|
|
@@ -811,16 +837,14 @@ async function outputJsonReport(
|
|
|
811
837
|
reportType: JsonReportType,
|
|
812
838
|
options: FilterOptions & DateFilterOptions
|
|
813
839
|
) {
|
|
814
|
-
logNativeStatus();
|
|
815
|
-
|
|
816
840
|
const dateFilters = getDateFilters(options);
|
|
817
841
|
const enabledSources = getEnabledSources(options);
|
|
818
842
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
819
843
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
820
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
844
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
821
845
|
.filter(s => s !== 'cursor');
|
|
822
846
|
|
|
823
|
-
const {
|
|
847
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
824
848
|
onlyCursor ? [] : localSources,
|
|
825
849
|
dateFilters
|
|
826
850
|
);
|
|
@@ -830,12 +854,11 @@ async function outputJsonReport(
|
|
|
830
854
|
process.exit(1);
|
|
831
855
|
}
|
|
832
856
|
|
|
833
|
-
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 };
|
|
857
|
+
const emptyMessages: ParsedMessages = { messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, processingTimeMs: 0 };
|
|
834
858
|
|
|
835
859
|
if (reportType === "models") {
|
|
836
860
|
const report = await finalizeReportAsync({
|
|
837
861
|
localMessages: localMessages || emptyMessages,
|
|
838
|
-
pricing: fetcher.toPricingEntries(),
|
|
839
862
|
includeCursor: includeCursor && cursorSync.synced,
|
|
840
863
|
since: dateFilters.since,
|
|
841
864
|
until: dateFilters.until,
|
|
@@ -845,7 +868,6 @@ async function outputJsonReport(
|
|
|
845
868
|
} else {
|
|
846
869
|
const report = await finalizeMonthlyReportAsync({
|
|
847
870
|
localMessages: localMessages || emptyMessages,
|
|
848
|
-
pricing: fetcher.toPricingEntries(),
|
|
849
871
|
includeCursor: includeCursor && cursorSync.synced,
|
|
850
872
|
since: dateFilters.since,
|
|
851
873
|
until: dateFilters.until,
|
|
@@ -858,24 +880,26 @@ async function outputJsonReport(
|
|
|
858
880
|
interface GraphCommandOptions extends FilterOptions, DateFilterOptions {
|
|
859
881
|
output?: string;
|
|
860
882
|
benchmark?: boolean;
|
|
883
|
+
spinner?: boolean;
|
|
861
884
|
}
|
|
862
885
|
|
|
863
886
|
async function handleGraphCommand(options: GraphCommandOptions) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// Start spinner for loading phase (only if outputting to file, not stdout)
|
|
867
|
-
const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
|
|
868
|
-
spinner?.start(pc.gray("Loading data sources..."));
|
|
887
|
+
const useSpinner = options.output && options.spinner !== false;
|
|
888
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
869
889
|
|
|
870
890
|
const dateFilters = getDateFilters(options);
|
|
871
891
|
const enabledSources = getEnabledSources(options);
|
|
872
|
-
|
|
873
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
892
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
874
893
|
.filter(s => s !== 'cursor');
|
|
875
894
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
876
895
|
|
|
877
|
-
|
|
878
|
-
|
|
896
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
897
|
+
|
|
898
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
899
|
+
localSources,
|
|
900
|
+
dateFilters,
|
|
901
|
+
(phase) => spinner?.update(phase)
|
|
902
|
+
);
|
|
879
903
|
|
|
880
904
|
if (!localMessages) {
|
|
881
905
|
spinner?.error('Failed to parse local session files');
|
|
@@ -887,7 +911,6 @@ async function handleGraphCommand(options: GraphCommandOptions) {
|
|
|
887
911
|
|
|
888
912
|
const data = await finalizeGraphAsync({
|
|
889
913
|
localMessages,
|
|
890
|
-
pricing: fetcher.toPricingEntries(),
|
|
891
914
|
includeCursor: includeCursor && cursorSync.synced,
|
|
892
915
|
since: dateFilters.since,
|
|
893
916
|
until: dateFilters.until,
|
|
@@ -968,6 +991,114 @@ async function handleWrappedCommand(options: WrappedCommandOptions) {
|
|
|
968
991
|
}
|
|
969
992
|
}
|
|
970
993
|
|
|
994
|
+
async function handlePricingCommand(modelId: string, options: { json?: boolean; provider?: string; spinner?: boolean }) {
|
|
995
|
+
const validProviders = ["litellm", "openrouter"];
|
|
996
|
+
if (options.provider && !validProviders.includes(options.provider.toLowerCase())) {
|
|
997
|
+
console.log(pc.red(`\n Invalid provider: ${options.provider}`));
|
|
998
|
+
console.log(pc.gray(` Valid providers: ${validProviders.join(", ")}\n`));
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const useSpinner = options.spinner !== false;
|
|
1003
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
1004
|
+
const providerLabel = options.provider ? ` from ${options.provider}` : "";
|
|
1005
|
+
spinner?.start(pc.gray(`Fetching pricing data${providerLabel}...`));
|
|
1006
|
+
|
|
1007
|
+
let core: typeof import("@tokscale/core");
|
|
1008
|
+
try {
|
|
1009
|
+
const mod = await import("@tokscale/core");
|
|
1010
|
+
core = (mod.default ?? mod) as typeof import("@tokscale/core");
|
|
1011
|
+
} catch (importErr) {
|
|
1012
|
+
spinner?.stop();
|
|
1013
|
+
const errorMsg = (importErr as Error).message || "Unknown error";
|
|
1014
|
+
if (options.json) {
|
|
1015
|
+
console.log(JSON.stringify({ error: "Native module not available", details: errorMsg }, null, 2));
|
|
1016
|
+
} else {
|
|
1017
|
+
console.log(pc.red(`\n Native module not available: ${errorMsg}`));
|
|
1018
|
+
console.log(pc.gray(" Run 'bun run build:core' to build the native module.\n"));
|
|
1019
|
+
}
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
const provider = options.provider?.toLowerCase() || undefined;
|
|
1025
|
+
const nativeResult = await core.lookupPricing(modelId, provider);
|
|
1026
|
+
spinner?.stop();
|
|
1027
|
+
|
|
1028
|
+
const result = {
|
|
1029
|
+
matchedKey: nativeResult.matchedKey,
|
|
1030
|
+
source: nativeResult.source as "litellm" | "openrouter",
|
|
1031
|
+
pricing: {
|
|
1032
|
+
input_cost_per_token: nativeResult.pricing.inputCostPerToken,
|
|
1033
|
+
output_cost_per_token: nativeResult.pricing.outputCostPerToken,
|
|
1034
|
+
cache_read_input_token_cost: nativeResult.pricing.cacheReadInputTokenCost,
|
|
1035
|
+
cache_creation_input_token_cost: nativeResult.pricing.cacheCreationInputTokenCost,
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
if (options.json) {
|
|
1040
|
+
console.log(JSON.stringify({
|
|
1041
|
+
modelId,
|
|
1042
|
+
matchedKey: result.matchedKey,
|
|
1043
|
+
source: result.source,
|
|
1044
|
+
pricing: {
|
|
1045
|
+
inputCostPerToken: result.pricing.input_cost_per_token ?? 0,
|
|
1046
|
+
outputCostPerToken: result.pricing.output_cost_per_token ?? 0,
|
|
1047
|
+
cacheReadInputTokenCost: result.pricing.cache_read_input_token_cost,
|
|
1048
|
+
cacheCreationInputTokenCost: result.pricing.cache_creation_input_token_cost,
|
|
1049
|
+
},
|
|
1050
|
+
}, null, 2));
|
|
1051
|
+
} else {
|
|
1052
|
+
const sourceLabel = result.source.toLowerCase() === "litellm" ? pc.blue("LiteLLM") : pc.magenta("OpenRouter");
|
|
1053
|
+
const inputCost = result.pricing.input_cost_per_token ?? 0;
|
|
1054
|
+
const outputCost = result.pricing.output_cost_per_token ?? 0;
|
|
1055
|
+
const cacheReadCost = result.pricing.cache_read_input_token_cost;
|
|
1056
|
+
const cacheWriteCost = result.pricing.cache_creation_input_token_cost;
|
|
1057
|
+
|
|
1058
|
+
console.log(pc.cyan(`\n Pricing for: ${pc.white(modelId)}`));
|
|
1059
|
+
console.log(pc.gray(` Matched key: ${result.matchedKey}`));
|
|
1060
|
+
console.log(pc.gray(` Source: `) + sourceLabel);
|
|
1061
|
+
console.log();
|
|
1062
|
+
console.log(pc.white(` Input: `) + formatPricePerMillion(inputCost));
|
|
1063
|
+
console.log(pc.white(` Output: `) + formatPricePerMillion(outputCost));
|
|
1064
|
+
if (cacheReadCost !== undefined) {
|
|
1065
|
+
console.log(pc.white(` Cache Read: `) + formatPricePerMillion(cacheReadCost));
|
|
1066
|
+
}
|
|
1067
|
+
if (cacheWriteCost !== undefined) {
|
|
1068
|
+
console.log(pc.white(` Cache Write: `) + formatPricePerMillion(cacheWriteCost));
|
|
1069
|
+
}
|
|
1070
|
+
console.log();
|
|
1071
|
+
}
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
spinner?.stop();
|
|
1074
|
+
const errorMsg = (err as Error).message || "Unknown error";
|
|
1075
|
+
|
|
1076
|
+
// Check if this is a "model not found" error from Rust or a different error
|
|
1077
|
+
const isModelNotFound = errorMsg.toLowerCase().includes("not found") ||
|
|
1078
|
+
errorMsg.toLowerCase().includes("no pricing");
|
|
1079
|
+
|
|
1080
|
+
if (options.json) {
|
|
1081
|
+
if (isModelNotFound) {
|
|
1082
|
+
console.log(JSON.stringify({ error: "Model not found", modelId }, null, 2));
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log(JSON.stringify({ error: errorMsg, modelId }, null, 2));
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
if (isModelNotFound) {
|
|
1088
|
+
console.log(pc.red(`\n Model not found: ${modelId}\n`));
|
|
1089
|
+
} else {
|
|
1090
|
+
console.log(pc.red(`\n Error looking up pricing: ${errorMsg}\n`));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function formatPricePerMillion(costPerToken: number): string {
|
|
1098
|
+
const perMillion = costPerToken * 1_000_000;
|
|
1099
|
+
return pc.green(`$${perMillion.toFixed(2)}`) + pc.gray(" / 1M tokens");
|
|
1100
|
+
}
|
|
1101
|
+
|
|
971
1102
|
function getSourceLabel(source: string): string {
|
|
972
1103
|
switch (source) {
|
|
973
1104
|
case "opencode":
|
|
@@ -980,6 +1111,10 @@ function getSourceLabel(source: string): string {
|
|
|
980
1111
|
return "Gemini";
|
|
981
1112
|
case "cursor":
|
|
982
1113
|
return "Cursor";
|
|
1114
|
+
case "amp":
|
|
1115
|
+
return "Amp";
|
|
1116
|
+
case "droid":
|
|
1117
|
+
return "Droid";
|
|
983
1118
|
default:
|
|
984
1119
|
return source;
|
|
985
1120
|
}
|