@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/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* All heavy computation is done in the native Rust module.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Command } from "commander";
|
|
9
|
+
import { Command, Option } from "commander";
|
|
10
10
|
import { createRequire } from "module";
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const pkg = require("../package.json") as { version: string };
|
|
@@ -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,10 +315,13 @@ 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
|
-
.
|
|
309
|
-
.
|
|
322
|
+
.addOption(new Option("--agents", "Show Top OpenCode Agents (default)").conflicts("clients"))
|
|
323
|
+
.addOption(new Option("--clients", "Show Top Clients instead of Top OpenCode Agents").conflicts("agents"))
|
|
324
|
+
.option("--disable-pinned", "Disable pinning of Sisyphus agents in rankings")
|
|
310
325
|
.action(async (options) => {
|
|
311
326
|
await handleWrappedCommand(options);
|
|
312
327
|
});
|
|
@@ -344,6 +359,8 @@ async function main() {
|
|
|
344
359
|
.option("--codex", "Include only Codex CLI data")
|
|
345
360
|
.option("--gemini", "Include only Gemini CLI data")
|
|
346
361
|
.option("--cursor", "Include only Cursor IDE data")
|
|
362
|
+
.option("--amp", "Include only Amp data")
|
|
363
|
+
.option("--droid", "Include only Factory Droid data")
|
|
347
364
|
.option("--since <date>", "Start date (YYYY-MM-DD)")
|
|
348
365
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
349
366
|
.option("--year <year>", "Filter to specific year")
|
|
@@ -355,6 +372,8 @@ async function main() {
|
|
|
355
372
|
codex: options.codex,
|
|
356
373
|
gemini: options.gemini,
|
|
357
374
|
cursor: options.cursor,
|
|
375
|
+
amp: options.amp,
|
|
376
|
+
droid: options.droid,
|
|
358
377
|
since: options.since,
|
|
359
378
|
until: options.until,
|
|
360
379
|
year: options.year,
|
|
@@ -374,6 +393,8 @@ async function main() {
|
|
|
374
393
|
.option("--codex", "Show only Codex CLI usage")
|
|
375
394
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
376
395
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
396
|
+
.option("--amp", "Show only Amp usage")
|
|
397
|
+
.option("--droid", "Show only Factory Droid usage")
|
|
377
398
|
.option("--today", "Show only today's usage")
|
|
378
399
|
.option("--week", "Show last 7 days")
|
|
379
400
|
.option("--month", "Show current month")
|
|
@@ -390,9 +411,15 @@ async function main() {
|
|
|
390
411
|
}
|
|
391
412
|
});
|
|
392
413
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
});
|
|
396
423
|
|
|
397
424
|
const cursorCommand = program
|
|
398
425
|
.command("cursor")
|
|
@@ -425,7 +452,7 @@ async function main() {
|
|
|
425
452
|
// Global flags should go to main program
|
|
426
453
|
const isGlobalFlag = ['--help', '-h', '--version', '-V'].includes(firstArg);
|
|
427
454
|
const hasSubcommand = args.length > 0 && !firstArg.startsWith('-');
|
|
428
|
-
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'];
|
|
429
456
|
const isKnownCommand = hasSubcommand && knownCommands.includes(firstArg);
|
|
430
457
|
|
|
431
458
|
if (isKnownCommand || isGlobalFlag) {
|
|
@@ -442,6 +469,7 @@ async function main() {
|
|
|
442
469
|
.option("--codex", "Show only Codex CLI usage")
|
|
443
470
|
.option("--gemini", "Show only Gemini CLI usage")
|
|
444
471
|
.option("--cursor", "Show only Cursor IDE usage")
|
|
472
|
+
.option("--amp", "Show only Amp usage")
|
|
445
473
|
.option("--today", "Show only today's usage")
|
|
446
474
|
.option("--week", "Show last 7 days")
|
|
447
475
|
.option("--month", "Show current month")
|
|
@@ -449,27 +477,28 @@ async function main() {
|
|
|
449
477
|
.option("--until <date>", "End date (YYYY-MM-DD)")
|
|
450
478
|
.option("--year <year>", "Filter to specific year")
|
|
451
479
|
.option("--benchmark", "Show processing time")
|
|
480
|
+
.option("--no-spinner", "Disable spinner (for AI agents and scripts - keeps stdout clean)")
|
|
452
481
|
.parse();
|
|
453
482
|
|
|
454
483
|
const opts = defaultProgram.opts();
|
|
455
484
|
if (opts.json) {
|
|
456
485
|
await outputJsonReport("models", opts);
|
|
457
486
|
} else if (opts.light) {
|
|
458
|
-
await showModelReport(opts);
|
|
487
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
459
488
|
} else {
|
|
460
489
|
const launchTUI = await tryLoadTUI();
|
|
461
490
|
if (launchTUI) {
|
|
462
491
|
await launchTUI(buildTUIOptions(opts));
|
|
463
492
|
} else {
|
|
464
493
|
showTUIUnavailableMessage();
|
|
465
|
-
await showModelReport(opts);
|
|
494
|
+
await showModelReport(opts, { spinner: opts.spinner });
|
|
466
495
|
}
|
|
467
496
|
}
|
|
468
497
|
}
|
|
469
498
|
}
|
|
470
499
|
|
|
471
500
|
function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
|
|
472
|
-
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;
|
|
473
502
|
if (!hasFilter) return undefined; // All sources
|
|
474
503
|
|
|
475
504
|
const sources: SourceType[] = [];
|
|
@@ -478,21 +507,14 @@ function getEnabledSources(options: FilterOptions): SourceType[] | undefined {
|
|
|
478
507
|
if (options.codex) sources.push("codex");
|
|
479
508
|
if (options.gemini) sources.push("gemini");
|
|
480
509
|
if (options.cursor) sources.push("cursor");
|
|
510
|
+
if (options.amp) sources.push("amp");
|
|
511
|
+
if (options.droid) sources.push("droid");
|
|
481
512
|
return sources;
|
|
482
513
|
}
|
|
483
514
|
|
|
484
|
-
function logNativeStatus(): void {
|
|
485
|
-
if (!isNativeAvailable()) {
|
|
486
|
-
console.log(pc.yellow(" Note: Using TypeScript fallback (native module not available)"));
|
|
487
|
-
console.log(pc.gray(" Run 'bun run build:core' for ~10x faster processing.\n"));
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
515
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
await fetcher.fetchPricing();
|
|
494
|
-
return fetcher;
|
|
495
|
-
}
|
|
516
|
+
|
|
517
|
+
|
|
496
518
|
|
|
497
519
|
/**
|
|
498
520
|
* Sync Cursor usage data from API to local cache.
|
|
@@ -514,31 +536,19 @@ async function syncCursorData(): Promise<CursorSyncResult> {
|
|
|
514
536
|
}
|
|
515
537
|
|
|
516
538
|
interface LoadedDataSources {
|
|
517
|
-
fetcher: PricingFetcher;
|
|
518
539
|
cursorSync: CursorSyncResult;
|
|
519
540
|
localMessages: ParsedMessages | null;
|
|
520
541
|
}
|
|
521
542
|
|
|
522
|
-
/**
|
|
523
|
-
* Load all data sources in parallel (two-phase optimization):
|
|
524
|
-
* - Cursor API sync (network)
|
|
525
|
-
* - Pricing fetch (network)
|
|
526
|
-
* - Local file parsing (CPU/IO) - OpenCode, Claude, Codex, Gemini
|
|
527
|
-
*
|
|
528
|
-
* This overlaps network I/O with local file parsing for better performance.
|
|
529
|
-
*/
|
|
530
543
|
async function loadDataSourcesParallel(
|
|
531
544
|
localSources: SourceType[],
|
|
532
|
-
dateFilters: { since?: string; until?: string; year?: string }
|
|
545
|
+
dateFilters: { since?: string; until?: string; year?: string },
|
|
546
|
+
onPhase?: (phase: string) => void
|
|
533
547
|
): Promise<LoadedDataSources> {
|
|
534
|
-
// Skip local parsing if no local sources requested (e.g., cursor-only mode)
|
|
535
548
|
const shouldParseLocal = localSources.length > 0;
|
|
536
549
|
|
|
537
|
-
|
|
538
|
-
const [cursorResult, pricingResult, localResult] = await Promise.allSettled([
|
|
550
|
+
const [cursorResult, localResult] = await Promise.allSettled([
|
|
539
551
|
syncCursorData(),
|
|
540
|
-
fetchPricingData(),
|
|
541
|
-
// Parse local sources in parallel (excludes Cursor) - skip if empty
|
|
542
552
|
shouldParseLocal
|
|
543
553
|
? parseLocalSourcesAsync({
|
|
544
554
|
sources: localSources.filter(s => s !== 'cursor'),
|
|
@@ -549,25 +559,18 @@ async function loadDataSourcesParallel(
|
|
|
549
559
|
: Promise.resolve(null),
|
|
550
560
|
]);
|
|
551
561
|
|
|
552
|
-
// Handle partial failures gracefully
|
|
553
562
|
const cursorSync: CursorSyncResult = cursorResult.status === 'fulfilled'
|
|
554
563
|
? cursorResult.value
|
|
555
564
|
: { attempted: true, synced: false, rows: 0, error: 'Cursor sync failed' };
|
|
556
565
|
|
|
557
|
-
const fetcher: PricingFetcher = pricingResult.status === 'fulfilled'
|
|
558
|
-
? pricingResult.value
|
|
559
|
-
: new PricingFetcher(); // Empty pricing → costs = 0
|
|
560
|
-
|
|
561
566
|
const localMessages: ParsedMessages | null = localResult.status === 'fulfilled'
|
|
562
567
|
? localResult.value
|
|
563
568
|
: null;
|
|
564
569
|
|
|
565
|
-
return {
|
|
570
|
+
return { cursorSync, localMessages };
|
|
566
571
|
}
|
|
567
572
|
|
|
568
|
-
async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
|
|
569
|
-
logNativeStatus();
|
|
570
|
-
|
|
573
|
+
async function showModelReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
571
574
|
const dateFilters = getDateFilters(options);
|
|
572
575
|
const enabledSources = getEnabledSources(options);
|
|
573
576
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
@@ -594,47 +597,53 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
594
597
|
}
|
|
595
598
|
console.log();
|
|
596
599
|
|
|
597
|
-
|
|
598
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
599
|
-
spinner.start(pc.gray("Loading data sources..."));
|
|
600
|
+
const useSpinner = extraOptions?.spinner !== false;
|
|
601
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
600
602
|
|
|
601
|
-
|
|
602
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
603
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
603
604
|
.filter(s => s !== 'cursor');
|
|
604
605
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const {
|
|
606
|
+
spinner?.start(pc.gray("Scanning session data..."));
|
|
607
|
+
|
|
608
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
608
609
|
onlyCursor ? [] : localSources,
|
|
609
|
-
dateFilters
|
|
610
|
+
dateFilters,
|
|
611
|
+
(phase) => spinner?.update(phase)
|
|
610
612
|
);
|
|
611
613
|
|
|
612
614
|
if (!localMessages && !onlyCursor) {
|
|
613
|
-
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
|
+
}
|
|
614
620
|
process.exit(1);
|
|
615
621
|
}
|
|
616
622
|
|
|
617
|
-
spinner
|
|
623
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
618
624
|
const startTime = performance.now();
|
|
619
625
|
|
|
620
626
|
let report: ModelReport;
|
|
621
627
|
try {
|
|
622
|
-
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 };
|
|
623
629
|
report = await finalizeReportAsync({
|
|
624
630
|
localMessages: localMessages || emptyMessages,
|
|
625
|
-
pricing: fetcher.toPricingEntries(),
|
|
626
631
|
includeCursor: includeCursor && cursorSync.synced,
|
|
627
632
|
since: dateFilters.since,
|
|
628
633
|
until: dateFilters.until,
|
|
629
634
|
year: dateFilters.year,
|
|
630
635
|
});
|
|
631
636
|
} catch (e) {
|
|
632
|
-
spinner
|
|
637
|
+
if (spinner) {
|
|
638
|
+
spinner.error(`Error: ${(e as Error).message}`);
|
|
639
|
+
} else {
|
|
640
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
641
|
+
}
|
|
633
642
|
process.exit(1);
|
|
634
643
|
}
|
|
635
644
|
|
|
636
645
|
const processingTime = performance.now() - startTime;
|
|
637
|
-
spinner
|
|
646
|
+
spinner?.stop();
|
|
638
647
|
|
|
639
648
|
if (report.entries.length === 0) {
|
|
640
649
|
if (onlyCursor && !cursorSync.synced) {
|
|
@@ -648,8 +657,13 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
648
657
|
|
|
649
658
|
// Create table
|
|
650
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);
|
|
651
665
|
|
|
652
|
-
for (const entry of
|
|
666
|
+
for (const entry of filteredEntries) {
|
|
653
667
|
const sourceLabel = getSourceLabel(entry.source);
|
|
654
668
|
const modelDisplay = `${pc.dim(sourceLabel)} ${formatModelName(entry.model)}`;
|
|
655
669
|
table.push(
|
|
@@ -701,9 +715,7 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
701
715
|
console.log();
|
|
702
716
|
}
|
|
703
717
|
|
|
704
|
-
async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }) {
|
|
705
|
-
logNativeStatus();
|
|
706
|
-
|
|
718
|
+
async function showMonthlyReport(options: FilterOptions & DateFilterOptions & { benchmark?: boolean }, extraOptions?: { spinner?: boolean }) {
|
|
707
719
|
const dateRange = getDateRangeLabel(options);
|
|
708
720
|
const title = dateRange
|
|
709
721
|
? `Monthly Token Usage Report (${dateRange})`
|
|
@@ -715,45 +727,55 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
|
|
|
715
727
|
}
|
|
716
728
|
console.log();
|
|
717
729
|
|
|
718
|
-
|
|
719
|
-
const spinner = createSpinner({ color: "cyan" });
|
|
720
|
-
spinner.start(pc.gray("Loading data sources..."));
|
|
730
|
+
const useSpinner = extraOptions?.spinner !== false;
|
|
731
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
721
732
|
|
|
722
733
|
const dateFilters = getDateFilters(options);
|
|
723
734
|
const enabledSources = getEnabledSources(options);
|
|
724
|
-
|
|
725
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
735
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
726
736
|
.filter(s => s !== 'cursor');
|
|
727
737
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
728
738
|
|
|
729
|
-
|
|
730
|
-
|
|
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
|
+
);
|
|
731
746
|
|
|
732
747
|
if (!localMessages) {
|
|
733
|
-
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
|
+
}
|
|
734
753
|
process.exit(1);
|
|
735
754
|
}
|
|
736
755
|
|
|
737
|
-
spinner
|
|
756
|
+
spinner?.update(pc.gray("Finalizing report..."));
|
|
738
757
|
const startTime = performance.now();
|
|
739
758
|
|
|
740
759
|
let report: MonthlyReport;
|
|
741
760
|
try {
|
|
742
761
|
report = await finalizeMonthlyReportAsync({
|
|
743
762
|
localMessages,
|
|
744
|
-
pricing: fetcher.toPricingEntries(),
|
|
745
763
|
includeCursor: includeCursor && cursorSync.synced,
|
|
746
764
|
since: dateFilters.since,
|
|
747
765
|
until: dateFilters.until,
|
|
748
766
|
year: dateFilters.year,
|
|
749
767
|
});
|
|
750
768
|
} catch (e) {
|
|
751
|
-
spinner
|
|
769
|
+
if (spinner) {
|
|
770
|
+
spinner.error(`Error: ${(e as Error).message}`);
|
|
771
|
+
} else {
|
|
772
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
773
|
+
}
|
|
752
774
|
process.exit(1);
|
|
753
775
|
}
|
|
754
776
|
|
|
755
777
|
const processingTime = performance.now() - startTime;
|
|
756
|
-
spinner
|
|
778
|
+
spinner?.stop();
|
|
757
779
|
|
|
758
780
|
if (report.entries.length === 0) {
|
|
759
781
|
console.log(pc.yellow(" No usage data found.\n"));
|
|
@@ -763,7 +785,12 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
|
|
|
763
785
|
// Create table
|
|
764
786
|
const table = createUsageTable("Month");
|
|
765
787
|
|
|
766
|
-
|
|
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) {
|
|
767
794
|
table.push(
|
|
768
795
|
formatUsageRow(
|
|
769
796
|
entry.month,
|
|
@@ -810,16 +837,14 @@ async function outputJsonReport(
|
|
|
810
837
|
reportType: JsonReportType,
|
|
811
838
|
options: FilterOptions & DateFilterOptions
|
|
812
839
|
) {
|
|
813
|
-
logNativeStatus();
|
|
814
|
-
|
|
815
840
|
const dateFilters = getDateFilters(options);
|
|
816
841
|
const enabledSources = getEnabledSources(options);
|
|
817
842
|
const onlyCursor = enabledSources?.length === 1 && enabledSources[0] === 'cursor';
|
|
818
843
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
819
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
844
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
820
845
|
.filter(s => s !== 'cursor');
|
|
821
846
|
|
|
822
|
-
const {
|
|
847
|
+
const { cursorSync, localMessages } = await loadDataSourcesParallel(
|
|
823
848
|
onlyCursor ? [] : localSources,
|
|
824
849
|
dateFilters
|
|
825
850
|
);
|
|
@@ -829,12 +854,11 @@ async function outputJsonReport(
|
|
|
829
854
|
process.exit(1);
|
|
830
855
|
}
|
|
831
856
|
|
|
832
|
-
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 };
|
|
833
858
|
|
|
834
859
|
if (reportType === "models") {
|
|
835
860
|
const report = await finalizeReportAsync({
|
|
836
861
|
localMessages: localMessages || emptyMessages,
|
|
837
|
-
pricing: fetcher.toPricingEntries(),
|
|
838
862
|
includeCursor: includeCursor && cursorSync.synced,
|
|
839
863
|
since: dateFilters.since,
|
|
840
864
|
until: dateFilters.until,
|
|
@@ -844,7 +868,6 @@ async function outputJsonReport(
|
|
|
844
868
|
} else {
|
|
845
869
|
const report = await finalizeMonthlyReportAsync({
|
|
846
870
|
localMessages: localMessages || emptyMessages,
|
|
847
|
-
pricing: fetcher.toPricingEntries(),
|
|
848
871
|
includeCursor: includeCursor && cursorSync.synced,
|
|
849
872
|
since: dateFilters.since,
|
|
850
873
|
until: dateFilters.until,
|
|
@@ -857,24 +880,26 @@ async function outputJsonReport(
|
|
|
857
880
|
interface GraphCommandOptions extends FilterOptions, DateFilterOptions {
|
|
858
881
|
output?: string;
|
|
859
882
|
benchmark?: boolean;
|
|
883
|
+
spinner?: boolean;
|
|
860
884
|
}
|
|
861
885
|
|
|
862
886
|
async function handleGraphCommand(options: GraphCommandOptions) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
// Start spinner for loading phase (only if outputting to file, not stdout)
|
|
866
|
-
const spinner = options.output ? createSpinner({ color: "cyan" }) : null;
|
|
867
|
-
spinner?.start(pc.gray("Loading data sources..."));
|
|
887
|
+
const useSpinner = options.output && options.spinner !== false;
|
|
888
|
+
const spinner = useSpinner ? createSpinner({ color: "cyan" }) : null;
|
|
868
889
|
|
|
869
890
|
const dateFilters = getDateFilters(options);
|
|
870
891
|
const enabledSources = getEnabledSources(options);
|
|
871
|
-
|
|
872
|
-
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor'])
|
|
892
|
+
const localSources: SourceType[] = (enabledSources || ['opencode', 'claude', 'codex', 'gemini', 'cursor', 'amp', 'droid'])
|
|
873
893
|
.filter(s => s !== 'cursor');
|
|
874
894
|
const includeCursor = !enabledSources || enabledSources.includes('cursor');
|
|
875
895
|
|
|
876
|
-
|
|
877
|
-
|
|
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
|
+
);
|
|
878
903
|
|
|
879
904
|
if (!localMessages) {
|
|
880
905
|
spinner?.error('Failed to parse local session files');
|
|
@@ -886,7 +911,6 @@ async function handleGraphCommand(options: GraphCommandOptions) {
|
|
|
886
911
|
|
|
887
912
|
const data = await finalizeGraphAsync({
|
|
888
913
|
localMessages,
|
|
889
|
-
pricing: fetcher.toPricingEntries(),
|
|
890
914
|
includeCursor: includeCursor && cursorSync.synced,
|
|
891
915
|
since: dateFilters.since,
|
|
892
916
|
until: dateFilters.until,
|
|
@@ -929,7 +953,8 @@ interface WrappedCommandOptions extends FilterOptions {
|
|
|
929
953
|
spinner?: boolean;
|
|
930
954
|
short?: boolean;
|
|
931
955
|
agents?: boolean;
|
|
932
|
-
|
|
956
|
+
clients?: boolean;
|
|
957
|
+
disablePinned?: boolean;
|
|
933
958
|
}
|
|
934
959
|
|
|
935
960
|
async function handleWrappedCommand(options: WrappedCommandOptions) {
|
|
@@ -946,8 +971,8 @@ async function handleWrappedCommand(options: WrappedCommandOptions) {
|
|
|
946
971
|
year,
|
|
947
972
|
sources: enabledSources,
|
|
948
973
|
short: options.short,
|
|
949
|
-
includeAgents: options.
|
|
950
|
-
pinSisyphus: options.
|
|
974
|
+
includeAgents: !options.clients,
|
|
975
|
+
pinSisyphus: !options.disablePinned,
|
|
951
976
|
});
|
|
952
977
|
|
|
953
978
|
spinner?.stop();
|
|
@@ -966,6 +991,114 @@ async function handleWrappedCommand(options: WrappedCommandOptions) {
|
|
|
966
991
|
}
|
|
967
992
|
}
|
|
968
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
|
+
|
|
969
1102
|
function getSourceLabel(source: string): string {
|
|
970
1103
|
switch (source) {
|
|
971
1104
|
case "opencode":
|
|
@@ -978,6 +1111,10 @@ function getSourceLabel(source: string): string {
|
|
|
978
1111
|
return "Gemini";
|
|
979
1112
|
case "cursor":
|
|
980
1113
|
return "Cursor";
|
|
1114
|
+
case "amp":
|
|
1115
|
+
return "Amp";
|
|
1116
|
+
case "droid":
|
|
1117
|
+
return "Droid";
|
|
981
1118
|
default:
|
|
982
1119
|
return source;
|
|
983
1120
|
}
|