@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/dist/cli.js
CHANGED
|
@@ -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,6 +241,8 @@ 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
248
|
.addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
|
|
@@ -269,6 +280,8 @@ async function main() {
|
|
|
269
280
|
.option("--codex", "Include only Codex CLI data")
|
|
270
281
|
.option("--gemini", "Include only Gemini CLI data")
|
|
271
282
|
.option("--cursor", "Include only Cursor IDE data")
|
|
283
|
+
.option("--amp", "Include only Amp data")
|
|
284
|
+
.option("--droid", "Include only Factory Droid data")
|
|
272
285
|
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
273
286
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
274
287
|
.option("--year <year>", "Filter to specific year")
|
|
@@ -280,6 +293,8 @@ async function main() {
|
|
|
280
293
|
codex: options.codex,
|
|
281
294
|
gemini: options.gemini,
|
|
282
295
|
cursor: options.cursor,
|
|
296
|
+
amp: options.amp,
|
|
297
|
+
droid: options.droid,
|
|
283
298
|
since: options.since,
|
|
284
299
|
until: options.until,
|
|
285
300
|
year: options.year,
|
|
@@ -297,6 +312,8 @@ async function main() {
|
|
|
297
312
|
.option("--codex", "Show only Codex CLI usage")
|
|
298
313
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
299
314
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
315
|
+
.option("--amp", "Show only Amp usage")
|
|
316
|
+
.option("--droid", "Show only Factory Droid usage")
|
|
300
317
|
.option("--today", "Show only today's usage")
|
|
301
318
|
.option("--week", "Show last 7 days")
|
|
302
319
|
.option("--month", "Show current month")
|
|
@@ -313,9 +330,15 @@ async function main() {
|
|
|
313
330
|
process.exit(1);
|
|
314
331
|
}
|
|
315
332
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
});
|
|
319
342
|
const cursorCommand = program
|
|
320
343
|
.command("cursor")
|
|
321
344
|
.description("Cursor IDE integration commands");
|
|
@@ -343,7 +366,7 @@ async function main() {
|
|
|
343
366
|
// Global flags should go to main program
|
|
344
367
|
const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
|
|
345
368
|
const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
|
|
346
|
-
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'];
|
|
347
370
|
const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
|
|
348
371
|
if (isKnownCommand || isGlobalFlag) {
|
|
349
372
|
// Run the specified subcommand or show full help/version
|
|
@@ -360,6 +383,7 @@ async function main() {
|
|
|
360
383
|
.option("--codex", "Show only Codex CLI usage")
|
|
361
384
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
362
385
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
386
|
+
.option("--amp", "Show only Amp usage")
|
|
363
387
|
.option("--today", "Show only today's usage")
|
|
364
388
|
.option("--week", "Show last 7 days")
|
|
365
389
|
.option("--month", "Show current month")
|
|
@@ -367,13 +391,14 @@ async function main() {
|
|
|
367
391
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
368
392
|
.option("--year <year>", "Filter to specific year")
|
|
369
393
|
.option("--benchmark", "Show processing time")
|
|
394
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
370
395
|
.parse();
|
|
371
396
|
const opts = defaultProgram.opts();
|
|
372
397
|
if (opts.json) {
|
|
373
398
|
await outputJsonReport("models", opts);
|
|
374
399
|
}
|
|
375
400
|
else if (opts.light) {
|
|
376
|
-
await showModelReport(opts);
|
|
401
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
377
402
|
}
|
|
378
403
|
else {
|
|
379
404
|
const launchTUI = await tryLoadTUI();
|
|
@@ -382,13 +407,13 @@ async function main() {
|
|
|
382
407
|
}
|
|
383
408
|
else {
|
|
384
409
|
showTUIUnavailableMessage();
|
|
385
|
-
await showModelReport(opts);
|
|
410
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
386
411
|
}
|
|
387
412
|
}
|
|
388
413
|
}
|
|
389
414
|
}
|
|
390
415
|
function getEnabledSources(options) {
|
|
391
|
-
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;
|
|
392
417
|
if (!hasFilter)
|
|
393
418
|
return undefined; // All sources
|
|
394
419
|
const sources = [];
|
|
@@ -402,19 +427,12 @@ function getEnabledSources(options) {
|
|
|
402
427
|
sources.push("gemini");
|
|
403
428
|
if (options.cursor)
|
|
404
429
|
sources.push("cursor");
|
|
430
|
+
if (options.amp)
|
|
431
|
+
sources.push("amp");
|
|
432
|
+
if (options.droid)
|
|
433
|
+
sources.push("droid");
|
|
405
434
|
return sources;
|
|
406
435
|
}
|
|
407
|
-
function logNativeStatus() {
|
|
408
|
-
if (!isNativeAvailable()) {
|
|
409
|
-
console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
|
|
410
|
-
console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
async function fetchPricingData() {
|
|
414
|
-
const fetcher = new PricingFetcher();
|
|
415
|
-
await fetcher.fetchPricing();
|
|
416
|
-
return fetcher;
|
|
417
|
-
}
|
|
418
436
|
/**
|
|
419
437
|
* Sync Cursor usage data from API to local cache.
|
|
420
438
|
* Only attempts sync if user is authenticated with Cursor.
|
|
@@ -432,22 +450,10 @@ async function syncCursorData() {
|
|
|
432
450
|
error: result.error,
|
|
433
451
|
};
|
|
434
452
|
}
|
|
435
|
-
|
|
436
|
-
* Load all data sources in parallel (two-phase optimization):
|
|
437
|
-
* - Cursor API sync (network)
|
|
438
|
-
* - Pricing fetch (network)
|
|
439
|
-
* - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
|
|
440
|
-
*
|
|
441
|
-
* This overlaps network I/O with local file parsing for better performance.
|
|
442
|
-
*/
|
|
443
|
-
async function loadDataSourcesParallel(localSources, dateFilters) {
|
|
444
|
-
// Skip local parsing if no local sources requested (e.g., cursor-only mode)
|
|
453
|
+
async function loadDataSourcesParallel(localSources, dateFilters, onPhase) {
|
|
445
454
|
const shouldParseLocal = localSources.length > 0;
|
|
446
|
-
|
|
447
|
-
const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
|
|
455
|
+
const [cursorResult, localResult] = await Promise.allSettled([
|
|
448
456
|
syncCursorData(),
|
|
449
|
-
fetchPricingData(),
|
|
450
|
-
// Parse local sources in parallel (excludes Cursor) - skip if empty
|
|
451
457
|
shouldParseLocal
|
|
452
458
|
? parseLocalSourcesAsync({
|
|
453
459
|
sources: localSources.filter(s => s !== 'cursor'),
|
|
@@ -457,20 +463,15 @@ async function loadDataSourcesParallel(localSources, dateFilters) {
|
|
|
457
463
|
})
|
|
458
464
|
: Promise.resolve(null),
|
|
459
465
|
]);
|
|
460
|
-
// Handle partial failures gracefully
|
|
461
466
|
const cursorSync = cursorResult.status === 'fulfilled'
|
|
462
467
|
? cursorResult.value
|
|
463
468
|
: { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
|
|
464
|
-
const fetcher = pricingResult.status === 'fulfilled'
|
|
465
|
-
? pricingResult.value
|
|
466
|
-
: new PricingFetcher(); // Empty pricing → costs = 0
|
|
467
469
|
const localMessages = localResult.status === 'fulfilled'
|
|
468
470
|
? localResult.value
|
|
469
471
|
: null;
|
|
470
|
-
return {
|
|
472
|
+
return { cursorSync, localMessages };
|
|
471
473
|
}
|
|
472
|
-
async function showModelReport(options) {
|
|
473
|
-
logNativeStatus();
|
|
474
|
+
async function showModelReport(options, extraOptions) {
|
|
474
475
|
const dateFilters = getDateFilters(options);
|
|
475
476
|
const enabledSources = getEnabledSources(options);
|
|
476
477
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
@@ -493,27 +494,28 @@ async function showModelReport(options) {
|
|
|
493
494
|
console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
|
|
494
495
|
}
|
|
495
496
|
console.log();
|
|
496
|
-
|
|
497
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
498
|
-
|
|
499
|
-
// Filter out cursor for local parsing (it's synced separately via network)
|
|
500
|
-
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'])
|
|
501
500
|
.filter(s => s !== 'cursor');
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
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));
|
|
505
503
|
if (!localMessages && !onlyCursor) {
|
|
506
|
-
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
|
+
}
|
|
507
510
|
process.exit(1);
|
|
508
511
|
}
|
|
509
|
-
spinner
|
|
512
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
510
513
|
const startTime = performance.now();
|
|
511
514
|
let report;
|
|
512
515
|
try {
|
|
513
|
-
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 };
|
|
514
517
|
report = await finalizeReportAsync({
|
|
515
518
|
localMessages: localMessages || emptyMessages,
|
|
516
|
-
pricing: fetcher.toPricingEntries(),
|
|
517
519
|
includeCursor: includeCursor && cursorSync.synced,
|
|
518
520
|
since: dateFilters.since,
|
|
519
521
|
until: dateFilters.until,
|
|
@@ -521,11 +523,16 @@ async function showModelReport(options) {
|
|
|
521
523
|
});
|
|
522
524
|
}
|
|
523
525
|
catch (e) {
|
|
524
|
-
spinner
|
|
526
|
+
if (spinner) {
|
|
527
|
+
spinner.error(`Error: ${e.message}`);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.error(`Error: ${e.message}`);
|
|
531
|
+
}
|
|
525
532
|
process.exit(1);
|
|
526
533
|
}
|
|
527
534
|
const processingTime = performance.now() - startTime;
|
|
528
|
-
spinner
|
|
535
|
+
spinner?.stop();
|
|
529
536
|
if (report.entries.length === 0) {
|
|
530
537
|
if (onlyCursor && !cursorSync.synced) {
|
|
531
538
|
console.log(pc.yellow(" No Cursor data available."));
|
|
@@ -538,7 +545,11 @@ async function showModelReport(options) {
|
|
|
538
545
|
}
|
|
539
546
|
// Create table
|
|
540
547
|
const table = createUsageTable("Source/Model");
|
|
541
|
-
|
|
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) {
|
|
542
553
|
const sourceLabel = getSourceLabel(entry.source);
|
|
543
554
|
const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
|
|
544
555
|
table.push(formatUsageRow(modelDisplay, [entry.model], entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
|
|
@@ -563,8 +574,7 @@ async function showModelReport(options) {
|
|
|
563
574
|
}
|
|
564
575
|
console.log();
|
|
565
576
|
}
|
|
566
|
-
async function showMonthlyReport(options) {
|
|
567
|
-
logNativeStatus();
|
|
577
|
+
async function showMonthlyReport(options, extraOptions) {
|
|
568
578
|
const dateRange = getDateRangeLabel(options);
|
|
569
579
|
const title = dateRange
|
|
570
580
|
? `Monthly Token Usage Report (${dateRange})`
|
|
@@ -574,28 +584,30 @@ async function showMonthlyReport(options) {
|
|
|
574
584
|
console.log(pc.gray(` Using: Rust native module v${getNativeVersion()}`));
|
|
575
585
|
}
|
|
576
586
|
console.log();
|
|
577
|
-
|
|
578
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
579
|
-
spinner.start(pc.gray("Loading data sources..."));
|
|
587
|
+
const useSpinner = extraOptions?.spinner !== false;
|
|
588
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
580
589
|
const dateFilters = getDateFilters(options);
|
|
581
590
|
const enabledSources = getEnabledSources(options);
|
|
582
|
-
|
|
583
|
-
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
591
|
+
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
584
592
|
.filter(s => s !== 'cursor');
|
|
585
593
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
586
|
-
|
|
587
|
-
const {
|
|
594
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
595
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters, (phase) => spinner?.update(phase));
|
|
588
596
|
if (!localMessages) {
|
|
589
|
-
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
|
+
}
|
|
590
603
|
process.exit(1);
|
|
591
604
|
}
|
|
592
|
-
spinner
|
|
605
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
593
606
|
const startTime = performance.now();
|
|
594
607
|
let report;
|
|
595
608
|
try {
|
|
596
609
|
report = await finalizeMonthlyReportAsync({
|
|
597
610
|
localMessages,
|
|
598
|
-
pricing: fetcher.toPricingEntries(),
|
|
599
611
|
includeCursor: includeCursor && cursorSync.synced,
|
|
600
612
|
since: dateFilters.since,
|
|
601
613
|
until: dateFilters.until,
|
|
@@ -603,18 +615,27 @@ async function showMonthlyReport(options) {
|
|
|
603
615
|
});
|
|
604
616
|
}
|
|
605
617
|
catch (e) {
|
|
606
|
-
spinner
|
|
618
|
+
if (spinner) {
|
|
619
|
+
spinner.error(`Error: ${e.message}`);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
console.error(`Error: ${e.message}`);
|
|
623
|
+
}
|
|
607
624
|
process.exit(1);
|
|
608
625
|
}
|
|
609
626
|
const processingTime = performance.now() - startTime;
|
|
610
|
-
spinner
|
|
627
|
+
spinner?.stop();
|
|
611
628
|
if (report.entries.length === 0) {
|
|
612
629
|
console.log(pc.yellow(" No usage data found.\n"));
|
|
613
630
|
return;
|
|
614
631
|
}
|
|
615
632
|
// Create table
|
|
616
633
|
const table = createUsageTable("Month");
|
|
617
|
-
|
|
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) {
|
|
618
639
|
table.push(formatUsageRow(entry.month, entry.models, entry.input, entry.output, entry.cacheWrite, entry.cacheRead, entry.cost));
|
|
619
640
|
}
|
|
620
641
|
// Add totals row
|
|
@@ -639,23 +660,21 @@ async function showMonthlyReport(options) {
|
|
|
639
660
|
console.log();
|
|
640
661
|
}
|
|
641
662
|
async function outputJsonReport(reportType, options) {
|
|
642
|
-
logNativeStatus();
|
|
643
663
|
const dateFilters = getDateFilters(options);
|
|
644
664
|
const enabledSources = getEnabledSources(options);
|
|
645
665
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
646
666
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
647
|
-
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
667
|
+
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
648
668
|
.filter(s => s !== 'cursor');
|
|
649
|
-
const {
|
|
669
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(onlyCursor ? [] : localSources, dateFilters);
|
|
650
670
|
if (!localMessages && !onlyCursor) {
|
|
651
671
|
console.error(JSON.stringify({ error: "Failed to parse local session files" }));
|
|
652
672
|
process.exit(1);
|
|
653
673
|
}
|
|
654
|
-
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 };
|
|
655
675
|
if (reportType === "models") {
|
|
656
676
|
const report = await finalizeReportAsync({
|
|
657
677
|
localMessages: localMessages || emptyMessages,
|
|
658
|
-
pricing: fetcher.toPricingEntries(),
|
|
659
678
|
includeCursor: includeCursor && cursorSync.synced,
|
|
660
679
|
since: dateFilters.since,
|
|
661
680
|
until: dateFilters.until,
|
|
@@ -666,7 +685,6 @@ async function outputJsonReport(reportType, options) {
|
|
|
666
685
|
else {
|
|
667
686
|
const report = await finalizeMonthlyReportAsync({
|
|
668
687
|
localMessages: localMessages || emptyMessages,
|
|
669
|
-
pricing: fetcher.toPricingEntries(),
|
|
670
688
|
includeCursor: includeCursor && cursorSync.synced,
|
|
671
689
|
since: dateFilters.since,
|
|
672
690
|
until: dateFilters.until,
|
|
@@ -676,18 +694,15 @@ async function outputJsonReport(reportType, options) {
|
|
|
676
694
|
}
|
|
677
695
|
}
|
|
678
696
|
async function handleGraphCommand(options) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
|
|
682
|
-
spinner?.start(pc.gray("Loading data sources..."));
|
|
697
|
+
const useSpinner = options.output && options.spinner !== false;
|
|
698
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
683
699
|
const dateFilters = getDateFilters(options);
|
|
684
700
|
const enabledSources = getEnabledSources(options);
|
|
685
|
-
|
|
686
|
-
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
701
|
+
const localSources = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
687
702
|
.filter(s => s !== 'cursor');
|
|
688
703
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
689
|
-
|
|
690
|
-
const {
|
|
704
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
705
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(localSources, dateFilters, (phase) => spinner?.update(phase));
|
|
691
706
|
if (!localMessages) {
|
|
692
707
|
spinner?.error('Failed to parse local session files');
|
|
693
708
|
process.exit(1);
|
|
@@ -696,7 +711,6 @@ async function handleGraphCommand(options) {
|
|
|
696
711
|
const startTime = performance.now();
|
|
697
712
|
const data = await finalizeGraphAsync({
|
|
698
713
|
localMessages,
|
|
699
|
-
pricing: fetcher.toPricingEntries(),
|
|
700
714
|
includeCursor: includeCursor && cursorSync.synced,
|
|
701
715
|
since: dateFilters.since,
|
|
702
716
|
until: dateFilters.until,
|
|
@@ -760,6 +774,111 @@ async function handleWrappedCommand(options) {
|
|
|
760
774
|
process.exit(1);
|
|
761
775
|
}
|
|
762
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
|
+
}
|
|
763
882
|
function getSourceLabel(source) {
|
|
764
883
|
switch (source) {
|
|
765
884
|
case "opencode":
|
|
@@ -772,6 +891,10 @@ function getSourceLabel(source) {
|
|
|
772
891
|
return "Gemini";
|
|
773
892
|
case "cursor":
|
|
774
893
|
return "Cursor";
|
|
894
|
+
case "amp":
|
|
895
|
+
return "Amp";
|
|
896
|
+
case "droid":
|
|
897
|
+
return "Droid";
|
|
775
898
|
default:
|
|
776
899
|
return source;
|
|
777
900
|
}
|