@tokscale/cli 1.0.13 → 1.0.14

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/src/wrapped.ts CHANGED
@@ -24,6 +24,7 @@ interface WrappedData {
24
24
  longestStreak: number;
25
25
  topModels: Array<{ name: string; cost: number; tokens: number }>;
26
26
  topClients: Array<{ name: string; cost: number; tokens: number }>;
27
+ topAgents?: Array<{ name: string; cost: number; tokens: number; messages: number }>;
27
28
  contributions: Array<{ date: string; level: 0 | 1 | 2 | 3 | 4 }>;
28
29
  totalMessages: number;
29
30
  }
@@ -33,6 +34,8 @@ export interface WrappedOptions {
33
34
  year?: string;
34
35
  sources?: SourceType[];
35
36
  short?: boolean;
37
+ includeAgents?: boolean;
38
+ pinSisyphus?: boolean;
36
39
  }
37
40
 
38
41
  const SCALE = 2;
@@ -61,16 +64,63 @@ const SOURCE_DISPLAY_NAMES: Record<string, string> = {
61
64
  cursor: "Cursor IDE",
62
65
  };
63
66
 
64
- const ASSETS_BASE_URL = "https://tokscale.ai/assets";
67
+ const ASSETS_BASE_URL = "https://tokscale.ai/assets/logos";
68
+
69
+ const PINNED_AGENTS = ["Sisyphus", "Planner-Sisyphus"];
70
+
71
+ function normalizeAgentName(agent: string): string {
72
+ const agentLower = agent.toLowerCase();
73
+
74
+ if (agentLower.includes("plan")) {
75
+ if (agentLower.includes("omo") || agentLower.includes("sisyphus")) {
76
+ return "Planner-Sisyphus";
77
+ }
78
+ return agent;
79
+ }
80
+
81
+ if (agentLower === "omo" || agentLower === "sisyphus") {
82
+ return "Sisyphus";
83
+ }
84
+
85
+ return agent;
86
+ }
65
87
 
66
88
  const CLIENT_LOGO_URLS: Record<string, string> = {
67
- "OpenCode": `${ASSETS_BASE_URL}/client-opencode.png`,
68
- "Claude Code": `${ASSETS_BASE_URL}/client-claude.jpg`,
69
- "Codex CLI": `${ASSETS_BASE_URL}/client-openai.jpg`,
70
- "Gemini CLI": `${ASSETS_BASE_URL}/client-gemini.png`,
71
- "Cursor IDE": `${ASSETS_BASE_URL}/client-cursor.jpg`,
89
+ "OpenCode": `${ASSETS_BASE_URL}/opencode.png`,
90
+ "Claude Code": `${ASSETS_BASE_URL}/claude.jpg`,
91
+ "Codex CLI": `${ASSETS_BASE_URL}/openai.jpg`,
92
+ "Gemini CLI": `${ASSETS_BASE_URL}/gemini.png`,
93
+ "Cursor IDE": `${ASSETS_BASE_URL}/cursor.jpg`,
94
+ };
95
+
96
+ const PROVIDER_LOGO_URLS: Record<string, string> = {
97
+ "anthropic": `${ASSETS_BASE_URL}/claude.jpg`,
98
+ "openai": `${ASSETS_BASE_URL}/openai.jpg`,
99
+ "google": `${ASSETS_BASE_URL}/gemini.png`,
100
+ "xai": `${ASSETS_BASE_URL}/grok.jpg`,
101
+ "zai": `${ASSETS_BASE_URL}/zai.jpg`,
72
102
  };
73
103
 
104
+ function getProviderFromModel(modelId: string): string | null {
105
+ const lower = modelId.toLowerCase();
106
+ if (lower.includes("claude") || lower.includes("opus") || lower.includes("sonnet") || lower.includes("haiku")) {
107
+ return "anthropic";
108
+ }
109
+ if (lower.includes("gpt") || lower.includes("o1") || lower.includes("o3") || lower.includes("codex")) {
110
+ return "openai";
111
+ }
112
+ if (lower.includes("gemini")) {
113
+ return "google";
114
+ }
115
+ if (lower.includes("grok")) {
116
+ return "xai";
117
+ }
118
+ if (lower.includes("glm") || lower.includes("pickle")) {
119
+ return "zai";
120
+ }
121
+ return null;
122
+ }
123
+
74
124
  const TOKSCALE_LOGO_SVG_URL = "https://tokscale.ai/tokscale-logo.svg";
75
125
  const TOKSCALE_LOGO_PNG_SIZE = 400;
76
126
 
@@ -171,7 +221,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
171
221
  pricingFetcher.fetchPricing(),
172
222
  includeCursor && loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 }),
173
223
  localSources.length > 0
174
- ? parseLocalSourcesAsync({ sources: localSources, since, until, year })
224
+ ? parseLocalSourcesAsync({ sources: localSources, since, until, year, forceTypescript: options.includeAgents })
175
225
  : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, processingTimeMs: 0 } as ParsedMessages),
176
226
  ]);
177
227
 
@@ -247,6 +297,54 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
247
297
  .sort((a, b) => b.cost - a.cost)
248
298
  .slice(0, 3);
249
299
 
300
+ let topAgents: Array<{ name: string; cost: number; tokens: number; messages: number }> | undefined;
301
+ if (options.includeAgents && localMessages) {
302
+ const pricingEntries = pricingFetcher.toPricingEntries();
303
+ const pricingMap = new Map(pricingEntries.map(p => [p.modelId, p.pricing]));
304
+
305
+ const agentMap = new Map<string, { cost: number; tokens: number; messages: number }>();
306
+ for (const msg of localMessages.messages) {
307
+ if (msg.source === "opencode" && msg.agent) {
308
+ const normalizedAgent = normalizeAgentName(msg.agent);
309
+ const existing = agentMap.get(normalizedAgent) || { cost: 0, tokens: 0, messages: 0 };
310
+
311
+ const msgTokens = msg.input + msg.output + msg.cacheRead + msg.cacheWrite + msg.reasoning;
312
+ const pricing = pricingMap.get(msg.modelId);
313
+ let msgCost = 0;
314
+ if (pricing) {
315
+ msgCost = (msg.input * pricing.inputCostPerToken) +
316
+ (msg.output * pricing.outputCostPerToken) +
317
+ (msg.cacheRead * (pricing.cacheReadInputTokenCost || 0)) +
318
+ (msg.cacheWrite * (pricing.cacheCreationInputTokenCost || 0));
319
+ }
320
+
321
+ agentMap.set(normalizedAgent, {
322
+ cost: existing.cost + msgCost,
323
+ tokens: existing.tokens + msgTokens,
324
+ messages: existing.messages + 1,
325
+ });
326
+ }
327
+ }
328
+
329
+ let agentsList = Array.from(agentMap.entries())
330
+ .map(([name, data]) => ({ name, ...data }));
331
+
332
+ if (options.pinSisyphus) {
333
+ const pinned = agentsList.filter(a => PINNED_AGENTS.includes(a.name));
334
+ const unpinned = agentsList.filter(a => !PINNED_AGENTS.includes(a.name));
335
+
336
+ pinned.sort((a, b) => PINNED_AGENTS.indexOf(a.name) - PINNED_AGENTS.indexOf(b.name));
337
+ unpinned.sort((a, b) => b.messages - a.messages);
338
+
339
+ agentsList = [...pinned, ...unpinned.slice(0, 2)];
340
+ } else {
341
+ agentsList.sort((a, b) => b.messages - a.messages);
342
+ agentsList = agentsList.slice(0, 3);
343
+ }
344
+
345
+ topAgents = agentsList.length > 0 ? agentsList : undefined;
346
+ }
347
+
250
348
  const maxCost = Math.max(...graph.contributions.map(c => c.totals.cost), 1);
251
349
  const contributions = graph.contributions.map(c => ({
252
350
  date: c.date,
@@ -269,6 +367,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
269
367
  longestStreak,
270
368
  topModels,
271
369
  topClients,
370
+ topAgents,
272
371
  contributions,
273
372
  totalMessages: report.totalMessages,
274
373
  };
@@ -526,7 +625,7 @@ function formatDate(dateStr: string): string {
526
625
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
527
626
  }
528
627
 
529
- async function generateWrappedImage(data: WrappedData, options: { short?: boolean } = {}): Promise<Buffer> {
628
+ async function generateWrappedImage(data: WrappedData, options: { short?: boolean; includeAgents?: boolean; pinSisyphus?: boolean } = {}): Promise<Buffer> {
530
629
  await ensureFontsLoaded();
531
630
 
532
631
  const canvas = createCanvas(IMAGE_WIDTH, IMAGE_HEIGHT);
@@ -559,6 +658,9 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
559
658
  ctx.fillText(totalTokensDisplay, PADDING, yPos);
560
659
  yPos += 50 * SCALE + 40 * SCALE;
561
660
 
661
+ const logoSize = 32 * SCALE;
662
+ const logoRadius = 6 * SCALE;
663
+
562
664
  ctx.fillStyle = COLORS.textSecondary;
563
665
  ctx.font = `${20 * SCALE}px Figtree, sans-serif`;
564
666
  ctx.fillText("Top Models", PADDING, yPos);
@@ -569,57 +671,118 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
569
671
  ctx.fillStyle = COLORS.textPrimary;
570
672
  ctx.font = `bold ${32 * SCALE}px Figtree, sans-serif`;
571
673
  ctx.fillText(`${i + 1}`, PADDING, yPos);
572
-
573
- ctx.font = `${32 * SCALE}px Figtree, sans-serif`;
574
- ctx.fillText(formatModelName(model.name), PADDING + 40 * SCALE, yPos);
575
- yPos += 50 * SCALE;
576
- }
577
- yPos += 40 * SCALE;
578
674
 
579
- ctx.fillStyle = COLORS.textSecondary;
580
- ctx.font = `${20 * SCALE}px Figtree, sans-serif`;
581
- ctx.fillText("Top Clients", PADDING, yPos);
582
- yPos += 48 * SCALE;
675
+ const provider = getProviderFromModel(model.name);
676
+ const providerLogoUrl = provider ? PROVIDER_LOGO_URLS[provider] : null;
677
+ let textX = PADDING + 40 * SCALE;
583
678
 
584
- const logoSize = 32 * SCALE;
585
-
586
- for (let i = 0; i < data.topClients.length; i++) {
587
- const client = data.topClients[i];
588
- ctx.fillStyle = COLORS.textPrimary;
589
- ctx.font = `bold ${32 * SCALE}px Figtree, sans-serif`;
590
- ctx.fillText(`${i + 1}`, PADDING, yPos);
591
-
592
- const logoUrl = CLIENT_LOGO_URLS[client.name];
593
- if (logoUrl) {
679
+ if (providerLogoUrl) {
594
680
  try {
595
- const filename = `client-${client.name.toLowerCase().replace(/\s+/g, "-")}@2x.png`;
596
- const logoPath = await fetchAndCacheImage(logoUrl, filename);
681
+ const filename = `provider-${provider}@2x.jpg`;
682
+ const logoPath = await fetchAndCacheImage(providerLogoUrl, filename);
597
683
  const logo = await loadImage(logoPath);
598
684
  const logoY = yPos - logoSize + 6 * SCALE;
599
-
600
685
  const logoX = PADDING + 40 * SCALE;
601
- const logoRadius = 6 * SCALE;
602
-
686
+
603
687
  ctx.save();
604
688
  drawRoundedRect(ctx, logoX, logoY, logoSize, logoSize, logoRadius);
605
689
  ctx.clip();
606
690
  ctx.drawImage(logo, logoX, logoY, logoSize, logoSize);
607
691
  ctx.restore();
608
-
692
+
609
693
  drawRoundedRect(ctx, logoX, logoY, logoSize, logoSize, logoRadius);
610
694
  ctx.strokeStyle = "#141A25";
611
695
  ctx.lineWidth = 1 * SCALE;
612
696
  ctx.stroke();
697
+
698
+ textX = logoX + logoSize + 12 * SCALE;
613
699
  } catch {
614
700
  }
615
701
  }
616
-
702
+
703
+ ctx.fillStyle = COLORS.textPrimary;
617
704
  ctx.font = `${32 * SCALE}px Figtree, sans-serif`;
618
- ctx.fillText(client.name, PADDING + 40 * SCALE + logoSize + 12 * SCALE, yPos);
705
+ ctx.fillText(formatModelName(model.name), textX, yPos);
619
706
  yPos += 50 * SCALE;
620
707
  }
621
708
  yPos += 40 * SCALE;
622
709
 
710
+ if (options.includeAgents) {
711
+ ctx.fillStyle = COLORS.textSecondary;
712
+ ctx.font = `${20 * SCALE}px Figtree, sans-serif`;
713
+ ctx.fillText("Top OpenCode Agents", PADDING, yPos);
714
+ yPos += 48 * SCALE;
715
+
716
+ const agents = data.topAgents || [];
717
+ const SISYPHUS_COLOR = "#00CED1";
718
+ let rankIndex = 1;
719
+
720
+ for (let i = 0; i < agents.length; i++) {
721
+ const agent = agents[i];
722
+ const isSisyphusAgent = PINNED_AGENTS.includes(agent.name);
723
+ const showWithDash = options.pinSisyphus && isSisyphusAgent;
724
+
725
+ ctx.fillStyle = showWithDash ? SISYPHUS_COLOR : COLORS.textPrimary;
726
+ ctx.font = `bold ${32 * SCALE}px Figtree, sans-serif`;
727
+ const prefix = showWithDash ? "•" : `${rankIndex}`;
728
+ ctx.fillText(prefix, PADDING, yPos);
729
+ if (!showWithDash) rankIndex++;
730
+
731
+ const nameX = PADDING + 40 * SCALE;
732
+ ctx.font = `${32 * SCALE}px Figtree, sans-serif`;
733
+ ctx.fillStyle = isSisyphusAgent ? SISYPHUS_COLOR : COLORS.textPrimary;
734
+ ctx.fillText(agent.name, nameX, yPos);
735
+
736
+ const nameWidth = ctx.measureText(agent.name).width;
737
+ ctx.fillStyle = COLORS.textSecondary;
738
+ ctx.fillText(` (${agent.messages.toLocaleString()})`, nameX + nameWidth, yPos);
739
+
740
+ yPos += 50 * SCALE;
741
+ }
742
+ } else {
743
+ ctx.fillStyle = COLORS.textSecondary;
744
+ ctx.font = `${20 * SCALE}px Figtree, sans-serif`;
745
+ ctx.fillText("Top Clients", PADDING, yPos);
746
+ yPos += 48 * SCALE;
747
+
748
+ for (let i = 0; i < data.topClients.length; i++) {
749
+ const client = data.topClients[i];
750
+ ctx.fillStyle = COLORS.textPrimary;
751
+ ctx.font = `bold ${32 * SCALE}px Figtree, sans-serif`;
752
+ ctx.fillText(`${i + 1}`, PADDING, yPos);
753
+
754
+ const logoUrl = CLIENT_LOGO_URLS[client.name];
755
+ if (logoUrl) {
756
+ try {
757
+ const filename = `client-${client.name.toLowerCase().replace(/\s+/g, "-")}@2x.png`;
758
+ const logoPath = await fetchAndCacheImage(logoUrl, filename);
759
+ const logo = await loadImage(logoPath);
760
+ const logoY = yPos - logoSize + 6 * SCALE;
761
+
762
+ const logoX = PADDING + 40 * SCALE;
763
+ const logoRadius = 6 * SCALE;
764
+
765
+ ctx.save();
766
+ drawRoundedRect(ctx, logoX, logoY, logoSize, logoSize, logoRadius);
767
+ ctx.clip();
768
+ ctx.drawImage(logo, logoX, logoY, logoSize, logoSize);
769
+ ctx.restore();
770
+
771
+ drawRoundedRect(ctx, logoX, logoY, logoSize, logoSize, logoRadius);
772
+ ctx.strokeStyle = "#141A25";
773
+ ctx.lineWidth = 1 * SCALE;
774
+ ctx.stroke();
775
+ } catch {
776
+ }
777
+ }
778
+
779
+ ctx.font = `${32 * SCALE}px Figtree, sans-serif`;
780
+ ctx.fillText(client.name, PADDING + 40 * SCALE + logoSize + 12 * SCALE, yPos);
781
+ yPos += 50 * SCALE;
782
+ }
783
+ }
784
+ yPos += 40 * SCALE;
785
+
623
786
  const statsStartY = yPos;
624
787
  const statWidth = (leftWidth - PADDING * 2) / 2;
625
788
 
@@ -660,11 +823,15 @@ async function generateWrappedImage(data: WrappedData, options: { short?: boolea
660
823
 
661
824
  export async function generateWrapped(options: WrappedOptions): Promise<string> {
662
825
  const data = await loadWrappedData(options);
663
- const imageBuffer = await generateWrappedImage(data, { short: options.short });
664
-
826
+ const imageBuffer = await generateWrappedImage(data, {
827
+ short: options.short,
828
+ includeAgents: options.includeAgents,
829
+ pinSisyphus: options.pinSisyphus,
830
+ });
831
+
665
832
  const outputPath = options.output || `tokscale-${data.year}-wrapped.png`;
666
833
  const absolutePath = path.resolve(outputPath);
667
-
834
+
668
835
  fs.writeFileSync(absolutePath, imageBuffer);
669
836
 
670
837
  return absolutePath;