agent-leaderboard 0.1.0
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 +2065 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2065 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ../shared/dist/parsers/ccusage.js
|
|
22
|
+
import { execSync as execSync2 } from "child_process";
|
|
23
|
+
function isCcusageAvailable() {
|
|
24
|
+
return CcusageParser.isInstalled();
|
|
25
|
+
}
|
|
26
|
+
var CcusageParser;
|
|
27
|
+
var init_ccusage = __esm({
|
|
28
|
+
"../shared/dist/parsers/ccusage.js"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
init_esm_shims();
|
|
31
|
+
CcusageParser = class {
|
|
32
|
+
/**
|
|
33
|
+
* Check if ccusage is installed
|
|
34
|
+
*/
|
|
35
|
+
static isInstalled() {
|
|
36
|
+
try {
|
|
37
|
+
execSync2("which ccusage", { stdio: "pipe" });
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run ccusage and get JSON output
|
|
45
|
+
*/
|
|
46
|
+
static async fetchData(options = {}) {
|
|
47
|
+
const raw = await this.fetchRawData(options);
|
|
48
|
+
return this.normalize(raw);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run ccusage and get raw JSON output (includes totals)
|
|
52
|
+
*/
|
|
53
|
+
static async fetchRawData(options = {}) {
|
|
54
|
+
if (!this.isInstalled()) {
|
|
55
|
+
throw new Error("ccusage is not installed. Install it from: https://github.com/ryoppippi/ccusage");
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const daysArg = options.days ? `--days ${options.days}` : "";
|
|
59
|
+
const command = `ccusage --json ${daysArg}`;
|
|
60
|
+
const output = execSync2(command, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
|
61
|
+
return JSON.parse(output);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Failed to fetch ccusage data: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse ccusage JSON output
|
|
68
|
+
*/
|
|
69
|
+
static parse(jsonOutput) {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(jsonOutput);
|
|
72
|
+
return this.normalize(data);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`Failed to parse ccusage output: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalize ccusage data to DailyUsage format
|
|
79
|
+
*/
|
|
80
|
+
static normalize(data) {
|
|
81
|
+
const dailyUsage = [];
|
|
82
|
+
for (const day of data.daily) {
|
|
83
|
+
dailyUsage.push(this.normalizeDayEntry(day));
|
|
84
|
+
}
|
|
85
|
+
dailyUsage.sort((a, b) => a.date.localeCompare(b.date));
|
|
86
|
+
return dailyUsage;
|
|
87
|
+
}
|
|
88
|
+
static normalizeDayEntry(day) {
|
|
89
|
+
const tokensInput = day.inputTokens + day.cacheCreationTokens;
|
|
90
|
+
const tokensOutput = day.outputTokens;
|
|
91
|
+
const tokensCached = day.cacheReadTokens;
|
|
92
|
+
const model = day.modelsUsed.length > 0 ? day.modelsUsed[0] : "unknown";
|
|
93
|
+
return {
|
|
94
|
+
date: day.date,
|
|
95
|
+
provider: "claude",
|
|
96
|
+
model,
|
|
97
|
+
tokensInput,
|
|
98
|
+
tokensOutput,
|
|
99
|
+
tokensCached,
|
|
100
|
+
cost: day.totalCost,
|
|
101
|
+
calls: 1
|
|
102
|
+
// ccusage doesn't track individual API calls, use 1 as placeholder
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Aggregate multiple days into a single entry
|
|
107
|
+
*/
|
|
108
|
+
static aggregate(usageData) {
|
|
109
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const entry of usageData) {
|
|
111
|
+
const existing = aggregated.get(entry.date);
|
|
112
|
+
if (existing) {
|
|
113
|
+
existing.tokensInput += entry.tokensInput;
|
|
114
|
+
existing.tokensOutput += entry.tokensOutput;
|
|
115
|
+
existing.tokensCached = (existing.tokensCached || 0) + (entry.tokensCached || 0);
|
|
116
|
+
existing.cost += entry.cost;
|
|
117
|
+
existing.calls += entry.calls;
|
|
118
|
+
} else {
|
|
119
|
+
aggregated.set(entry.date, { ...entry });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return aggregated;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get usage data for a date range
|
|
126
|
+
*/
|
|
127
|
+
static async getDateRange(startDate, endDate) {
|
|
128
|
+
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
129
|
+
const allData = await this.fetchData({ days: daysDiff + 10 });
|
|
130
|
+
const startStr = startDate.toISOString().split("T")[0];
|
|
131
|
+
const endStr = endDate.toISOString().split("T")[0];
|
|
132
|
+
return allData.filter((d) => d.date >= startStr && d.date <= endStr);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ../shared/dist/utils/validation.js
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
function escapeXML(unsafe) {
|
|
141
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
142
|
+
}
|
|
143
|
+
var tokenSchema, deviceIdSchema, dateSchema, providerSchema, modelSchema, userIdSchema, dailyUsageSchema, syncPayloadSchema, graphQuerySchema, statsQuerySchema;
|
|
144
|
+
var init_validation = __esm({
|
|
145
|
+
"../shared/dist/utils/validation.js"() {
|
|
146
|
+
"use strict";
|
|
147
|
+
init_esm_shims();
|
|
148
|
+
tokenSchema = z.string().regex(/^[a-f0-9]{64}$/, "Invalid token format");
|
|
149
|
+
deviceIdSchema = z.string().regex(/^[a-f0-9]{64}$/, "Invalid device ID format");
|
|
150
|
+
dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format (expected YYYY-MM-DD)");
|
|
151
|
+
providerSchema = z.string().regex(/^[a-z0-9-]+$/i, "Invalid provider name").max(50);
|
|
152
|
+
modelSchema = z.string().regex(/^[a-z0-9._-]+$/i, "Invalid model name").max(100);
|
|
153
|
+
userIdSchema = z.string().uuid("Invalid user ID format");
|
|
154
|
+
dailyUsageSchema = z.object({
|
|
155
|
+
date: dateSchema,
|
|
156
|
+
provider: providerSchema,
|
|
157
|
+
model: modelSchema.nullable().optional(),
|
|
158
|
+
tokensInput: z.number().int().min(0).max(1e9),
|
|
159
|
+
tokensOutput: z.number().int().min(0).max(1e9),
|
|
160
|
+
tokensCached: z.number().int().min(0).max(1e9).optional().default(0),
|
|
161
|
+
cost: z.number().min(0).max(1e6),
|
|
162
|
+
calls: z.number().int().min(0).max(1e6)
|
|
163
|
+
});
|
|
164
|
+
syncPayloadSchema = z.object({
|
|
165
|
+
token: tokenSchema,
|
|
166
|
+
deviceId: deviceIdSchema,
|
|
167
|
+
timestamp: z.number().int().positive(),
|
|
168
|
+
data: z.array(dailyUsageSchema).min(0).max(1e3),
|
|
169
|
+
generateGraphs: z.boolean().optional().default(false)
|
|
170
|
+
});
|
|
171
|
+
graphQuerySchema = z.object({
|
|
172
|
+
token: tokenSchema,
|
|
173
|
+
days: z.string().regex(/^\d+$/).optional(),
|
|
174
|
+
theme: z.enum(["github-dark", "github-light"]).optional(),
|
|
175
|
+
view: z.enum(["yearly", "monthly"]).optional()
|
|
176
|
+
});
|
|
177
|
+
statsQuerySchema = z.object({
|
|
178
|
+
token: tokenSchema
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ../shared/dist/svg-generator.js
|
|
184
|
+
function generateSVG(dailyUsage, config2) {
|
|
185
|
+
const generator = new SVGGenerator(config2);
|
|
186
|
+
return generator.generate(dailyUsage);
|
|
187
|
+
}
|
|
188
|
+
var DEFAULT_CONFIG, THEMES, SVGGenerator;
|
|
189
|
+
var init_svg_generator = __esm({
|
|
190
|
+
"../shared/dist/svg-generator.js"() {
|
|
191
|
+
"use strict";
|
|
192
|
+
init_esm_shims();
|
|
193
|
+
init_validation();
|
|
194
|
+
DEFAULT_CONFIG = {
|
|
195
|
+
theme: "dark",
|
|
196
|
+
days: 365,
|
|
197
|
+
width: 800,
|
|
198
|
+
height: 150,
|
|
199
|
+
showStats: true,
|
|
200
|
+
provider: "",
|
|
201
|
+
view: "yearly",
|
|
202
|
+
customColors: {
|
|
203
|
+
empty: "#161b22",
|
|
204
|
+
level1: "#0e4429",
|
|
205
|
+
level2: "#006d32",
|
|
206
|
+
level3: "#26a641",
|
|
207
|
+
level4: "#39d353"
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
THEMES = {
|
|
211
|
+
"dark": {
|
|
212
|
+
empty: "#161b22",
|
|
213
|
+
level1: "#0e4429",
|
|
214
|
+
level2: "#006d32",
|
|
215
|
+
level3: "#26a641",
|
|
216
|
+
level4: "#39d353",
|
|
217
|
+
text: "#8b949e",
|
|
218
|
+
background: "#0d1117"
|
|
219
|
+
},
|
|
220
|
+
"light": {
|
|
221
|
+
empty: "#ebedf0",
|
|
222
|
+
level1: "#9be9a8",
|
|
223
|
+
level2: "#40c463",
|
|
224
|
+
level3: "#30a14e",
|
|
225
|
+
level4: "#216e39",
|
|
226
|
+
text: "#57606a",
|
|
227
|
+
background: "#ffffff"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
SVGGenerator = class {
|
|
231
|
+
config;
|
|
232
|
+
colors;
|
|
233
|
+
cellSize = 10;
|
|
234
|
+
cellGap = 3;
|
|
235
|
+
monthLabelHeight = 20;
|
|
236
|
+
weekLabelWidth = 30;
|
|
237
|
+
constructor(config2 = {}) {
|
|
238
|
+
this.config = {
|
|
239
|
+
...DEFAULT_CONFIG,
|
|
240
|
+
...config2,
|
|
241
|
+
customColors: config2.customColors || DEFAULT_CONFIG.customColors
|
|
242
|
+
};
|
|
243
|
+
const theme = this.config.theme;
|
|
244
|
+
this.colors = config2.customColors && config2.theme === "custom" ? { ...THEMES["dark"], ...config2.customColors } : theme === "light" ? THEMES["light"] : THEMES["dark"];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Generate SVG from daily usage data
|
|
248
|
+
*/
|
|
249
|
+
generate(dailyUsageMap) {
|
|
250
|
+
if (this.config.view === "monthly") {
|
|
251
|
+
return this.generateMonthlyView(dailyUsageMap);
|
|
252
|
+
}
|
|
253
|
+
if (this.config.view === "biweekly") {
|
|
254
|
+
return this.generateBiweeklyView(dailyUsageMap);
|
|
255
|
+
}
|
|
256
|
+
if (this.config.view === "biweekly-compact") {
|
|
257
|
+
return this.generateBiweeklyCompactView(dailyUsageMap);
|
|
258
|
+
}
|
|
259
|
+
const gridData = this.generateGridData(dailyUsageMap);
|
|
260
|
+
const monthLabels = this.computeMonthLabels(gridData.grid);
|
|
261
|
+
const totalWeeks = gridData.grid.length;
|
|
262
|
+
const width = this.weekLabelWidth + totalWeeks * (this.cellSize + this.cellGap);
|
|
263
|
+
const height = this.monthLabelHeight + 7 * (this.cellSize + this.cellGap) + 40;
|
|
264
|
+
let svg = this.generateHeader(width, height);
|
|
265
|
+
svg += this.renderMonthLabels(monthLabels);
|
|
266
|
+
svg += this.generateDayLabels();
|
|
267
|
+
svg += this.generateCells(gridData.grid);
|
|
268
|
+
if (this.config.showStats) {
|
|
269
|
+
svg += this.generateStats(gridData.stats, width, height);
|
|
270
|
+
}
|
|
271
|
+
svg += this.generateLegend(width, height);
|
|
272
|
+
svg += "</svg>";
|
|
273
|
+
return svg;
|
|
274
|
+
}
|
|
275
|
+
generateHeader(width, height) {
|
|
276
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
277
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
278
|
+
<style>
|
|
279
|
+
.cell {
|
|
280
|
+
rx: 2;
|
|
281
|
+
transition: all 0.1s ease;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
.cell:hover {
|
|
285
|
+
stroke: ${this.colors.text};
|
|
286
|
+
stroke-width: 2;
|
|
287
|
+
opacity: 0.8;
|
|
288
|
+
}
|
|
289
|
+
text {
|
|
290
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica', 'Arial', sans-serif;
|
|
291
|
+
font-size: 11px;
|
|
292
|
+
fill: ${this.colors.text};
|
|
293
|
+
}
|
|
294
|
+
.month-label { font-size: 10px; }
|
|
295
|
+
</style>
|
|
296
|
+
<rect width="${width}" height="${height}" fill="${this.colors.background}"/>
|
|
297
|
+
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
renderMonthLabels(labels) {
|
|
301
|
+
let svg = ` <!-- Month labels -->
|
|
302
|
+
<g transform="translate(${this.weekLabelWidth}, 0)">
|
|
303
|
+
`;
|
|
304
|
+
labels.forEach((label) => {
|
|
305
|
+
const x = label.weekIndex * (this.cellSize + this.cellGap);
|
|
306
|
+
svg += ` <text x="${x}" y="10" class="month-label">${label.month}</text>
|
|
307
|
+
`;
|
|
308
|
+
});
|
|
309
|
+
svg += ` </g>
|
|
310
|
+
|
|
311
|
+
`;
|
|
312
|
+
return svg;
|
|
313
|
+
}
|
|
314
|
+
generateDayLabels() {
|
|
315
|
+
return ` <!-- Day labels -->
|
|
316
|
+
<g transform="translate(0, ${this.monthLabelHeight})">
|
|
317
|
+
<text x="0" y="${this.cellSize + this.cellGap * 1.5}" text-anchor="start">Mon</text>
|
|
318
|
+
<text x="0" y="${(this.cellSize + this.cellGap) * 3 + this.cellGap * 1.5}" text-anchor="start">Wed</text>
|
|
319
|
+
<text x="0" y="${(this.cellSize + this.cellGap) * 5 + this.cellGap * 1.5}" text-anchor="start">Fri</text>
|
|
320
|
+
</g>
|
|
321
|
+
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
generateCells(grid) {
|
|
325
|
+
let svg = ` <!-- Contribution cells -->
|
|
326
|
+
<g transform="translate(${this.weekLabelWidth}, ${this.monthLabelHeight})">
|
|
327
|
+
`;
|
|
328
|
+
grid.forEach((week, weekIndex) => {
|
|
329
|
+
week.forEach((day, dayIndex) => {
|
|
330
|
+
if (!day.date)
|
|
331
|
+
return;
|
|
332
|
+
const x = weekIndex * (this.cellSize + this.cellGap);
|
|
333
|
+
const y = dayIndex * (this.cellSize + this.cellGap);
|
|
334
|
+
const color = this.getColorForLevel(day.level);
|
|
335
|
+
const tooltip = this.generateTooltip(day);
|
|
336
|
+
svg += ` <rect class="cell" x="${x}" y="${y}" width="${this.cellSize}" height="${this.cellSize}" fill="${color}">
|
|
337
|
+
`;
|
|
338
|
+
svg += ` <title>${tooltip}</title>
|
|
339
|
+
`;
|
|
340
|
+
svg += ` </rect>
|
|
341
|
+
`;
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
svg += ` </g>
|
|
345
|
+
|
|
346
|
+
`;
|
|
347
|
+
return svg;
|
|
348
|
+
}
|
|
349
|
+
generateTooltip(day) {
|
|
350
|
+
if (day.tokens === 0) {
|
|
351
|
+
return escapeXML(`${day.date}: No activity`);
|
|
352
|
+
}
|
|
353
|
+
const parts = [
|
|
354
|
+
escapeXML(day.date),
|
|
355
|
+
escapeXML(`${day.tokens.toLocaleString()} tokens`),
|
|
356
|
+
escapeXML(`$${day.cost.toFixed(3)}`),
|
|
357
|
+
escapeXML(`${day.calls} ${day.calls === 1 ? "call" : "calls"}`)
|
|
358
|
+
];
|
|
359
|
+
if (day.providers.length > 0) {
|
|
360
|
+
const escapedProviders = day.providers.map((p) => escapeXML(p)).join(", ");
|
|
361
|
+
parts.push(`(${escapedProviders})`);
|
|
362
|
+
}
|
|
363
|
+
return parts.join(" \u2022 ");
|
|
364
|
+
}
|
|
365
|
+
generateStats(stats, width, height) {
|
|
366
|
+
return ` <!-- Stats footer -->
|
|
367
|
+
<g transform="translate(0, ${height - 30})">
|
|
368
|
+
<text x="5" y="10" style="font-size: 10px;">
|
|
369
|
+
${stats.activeDays} active ${stats.activeDays === 1 ? "day" : "days"} \u2022 ${stats.totalTokens.toLocaleString()} tokens \u2022 $${stats.totalCost.toFixed(2)} \u2022 Max: ${stats.maxTokens.toLocaleString()}/day
|
|
370
|
+
</text>
|
|
371
|
+
</g>
|
|
372
|
+
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
generateLegend(width, height) {
|
|
376
|
+
const legendY = height - 15;
|
|
377
|
+
const startX = width - 130;
|
|
378
|
+
let svg = ` <!-- Legend -->
|
|
379
|
+
<g transform="translate(${startX}, ${legendY})">
|
|
380
|
+
`;
|
|
381
|
+
svg += ` <text x="0" y="10" style="font-size: 9px;">Less</text>
|
|
382
|
+
`;
|
|
383
|
+
for (let level = 0; level <= 4; level++) {
|
|
384
|
+
const color = this.getColorForLevel(level);
|
|
385
|
+
const x = 25 + level * 13;
|
|
386
|
+
svg += ` <rect x="${x}" y="2" width="${this.cellSize}" height="${this.cellSize}" fill="${color}" rx="2"/>
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
svg += ` <text x="${25 + 5 * 13 + 5}" y="10" style="font-size: 9px;">More</text>
|
|
390
|
+
`;
|
|
391
|
+
svg += ` </g>
|
|
392
|
+
`;
|
|
393
|
+
return svg;
|
|
394
|
+
}
|
|
395
|
+
getColorForLevel(level) {
|
|
396
|
+
const colorMap = {
|
|
397
|
+
0: "empty",
|
|
398
|
+
1: "level1",
|
|
399
|
+
2: "level2",
|
|
400
|
+
3: "level3",
|
|
401
|
+
4: "level4"
|
|
402
|
+
};
|
|
403
|
+
const key = colorMap[level];
|
|
404
|
+
return key ? this.colors[key] : this.colors.empty;
|
|
405
|
+
}
|
|
406
|
+
calculateLevel(tokens, maxTokens) {
|
|
407
|
+
if (tokens === 0 || maxTokens === 0)
|
|
408
|
+
return 0;
|
|
409
|
+
const ratio = tokens / maxTokens;
|
|
410
|
+
if (ratio < 0.25)
|
|
411
|
+
return 1;
|
|
412
|
+
if (ratio < 0.5)
|
|
413
|
+
return 2;
|
|
414
|
+
if (ratio < 0.75)
|
|
415
|
+
return 3;
|
|
416
|
+
return 4;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get the date range for one month back from today
|
|
420
|
+
*/
|
|
421
|
+
getOneMonthDateRange() {
|
|
422
|
+
const now = /* @__PURE__ */ new Date();
|
|
423
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
424
|
+
const startDate = new Date(today);
|
|
425
|
+
startDate.setMonth(today.getMonth() - 1);
|
|
426
|
+
const tomorrow = new Date(today);
|
|
427
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
428
|
+
const daysToShow = Math.round((tomorrow.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
429
|
+
return { today, startDate, daysToShow };
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Aggregate daily usage data into periods
|
|
433
|
+
*/
|
|
434
|
+
aggregateIntoPeriods(dailyUsageMap, startDate, daysToShow, numPeriods) {
|
|
435
|
+
const periods = [];
|
|
436
|
+
let currentDate = new Date(startDate);
|
|
437
|
+
const daysPerPeriod = Math.ceil(daysToShow / numPeriods);
|
|
438
|
+
for (let periodNum = 0; periodNum < numPeriods; periodNum++) {
|
|
439
|
+
const periodStart = new Date(currentDate);
|
|
440
|
+
let periodCost = 0;
|
|
441
|
+
let periodTokens = 0;
|
|
442
|
+
let periodCalls = 0;
|
|
443
|
+
let periodActiveDays = 0;
|
|
444
|
+
const daysInThisPeriod = Math.min(daysPerPeriod, daysToShow - periodNum * daysPerPeriod);
|
|
445
|
+
let periodEnd = new Date(periodStart);
|
|
446
|
+
for (let dayInPeriod = 0; dayInPeriod < daysInThisPeriod; dayInPeriod++) {
|
|
447
|
+
const year = currentDate.getFullYear();
|
|
448
|
+
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
|
449
|
+
const day = String(currentDate.getDate()).padStart(2, "0");
|
|
450
|
+
const dateStr = `${year}-${month}-${day}`;
|
|
451
|
+
const data = dailyUsageMap.get(dateStr);
|
|
452
|
+
if (data) {
|
|
453
|
+
periodCost += data.totalCost;
|
|
454
|
+
periodTokens += data.totalTokens;
|
|
455
|
+
periodCalls += data.totalCalls;
|
|
456
|
+
periodActiveDays++;
|
|
457
|
+
}
|
|
458
|
+
periodEnd = new Date(currentDate);
|
|
459
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
460
|
+
}
|
|
461
|
+
const startMonth = periodStart.toLocaleDateString("en-US", { month: "short" });
|
|
462
|
+
const startDay = periodStart.getDate();
|
|
463
|
+
const endMonth = periodEnd.toLocaleDateString("en-US", { month: "short" });
|
|
464
|
+
const endDay = periodEnd.getDate();
|
|
465
|
+
const periodLabel = startMonth === endMonth ? `${startMonth} ${startDay} - ${endDay}` : `${startMonth} ${startDay} - ${endMonth} ${endDay}`;
|
|
466
|
+
periods.push({
|
|
467
|
+
periodLabel,
|
|
468
|
+
startDate: periodStart,
|
|
469
|
+
endDate: periodEnd,
|
|
470
|
+
cost: periodCost,
|
|
471
|
+
tokens: periodTokens,
|
|
472
|
+
calls: periodCalls,
|
|
473
|
+
activeDays: periodActiveDays
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
let totalCost = 0;
|
|
477
|
+
let totalTokens = 0;
|
|
478
|
+
let totalActiveDays = 0;
|
|
479
|
+
let maxPeriodCost = 0;
|
|
480
|
+
periods.forEach((period) => {
|
|
481
|
+
totalCost += period.cost;
|
|
482
|
+
totalTokens += period.tokens;
|
|
483
|
+
totalActiveDays += period.activeDays;
|
|
484
|
+
if (period.cost > maxPeriodCost)
|
|
485
|
+
maxPeriodCost = period.cost;
|
|
486
|
+
});
|
|
487
|
+
return { periods, totalCost, totalTokens, totalActiveDays, maxPeriodCost };
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Format token count with units (K, M, B, T)
|
|
491
|
+
*/
|
|
492
|
+
formatTokens(tokens) {
|
|
493
|
+
if (tokens >= 1e12)
|
|
494
|
+
return `${(tokens / 1e12).toFixed(1)}T`;
|
|
495
|
+
if (tokens >= 1e9)
|
|
496
|
+
return `${(tokens / 1e9).toFixed(1)}B`;
|
|
497
|
+
if (tokens >= 1e6)
|
|
498
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
499
|
+
if (tokens >= 1e3)
|
|
500
|
+
return `${(tokens / 1e3).toFixed(1)}K`;
|
|
501
|
+
return tokens.toString();
|
|
502
|
+
}
|
|
503
|
+
generateGridData(dailyUsageMap) {
|
|
504
|
+
const grid = [];
|
|
505
|
+
const today = /* @__PURE__ */ new Date();
|
|
506
|
+
const startDate = new Date(today);
|
|
507
|
+
startDate.setDate(today.getDate() - this.config.days + 1);
|
|
508
|
+
const dayOfWeek = startDate.getDay();
|
|
509
|
+
startDate.setDate(startDate.getDate() - dayOfWeek);
|
|
510
|
+
let maxTokens = 0;
|
|
511
|
+
let totalTokens = 0;
|
|
512
|
+
let totalCost = 0;
|
|
513
|
+
let activeDays = 0;
|
|
514
|
+
for (const data of dailyUsageMap.values()) {
|
|
515
|
+
if (data.totalTokens > maxTokens)
|
|
516
|
+
maxTokens = data.totalTokens;
|
|
517
|
+
if (data.totalTokens > 0)
|
|
518
|
+
activeDays++;
|
|
519
|
+
totalTokens += data.totalTokens;
|
|
520
|
+
totalCost += data.totalCost;
|
|
521
|
+
}
|
|
522
|
+
let currentDate = new Date(startDate);
|
|
523
|
+
const endDate = new Date(today);
|
|
524
|
+
let week = [];
|
|
525
|
+
while (currentDate <= endDate) {
|
|
526
|
+
const dateStr = currentDate.toISOString().split("T")[0];
|
|
527
|
+
const data = dailyUsageMap.get(dateStr);
|
|
528
|
+
const tokens = data?.totalTokens || 0;
|
|
529
|
+
const cost = data?.totalCost || 0;
|
|
530
|
+
const calls = data?.totalCalls || 0;
|
|
531
|
+
const providers = data ? Object.keys(data.byProvider) : [];
|
|
532
|
+
const level = this.calculateLevel(tokens, maxTokens);
|
|
533
|
+
week.push({
|
|
534
|
+
date: dateStr,
|
|
535
|
+
level,
|
|
536
|
+
tokens,
|
|
537
|
+
cost,
|
|
538
|
+
calls,
|
|
539
|
+
providers,
|
|
540
|
+
dayOfWeek: currentDate.getDay()
|
|
541
|
+
});
|
|
542
|
+
if (currentDate.getDay() === 6) {
|
|
543
|
+
grid.push(week);
|
|
544
|
+
week = [];
|
|
545
|
+
}
|
|
546
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
547
|
+
}
|
|
548
|
+
if (week.length > 0) {
|
|
549
|
+
while (week.length < 7) {
|
|
550
|
+
week.push({
|
|
551
|
+
date: "",
|
|
552
|
+
level: 0,
|
|
553
|
+
tokens: 0,
|
|
554
|
+
cost: 0,
|
|
555
|
+
calls: 0,
|
|
556
|
+
providers: [],
|
|
557
|
+
dayOfWeek: week.length
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
grid.push(week);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
grid,
|
|
564
|
+
stats: {
|
|
565
|
+
maxTokens,
|
|
566
|
+
totalTokens,
|
|
567
|
+
totalCost,
|
|
568
|
+
activeDays
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
computeMonthLabels(grid) {
|
|
573
|
+
const labels = [];
|
|
574
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
575
|
+
let lastMonth = -1;
|
|
576
|
+
grid.forEach((week, weekIndex) => {
|
|
577
|
+
const firstDay = week.find((d) => d.date);
|
|
578
|
+
if (firstDay && firstDay.date) {
|
|
579
|
+
const month = new Date(firstDay.date).getMonth();
|
|
580
|
+
if (month !== lastMonth && weekIndex > 0) {
|
|
581
|
+
labels.push({
|
|
582
|
+
month: months[month],
|
|
583
|
+
weekIndex
|
|
584
|
+
});
|
|
585
|
+
lastMonth = month;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return labels;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Generate monthly view aggregated by weeks
|
|
593
|
+
* Shows 4 weeks of data, each week in one cell
|
|
594
|
+
*/
|
|
595
|
+
generateMonthlyView(dailyUsageMap) {
|
|
596
|
+
const { today, startDate, daysToShow } = this.getOneMonthDateRange();
|
|
597
|
+
const { periods, totalCost, totalTokens, totalActiveDays, maxPeriodCost } = this.aggregateIntoPeriods(dailyUsageMap, startDate, daysToShow, 4);
|
|
598
|
+
const cellSize = 180;
|
|
599
|
+
const cellGap = 20;
|
|
600
|
+
const titleHeight = 35;
|
|
601
|
+
const footerHeight = 45;
|
|
602
|
+
const sidePadding = 30;
|
|
603
|
+
const cols = 4;
|
|
604
|
+
const width = cols * (cellSize + cellGap) - cellGap + sidePadding * 2;
|
|
605
|
+
const height = titleHeight + cellSize + footerHeight;
|
|
606
|
+
let svg = this.generateHeader(width, height);
|
|
607
|
+
const lastUpdated = today.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
608
|
+
svg += ` <!-- Header -->
|
|
609
|
+
`;
|
|
610
|
+
svg += ` <g transform="translate(0, 0)">
|
|
611
|
+
`;
|
|
612
|
+
svg += ` <text x="${width / 2}" y="22" text-anchor="middle" style="font-size: 16px; font-weight: 600; fill: ${this.colors.text};">LLM Activity</text>
|
|
613
|
+
`;
|
|
614
|
+
svg += ` </g>
|
|
615
|
+
|
|
616
|
+
`;
|
|
617
|
+
svg += ` <!-- Week Cells -->
|
|
618
|
+
<g transform="translate(${sidePadding}, ${titleHeight})">
|
|
619
|
+
`;
|
|
620
|
+
for (let i = 0; i < periods.length; i++) {
|
|
621
|
+
const period = periods[i];
|
|
622
|
+
const x = i * (cellSize + cellGap);
|
|
623
|
+
const y = 0;
|
|
624
|
+
const level = this.calculateLevel(period.cost * 1e3, maxPeriodCost * 1e3);
|
|
625
|
+
const costColor = this.getColorForLevel(level);
|
|
626
|
+
const periodStartStr = period.startDate.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
627
|
+
const periodEndStr = period.endDate.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
628
|
+
const tooltipText = `${periodStartStr} - ${periodEndStr}\\n$${period.cost.toFixed(2)} \u2022 ${period.tokens.toLocaleString()} tokens\\n${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
629
|
+
const escapedTooltip = escapeXML(tooltipText);
|
|
630
|
+
svg += ` <g>
|
|
631
|
+
`;
|
|
632
|
+
svg += ` <rect class="cell" x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" fill="${this.colors.empty}" rx="8">
|
|
633
|
+
`;
|
|
634
|
+
svg += ` <title>${escapedTooltip}</title>
|
|
635
|
+
`;
|
|
636
|
+
svg += ` </rect>
|
|
637
|
+
`;
|
|
638
|
+
const escapedDateRange = escapeXML(`${periodStartStr} - ${periodEndStr}`);
|
|
639
|
+
svg += ` <text x="${x + cellSize / 2}" y="${y + 24}" text-anchor="middle" style="font-size: 12px; fill: ${this.colors.text}; opacity: 0.7;">${escapedDateRange}</text>
|
|
640
|
+
`;
|
|
641
|
+
if (period.cost > 0) {
|
|
642
|
+
const costStr = period.cost >= 1 ? `$${period.cost.toFixed(2)}` : `$${period.cost.toFixed(3)}`;
|
|
643
|
+
const escapedCostStr = escapeXML(costStr);
|
|
644
|
+
svg += ` <text x="${x + cellSize / 2}" y="${y + cellSize / 2 + 6}" text-anchor="middle" style="font-size: 32px; font-weight: 700; fill: ${costColor};">${escapedCostStr}</text>
|
|
645
|
+
`;
|
|
646
|
+
const tokenStr = period.tokens >= 1e6 ? `${(period.tokens / 1e6).toFixed(1)}M` : period.tokens >= 1e3 ? `${(period.tokens / 1e3).toFixed(1)}k` : period.tokens.toString();
|
|
647
|
+
const escapedTokenStr = escapeXML(`${tokenStr} tokens`);
|
|
648
|
+
svg += ` <text x="${x + cellSize / 2}" y="${y + cellSize - 42}" text-anchor="middle" style="font-size: 13px; fill: ${this.colors.text}; opacity: 0.7;">${escapedTokenStr}</text>
|
|
649
|
+
`;
|
|
650
|
+
const activeDaysStr = `${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
651
|
+
const escapedActiveDays = escapeXML(activeDaysStr);
|
|
652
|
+
svg += ` <text x="${x + cellSize / 2}" y="${y + cellSize - 22}" text-anchor="middle" style="font-size: 12px; fill: ${this.colors.text}; opacity: 0.6;">${escapedActiveDays}</text>
|
|
653
|
+
`;
|
|
654
|
+
} else {
|
|
655
|
+
svg += ` <text x="${x + cellSize / 2}" y="${y + cellSize / 2 + 5}" text-anchor="middle" style="font-size: 16px; fill: ${this.colors.text}; opacity: 0.5;">No activity</text>
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
svg += ` </g>
|
|
659
|
+
`;
|
|
660
|
+
}
|
|
661
|
+
svg += ` </g>
|
|
662
|
+
|
|
663
|
+
`;
|
|
664
|
+
const footerText = `${totalActiveDays} active ${totalActiveDays === 1 ? "day" : "days"} \u2022 ${this.formatTokens(totalTokens)} tokens \u2022 $${totalCost.toFixed(2)} total \u2022 Updated ${lastUpdated}`;
|
|
665
|
+
const escapedFooter = escapeXML(footerText);
|
|
666
|
+
svg += ` <!-- Stats Footer -->
|
|
667
|
+
`;
|
|
668
|
+
svg += ` <g transform="translate(0, ${height - 20})">
|
|
669
|
+
`;
|
|
670
|
+
svg += ` <text x="${width / 2}" y="10" text-anchor="middle" style="font-size: 11px; fill: ${this.colors.text}; opacity: 0.7;">
|
|
671
|
+
`;
|
|
672
|
+
svg += ` ${escapedFooter}
|
|
673
|
+
`;
|
|
674
|
+
svg += ` </text>
|
|
675
|
+
`;
|
|
676
|
+
svg += ` </g>
|
|
677
|
+
|
|
678
|
+
`;
|
|
679
|
+
svg += "</svg>";
|
|
680
|
+
return svg;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Generate biweekly view - 2 cards, each showing 2 weeks of data
|
|
684
|
+
* Designed for 750x392 aspect ratio (roughly 1.9:1)
|
|
685
|
+
*/
|
|
686
|
+
generateBiweeklyView(dailyUsageMap) {
|
|
687
|
+
const { today, startDate, daysToShow } = this.getOneMonthDateRange();
|
|
688
|
+
const { periods, totalCost, totalTokens, totalActiveDays } = this.aggregateIntoPeriods(dailyUsageMap, startDate, daysToShow, 2);
|
|
689
|
+
const cardWidth = 350;
|
|
690
|
+
const cardHeight = 300;
|
|
691
|
+
const cardGap = 30;
|
|
692
|
+
const titleHeight = 40;
|
|
693
|
+
const footerHeight = 52;
|
|
694
|
+
const sidePadding = 25;
|
|
695
|
+
const cols = 2;
|
|
696
|
+
const width = cols * cardWidth + (cols - 1) * cardGap + sidePadding * 2;
|
|
697
|
+
const height = titleHeight + cardHeight + footerHeight;
|
|
698
|
+
let svg = this.generateHeader(width, height);
|
|
699
|
+
const lastUpdated = today.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
700
|
+
svg += ` <!-- Header -->
|
|
701
|
+
`;
|
|
702
|
+
svg += ` <g transform="translate(0, 0)">
|
|
703
|
+
`;
|
|
704
|
+
svg += ` <text x="${width / 2}" y="25" text-anchor="middle" style="font-size: 18px; font-weight: 600; fill: ${this.colors.text};">LLM Activity</text>
|
|
705
|
+
`;
|
|
706
|
+
svg += ` </g>
|
|
707
|
+
|
|
708
|
+
`;
|
|
709
|
+
svg += ` <!-- Period Cards -->
|
|
710
|
+
<g transform="translate(${sidePadding}, ${titleHeight})">
|
|
711
|
+
`;
|
|
712
|
+
for (let i = 0; i < periods.length; i++) {
|
|
713
|
+
const period = periods[i];
|
|
714
|
+
const x = i * (cardWidth + cardGap);
|
|
715
|
+
const y = 0;
|
|
716
|
+
const costColor = this.getColorForLevel(4);
|
|
717
|
+
const tooltipText = `${period.periodLabel}\\n$${period.cost.toFixed(2)} \u2022 ${period.tokens.toLocaleString()} tokens\\n${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
718
|
+
const escapedTooltip = escapeXML(tooltipText);
|
|
719
|
+
svg += ` <g>
|
|
720
|
+
`;
|
|
721
|
+
svg += ` <rect class="cell" x="${x}" y="${y}" width="${cardWidth}" height="${cardHeight}" fill="${this.colors.empty}" rx="8">
|
|
722
|
+
`;
|
|
723
|
+
svg += ` <title>${escapedTooltip}</title>
|
|
724
|
+
`;
|
|
725
|
+
svg += ` </rect>
|
|
726
|
+
`;
|
|
727
|
+
const escapedDateRange = escapeXML(period.periodLabel);
|
|
728
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + 30}" text-anchor="middle" style="font-size: 16px; fill: ${this.colors.text}; opacity: 0.7;">${escapedDateRange}</text>
|
|
729
|
+
`;
|
|
730
|
+
if (period.cost > 0) {
|
|
731
|
+
const costStr = period.cost >= 1 ? `$${period.cost.toFixed(2)}` : `$${period.cost.toFixed(3)}`;
|
|
732
|
+
const escapedCostStr = escapeXML(costStr);
|
|
733
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight / 2 + 10}" text-anchor="middle" style="font-size: 42px; font-weight: 700; fill: ${costColor};">${escapedCostStr}</text>
|
|
734
|
+
`;
|
|
735
|
+
const escapedTokenStr = escapeXML(`${this.formatTokens(period.tokens)} tokens`);
|
|
736
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight - 60}" text-anchor="middle" style="font-size: 18px; fill: ${this.colors.text}; opacity: 0.7;">${escapedTokenStr}</text>
|
|
737
|
+
`;
|
|
738
|
+
const activeDaysStr = `${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
739
|
+
const escapedActiveDays = escapeXML(activeDaysStr);
|
|
740
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight - 35}" text-anchor="middle" style="font-size: 16px; fill: ${this.colors.text}; opacity: 0.6;">${escapedActiveDays}</text>
|
|
741
|
+
`;
|
|
742
|
+
} else {
|
|
743
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight / 2 + 5}" text-anchor="middle" style="font-size: 18px; fill: ${this.colors.text}; opacity: 0.5;">No activity</text>
|
|
744
|
+
`;
|
|
745
|
+
}
|
|
746
|
+
svg += ` </g>
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
749
|
+
svg += ` </g>
|
|
750
|
+
|
|
751
|
+
`;
|
|
752
|
+
const footerText = `${totalActiveDays} active ${totalActiveDays === 1 ? "day" : "days"} \u2022 ${this.formatTokens(totalTokens)} tokens \u2022 $${totalCost.toFixed(2)} total \u2022 Updated ${lastUpdated}`;
|
|
753
|
+
const escapedFooter = escapeXML(footerText);
|
|
754
|
+
svg += ` <!-- Stats Footer -->
|
|
755
|
+
`;
|
|
756
|
+
svg += ` <g transform="translate(0, ${height - 25})">
|
|
757
|
+
`;
|
|
758
|
+
svg += ` <text x="${width / 2}" y="10" text-anchor="middle" style="font-size: 12px; fill: ${this.colors.text}; opacity: 0.7;">
|
|
759
|
+
`;
|
|
760
|
+
svg += ` ${escapedFooter}
|
|
761
|
+
`;
|
|
762
|
+
svg += ` </text>
|
|
763
|
+
`;
|
|
764
|
+
svg += ` </g>
|
|
765
|
+
|
|
766
|
+
`;
|
|
767
|
+
svg += "</svg>";
|
|
768
|
+
return svg;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Generate compact biweekly view - 2 cards with minimal vertical padding
|
|
772
|
+
* Same as biweekly but with reduced spacing around cost display
|
|
773
|
+
*/
|
|
774
|
+
generateBiweeklyCompactView(dailyUsageMap) {
|
|
775
|
+
const { today, startDate, daysToShow } = this.getOneMonthDateRange();
|
|
776
|
+
const { periods, totalCost, totalTokens, totalActiveDays } = this.aggregateIntoPeriods(dailyUsageMap, startDate, daysToShow, 2);
|
|
777
|
+
const cardWidth = 350;
|
|
778
|
+
const cardHeight = 180;
|
|
779
|
+
const cardGap = 30;
|
|
780
|
+
const titleHeight = 40;
|
|
781
|
+
const footerHeight = 52;
|
|
782
|
+
const sidePadding = 25;
|
|
783
|
+
const outerPadding = 10;
|
|
784
|
+
const cols = 2;
|
|
785
|
+
const width = cols * cardWidth + (cols - 1) * cardGap + sidePadding * 2 + outerPadding * 2;
|
|
786
|
+
const height = titleHeight + cardHeight + footerHeight + outerPadding * 2;
|
|
787
|
+
let svg = this.generateHeader(width, height);
|
|
788
|
+
const lastUpdated = today.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
789
|
+
svg += ` <!-- Header -->
|
|
790
|
+
`;
|
|
791
|
+
svg += ` <g transform="translate(0, 0)">
|
|
792
|
+
`;
|
|
793
|
+
svg += ` <text x="${width / 2 - outerPadding}" y="25" text-anchor="middle" style="font-size: 18px; font-weight: 600; fill: ${this.colors.text};">LLM Activity</text>
|
|
794
|
+
`;
|
|
795
|
+
svg += ` </g>
|
|
796
|
+
|
|
797
|
+
`;
|
|
798
|
+
svg += ` <!-- Period Cards -->
|
|
799
|
+
<g transform="translate(${sidePadding + outerPadding}, ${titleHeight + outerPadding})">
|
|
800
|
+
`;
|
|
801
|
+
for (let i = 0; i < periods.length; i++) {
|
|
802
|
+
const period = periods[i];
|
|
803
|
+
const x = i * (cardWidth + cardGap);
|
|
804
|
+
const y = 0;
|
|
805
|
+
const costColor = this.getColorForLevel(4);
|
|
806
|
+
const tooltipText = `${period.periodLabel}\\n$${period.cost.toFixed(2)} \u2022 ${period.tokens.toLocaleString()} tokens\\n${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
807
|
+
const escapedTooltip = escapeXML(tooltipText);
|
|
808
|
+
svg += ` <g>
|
|
809
|
+
`;
|
|
810
|
+
svg += ` <rect class="cell" x="${x}" y="${y}" width="${cardWidth}" height="${cardHeight}" fill="${this.colors.empty}" rx="8">
|
|
811
|
+
`;
|
|
812
|
+
svg += ` <title>${escapedTooltip}</title>
|
|
813
|
+
`;
|
|
814
|
+
svg += ` </rect>
|
|
815
|
+
`;
|
|
816
|
+
const escapedDateRange = escapeXML(period.periodLabel);
|
|
817
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + 35}" text-anchor="middle" style="font-size: 16px; fill: ${this.colors.text}; opacity: 0.7;">${escapedDateRange}</text>
|
|
818
|
+
`;
|
|
819
|
+
if (period.cost > 0) {
|
|
820
|
+
const costStr = period.cost >= 1 ? `$${period.cost.toFixed(2)}` : `$${period.cost.toFixed(3)}`;
|
|
821
|
+
const escapedCostStr = escapeXML(costStr);
|
|
822
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight / 2 + 5}" text-anchor="middle" style="font-size: 42px; font-weight: 700; fill: ${costColor};">${escapedCostStr}</text>
|
|
823
|
+
`;
|
|
824
|
+
const escapedTokenStr = escapeXML(`${this.formatTokens(period.tokens)} tokens`);
|
|
825
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight - 50}" text-anchor="middle" style="font-size: 18px; fill: ${this.colors.text}; opacity: 0.7;">${escapedTokenStr}</text>
|
|
826
|
+
`;
|
|
827
|
+
const activeDaysStr = `${period.activeDays} active ${period.activeDays === 1 ? "day" : "days"}`;
|
|
828
|
+
const escapedActiveDays = escapeXML(activeDaysStr);
|
|
829
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight - 25}" text-anchor="middle" style="font-size: 16px; fill: ${this.colors.text}; opacity: 0.6;">${escapedActiveDays}</text>
|
|
830
|
+
`;
|
|
831
|
+
} else {
|
|
832
|
+
svg += ` <text x="${x + cardWidth / 2}" y="${y + cardHeight / 2 + 5}" text-anchor="middle" style="font-size: 18px; fill: ${this.colors.text}; opacity: 0.5;">No activity</text>
|
|
833
|
+
`;
|
|
834
|
+
}
|
|
835
|
+
svg += ` </g>
|
|
836
|
+
`;
|
|
837
|
+
}
|
|
838
|
+
svg += ` </g>
|
|
839
|
+
|
|
840
|
+
`;
|
|
841
|
+
const footerText = `${totalActiveDays} active ${totalActiveDays === 1 ? "day" : "days"} \u2022 ${this.formatTokens(totalTokens)} tokens \u2022 $${totalCost.toFixed(2)} total \u2022 Updated ${lastUpdated}`;
|
|
842
|
+
const escapedFooter = escapeXML(footerText);
|
|
843
|
+
svg += ` <!-- Stats Footer -->
|
|
844
|
+
`;
|
|
845
|
+
svg += ` <g transform="translate(${outerPadding}, ${height - 25 - outerPadding})">
|
|
846
|
+
`;
|
|
847
|
+
svg += ` <text x="${width / 2 - outerPadding}" y="10" text-anchor="middle" style="font-size: 12px; fill: ${this.colors.text}; opacity: 0.7;">
|
|
848
|
+
`;
|
|
849
|
+
svg += ` ${escapedFooter}
|
|
850
|
+
`;
|
|
851
|
+
svg += ` </text>
|
|
852
|
+
`;
|
|
853
|
+
svg += ` </g>
|
|
854
|
+
|
|
855
|
+
`;
|
|
856
|
+
svg += "</svg>";
|
|
857
|
+
return svg;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// src/commands/graph.ts
|
|
864
|
+
var graph_exports = {};
|
|
865
|
+
__export(graph_exports, {
|
|
866
|
+
generateLocalGraph: () => generateLocalGraph
|
|
867
|
+
});
|
|
868
|
+
import chalk8 from "chalk";
|
|
869
|
+
import ora5 from "ora";
|
|
870
|
+
import { writeFile } from "fs/promises";
|
|
871
|
+
async function generateLocalGraph(options) {
|
|
872
|
+
const outputFile = options.output || "llm-activity.svg";
|
|
873
|
+
const days = parseInt(options.days || "365", 10);
|
|
874
|
+
console.log(chalk8.bold.cyan(`
|
|
875
|
+
\u{1F3A8} Generating activity graph...
|
|
876
|
+
`));
|
|
877
|
+
const spinner = ora5("Fetching usage data...").start();
|
|
878
|
+
let usageData = [];
|
|
879
|
+
try {
|
|
880
|
+
usageData = await CcusageParser.fetchData({ days });
|
|
881
|
+
spinner.succeed(`Fetched ${usageData.length} days of data`);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
spinner.fail("Failed to fetch data");
|
|
884
|
+
console.log(chalk8.red(`
|
|
885
|
+
\u274C Error: ${error.message}
|
|
886
|
+
`));
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
890
|
+
for (const entry of usageData) {
|
|
891
|
+
if (!aggregated.has(entry.date)) {
|
|
892
|
+
aggregated.set(entry.date, {
|
|
893
|
+
date: entry.date,
|
|
894
|
+
totalTokens: 0,
|
|
895
|
+
totalCost: 0,
|
|
896
|
+
totalCalls: 0,
|
|
897
|
+
byProvider: {}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const day = aggregated.get(entry.date);
|
|
901
|
+
const tokens = entry.tokensInput + entry.tokensOutput;
|
|
902
|
+
day.totalTokens += tokens;
|
|
903
|
+
day.totalCost += entry.cost;
|
|
904
|
+
day.totalCalls += entry.calls;
|
|
905
|
+
if (!day.byProvider[entry.provider]) {
|
|
906
|
+
day.byProvider[entry.provider] = { tokens: 0, cost: 0, calls: 0 };
|
|
907
|
+
}
|
|
908
|
+
day.byProvider[entry.provider].tokens += tokens;
|
|
909
|
+
day.byProvider[entry.provider].cost += entry.cost;
|
|
910
|
+
day.byProvider[entry.provider].calls += entry.calls;
|
|
911
|
+
}
|
|
912
|
+
const svgSpinner = ora5("Generating SVG...").start();
|
|
913
|
+
try {
|
|
914
|
+
const svg = generateSVG(aggregated, { days });
|
|
915
|
+
await writeFile(outputFile, svg);
|
|
916
|
+
svgSpinner.succeed(`SVG generated: ${outputFile}`);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
svgSpinner.fail("Failed to generate SVG");
|
|
919
|
+
console.log(chalk8.red(`
|
|
920
|
+
\u274C Error: ${error.message}
|
|
921
|
+
`));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
let totalTokens = 0;
|
|
925
|
+
let totalCost = 0;
|
|
926
|
+
let activeDays = 0;
|
|
927
|
+
for (const day of aggregated.values()) {
|
|
928
|
+
if (day.totalTokens > 0) activeDays++;
|
|
929
|
+
totalTokens += day.totalTokens;
|
|
930
|
+
totalCost += day.totalCost;
|
|
931
|
+
}
|
|
932
|
+
console.log(chalk8.gray(`
|
|
933
|
+
\u{1F4C8} Statistics:`));
|
|
934
|
+
console.log(chalk8.gray(` Active days: ${activeDays}`));
|
|
935
|
+
console.log(chalk8.gray(` Total tokens: ${totalTokens.toLocaleString()}`));
|
|
936
|
+
console.log(chalk8.gray(` Total cost: $${totalCost.toFixed(2)}`));
|
|
937
|
+
console.log(chalk8.green(`
|
|
938
|
+
\u2705 Done!
|
|
939
|
+
`));
|
|
940
|
+
}
|
|
941
|
+
var init_graph = __esm({
|
|
942
|
+
"src/commands/graph.ts"() {
|
|
943
|
+
"use strict";
|
|
944
|
+
init_esm_shims();
|
|
945
|
+
init_ccusage();
|
|
946
|
+
init_svg_generator();
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// src/cli.ts
|
|
951
|
+
init_esm_shims();
|
|
952
|
+
import { Command } from "commander";
|
|
953
|
+
|
|
954
|
+
// src/commands/login.ts
|
|
955
|
+
init_esm_shims();
|
|
956
|
+
import chalk from "chalk";
|
|
957
|
+
import ora from "ora";
|
|
958
|
+
import open from "open";
|
|
959
|
+
import http from "http";
|
|
960
|
+
import { URL } from "url";
|
|
961
|
+
import { randomBytes } from "crypto";
|
|
962
|
+
|
|
963
|
+
// src/utils/config.ts
|
|
964
|
+
init_esm_shims();
|
|
965
|
+
import Conf from "conf";
|
|
966
|
+
var PRODUCTION_API_URL = "https://llm-activity.timoreichert.com";
|
|
967
|
+
var config = new Conf({
|
|
968
|
+
projectName: "llm-activity",
|
|
969
|
+
defaults: {
|
|
970
|
+
token: "",
|
|
971
|
+
userId: "",
|
|
972
|
+
githubLogin: "",
|
|
973
|
+
apiUrl: "",
|
|
974
|
+
providers: {
|
|
975
|
+
claude: { enabled: false },
|
|
976
|
+
cursor: { enabled: false },
|
|
977
|
+
openai: { enabled: false }
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
function getApiUrl() {
|
|
982
|
+
const raw = process.env.LEADERBOARD_URL || config.get("apiUrl") || PRODUCTION_API_URL;
|
|
983
|
+
return raw.replace(/\/api\/?$/, "");
|
|
984
|
+
}
|
|
985
|
+
function getConfig() {
|
|
986
|
+
return config.store;
|
|
987
|
+
}
|
|
988
|
+
function saveConfig(data) {
|
|
989
|
+
config.set(data);
|
|
990
|
+
}
|
|
991
|
+
function resetConfig() {
|
|
992
|
+
config.clear();
|
|
993
|
+
}
|
|
994
|
+
function getConfigPath() {
|
|
995
|
+
return config.path;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/commands/login.ts
|
|
999
|
+
import fetch from "node-fetch";
|
|
1000
|
+
async function loginCommand() {
|
|
1001
|
+
const config2 = getConfig();
|
|
1002
|
+
if (!config2.token) {
|
|
1003
|
+
const spinner2 = ora("Creating account...").start();
|
|
1004
|
+
try {
|
|
1005
|
+
const token = randomBytes(32).toString("hex");
|
|
1006
|
+
const apiUrl = getApiUrl();
|
|
1007
|
+
const res = await fetch(`${apiUrl}/api/sync`, {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
headers: { "Content-Type": "application/json" },
|
|
1010
|
+
body: JSON.stringify({
|
|
1011
|
+
token,
|
|
1012
|
+
deviceId: randomBytes(32).toString("hex"),
|
|
1013
|
+
data: [],
|
|
1014
|
+
timestamp: Date.now()
|
|
1015
|
+
})
|
|
1016
|
+
});
|
|
1017
|
+
if (!res.ok) {
|
|
1018
|
+
throw new Error("Failed to create account");
|
|
1019
|
+
}
|
|
1020
|
+
saveConfig({ ...config2, token, createdAt: Date.now() });
|
|
1021
|
+
spinner2.succeed("Account created");
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
spinner2.fail("Failed to create account");
|
|
1024
|
+
console.log(chalk.red(error.message));
|
|
1025
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
1026
|
+
console.log(chalk.dim("Make sure the API server is running."));
|
|
1027
|
+
}
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const updatedConfig = getConfig();
|
|
1032
|
+
const port = await findAvailablePort(9876);
|
|
1033
|
+
const { promise: callbackPromise, server } = createCallbackServer(port);
|
|
1034
|
+
const spinner = ora("Starting Slack login...").start();
|
|
1035
|
+
try {
|
|
1036
|
+
const params = new URLSearchParams({
|
|
1037
|
+
redirect_port: String(port),
|
|
1038
|
+
link_token: updatedConfig.token
|
|
1039
|
+
});
|
|
1040
|
+
const apiUrl = getApiUrl();
|
|
1041
|
+
const authUrl = `${apiUrl}/api/auth/slack?${params}`;
|
|
1042
|
+
spinner.succeed("Opening browser");
|
|
1043
|
+
console.log(chalk.dim(`If browser doesn't open, visit: ${authUrl}`));
|
|
1044
|
+
await open(authUrl);
|
|
1045
|
+
const waitSpinner = ora("Waiting for Slack authorization...").start();
|
|
1046
|
+
const result = await Promise.race([
|
|
1047
|
+
callbackPromise,
|
|
1048
|
+
timeout(12e4, "Authentication timed out after 2 minutes")
|
|
1049
|
+
]);
|
|
1050
|
+
waitSpinner.stop();
|
|
1051
|
+
server.close();
|
|
1052
|
+
if ("error" in result) {
|
|
1053
|
+
throw new Error(result.error);
|
|
1054
|
+
}
|
|
1055
|
+
console.log(chalk.green(`
|
|
1056
|
+
Logged in as ${chalk.bold(result.slackName)}`));
|
|
1057
|
+
console.log(chalk.dim('Run "leaderboard sync" to sync your data.'));
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
spinner.stop();
|
|
1060
|
+
server.close();
|
|
1061
|
+
console.log(chalk.red(`
|
|
1062
|
+
Login failed: ${error.message}`));
|
|
1063
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
1064
|
+
console.log(chalk.dim("Make sure the API server is running."));
|
|
1065
|
+
} else if (error.message.includes("not configured")) {
|
|
1066
|
+
console.log(chalk.dim("Slack OAuth is not configured on the server."));
|
|
1067
|
+
}
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function findAvailablePort(startPort) {
|
|
1072
|
+
return new Promise((resolve) => {
|
|
1073
|
+
const server = http.createServer();
|
|
1074
|
+
server.listen(startPort, () => {
|
|
1075
|
+
server.close(() => resolve(startPort));
|
|
1076
|
+
});
|
|
1077
|
+
server.on("error", () => {
|
|
1078
|
+
resolve(findAvailablePort(startPort + 1));
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
function createCallbackServer(port) {
|
|
1083
|
+
let resolveCallback;
|
|
1084
|
+
const promise = new Promise((resolve) => {
|
|
1085
|
+
resolveCallback = resolve;
|
|
1086
|
+
});
|
|
1087
|
+
const server = http.createServer((req, res) => {
|
|
1088
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
1089
|
+
if (url.pathname === "/callback") {
|
|
1090
|
+
const slackName = url.searchParams.get("slack_name");
|
|
1091
|
+
const error = url.searchParams.get("error");
|
|
1092
|
+
if (error) {
|
|
1093
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1094
|
+
res.end(`
|
|
1095
|
+
<html>
|
|
1096
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1097
|
+
<h1>Login Failed</h1>
|
|
1098
|
+
<p>Error: ${error}</p>
|
|
1099
|
+
<p>You can close this window.</p>
|
|
1100
|
+
</body>
|
|
1101
|
+
</html>
|
|
1102
|
+
`);
|
|
1103
|
+
resolveCallback({ error });
|
|
1104
|
+
} else if (slackName) {
|
|
1105
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1106
|
+
res.end(`
|
|
1107
|
+
<html>
|
|
1108
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1109
|
+
<h1>Login Successful!</h1>
|
|
1110
|
+
<p>Logged in as <strong>${slackName}</strong></p>
|
|
1111
|
+
<p>You can close this window and return to the terminal.</p>
|
|
1112
|
+
</body>
|
|
1113
|
+
</html>
|
|
1114
|
+
`);
|
|
1115
|
+
resolveCallback({ slackName });
|
|
1116
|
+
} else {
|
|
1117
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1118
|
+
res.end(`
|
|
1119
|
+
<html>
|
|
1120
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1121
|
+
<h1>Invalid Callback</h1>
|
|
1122
|
+
<p>Something went wrong. Please try again.</p>
|
|
1123
|
+
</body>
|
|
1124
|
+
</html>
|
|
1125
|
+
`);
|
|
1126
|
+
resolveCallback({ error: "Invalid callback" });
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
res.writeHead(404);
|
|
1130
|
+
res.end("Not found");
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
server.listen(port);
|
|
1134
|
+
return { promise, server };
|
|
1135
|
+
}
|
|
1136
|
+
function timeout(ms, message) {
|
|
1137
|
+
return new Promise((_, reject) => {
|
|
1138
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/commands/login-github.ts
|
|
1143
|
+
init_esm_shims();
|
|
1144
|
+
import chalk2 from "chalk";
|
|
1145
|
+
import ora2 from "ora";
|
|
1146
|
+
import open2 from "open";
|
|
1147
|
+
import http2 from "http";
|
|
1148
|
+
import { URL as URL2 } from "url";
|
|
1149
|
+
import fetch2 from "node-fetch";
|
|
1150
|
+
async function loginCommand2(_options) {
|
|
1151
|
+
const config2 = getConfig();
|
|
1152
|
+
if (config2.token && config2.githubLogin) {
|
|
1153
|
+
console.log(chalk2.yellow(`Already logged in as ${chalk2.bold(config2.githubLogin)}.`));
|
|
1154
|
+
console.log(chalk2.dim('Run "leaderboard config --reset" to logout first.'));
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const existingToken = config2.token || null;
|
|
1158
|
+
if (existingToken) {
|
|
1159
|
+
console.log(chalk2.dim("Existing account found. Linking to GitHub..."));
|
|
1160
|
+
}
|
|
1161
|
+
const port = await findAvailablePort2(9876);
|
|
1162
|
+
const { promise: callbackPromise, server } = createCallbackServer2(port);
|
|
1163
|
+
const spinner = ora2("Getting authorization URL...").start();
|
|
1164
|
+
try {
|
|
1165
|
+
const params = new URLSearchParams({ redirect_port: String(port) });
|
|
1166
|
+
if (existingToken) {
|
|
1167
|
+
params.set("link_token", existingToken);
|
|
1168
|
+
}
|
|
1169
|
+
const response = await fetch2(`${getApiUrl()}/api/auth/github?${params}`);
|
|
1170
|
+
if (!response.ok) {
|
|
1171
|
+
const error = await response.json();
|
|
1172
|
+
throw new Error(error.error || "Failed to get authorization URL");
|
|
1173
|
+
}
|
|
1174
|
+
const { url } = await response.json();
|
|
1175
|
+
spinner.succeed("Opening browser");
|
|
1176
|
+
console.log(chalk2.dim(`If browser doesn't open, visit: ${url}`));
|
|
1177
|
+
await open2(url);
|
|
1178
|
+
const timeoutSpinner = ora2("Waiting for authentication...").start();
|
|
1179
|
+
const result = await Promise.race([
|
|
1180
|
+
callbackPromise,
|
|
1181
|
+
timeout2(12e4, "Authentication timed out after 2 minutes")
|
|
1182
|
+
]);
|
|
1183
|
+
timeoutSpinner.stop();
|
|
1184
|
+
server.close();
|
|
1185
|
+
if ("error" in result) {
|
|
1186
|
+
throw new Error(result.error);
|
|
1187
|
+
}
|
|
1188
|
+
saveConfig({
|
|
1189
|
+
...config2,
|
|
1190
|
+
token: result.token,
|
|
1191
|
+
githubLogin: result.username
|
|
1192
|
+
});
|
|
1193
|
+
if (existingToken) {
|
|
1194
|
+
console.log(chalk2.green(`Linked to GitHub as ${chalk2.bold(result.username)}`));
|
|
1195
|
+
} else {
|
|
1196
|
+
console.log(chalk2.green(`Logged in as ${chalk2.bold(result.username)}`));
|
|
1197
|
+
}
|
|
1198
|
+
console.log(chalk2.dim('Run "leaderboard sync" to sync your data.'));
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
spinner.stop();
|
|
1201
|
+
server.close();
|
|
1202
|
+
console.log(chalk2.red(`Login failed: ${error.message}`));
|
|
1203
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
1204
|
+
console.log(chalk2.dim("Make sure the API server is running."));
|
|
1205
|
+
}
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function findAvailablePort2(startPort) {
|
|
1210
|
+
return new Promise((resolve) => {
|
|
1211
|
+
const server = http2.createServer();
|
|
1212
|
+
server.listen(startPort, () => {
|
|
1213
|
+
server.close(() => resolve(startPort));
|
|
1214
|
+
});
|
|
1215
|
+
server.on("error", () => {
|
|
1216
|
+
resolve(findAvailablePort2(startPort + 1));
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
function createCallbackServer2(port) {
|
|
1221
|
+
let resolveCallback;
|
|
1222
|
+
const promise = new Promise((resolve) => {
|
|
1223
|
+
resolveCallback = resolve;
|
|
1224
|
+
});
|
|
1225
|
+
const server = http2.createServer((req, res) => {
|
|
1226
|
+
const url = new URL2(req.url || "/", `http://localhost:${port}`);
|
|
1227
|
+
if (url.pathname === "/callback") {
|
|
1228
|
+
const token = url.searchParams.get("token");
|
|
1229
|
+
const username = url.searchParams.get("username");
|
|
1230
|
+
const error = url.searchParams.get("error");
|
|
1231
|
+
if (error) {
|
|
1232
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1233
|
+
res.end(`
|
|
1234
|
+
<html>
|
|
1235
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1236
|
+
<h1>Authentication Failed</h1>
|
|
1237
|
+
<p>Error: ${error}</p>
|
|
1238
|
+
<p>You can close this window.</p>
|
|
1239
|
+
</body>
|
|
1240
|
+
</html>
|
|
1241
|
+
`);
|
|
1242
|
+
resolveCallback({ error });
|
|
1243
|
+
} else if (token && username) {
|
|
1244
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1245
|
+
res.end(`
|
|
1246
|
+
<html>
|
|
1247
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1248
|
+
<h1>Authentication Successful!</h1>
|
|
1249
|
+
<p>Logged in as <strong>${username}</strong></p>
|
|
1250
|
+
<p>You can close this window and return to the terminal.</p>
|
|
1251
|
+
</body>
|
|
1252
|
+
</html>
|
|
1253
|
+
`);
|
|
1254
|
+
resolveCallback({ token, username });
|
|
1255
|
+
} else {
|
|
1256
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1257
|
+
res.end(`
|
|
1258
|
+
<html>
|
|
1259
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
1260
|
+
<h1>Invalid Callback</h1>
|
|
1261
|
+
<p>Missing token or username.</p>
|
|
1262
|
+
</body>
|
|
1263
|
+
</html>
|
|
1264
|
+
`);
|
|
1265
|
+
resolveCallback({ error: "Invalid callback - missing token or username" });
|
|
1266
|
+
}
|
|
1267
|
+
} else {
|
|
1268
|
+
res.writeHead(404);
|
|
1269
|
+
res.end("Not found");
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
server.listen(port);
|
|
1273
|
+
return { promise, server };
|
|
1274
|
+
}
|
|
1275
|
+
function timeout2(ms, message) {
|
|
1276
|
+
return new Promise((_, reject) => {
|
|
1277
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/commands/init.ts
|
|
1282
|
+
init_esm_shims();
|
|
1283
|
+
import chalk4 from "chalk";
|
|
1284
|
+
import prompts from "prompts";
|
|
1285
|
+
import ora3 from "ora";
|
|
1286
|
+
import { execSync as execSync3 } from "child_process";
|
|
1287
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1288
|
+
import { homedir } from "os";
|
|
1289
|
+
import { join } from "path";
|
|
1290
|
+
|
|
1291
|
+
// src/utils/cron.ts
|
|
1292
|
+
init_esm_shims();
|
|
1293
|
+
import { spawnSync } from "child_process";
|
|
1294
|
+
import { platform } from "os";
|
|
1295
|
+
import { mkdirSync } from "fs";
|
|
1296
|
+
import chalk3 from "chalk";
|
|
1297
|
+
var CRON_SCHEDULES = {
|
|
1298
|
+
"every-30-min": "*/30 * * * *",
|
|
1299
|
+
// Every 30 minutes
|
|
1300
|
+
hourly: "0 * * * *",
|
|
1301
|
+
// Every hour
|
|
1302
|
+
"four-times-daily": "0 0,6,12,18 * * *",
|
|
1303
|
+
// Every 6 hours
|
|
1304
|
+
daily: "0 0 * * *"
|
|
1305
|
+
// Midnight every day
|
|
1306
|
+
};
|
|
1307
|
+
async function setupCronJob(frequency) {
|
|
1308
|
+
const os = platform();
|
|
1309
|
+
const cronSchedule = CRON_SCHEDULES[frequency];
|
|
1310
|
+
if (os === "darwin") {
|
|
1311
|
+
await setupLaunchd(frequency);
|
|
1312
|
+
} else if (os === "linux") {
|
|
1313
|
+
await setupCrontab(cronSchedule);
|
|
1314
|
+
} else if (os === "win32") {
|
|
1315
|
+
await setupWindowsTask(frequency);
|
|
1316
|
+
} else {
|
|
1317
|
+
throw new Error(`Unsupported platform: ${os}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function setupLaunchd(frequency) {
|
|
1321
|
+
const intervals = {
|
|
1322
|
+
"every-30-min": 1800,
|
|
1323
|
+
// 30 minutes
|
|
1324
|
+
hourly: 3600,
|
|
1325
|
+
// 1 hour
|
|
1326
|
+
"four-times-daily": 21600,
|
|
1327
|
+
// 6 hours
|
|
1328
|
+
daily: 86400
|
|
1329
|
+
// 24 hours
|
|
1330
|
+
};
|
|
1331
|
+
const interval = intervals[frequency] || 1800;
|
|
1332
|
+
const label = "com.llm-activity.sync";
|
|
1333
|
+
const plistPath = `${process.env.HOME}/Library/LaunchAgents/${label}.plist`;
|
|
1334
|
+
let binaryPath;
|
|
1335
|
+
try {
|
|
1336
|
+
const result = spawnSync("which", ["llm-activity"], { encoding: "utf8" });
|
|
1337
|
+
binaryPath = result.stdout.trim();
|
|
1338
|
+
if (!binaryPath || result.status !== 0) {
|
|
1339
|
+
throw new Error("llm-activity not found in PATH");
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
throw new Error("llm-activity command not found. Please build and install the CLI first:\n npm run build\n npm install -g .");
|
|
1343
|
+
}
|
|
1344
|
+
const homeDir = process.env.HOME;
|
|
1345
|
+
if (!homeDir) {
|
|
1346
|
+
throw new Error("HOME environment variable not set");
|
|
1347
|
+
}
|
|
1348
|
+
const logDir = `${homeDir}/.llm-activity`;
|
|
1349
|
+
const launchAgentsDir = `${homeDir}/Library/LaunchAgents`;
|
|
1350
|
+
const escapedBinaryPath = binaryPath.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1351
|
+
const escapedLogDir = logDir.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1352
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1353
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1354
|
+
<plist version="1.0">
|
|
1355
|
+
<dict>
|
|
1356
|
+
<key>Label</key>
|
|
1357
|
+
<string>${label}</string>
|
|
1358
|
+
<key>ProgramArguments</key>
|
|
1359
|
+
<array>
|
|
1360
|
+
<string>${escapedBinaryPath}</string>
|
|
1361
|
+
<string>sync</string>
|
|
1362
|
+
</array>
|
|
1363
|
+
<key>StartInterval</key>
|
|
1364
|
+
<integer>${interval}</integer>
|
|
1365
|
+
<key>RunAtLoad</key>
|
|
1366
|
+
<false/>
|
|
1367
|
+
<key>StandardErrorPath</key>
|
|
1368
|
+
<string>${escapedLogDir}/sync-error.log</string>
|
|
1369
|
+
<key>StandardOutPath</key>
|
|
1370
|
+
<string>${escapedLogDir}/sync-out.log</string>
|
|
1371
|
+
</dict>
|
|
1372
|
+
</plist>`;
|
|
1373
|
+
try {
|
|
1374
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
throw new Error(`Failed to create LaunchAgents directory: ${error.message}`);
|
|
1377
|
+
}
|
|
1378
|
+
const fs = await import("fs/promises");
|
|
1379
|
+
await fs.writeFile(plistPath, plist);
|
|
1380
|
+
try {
|
|
1381
|
+
spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
}
|
|
1384
|
+
console.log(chalk3.gray(`
|
|
1385
|
+
Configured launchd at: ${plistPath}`));
|
|
1386
|
+
}
|
|
1387
|
+
async function setupCrontab(schedule) {
|
|
1388
|
+
let currentCrontab = "";
|
|
1389
|
+
try {
|
|
1390
|
+
const result2 = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
1391
|
+
if (result2.status === 0) {
|
|
1392
|
+
currentCrontab = result2.stdout;
|
|
1393
|
+
}
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
if (currentCrontab.includes("llm-activity sync")) {
|
|
1397
|
+
currentCrontab = currentCrontab.split("\n").filter((line) => !line.includes("llm-activity sync")).join("\n");
|
|
1398
|
+
}
|
|
1399
|
+
let binaryPath;
|
|
1400
|
+
try {
|
|
1401
|
+
const result2 = spawnSync("which", ["llm-activity"], { encoding: "utf8" });
|
|
1402
|
+
binaryPath = result2.stdout.trim();
|
|
1403
|
+
if (!binaryPath || result2.status !== 0) {
|
|
1404
|
+
throw new Error("llm-activity not found in PATH");
|
|
1405
|
+
}
|
|
1406
|
+
} catch {
|
|
1407
|
+
throw new Error("llm-activity command not found. Please build and install the CLI first:\n npm run build\n npm install -g .");
|
|
1408
|
+
}
|
|
1409
|
+
const homeDir = process.env.HOME;
|
|
1410
|
+
if (!homeDir) {
|
|
1411
|
+
throw new Error("HOME environment variable not set");
|
|
1412
|
+
}
|
|
1413
|
+
const logFile = `${homeDir}/.llm-activity/sync.log`;
|
|
1414
|
+
const cronEntry = `${schedule} ${binaryPath} sync >> ${logFile} 2>&1`;
|
|
1415
|
+
const newCrontab = currentCrontab ? `${currentCrontab}
|
|
1416
|
+
${cronEntry}
|
|
1417
|
+
` : `${cronEntry}
|
|
1418
|
+
`;
|
|
1419
|
+
const fs = await import("fs/promises");
|
|
1420
|
+
const tmpFile = `/tmp/llm-activity-crontab-${Date.now()}`;
|
|
1421
|
+
await fs.writeFile(tmpFile, newCrontab);
|
|
1422
|
+
const result = spawnSync("crontab", [tmpFile], { encoding: "utf8" });
|
|
1423
|
+
if (result.status !== 0) {
|
|
1424
|
+
throw new Error(`Failed to install crontab: ${result.stderr}`);
|
|
1425
|
+
}
|
|
1426
|
+
await fs.unlink(tmpFile);
|
|
1427
|
+
console.log(chalk3.gray(`
|
|
1428
|
+
Configured crontab: ${schedule}`));
|
|
1429
|
+
}
|
|
1430
|
+
async function setupWindowsTask(frequency) {
|
|
1431
|
+
const taskName = "LLM-Activity-Sync";
|
|
1432
|
+
let binaryPath;
|
|
1433
|
+
try {
|
|
1434
|
+
const result = spawnSync("where", ["llm-activity"], { encoding: "utf8", shell: false });
|
|
1435
|
+
binaryPath = result.stdout.trim().split("\n")[0];
|
|
1436
|
+
if (!binaryPath || result.status !== 0) {
|
|
1437
|
+
throw new Error("llm-activity not found in PATH");
|
|
1438
|
+
}
|
|
1439
|
+
} catch {
|
|
1440
|
+
throw new Error("llm-activity command not found. Please build and install the CLI first:\n npm run build\n npm install -g .");
|
|
1441
|
+
}
|
|
1442
|
+
const tasksToDelete = [taskName, `${taskName}-2`, `${taskName}-6`, `${taskName}-12`, `${taskName}-18`];
|
|
1443
|
+
for (const task of tasksToDelete) {
|
|
1444
|
+
try {
|
|
1445
|
+
spawnSync("schtasks", ["/Delete", "/TN", task, "/F"], { stdio: "ignore" });
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const tasksToCreate = [];
|
|
1450
|
+
if (frequency === "daily") {
|
|
1451
|
+
tasksToCreate.push({ name: taskName, time: "00:00" });
|
|
1452
|
+
} else if (frequency === "twice-daily") {
|
|
1453
|
+
tasksToCreate.push({ name: taskName, time: "00:00" });
|
|
1454
|
+
tasksToCreate.push({ name: `${taskName}-2`, time: "12:00" });
|
|
1455
|
+
} else if (frequency === "four-times-daily") {
|
|
1456
|
+
tasksToCreate.push({ name: taskName, time: "00:00" });
|
|
1457
|
+
tasksToCreate.push({ name: `${taskName}-6`, time: "06:00" });
|
|
1458
|
+
tasksToCreate.push({ name: `${taskName}-12`, time: "12:00" });
|
|
1459
|
+
tasksToCreate.push({ name: `${taskName}-18`, time: "18:00" });
|
|
1460
|
+
}
|
|
1461
|
+
for (const task of tasksToCreate) {
|
|
1462
|
+
const args = [
|
|
1463
|
+
"/Create",
|
|
1464
|
+
"/TN",
|
|
1465
|
+
task.name,
|
|
1466
|
+
"/TR",
|
|
1467
|
+
`"${binaryPath}" sync`,
|
|
1468
|
+
"/SC",
|
|
1469
|
+
"DAILY",
|
|
1470
|
+
"/ST",
|
|
1471
|
+
task.time,
|
|
1472
|
+
"/F"
|
|
1473
|
+
// Force create (overwrite if exists)
|
|
1474
|
+
];
|
|
1475
|
+
const result = spawnSync("schtasks", args, { encoding: "utf8", shell: false });
|
|
1476
|
+
if (result.status !== 0) {
|
|
1477
|
+
throw new Error(`Failed to create task ${task.name}: ${result.stderr}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
console.log(chalk3.gray(`
|
|
1481
|
+
Configured Windows Task(s): ${tasksToCreate.map((t) => t.name).join(", ")}`));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/commands/init.ts
|
|
1485
|
+
init_ccusage();
|
|
1486
|
+
function configureClaudeCodeRetention() {
|
|
1487
|
+
try {
|
|
1488
|
+
const claudeDir = join(homedir(), ".claude");
|
|
1489
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
1490
|
+
if (!existsSync(claudeDir)) {
|
|
1491
|
+
mkdirSync2(claudeDir, { recursive: true });
|
|
1492
|
+
}
|
|
1493
|
+
let settings = {};
|
|
1494
|
+
if (existsSync(settingsPath)) {
|
|
1495
|
+
try {
|
|
1496
|
+
const content = readFileSync(settingsPath, "utf8");
|
|
1497
|
+
settings = JSON.parse(content);
|
|
1498
|
+
} catch {
|
|
1499
|
+
settings = {};
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
settings.cleanupPeriodDays = 99999;
|
|
1503
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
1504
|
+
return true;
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
console.log(chalk4.yellow(`\u26A0\uFE0F Could not configure Claude Code settings: ${error.message}`));
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async function initCommand(options) {
|
|
1511
|
+
console.log(chalk4.bold.cyan("\n\u{1F680} LLM Activity - Provider Setup\n"));
|
|
1512
|
+
let existingConfig = getConfig();
|
|
1513
|
+
if (!existingConfig.token) {
|
|
1514
|
+
console.log(chalk4.yellow("Not logged in yet. Starting login...\n"));
|
|
1515
|
+
await loginCommand();
|
|
1516
|
+
existingConfig = getConfig();
|
|
1517
|
+
if (!existingConfig.token) {
|
|
1518
|
+
console.log(chalk4.red("\u274C Login required to continue."));
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
console.log("");
|
|
1522
|
+
}
|
|
1523
|
+
if (existingConfig.providers?.claude?.enabled || existingConfig.providers?.cursor?.enabled || existingConfig.providers?.openai?.enabled) {
|
|
1524
|
+
console.log(chalk4.yellow("\u26A0\uFE0F Providers already configured!"));
|
|
1525
|
+
const { proceed } = await prompts({
|
|
1526
|
+
type: "confirm",
|
|
1527
|
+
name: "proceed",
|
|
1528
|
+
message: "Do you want to reconfigure?",
|
|
1529
|
+
initial: false
|
|
1530
|
+
});
|
|
1531
|
+
if (!proceed) {
|
|
1532
|
+
console.log(chalk4.gray("Setup cancelled."));
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const spinner = ora3("Checking for ccusage...").start();
|
|
1537
|
+
const hasccusage = isCcusageAvailable();
|
|
1538
|
+
if (!hasccusage) {
|
|
1539
|
+
spinner.fail("ccusage not found");
|
|
1540
|
+
console.log(chalk4.yellow("\nccusage is required for Claude usage tracking."));
|
|
1541
|
+
console.log(chalk4.gray("You can install it with: ./dev.sh install-ccusage\n"));
|
|
1542
|
+
const { installNow } = await prompts({
|
|
1543
|
+
type: "confirm",
|
|
1544
|
+
name: "installNow",
|
|
1545
|
+
message: "Would you like to install ccusage now?",
|
|
1546
|
+
initial: true
|
|
1547
|
+
});
|
|
1548
|
+
if (installNow) {
|
|
1549
|
+
console.log(chalk4.cyan("\nInstalling ccusage...\n"));
|
|
1550
|
+
try {
|
|
1551
|
+
execSync3("npm install -g ccusage", { stdio: "inherit" });
|
|
1552
|
+
if (isCcusageAvailable()) {
|
|
1553
|
+
console.log(chalk4.green("\n\u2705 ccusage installed successfully!\n"));
|
|
1554
|
+
console.log(chalk4.gray("Continuing with setup...\n"));
|
|
1555
|
+
} else {
|
|
1556
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Installation completed but ccusage not found in PATH"));
|
|
1557
|
+
console.log(chalk4.gray("You may need to restart your terminal or check your PATH.\n"));
|
|
1558
|
+
console.log(chalk4.gray("Run init again after restarting: leaderboard init\n"));
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.log(chalk4.red("\n\u274C Installation failed"));
|
|
1563
|
+
console.log(chalk4.gray(`Error: ${error.message}
|
|
1564
|
+
`));
|
|
1565
|
+
console.log(chalk4.yellow("You can try installing manually:"));
|
|
1566
|
+
console.log(chalk4.white(" npm install -g ccusage\n"));
|
|
1567
|
+
console.log(chalk4.gray("Then run init again: leaderboard init\n"));
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
} else {
|
|
1571
|
+
console.log(chalk4.yellow("\nYou can still set up without Claude tracking."));
|
|
1572
|
+
console.log(chalk4.gray("You can install ccusage later and re-run: ./dev.sh cli init\n"));
|
|
1573
|
+
const { continueWithout } = await prompts({
|
|
1574
|
+
type: "confirm",
|
|
1575
|
+
name: "continueWithout",
|
|
1576
|
+
message: "Continue setup without Claude tracking?",
|
|
1577
|
+
initial: false
|
|
1578
|
+
});
|
|
1579
|
+
if (!continueWithout) {
|
|
1580
|
+
console.log(chalk4.gray("\nSetup cancelled.\n"));
|
|
1581
|
+
process.exit(0);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
} else {
|
|
1585
|
+
spinner.succeed("ccusage found");
|
|
1586
|
+
}
|
|
1587
|
+
console.log(chalk4.bold("\n\u{1F4CA} Provider Configuration\n"));
|
|
1588
|
+
const { enableClaude } = await prompts({
|
|
1589
|
+
type: "confirm",
|
|
1590
|
+
name: "enableClaude",
|
|
1591
|
+
message: "Enable Claude usage tracking? (via ccusage)",
|
|
1592
|
+
initial: hasccusage
|
|
1593
|
+
});
|
|
1594
|
+
if (enableClaude) {
|
|
1595
|
+
const settingsSpinner = ora3("Configuring Claude Code settings...").start();
|
|
1596
|
+
if (configureClaudeCodeRetention()) {
|
|
1597
|
+
settingsSpinner.succeed("Claude Code configured to retain usage data");
|
|
1598
|
+
console.log(chalk4.gray(" Set cleanupPeriodDays to 99999 in ~/.claude/settings.json"));
|
|
1599
|
+
} else {
|
|
1600
|
+
settingsSpinner.warn("Could not configure Claude Code settings automatically");
|
|
1601
|
+
console.log(chalk4.gray(" You can manually set cleanupPeriodDays to 99999 in ~/.claude/settings.json"));
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const { enableCursor } = await prompts({
|
|
1605
|
+
type: "confirm",
|
|
1606
|
+
name: "enableCursor",
|
|
1607
|
+
message: "Enable Cursor usage tracking?",
|
|
1608
|
+
initial: false
|
|
1609
|
+
});
|
|
1610
|
+
const { enableOpenAI } = await prompts({
|
|
1611
|
+
type: "confirm",
|
|
1612
|
+
name: "enableOpenAI",
|
|
1613
|
+
message: "Enable OpenAI usage tracking?",
|
|
1614
|
+
initial: false
|
|
1615
|
+
});
|
|
1616
|
+
let cursorApiKey, openaiApiKey;
|
|
1617
|
+
if (enableCursor) {
|
|
1618
|
+
const { apiKey } = await prompts({
|
|
1619
|
+
type: "password",
|
|
1620
|
+
name: "apiKey",
|
|
1621
|
+
message: "Cursor API key (optional, leave empty to skip):"
|
|
1622
|
+
});
|
|
1623
|
+
cursorApiKey = apiKey;
|
|
1624
|
+
}
|
|
1625
|
+
if (enableOpenAI) {
|
|
1626
|
+
const { apiKey } = await prompts({
|
|
1627
|
+
type: "password",
|
|
1628
|
+
name: "apiKey",
|
|
1629
|
+
message: "OpenAI API key (optional, leave empty to skip):"
|
|
1630
|
+
});
|
|
1631
|
+
openaiApiKey = apiKey;
|
|
1632
|
+
}
|
|
1633
|
+
saveConfig({
|
|
1634
|
+
...existingConfig,
|
|
1635
|
+
providers: {
|
|
1636
|
+
claude: { enabled: enableClaude },
|
|
1637
|
+
cursor: { enabled: enableCursor, apiKey: cursorApiKey },
|
|
1638
|
+
openai: { enabled: enableOpenAI, apiKey: openaiApiKey }
|
|
1639
|
+
},
|
|
1640
|
+
createdAt: existingConfig.createdAt || Date.now()
|
|
1641
|
+
});
|
|
1642
|
+
console.log(chalk4.green("\n\u2705 Configuration saved"));
|
|
1643
|
+
if (options.cron !== false) {
|
|
1644
|
+
console.log(chalk4.bold("\n\u23F0 Automatic Sync Setup\n"));
|
|
1645
|
+
const { setupCron } = await prompts({
|
|
1646
|
+
type: "confirm",
|
|
1647
|
+
name: "setupCron",
|
|
1648
|
+
message: "Set up automatic sync? (recommended)",
|
|
1649
|
+
initial: true
|
|
1650
|
+
});
|
|
1651
|
+
if (setupCron) {
|
|
1652
|
+
const { frequency } = await prompts({
|
|
1653
|
+
type: "select",
|
|
1654
|
+
name: "frequency",
|
|
1655
|
+
message: "How often should we sync?",
|
|
1656
|
+
choices: [
|
|
1657
|
+
{ title: "Every 30 minutes (recommended)", value: "every-30-min" },
|
|
1658
|
+
{ title: "Every hour", value: "hourly" },
|
|
1659
|
+
{ title: "Every 6 hours", value: "four-times-daily" },
|
|
1660
|
+
{ title: "Daily at midnight", value: "daily" }
|
|
1661
|
+
],
|
|
1662
|
+
initial: 0
|
|
1663
|
+
});
|
|
1664
|
+
try {
|
|
1665
|
+
await setupCronJob(frequency);
|
|
1666
|
+
console.log(chalk4.green("\u2705 Cron job configured"));
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
console.log(chalk4.yellow(`
|
|
1669
|
+
\u26A0\uFE0F Could not set up cron job`));
|
|
1670
|
+
if (error.message.includes("not found")) {
|
|
1671
|
+
console.log(chalk4.gray("\nThe CLI needs to be built and installed globally for cron jobs to work."));
|
|
1672
|
+
console.log(chalk4.gray("Run these commands to install:\n"));
|
|
1673
|
+
console.log(chalk4.white(" cd cli"));
|
|
1674
|
+
console.log(chalk4.white(" npm run build"));
|
|
1675
|
+
console.log(chalk4.white(" npm install -g .\n"));
|
|
1676
|
+
console.log(chalk4.gray("Then run init again to set up cron: leaderboard init\n"));
|
|
1677
|
+
} else {
|
|
1678
|
+
console.log(chalk4.gray(`
|
|
1679
|
+
Error: ${error.message}
|
|
1680
|
+
`));
|
|
1681
|
+
}
|
|
1682
|
+
console.log(chalk4.gray("For now, you can manually run sync with:"));
|
|
1683
|
+
console.log(chalk4.cyan(" ./dev.sh cli sync\n"));
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const token = existingConfig.token;
|
|
1688
|
+
const graphUrl = `${getApiUrl()}/graph.svg?token=${token}`;
|
|
1689
|
+
console.log(chalk4.bold.green("\n\u{1F389} Setup complete!\n"));
|
|
1690
|
+
console.log(chalk4.bold("Your activity graph URL:\n"));
|
|
1691
|
+
console.log(chalk4.cyan(` ${graphUrl}
|
|
1692
|
+
`));
|
|
1693
|
+
console.log(chalk4.bold("Add to your GitHub README:\n"));
|
|
1694
|
+
console.log(chalk4.gray(" ```markdown"));
|
|
1695
|
+
console.log(chalk4.white(` `));
|
|
1696
|
+
console.log(chalk4.gray(" ```\n"));
|
|
1697
|
+
console.log(chalk4.bold("Next steps:\n"));
|
|
1698
|
+
console.log(chalk4.gray(" 1. Run your first sync:"), chalk4.cyan("leaderboard sync"));
|
|
1699
|
+
console.log(chalk4.gray(" 2. Check status:"), chalk4.cyan("leaderboard status"));
|
|
1700
|
+
console.log(chalk4.gray(" 3. View graph:"), chalk4.cyan(`open ${graphUrl}
|
|
1701
|
+
`));
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/commands/sync.ts
|
|
1705
|
+
init_esm_shims();
|
|
1706
|
+
import chalk5 from "chalk";
|
|
1707
|
+
import ora4 from "ora";
|
|
1708
|
+
import { createHash } from "crypto";
|
|
1709
|
+
import { hostname } from "os";
|
|
1710
|
+
init_ccusage();
|
|
1711
|
+
import fetch3 from "node-fetch";
|
|
1712
|
+
function getDeviceId() {
|
|
1713
|
+
const machineId = hostname();
|
|
1714
|
+
return createHash("sha256").update(machineId).digest("hex");
|
|
1715
|
+
}
|
|
1716
|
+
async function syncCommand(options) {
|
|
1717
|
+
const config2 = getConfig();
|
|
1718
|
+
if (!config2.token) {
|
|
1719
|
+
console.log(chalk5.red("\u274C Not initialized. Run: leaderboard init"));
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
if (!options.force && config2.lastSync) {
|
|
1723
|
+
const hoursSinceSync = (Date.now() - config2.lastSync) / (1e3 * 60 * 60);
|
|
1724
|
+
if (hoursSinceSync < 1) {
|
|
1725
|
+
console.log(chalk5.yellow(`\u23ED\uFE0F Synced ${Math.round(hoursSinceSync * 60)} minutes ago. Use --force to sync anyway.`));
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const days = parseInt(options.days || "365", 10);
|
|
1730
|
+
console.log(chalk5.bold.cyan(`
|
|
1731
|
+
\u{1F4CA} Syncing ${days} days of activity...
|
|
1732
|
+
`));
|
|
1733
|
+
const allUsageData = [];
|
|
1734
|
+
if (config2.providers?.claude?.enabled) {
|
|
1735
|
+
const spinner = ora4("Fetching Claude usage data...").start();
|
|
1736
|
+
try {
|
|
1737
|
+
const claudeData = await CcusageParser.fetchData({ days });
|
|
1738
|
+
allUsageData.push(...claudeData);
|
|
1739
|
+
spinner.succeed(`Fetched ${claudeData.length} days from Claude`);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
spinner.fail(`Failed to fetch Claude data: ${error.message}`);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (config2.providers?.cursor?.enabled) {
|
|
1745
|
+
console.log(chalk5.gray("\u23ED\uFE0F Cursor sync not yet implemented"));
|
|
1746
|
+
}
|
|
1747
|
+
if (config2.providers?.openai?.enabled) {
|
|
1748
|
+
console.log(chalk5.gray("\u23ED\uFE0F OpenAI sync not yet implemented"));
|
|
1749
|
+
}
|
|
1750
|
+
if (allUsageData.length === 0) {
|
|
1751
|
+
console.log(chalk5.yellow("\n\u26A0\uFE0F No usage data found"));
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const stats = calculateStats(allUsageData);
|
|
1755
|
+
console.log(chalk5.gray(`
|
|
1756
|
+
\u{1F4C8} Statistics:`));
|
|
1757
|
+
console.log(chalk5.gray(` Active days: ${stats.activeDays}`));
|
|
1758
|
+
console.log(chalk5.gray(` Total tokens: ${stats.totalTokens.toLocaleString()}`));
|
|
1759
|
+
console.log(chalk5.gray(` Total cost: $${stats.totalCost.toFixed(2)}`));
|
|
1760
|
+
const uploadSpinner = ora4("Uploading to cloud...").start();
|
|
1761
|
+
try {
|
|
1762
|
+
const deviceId = getDeviceId();
|
|
1763
|
+
const response = await fetch3(`${getApiUrl()}/api/sync`, {
|
|
1764
|
+
method: "POST",
|
|
1765
|
+
headers: {
|
|
1766
|
+
"Content-Type": "application/json"
|
|
1767
|
+
},
|
|
1768
|
+
body: JSON.stringify({
|
|
1769
|
+
token: config2.token,
|
|
1770
|
+
deviceId,
|
|
1771
|
+
data: allUsageData,
|
|
1772
|
+
timestamp: Date.now()
|
|
1773
|
+
})
|
|
1774
|
+
});
|
|
1775
|
+
if (!response.ok) {
|
|
1776
|
+
const errorText = await response.text();
|
|
1777
|
+
throw new Error(`API error: ${response.status} - ${errorText}`);
|
|
1778
|
+
}
|
|
1779
|
+
const result = await response.json();
|
|
1780
|
+
uploadSpinner.succeed("Sync complete");
|
|
1781
|
+
config2.lastSync = Date.now();
|
|
1782
|
+
saveConfig(config2);
|
|
1783
|
+
console.log(chalk5.green("\n\u2705 Successfully synced!"));
|
|
1784
|
+
if (result.graph_urls && Object.keys(result.graph_urls).length > 0) {
|
|
1785
|
+
console.log(chalk5.bold.cyan("\n\u{1F3A8} Your Activity Graphs:\n"));
|
|
1786
|
+
const themes = ["dark", "light"];
|
|
1787
|
+
const views = ["biweekly", "biweekly-compact", "monthly", "yearly"];
|
|
1788
|
+
for (const theme of themes) {
|
|
1789
|
+
console.log(chalk5.bold(` ${theme === "dark" ? "\u{1F319}" : "\u2600\uFE0F "} ${theme}:`));
|
|
1790
|
+
for (const view of views) {
|
|
1791
|
+
const key = `${theme}-${view}`;
|
|
1792
|
+
if (result.graph_urls[key]) {
|
|
1793
|
+
console.log(chalk5.cyan(` ${view.padEnd(17)}: ${result.graph_urls[key]}`));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
console.log("");
|
|
1797
|
+
}
|
|
1798
|
+
} else {
|
|
1799
|
+
console.log(chalk5.gray(`
|
|
1800
|
+
\u{1F4CA} View your graph:`));
|
|
1801
|
+
console.log(chalk5.cyan(` ${getApiUrl()}/graph.svg?token=${config2.token}
|
|
1802
|
+
`));
|
|
1803
|
+
}
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
uploadSpinner.fail("Upload failed");
|
|
1806
|
+
console.log(chalk5.red(`
|
|
1807
|
+
\u274C Error: ${error.message}`));
|
|
1808
|
+
if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
|
|
1809
|
+
console.log(chalk5.yellow("\n\u26A0\uFE0F Could not connect to API server."));
|
|
1810
|
+
console.log(chalk5.gray(" Make sure the API is running or check your internet connection."));
|
|
1811
|
+
}
|
|
1812
|
+
process.exit(1);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function calculateStats(data) {
|
|
1816
|
+
const uniqueDates = new Set(data.map((d) => d.date));
|
|
1817
|
+
const totalTokens = data.reduce((sum, d) => sum + d.tokensInput + d.tokensOutput, 0);
|
|
1818
|
+
const totalCost = data.reduce((sum, d) => sum + d.cost, 0);
|
|
1819
|
+
return {
|
|
1820
|
+
activeDays: uniqueDates.size,
|
|
1821
|
+
totalTokens,
|
|
1822
|
+
totalCost
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/commands/status.ts
|
|
1827
|
+
init_esm_shims();
|
|
1828
|
+
import chalk6 from "chalk";
|
|
1829
|
+
async function statusCommand(options) {
|
|
1830
|
+
const config2 = getConfig();
|
|
1831
|
+
if (!config2.token) {
|
|
1832
|
+
console.log(chalk6.red("\n\u274C Not initialized. Run: leaderboard init\n"));
|
|
1833
|
+
process.exit(1);
|
|
1834
|
+
}
|
|
1835
|
+
console.log(chalk6.bold.cyan("\n\u{1F4CA} LLM Activity Status\n"));
|
|
1836
|
+
if (config2.lastSync) {
|
|
1837
|
+
const lastSyncDate = new Date(config2.lastSync);
|
|
1838
|
+
const hoursSince = (Date.now() - config2.lastSync) / (1e3 * 60 * 60);
|
|
1839
|
+
const timeAgo = hoursSince < 1 ? `${Math.round(hoursSince * 60)} minutes ago` : hoursSince < 24 ? `${Math.round(hoursSince)} hours ago` : `${Math.round(hoursSince / 24)} days ago`;
|
|
1840
|
+
console.log(chalk6.green("\u2705 Synced"), chalk6.gray(timeAgo));
|
|
1841
|
+
console.log(chalk6.gray(` ${lastSyncDate.toLocaleString()}`));
|
|
1842
|
+
} else {
|
|
1843
|
+
console.log(chalk6.yellow("\u26A0\uFE0F Never synced"));
|
|
1844
|
+
console.log(chalk6.gray(" Run: leaderboard sync"));
|
|
1845
|
+
}
|
|
1846
|
+
console.log(chalk6.bold("\n\u{1F4E6} Enabled Providers:\n"));
|
|
1847
|
+
const providers = config2.providers || {};
|
|
1848
|
+
if (providers.claude?.enabled) {
|
|
1849
|
+
console.log(chalk6.green(" \u2713 Claude"), chalk6.gray("(via ccusage)"));
|
|
1850
|
+
} else {
|
|
1851
|
+
console.log(chalk6.gray(" \u2717 Claude"));
|
|
1852
|
+
}
|
|
1853
|
+
if (providers.cursor?.enabled) {
|
|
1854
|
+
console.log(chalk6.green(" \u2713 Cursor"));
|
|
1855
|
+
} else {
|
|
1856
|
+
console.log(chalk6.gray(" \u2717 Cursor"));
|
|
1857
|
+
}
|
|
1858
|
+
if (providers.openai?.enabled) {
|
|
1859
|
+
console.log(chalk6.green(" \u2713 OpenAI"));
|
|
1860
|
+
} else {
|
|
1861
|
+
console.log(chalk6.gray(" \u2717 OpenAI"));
|
|
1862
|
+
}
|
|
1863
|
+
console.log(chalk6.bold("\n\u{1F517} Activity Graphs:\n"));
|
|
1864
|
+
console.log(chalk6.gray(' Run "leaderboard sync" to generate your graphs'));
|
|
1865
|
+
console.log(chalk6.gray(" URLs will be displayed after syncing\n"));
|
|
1866
|
+
if (options.verbose) {
|
|
1867
|
+
console.log(chalk6.bold("\u{1F527} Configuration:\n"));
|
|
1868
|
+
console.log(chalk6.gray(` User ID: ${config2.userId}`));
|
|
1869
|
+
console.log(chalk6.gray(` Token: ${config2.token?.substring(0, 16)}...`));
|
|
1870
|
+
console.log(chalk6.gray(` Created: ${config2.createdAt ? new Date(config2.createdAt).toLocaleString() : "unknown"}
|
|
1871
|
+
`));
|
|
1872
|
+
}
|
|
1873
|
+
if (config2.lastSync) {
|
|
1874
|
+
const nextSync = new Date(config2.lastSync + 24 * 60 * 60 * 1e3);
|
|
1875
|
+
if (nextSync > /* @__PURE__ */ new Date()) {
|
|
1876
|
+
console.log(chalk6.gray(`\u23F0 Next automatic sync: ${nextSync.toLocaleString()}
|
|
1877
|
+
`));
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/commands/config.ts
|
|
1883
|
+
init_esm_shims();
|
|
1884
|
+
import chalk7 from "chalk";
|
|
1885
|
+
import prompts2 from "prompts";
|
|
1886
|
+
async function configCommand(options) {
|
|
1887
|
+
if (options.show) {
|
|
1888
|
+
showConfig();
|
|
1889
|
+
} else if (options.reset) {
|
|
1890
|
+
await resetConfigCommand();
|
|
1891
|
+
} else if (options.set) {
|
|
1892
|
+
await setConfigValue(options.set);
|
|
1893
|
+
} else {
|
|
1894
|
+
await interactiveConfig();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
function maskSecret(value) {
|
|
1898
|
+
if (!value) return "";
|
|
1899
|
+
if (value.length <= 8) return "****";
|
|
1900
|
+
return value.slice(0, 4) + "..." + value.slice(-4);
|
|
1901
|
+
}
|
|
1902
|
+
function showConfig() {
|
|
1903
|
+
const config2 = getConfig();
|
|
1904
|
+
const safeConfig = {
|
|
1905
|
+
...config2,
|
|
1906
|
+
token: config2.token ? maskSecret(config2.token) : "",
|
|
1907
|
+
providers: config2.providers ? {
|
|
1908
|
+
claude: config2.providers.claude,
|
|
1909
|
+
cursor: {
|
|
1910
|
+
...config2.providers.cursor,
|
|
1911
|
+
apiKey: config2.providers.cursor?.apiKey ? maskSecret(config2.providers.cursor.apiKey) : void 0
|
|
1912
|
+
},
|
|
1913
|
+
openai: {
|
|
1914
|
+
...config2.providers.openai,
|
|
1915
|
+
apiKey: config2.providers.openai?.apiKey ? maskSecret(config2.providers.openai.apiKey) : void 0
|
|
1916
|
+
}
|
|
1917
|
+
} : void 0
|
|
1918
|
+
};
|
|
1919
|
+
console.log(chalk7.bold("Configuration"));
|
|
1920
|
+
console.log(chalk7.dim(`File: ${getConfigPath()}
|
|
1921
|
+
`));
|
|
1922
|
+
console.log(JSON.stringify(safeConfig, null, 2));
|
|
1923
|
+
console.log();
|
|
1924
|
+
}
|
|
1925
|
+
async function resetConfigCommand() {
|
|
1926
|
+
const { confirm } = await prompts2({
|
|
1927
|
+
type: "confirm",
|
|
1928
|
+
name: "confirm",
|
|
1929
|
+
message: "Are you sure you want to reset all configuration?",
|
|
1930
|
+
initial: false
|
|
1931
|
+
});
|
|
1932
|
+
if (confirm) {
|
|
1933
|
+
resetConfig();
|
|
1934
|
+
console.log(chalk7.green("\n\u2705 Configuration reset\n"));
|
|
1935
|
+
} else {
|
|
1936
|
+
console.log(chalk7.gray("\nCancelled\n"));
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function setConfigValue(keyValue) {
|
|
1940
|
+
const [key, ...valueParts] = keyValue.split("=");
|
|
1941
|
+
const value = valueParts.join("=");
|
|
1942
|
+
if (!key || value === void 0) {
|
|
1943
|
+
console.log(chalk7.red("\n\u274C Invalid format. Use: --set key=value\n"));
|
|
1944
|
+
process.exit(1);
|
|
1945
|
+
}
|
|
1946
|
+
const config2 = getConfig();
|
|
1947
|
+
const keys = key.split(".");
|
|
1948
|
+
let current = config2;
|
|
1949
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1950
|
+
if (!current[keys[i]]) {
|
|
1951
|
+
current[keys[i]] = {};
|
|
1952
|
+
}
|
|
1953
|
+
current = current[keys[i]];
|
|
1954
|
+
}
|
|
1955
|
+
let parsedValue = value;
|
|
1956
|
+
try {
|
|
1957
|
+
parsedValue = JSON.parse(value);
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
current[keys[keys.length - 1]] = parsedValue;
|
|
1961
|
+
saveConfig(config2);
|
|
1962
|
+
console.log(chalk7.green(`
|
|
1963
|
+
\u2705 Set ${key} = ${value}
|
|
1964
|
+
`));
|
|
1965
|
+
}
|
|
1966
|
+
async function interactiveConfig() {
|
|
1967
|
+
console.log(chalk7.bold.cyan("\n\u2699\uFE0F Interactive Configuration\n"));
|
|
1968
|
+
const config2 = getConfig();
|
|
1969
|
+
const { action } = await prompts2({
|
|
1970
|
+
type: "select",
|
|
1971
|
+
name: "action",
|
|
1972
|
+
message: "What would you like to configure?",
|
|
1973
|
+
choices: [
|
|
1974
|
+
{ title: "View current configuration", value: "show" },
|
|
1975
|
+
{ title: "Enable/disable providers", value: "providers" },
|
|
1976
|
+
{ title: "Change API keys", value: "apikeys" },
|
|
1977
|
+
{ title: "Reset configuration", value: "reset" },
|
|
1978
|
+
{ title: "Cancel", value: "cancel" }
|
|
1979
|
+
]
|
|
1980
|
+
});
|
|
1981
|
+
if (action === "show") {
|
|
1982
|
+
showConfig();
|
|
1983
|
+
} else if (action === "providers") {
|
|
1984
|
+
await configureProviders(config2);
|
|
1985
|
+
} else if (action === "apikeys") {
|
|
1986
|
+
await configureApiKeys(config2);
|
|
1987
|
+
} else if (action === "reset") {
|
|
1988
|
+
await resetConfigCommand();
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
async function configureProviders(config2) {
|
|
1992
|
+
const { enableClaude, enableCursor, enableOpenAI } = await prompts2([
|
|
1993
|
+
{
|
|
1994
|
+
type: "confirm",
|
|
1995
|
+
name: "enableClaude",
|
|
1996
|
+
message: "Enable Claude?",
|
|
1997
|
+
initial: config2.providers?.claude?.enabled || false
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
type: "confirm",
|
|
2001
|
+
name: "enableCursor",
|
|
2002
|
+
message: "Enable Cursor?",
|
|
2003
|
+
initial: config2.providers?.cursor?.enabled || false
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
type: "confirm",
|
|
2007
|
+
name: "enableOpenAI",
|
|
2008
|
+
message: "Enable OpenAI?",
|
|
2009
|
+
initial: config2.providers?.openai?.enabled || false
|
|
2010
|
+
}
|
|
2011
|
+
]);
|
|
2012
|
+
config2.providers = {
|
|
2013
|
+
...config2.providers,
|
|
2014
|
+
claude: { ...config2.providers?.claude, enabled: enableClaude },
|
|
2015
|
+
cursor: { ...config2.providers?.cursor, enabled: enableCursor },
|
|
2016
|
+
openai: { ...config2.providers?.openai, enabled: enableOpenAI }
|
|
2017
|
+
};
|
|
2018
|
+
saveConfig(config2);
|
|
2019
|
+
console.log(chalk7.green("\n\u2705 Providers updated\n"));
|
|
2020
|
+
}
|
|
2021
|
+
async function configureApiKeys(config2) {
|
|
2022
|
+
const { provider } = await prompts2({
|
|
2023
|
+
type: "select",
|
|
2024
|
+
name: "provider",
|
|
2025
|
+
message: "Which provider?",
|
|
2026
|
+
choices: [
|
|
2027
|
+
{ title: "Cursor", value: "cursor" },
|
|
2028
|
+
{ title: "OpenAI", value: "openai" }
|
|
2029
|
+
]
|
|
2030
|
+
});
|
|
2031
|
+
const { apiKey } = await prompts2({
|
|
2032
|
+
type: "password",
|
|
2033
|
+
name: "apiKey",
|
|
2034
|
+
message: `Enter ${provider} API key:`
|
|
2035
|
+
});
|
|
2036
|
+
if (!config2.providers) config2.providers = {};
|
|
2037
|
+
if (!config2.providers[provider]) config2.providers[provider] = {};
|
|
2038
|
+
config2.providers[provider].apiKey = apiKey;
|
|
2039
|
+
saveConfig(config2);
|
|
2040
|
+
console.log(chalk7.green(`
|
|
2041
|
+
\u2705 ${provider} API key updated
|
|
2042
|
+
`));
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/cli.ts
|
|
2046
|
+
var program = new Command();
|
|
2047
|
+
program.name("agent-leaderboard").description("Track and visualize your LLM usage activity in GitHub-style contribution graphs").version("0.1.0");
|
|
2048
|
+
program.command("login [provider]").description("Log in with Slack (default) or GitHub").action((provider) => {
|
|
2049
|
+
if (provider === "github") {
|
|
2050
|
+
return loginCommand2({});
|
|
2051
|
+
}
|
|
2052
|
+
return loginCommand();
|
|
2053
|
+
});
|
|
2054
|
+
program.command("init").description("Initialize LLM Activity tracking (configure providers)").option("--no-cron", "Skip cron job setup").action(initCommand);
|
|
2055
|
+
program.command("sync").description("Sync your LLM usage data to the cloud").option("-d, --days <number>", "Number of days to sync", "365").option("-f, --force", "Force sync even if recently synced").action(syncCommand);
|
|
2056
|
+
program.command("status").description("Show sync status and statistics").option("-v, --verbose", "Show detailed statistics").action(statusCommand);
|
|
2057
|
+
program.command("config").description("Manage configuration").option("-s, --show", "Show current configuration").option("-r, --reset", "Reset configuration").option("--set <key=value>", "Set configuration value").action(configCommand);
|
|
2058
|
+
program.command("graph").description("Generate SVG graph locally (for testing)").option("-o, --output <file>", "Output file", "llm-activity.svg").option("-d, --days <number>", "Number of days", "365").action(async (options) => {
|
|
2059
|
+
const { generateLocalGraph: generateLocalGraph2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
|
|
2060
|
+
await generateLocalGraph2(options);
|
|
2061
|
+
});
|
|
2062
|
+
if (process.argv.length === 2) {
|
|
2063
|
+
program.help();
|
|
2064
|
+
}
|
|
2065
|
+
program.parse();
|