@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/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/native.d.ts +3 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -3
- package/dist/native.js.map +1 -1
- package/dist/sessions/opencode.d.ts +1 -0
- package/dist/sessions/opencode.d.ts.map +1 -1
- package/dist/sessions/opencode.js +16 -1
- package/dist/sessions/opencode.js.map +1 -1
- package/dist/sessions/types.d.ts +2 -4
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +2 -4
- package/dist/sessions/types.js.map +1 -1
- package/dist/wrapped.d.ts +8 -0
- package/dist/wrapped.d.ts.map +1 -1
- package/dist/wrapped.js +170 -29
- package/dist/wrapped.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +7 -1
- package/src/native.ts +8 -3
- package/src/sessions/opencode.ts +24 -1
- package/src/sessions/types.ts +6 -6
- package/src/wrapped.ts +206 -39
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}/
|
|
68
|
-
"Claude Code": `${ASSETS_BASE_URL}/
|
|
69
|
-
"Codex CLI": `${ASSETS_BASE_URL}/
|
|
70
|
-
"Gemini CLI": `${ASSETS_BASE_URL}/
|
|
71
|
-
"Cursor IDE": `${ASSETS_BASE_URL}/
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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 = `
|
|
596
|
-
const logoPath = await fetchAndCacheImage(
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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;
|