tokenleak 1.2.0 → 1.3.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/README.md +94 -5
- package/package.json +1 -1
- package/tokenleak +2060 -1198
package/tokenleak
CHANGED
|
@@ -415,7 +415,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
// packages/cli/src/cli.ts
|
|
418
|
-
import { writeFileSync } from "fs";
|
|
418
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
419
419
|
|
|
420
420
|
// packages/core/dist/constants.js
|
|
421
421
|
var DEFAULT_DAYS = 90;
|
|
@@ -3170,21 +3170,306 @@ class CodexProvider {
|
|
|
3170
3170
|
};
|
|
3171
3171
|
}
|
|
3172
3172
|
}
|
|
3173
|
-
// packages/registry/dist/providers/
|
|
3174
|
-
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync } from "fs";
|
|
3173
|
+
// packages/registry/dist/providers/cursor.js
|
|
3174
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync, statSync as statSync3 } from "fs";
|
|
3175
3175
|
import { join as join3 } from "path";
|
|
3176
3176
|
import { homedir as homedir3 } from "os";
|
|
3177
|
+
var PROVIDER_NAME = "cursor";
|
|
3178
|
+
var DISPLAY_NAME = "Cursor";
|
|
3179
|
+
var CURSOR_COLORS = {
|
|
3180
|
+
primary: "#22c55e",
|
|
3181
|
+
secondary: "#86efac",
|
|
3182
|
+
gradient: ["#22c55e", "#86efac"]
|
|
3183
|
+
};
|
|
3184
|
+
var DASHED_DATE_SUFFIX2 = /-(\d{4})-(\d{2})-(\d{2})$/;
|
|
3185
|
+
function resolveCacheDir(baseDir) {
|
|
3186
|
+
return baseDir ?? join3(process.env["TOKENLEAK_CURSOR_DIR"] ?? join3(homedir3(), ".config", "tokenleak"), "cursor-cache");
|
|
3187
|
+
}
|
|
3188
|
+
function isCursorUsageFile(name) {
|
|
3189
|
+
if (name === "usage.csv") {
|
|
3190
|
+
return true;
|
|
3191
|
+
}
|
|
3192
|
+
if (!name.startsWith("usage.") || !name.endsWith(".csv")) {
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
const stem = name.slice("usage.".length, -".csv".length);
|
|
3196
|
+
return stem.length > 0;
|
|
3197
|
+
}
|
|
3198
|
+
function collectUsageFiles(dir) {
|
|
3199
|
+
if (!existsSync3(dir)) {
|
|
3200
|
+
return [];
|
|
3201
|
+
}
|
|
3202
|
+
const files = [];
|
|
3203
|
+
for (const entry of readdirSync3(dir)) {
|
|
3204
|
+
if (entry === "archive") {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
const fullPath = join3(dir, entry);
|
|
3208
|
+
const stats = statSync3(fullPath);
|
|
3209
|
+
if (stats.isFile() && isCursorUsageFile(entry)) {
|
|
3210
|
+
files.push(fullPath);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
3214
|
+
}
|
|
3215
|
+
function parseCsvLine(line) {
|
|
3216
|
+
const fields = [];
|
|
3217
|
+
let current = "";
|
|
3218
|
+
let inQuotes = false;
|
|
3219
|
+
for (let index = 0;index < line.length; index += 1) {
|
|
3220
|
+
const char = line[index];
|
|
3221
|
+
if (char === '"') {
|
|
3222
|
+
if (inQuotes && line[index + 1] === '"') {
|
|
3223
|
+
current += '"';
|
|
3224
|
+
index += 1;
|
|
3225
|
+
} else {
|
|
3226
|
+
inQuotes = !inQuotes;
|
|
3227
|
+
}
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
if (char === "," && !inQuotes) {
|
|
3231
|
+
fields.push(current);
|
|
3232
|
+
current = "";
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
current += char;
|
|
3236
|
+
}
|
|
3237
|
+
fields.push(current);
|
|
3238
|
+
return fields;
|
|
3239
|
+
}
|
|
3240
|
+
function extractDate2(timestamp) {
|
|
3241
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
3242
|
+
return match ? match[1] : null;
|
|
3243
|
+
}
|
|
3244
|
+
function toIsoTimestamp(value) {
|
|
3245
|
+
const trimmed = value.trim();
|
|
3246
|
+
if (!trimmed) {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
const dateOnlyMatch = /^(\d{4}-\d{2}-\d{2})$/.exec(trimmed);
|
|
3250
|
+
if (dateOnlyMatch) {
|
|
3251
|
+
return `${dateOnlyMatch[1]}T12:00:00.000Z`;
|
|
3252
|
+
}
|
|
3253
|
+
const millis = Date.parse(trimmed);
|
|
3254
|
+
if (!Number.isFinite(millis)) {
|
|
3255
|
+
return null;
|
|
3256
|
+
}
|
|
3257
|
+
return new Date(millis).toISOString();
|
|
3258
|
+
}
|
|
3259
|
+
function parseCost(value) {
|
|
3260
|
+
const cleaned = value.replaceAll("$", "").replaceAll(",", "").trim();
|
|
3261
|
+
if (!cleaned || cleaned.toLowerCase() === "nan") {
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
const parsed = Number(cleaned);
|
|
3265
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
3266
|
+
}
|
|
3267
|
+
function compactModelDateSuffix2(model) {
|
|
3268
|
+
return model.replace(DASHED_DATE_SUFFIX2, "-$1$2$3");
|
|
3269
|
+
}
|
|
3270
|
+
function toCachePricing3(pricing) {
|
|
3271
|
+
if (!pricing) {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
return {
|
|
3275
|
+
input: pricing.input,
|
|
3276
|
+
cacheRead: pricing.cacheRead,
|
|
3277
|
+
cacheWrite: pricing.cacheWrite
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
function parseUsageFile(filePath) {
|
|
3281
|
+
let raw;
|
|
3282
|
+
try {
|
|
3283
|
+
raw = readFileSync(filePath, "utf8");
|
|
3284
|
+
} catch {
|
|
3285
|
+
return [];
|
|
3286
|
+
}
|
|
3287
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3288
|
+
if (lines.length <= 1) {
|
|
3289
|
+
return [];
|
|
3290
|
+
}
|
|
3291
|
+
const header = parseCsvLine(lines[0]);
|
|
3292
|
+
const hasKindColumn = header.includes("Kind");
|
|
3293
|
+
const modelIndex = hasKindColumn ? 2 : 1;
|
|
3294
|
+
const inputWithCacheWriteIndex = hasKindColumn ? 4 : 2;
|
|
3295
|
+
const inputWithoutCacheWriteIndex = hasKindColumn ? 5 : 3;
|
|
3296
|
+
const cacheReadIndex = hasKindColumn ? 6 : 4;
|
|
3297
|
+
const outputIndex = hasKindColumn ? 7 : 5;
|
|
3298
|
+
const costIndex = hasKindColumn ? 9 : 7;
|
|
3299
|
+
const accountId = filePath.endsWith("/usage.csv") || filePath.endsWith("\\usage.csv") ? "active" : filePath.split(/[/\\]/).pop()?.replace(/^usage\./, "").replace(/\.csv$/, "") || "unknown";
|
|
3300
|
+
const records = [];
|
|
3301
|
+
for (const line of lines.slice(1)) {
|
|
3302
|
+
const fields = parseCsvLine(line);
|
|
3303
|
+
if (fields.length <= costIndex) {
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
const timestamp = toIsoTimestamp(fields[0] ?? "");
|
|
3307
|
+
if (!timestamp) {
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3310
|
+
const date = extractDate2(timestamp);
|
|
3311
|
+
if (!date) {
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
const rawModel = (fields[modelIndex] ?? "").trim();
|
|
3315
|
+
if (!rawModel) {
|
|
3316
|
+
continue;
|
|
3317
|
+
}
|
|
3318
|
+
const inputWithCacheWrite = Number((fields[inputWithCacheWriteIndex] ?? "").trim());
|
|
3319
|
+
const inputWithoutCacheWrite = Number((fields[inputWithoutCacheWriteIndex] ?? "").trim());
|
|
3320
|
+
const cacheReadTokens = Number((fields[cacheReadIndex] ?? "").trim());
|
|
3321
|
+
const outputTokens = Number((fields[outputIndex] ?? "").trim());
|
|
3322
|
+
if (!Number.isFinite(inputWithCacheWrite) || !Number.isFinite(inputWithoutCacheWrite) || !Number.isFinite(cacheReadTokens) || !Number.isFinite(outputTokens)) {
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
const inputTokens = Math.max(0, inputWithoutCacheWrite);
|
|
3326
|
+
const cacheWriteTokens = Math.max(0, inputWithCacheWrite - inputWithoutCacheWrite);
|
|
3327
|
+
const totalTokens = inputTokens + outputTokens + Math.max(0, cacheReadTokens) + cacheWriteTokens;
|
|
3328
|
+
if (totalTokens === 0) {
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
const model = compactModelDateSuffix2(rawModel);
|
|
3332
|
+
records.push({
|
|
3333
|
+
date,
|
|
3334
|
+
timestamp,
|
|
3335
|
+
model,
|
|
3336
|
+
normalizedModel: normalizeModelName2(model),
|
|
3337
|
+
inputTokens,
|
|
3338
|
+
outputTokens: Math.max(0, outputTokens),
|
|
3339
|
+
cacheReadTokens: Math.max(0, cacheReadTokens),
|
|
3340
|
+
cacheWriteTokens,
|
|
3341
|
+
explicitCost: parseCost(fields[costIndex] ?? ""),
|
|
3342
|
+
sessionId: `cursor-${accountId}-${timestamp}`
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
return records;
|
|
3346
|
+
}
|
|
3347
|
+
function getRecordCost(record) {
|
|
3348
|
+
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
3349
|
+
return record.explicitCost;
|
|
3350
|
+
}
|
|
3351
|
+
return estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).totalCost;
|
|
3352
|
+
}
|
|
3353
|
+
function toUsageEvent(record) {
|
|
3354
|
+
const pricing = estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).pricing;
|
|
3355
|
+
return {
|
|
3356
|
+
provider: PROVIDER_NAME,
|
|
3357
|
+
timestamp: record.timestamp,
|
|
3358
|
+
date: record.date,
|
|
3359
|
+
model: record.normalizedModel,
|
|
3360
|
+
inputTokens: record.inputTokens,
|
|
3361
|
+
outputTokens: record.outputTokens,
|
|
3362
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
3363
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
3364
|
+
totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
|
|
3365
|
+
cost: getRecordCost(record),
|
|
3366
|
+
pricing: toCachePricing3(pricing),
|
|
3367
|
+
sessionId: record.sessionId
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
function buildProviderData(records) {
|
|
3371
|
+
const byDate = new Map;
|
|
3372
|
+
const events = records.map(toUsageEvent);
|
|
3373
|
+
for (const event of events) {
|
|
3374
|
+
let dateMap = byDate.get(event.date);
|
|
3375
|
+
if (!dateMap) {
|
|
3376
|
+
dateMap = new Map;
|
|
3377
|
+
byDate.set(event.date, dateMap);
|
|
3378
|
+
}
|
|
3379
|
+
const existing = dateMap.get(event.model);
|
|
3380
|
+
if (existing) {
|
|
3381
|
+
existing.inputTokens += event.inputTokens;
|
|
3382
|
+
existing.outputTokens += event.outputTokens;
|
|
3383
|
+
existing.cacheReadTokens += event.cacheReadTokens;
|
|
3384
|
+
existing.cacheWriteTokens += event.cacheWriteTokens;
|
|
3385
|
+
existing.totalTokens += event.totalTokens;
|
|
3386
|
+
existing.cost += event.cost;
|
|
3387
|
+
if (!existing.pricing && event.pricing) {
|
|
3388
|
+
existing.pricing = event.pricing;
|
|
3389
|
+
}
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
dateMap.set(event.model, {
|
|
3393
|
+
model: event.model,
|
|
3394
|
+
inputTokens: event.inputTokens,
|
|
3395
|
+
outputTokens: event.outputTokens,
|
|
3396
|
+
cacheReadTokens: event.cacheReadTokens,
|
|
3397
|
+
cacheWriteTokens: event.cacheWriteTokens,
|
|
3398
|
+
totalTokens: event.totalTokens,
|
|
3399
|
+
cost: event.cost,
|
|
3400
|
+
pricing: event.pricing
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
let totalTokens = 0;
|
|
3404
|
+
let totalCost = 0;
|
|
3405
|
+
const daily = [...byDate.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([date, modelMap]) => {
|
|
3406
|
+
const models = [...modelMap.values()].sort((left, right) => left.model.localeCompare(right.model));
|
|
3407
|
+
const inputTokens = models.reduce((sum, model) => sum + model.inputTokens, 0);
|
|
3408
|
+
const outputTokens = models.reduce((sum, model) => sum + model.outputTokens, 0);
|
|
3409
|
+
const cacheReadTokens = models.reduce((sum, model) => sum + model.cacheReadTokens, 0);
|
|
3410
|
+
const cacheWriteTokens = models.reduce((sum, model) => sum + model.cacheWriteTokens, 0);
|
|
3411
|
+
const dayTotal = models.reduce((sum, model) => sum + model.totalTokens, 0);
|
|
3412
|
+
const dayCost = models.reduce((sum, model) => sum + model.cost, 0);
|
|
3413
|
+
totalTokens += dayTotal;
|
|
3414
|
+
totalCost += dayCost;
|
|
3415
|
+
return {
|
|
3416
|
+
date,
|
|
3417
|
+
inputTokens,
|
|
3418
|
+
outputTokens,
|
|
3419
|
+
cacheReadTokens,
|
|
3420
|
+
cacheWriteTokens,
|
|
3421
|
+
totalTokens: dayTotal,
|
|
3422
|
+
cost: dayCost,
|
|
3423
|
+
models
|
|
3424
|
+
};
|
|
3425
|
+
});
|
|
3426
|
+
return {
|
|
3427
|
+
provider: PROVIDER_NAME,
|
|
3428
|
+
displayName: DISPLAY_NAME,
|
|
3429
|
+
daily,
|
|
3430
|
+
totalTokens,
|
|
3431
|
+
totalCost,
|
|
3432
|
+
colors: CURSOR_COLORS,
|
|
3433
|
+
events
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
class CursorProvider {
|
|
3438
|
+
name = PROVIDER_NAME;
|
|
3439
|
+
displayName = DISPLAY_NAME;
|
|
3440
|
+
colors = CURSOR_COLORS;
|
|
3441
|
+
cacheDir;
|
|
3442
|
+
constructor(baseDir) {
|
|
3443
|
+
this.cacheDir = resolveCacheDir(baseDir);
|
|
3444
|
+
}
|
|
3445
|
+
async isAvailable() {
|
|
3446
|
+
try {
|
|
3447
|
+
return collectUsageFiles(this.cacheDir).length > 0;
|
|
3448
|
+
} catch {
|
|
3449
|
+
return false;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
async load(range) {
|
|
3453
|
+
const files = collectUsageFiles(this.cacheDir);
|
|
3454
|
+
const records = files.flatMap((filePath) => parseUsageFile(filePath)).filter((record) => isInRange(record.date, range));
|
|
3455
|
+
return buildProviderData(records);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
// packages/registry/dist/providers/open-code.js
|
|
3459
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync2 } from "fs";
|
|
3460
|
+
import { join as join4 } from "path";
|
|
3461
|
+
import { homedir as homedir4 } from "os";
|
|
3177
3462
|
import { Database } from "bun:sqlite";
|
|
3178
|
-
var
|
|
3179
|
-
var
|
|
3463
|
+
var PROVIDER_NAME2 = "open-code";
|
|
3464
|
+
var DISPLAY_NAME2 = "OpenCode";
|
|
3180
3465
|
var COLORS = {
|
|
3181
3466
|
primary: "#6366f1",
|
|
3182
3467
|
secondary: "#a78bfa",
|
|
3183
3468
|
gradient: ["#6366f1", "#a78bfa"]
|
|
3184
3469
|
};
|
|
3185
|
-
var CURRENT_DEFAULT_BASE_DIR =
|
|
3186
|
-
var LEGACY_DEFAULT_BASE_DIR =
|
|
3187
|
-
var CONFIG_DEFAULT_BASE_DIR =
|
|
3470
|
+
var CURRENT_DEFAULT_BASE_DIR = join4(homedir4(), ".local", "share", "opencode");
|
|
3471
|
+
var LEGACY_DEFAULT_BASE_DIR = join4(homedir4(), ".opencode");
|
|
3472
|
+
var CONFIG_DEFAULT_BASE_DIR = join4(homedir4(), ".config", "opencode");
|
|
3188
3473
|
function resolveBaseDir2(baseDir) {
|
|
3189
3474
|
if (baseDir) {
|
|
3190
3475
|
return baseDir;
|
|
@@ -3194,13 +3479,13 @@ function resolveBaseDir2(baseDir) {
|
|
|
3194
3479
|
LEGACY_DEFAULT_BASE_DIR,
|
|
3195
3480
|
CONFIG_DEFAULT_BASE_DIR
|
|
3196
3481
|
]) {
|
|
3197
|
-
if (
|
|
3482
|
+
if (existsSync4(candidate)) {
|
|
3198
3483
|
return candidate;
|
|
3199
3484
|
}
|
|
3200
3485
|
}
|
|
3201
3486
|
return CURRENT_DEFAULT_BASE_DIR;
|
|
3202
3487
|
}
|
|
3203
|
-
function
|
|
3488
|
+
function extractDate3(createdAt) {
|
|
3204
3489
|
const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
|
|
3205
3490
|
if (!Number.isFinite(timestamp)) {
|
|
3206
3491
|
return null;
|
|
@@ -3220,7 +3505,7 @@ function toTimestampMillis(createdAt) {
|
|
|
3220
3505
|
const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
|
|
3221
3506
|
return Number.isFinite(millis) ? millis : null;
|
|
3222
3507
|
}
|
|
3223
|
-
function
|
|
3508
|
+
function toIsoTimestamp2(createdAt) {
|
|
3224
3509
|
const millis = toTimestampMillis(createdAt);
|
|
3225
3510
|
if (millis === null) {
|
|
3226
3511
|
return null;
|
|
@@ -3228,7 +3513,7 @@ function toIsoTimestamp(createdAt) {
|
|
|
3228
3513
|
const date = new Date(millis);
|
|
3229
3514
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
3230
3515
|
}
|
|
3231
|
-
function
|
|
3516
|
+
function toCachePricing4(pricing) {
|
|
3232
3517
|
if (!pricing) {
|
|
3233
3518
|
return;
|
|
3234
3519
|
}
|
|
@@ -3241,17 +3526,17 @@ function toCachePricing3(pricing) {
|
|
|
3241
3526
|
function getEstimatedCostBreakdown(record) {
|
|
3242
3527
|
return estimateCostBreakdown(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
3243
3528
|
}
|
|
3244
|
-
function
|
|
3529
|
+
function getRecordCost2(record) {
|
|
3245
3530
|
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
3246
3531
|
return record.explicitCost;
|
|
3247
3532
|
}
|
|
3248
3533
|
return getEstimatedCostBreakdown(record).totalCost;
|
|
3249
3534
|
}
|
|
3250
|
-
function
|
|
3535
|
+
function toUsageEvent2(record) {
|
|
3251
3536
|
const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
3252
3537
|
const estimated = getEstimatedCostBreakdown(record);
|
|
3253
3538
|
return {
|
|
3254
|
-
provider:
|
|
3539
|
+
provider: PROVIDER_NAME2,
|
|
3255
3540
|
timestamp: record.timestamp,
|
|
3256
3541
|
date: record.date,
|
|
3257
3542
|
model: normalizeModelName2(record.model),
|
|
@@ -3260,14 +3545,14 @@ function toUsageEvent(record) {
|
|
|
3260
3545
|
cacheReadTokens: record.cacheReadTokens,
|
|
3261
3546
|
cacheWriteTokens: record.cacheWriteTokens,
|
|
3262
3547
|
totalTokens,
|
|
3263
|
-
cost:
|
|
3264
|
-
pricing:
|
|
3548
|
+
cost: getRecordCost2(record),
|
|
3549
|
+
pricing: toCachePricing4(estimated.pricing),
|
|
3265
3550
|
sessionId: record.sessionId,
|
|
3266
3551
|
projectId: record.projectId,
|
|
3267
3552
|
durationMs: record.durationMs
|
|
3268
3553
|
};
|
|
3269
3554
|
}
|
|
3270
|
-
function
|
|
3555
|
+
function buildProviderData2(records) {
|
|
3271
3556
|
const byDate = new Map;
|
|
3272
3557
|
for (const record of records) {
|
|
3273
3558
|
let dateMap = byDate.get(record.date);
|
|
@@ -3279,7 +3564,7 @@ function buildProviderData(records) {
|
|
|
3279
3564
|
const existing = dateMap.get(normalized);
|
|
3280
3565
|
const estimated = getEstimatedCostBreakdown(record);
|
|
3281
3566
|
const recordCost = typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost) ? record.explicitCost : estimated.totalCost;
|
|
3282
|
-
const pricing =
|
|
3567
|
+
const pricing = toCachePricing4(estimated.pricing);
|
|
3283
3568
|
if (existing) {
|
|
3284
3569
|
existing.inputTokens += record.inputTokens;
|
|
3285
3570
|
existing.outputTokens += record.outputTokens;
|
|
@@ -3327,13 +3612,13 @@ function buildProviderData(records) {
|
|
|
3327
3612
|
};
|
|
3328
3613
|
});
|
|
3329
3614
|
return {
|
|
3330
|
-
provider:
|
|
3331
|
-
displayName:
|
|
3615
|
+
provider: PROVIDER_NAME2,
|
|
3616
|
+
displayName: DISPLAY_NAME2,
|
|
3332
3617
|
daily,
|
|
3333
3618
|
totalTokens,
|
|
3334
3619
|
totalCost,
|
|
3335
3620
|
colors: COLORS,
|
|
3336
|
-
events: records.map(
|
|
3621
|
+
events: records.map(toUsageEvent2)
|
|
3337
3622
|
};
|
|
3338
3623
|
}
|
|
3339
3624
|
function loadFromSqlite(dbPath, range) {
|
|
@@ -3351,8 +3636,8 @@ function loadFromSqlite(dbPath, range) {
|
|
|
3351
3636
|
const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
3352
3637
|
const records = [];
|
|
3353
3638
|
for (const row of rows) {
|
|
3354
|
-
const date =
|
|
3355
|
-
const timestamp =
|
|
3639
|
+
const date = extractDate3(row.created_at);
|
|
3640
|
+
const timestamp = toIsoTimestamp2(row.created_at);
|
|
3356
3641
|
if (date && timestamp && isInRange(date, range)) {
|
|
3357
3642
|
records.push({
|
|
3358
3643
|
date,
|
|
@@ -3374,11 +3659,11 @@ function loadFromSqlite(dbPath, range) {
|
|
|
3374
3659
|
}
|
|
3375
3660
|
}
|
|
3376
3661
|
function loadFromLegacyJson(sessionsDir, range) {
|
|
3377
|
-
const files =
|
|
3662
|
+
const files = readdirSync4(sessionsDir).filter((file) => file.endsWith(".json"));
|
|
3378
3663
|
const records = [];
|
|
3379
3664
|
for (const file of files) {
|
|
3380
3665
|
try {
|
|
3381
|
-
const content =
|
|
3666
|
+
const content = readFileSync2(join4(sessionsDir, file), "utf-8");
|
|
3382
3667
|
const session = JSON.parse(content);
|
|
3383
3668
|
if (!Array.isArray(session.messages)) {
|
|
3384
3669
|
continue;
|
|
@@ -3387,8 +3672,8 @@ function loadFromLegacyJson(sessionsDir, range) {
|
|
|
3387
3672
|
if (msg.role !== "assistant" || !msg.usage) {
|
|
3388
3673
|
continue;
|
|
3389
3674
|
}
|
|
3390
|
-
const date =
|
|
3391
|
-
const timestamp =
|
|
3675
|
+
const date = extractDate3(msg.created_at);
|
|
3676
|
+
const timestamp = toIsoTimestamp2(msg.created_at);
|
|
3392
3677
|
if (date && timestamp && isInRange(date, range)) {
|
|
3393
3678
|
records.push({
|
|
3394
3679
|
date,
|
|
@@ -3409,23 +3694,23 @@ function loadFromLegacyJson(sessionsDir, range) {
|
|
|
3409
3694
|
return records;
|
|
3410
3695
|
}
|
|
3411
3696
|
function loadFromCurrentStorage(baseDir, range) {
|
|
3412
|
-
const messagesRoot =
|
|
3413
|
-
if (!
|
|
3697
|
+
const messagesRoot = join4(baseDir, "storage", "message");
|
|
3698
|
+
if (!existsSync4(messagesRoot)) {
|
|
3414
3699
|
return [];
|
|
3415
3700
|
}
|
|
3416
3701
|
const recordsById = new Map;
|
|
3417
3702
|
const recordsWithoutId = [];
|
|
3418
|
-
for (const sessionDir of
|
|
3419
|
-
const sessionPath =
|
|
3703
|
+
for (const sessionDir of readdirSync4(messagesRoot)) {
|
|
3704
|
+
const sessionPath = join4(messagesRoot, sessionDir);
|
|
3420
3705
|
let messageFiles;
|
|
3421
3706
|
try {
|
|
3422
|
-
messageFiles =
|
|
3707
|
+
messageFiles = readdirSync4(sessionPath).filter((file) => file.endsWith(".json"));
|
|
3423
3708
|
} catch {
|
|
3424
3709
|
continue;
|
|
3425
3710
|
}
|
|
3426
3711
|
for (const file of messageFiles) {
|
|
3427
3712
|
try {
|
|
3428
|
-
const content =
|
|
3713
|
+
const content = readFileSync2(join4(sessionPath, file), "utf-8");
|
|
3429
3714
|
const message = JSON.parse(content);
|
|
3430
3715
|
if (message.role !== "assistant") {
|
|
3431
3716
|
continue;
|
|
@@ -3435,8 +3720,8 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
3435
3720
|
if (typeof model !== "string" || typeof createdAt !== "string" && typeof createdAt !== "number") {
|
|
3436
3721
|
continue;
|
|
3437
3722
|
}
|
|
3438
|
-
const date =
|
|
3439
|
-
const timestamp =
|
|
3723
|
+
const date = extractDate3(createdAt);
|
|
3724
|
+
const timestamp = toIsoTimestamp2(createdAt);
|
|
3440
3725
|
if (!date || !timestamp || !isInRange(date, range)) {
|
|
3441
3726
|
continue;
|
|
3442
3727
|
}
|
|
@@ -3479,8 +3764,8 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
3479
3764
|
}
|
|
3480
3765
|
|
|
3481
3766
|
class OpenCodeProvider {
|
|
3482
|
-
name =
|
|
3483
|
-
displayName =
|
|
3767
|
+
name = PROVIDER_NAME2;
|
|
3768
|
+
displayName = DISPLAY_NAME2;
|
|
3484
3769
|
colors = COLORS;
|
|
3485
3770
|
baseDir;
|
|
3486
3771
|
constructor(baseDir) {
|
|
@@ -3488,51 +3773,51 @@ class OpenCodeProvider {
|
|
|
3488
3773
|
}
|
|
3489
3774
|
async isAvailable() {
|
|
3490
3775
|
try {
|
|
3491
|
-
if (!
|
|
3776
|
+
if (!existsSync4(this.baseDir)) {
|
|
3492
3777
|
return false;
|
|
3493
3778
|
}
|
|
3494
|
-
const hasCurrentStorage =
|
|
3495
|
-
const hasLegacyDb =
|
|
3496
|
-
const hasLegacySessionsDir =
|
|
3779
|
+
const hasCurrentStorage = existsSync4(join4(this.baseDir, "storage", "message"));
|
|
3780
|
+
const hasLegacyDb = existsSync4(join4(this.baseDir, "opencode.db")) || existsSync4(join4(this.baseDir, "sessions.db"));
|
|
3781
|
+
const hasLegacySessionsDir = existsSync4(join4(this.baseDir, "sessions"));
|
|
3497
3782
|
return hasCurrentStorage || hasLegacyDb || hasLegacySessionsDir;
|
|
3498
3783
|
} catch {
|
|
3499
3784
|
return false;
|
|
3500
3785
|
}
|
|
3501
3786
|
}
|
|
3502
3787
|
async load(range) {
|
|
3503
|
-
const currentMessagesRoot =
|
|
3504
|
-
if (
|
|
3788
|
+
const currentMessagesRoot = join4(this.baseDir, "storage", "message");
|
|
3789
|
+
if (existsSync4(currentMessagesRoot)) {
|
|
3505
3790
|
const currentRecords = loadFromCurrentStorage(this.baseDir, range);
|
|
3506
|
-
return
|
|
3791
|
+
return buildProviderData2(currentRecords);
|
|
3507
3792
|
}
|
|
3508
|
-
const opencodeDbPath =
|
|
3509
|
-
const sessionsDbPath =
|
|
3510
|
-
const sessionsDir =
|
|
3793
|
+
const opencodeDbPath = join4(this.baseDir, "opencode.db");
|
|
3794
|
+
const sessionsDbPath = join4(this.baseDir, "sessions.db");
|
|
3795
|
+
const sessionsDir = join4(this.baseDir, "sessions");
|
|
3511
3796
|
let records = [];
|
|
3512
|
-
if (
|
|
3797
|
+
if (existsSync4(opencodeDbPath)) {
|
|
3513
3798
|
records = loadFromSqlite(opencodeDbPath, range);
|
|
3514
|
-
} else if (
|
|
3799
|
+
} else if (existsSync4(sessionsDbPath)) {
|
|
3515
3800
|
records = loadFromSqlite(sessionsDbPath, range);
|
|
3516
|
-
} else if (
|
|
3801
|
+
} else if (existsSync4(sessionsDir)) {
|
|
3517
3802
|
records = loadFromLegacyJson(sessionsDir, range);
|
|
3518
3803
|
}
|
|
3519
|
-
return
|
|
3804
|
+
return buildProviderData2(records);
|
|
3520
3805
|
}
|
|
3521
3806
|
}
|
|
3522
3807
|
// packages/registry/dist/providers/pi.js
|
|
3523
|
-
import { existsSync as
|
|
3808
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3524
3809
|
import { readdir } from "fs/promises";
|
|
3525
|
-
import { homedir as
|
|
3526
|
-
import { join as
|
|
3527
|
-
var
|
|
3528
|
-
var
|
|
3529
|
-
var DEFAULT_AGENT_DIR =
|
|
3810
|
+
import { homedir as homedir5 } from "os";
|
|
3811
|
+
import { join as join5, relative as relative4, sep as sep3 } from "path";
|
|
3812
|
+
var PROVIDER_NAME3 = "pi";
|
|
3813
|
+
var DISPLAY_NAME3 = "Pi";
|
|
3814
|
+
var DEFAULT_AGENT_DIR = join5(homedir5(), ".pi", "agent");
|
|
3530
3815
|
var PI_COLORS = {
|
|
3531
3816
|
primary: "#0ea5e9",
|
|
3532
3817
|
secondary: "#67e8f9",
|
|
3533
3818
|
gradient: ["#0ea5e9", "#67e8f9"]
|
|
3534
3819
|
};
|
|
3535
|
-
var
|
|
3820
|
+
var DASHED_DATE_SUFFIX3 = /-(\d{4})-(\d{2})-(\d{2})$/;
|
|
3536
3821
|
function resolveAgentDir(baseDir) {
|
|
3537
3822
|
if (baseDir) {
|
|
3538
3823
|
return baseDir;
|
|
@@ -3540,15 +3825,15 @@ function resolveAgentDir(baseDir) {
|
|
|
3540
3825
|
return process.env["PI_CODING_AGENT_DIR"] ?? DEFAULT_AGENT_DIR;
|
|
3541
3826
|
}
|
|
3542
3827
|
function getSessionsDir(agentDir) {
|
|
3543
|
-
return
|
|
3828
|
+
return join5(agentDir, "sessions");
|
|
3544
3829
|
}
|
|
3545
3830
|
async function collectJsonlFiles3(dir) {
|
|
3546
|
-
if (!
|
|
3831
|
+
if (!existsSync5(dir)) {
|
|
3547
3832
|
return [];
|
|
3548
3833
|
}
|
|
3549
3834
|
const files = [];
|
|
3550
3835
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
3551
|
-
const fullPath =
|
|
3836
|
+
const fullPath = join5(dir, entry.name);
|
|
3552
3837
|
if (entry.isDirectory()) {
|
|
3553
3838
|
files.push(...await collectJsonlFiles3(fullPath));
|
|
3554
3839
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -3557,16 +3842,16 @@ async function collectJsonlFiles3(dir) {
|
|
|
3557
3842
|
}
|
|
3558
3843
|
return files;
|
|
3559
3844
|
}
|
|
3560
|
-
function
|
|
3845
|
+
function extractDate4(timestamp) {
|
|
3561
3846
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
3562
3847
|
return match ? match[1] : null;
|
|
3563
3848
|
}
|
|
3564
|
-
function
|
|
3565
|
-
return model.replace(
|
|
3849
|
+
function compactModelDateSuffix3(model) {
|
|
3850
|
+
return model.replace(DASHED_DATE_SUFFIX3, "-$1$2$3");
|
|
3566
3851
|
}
|
|
3567
|
-
function
|
|
3852
|
+
function toIsoTimestamp3(value) {
|
|
3568
3853
|
if (typeof value === "string") {
|
|
3569
|
-
return
|
|
3854
|
+
return extractDate4(value) ? value : null;
|
|
3570
3855
|
}
|
|
3571
3856
|
if (typeof value === "number") {
|
|
3572
3857
|
const date = new Date(value);
|
|
@@ -3574,7 +3859,7 @@ function toIsoTimestamp2(value) {
|
|
|
3574
3859
|
}
|
|
3575
3860
|
return null;
|
|
3576
3861
|
}
|
|
3577
|
-
function
|
|
3862
|
+
function toCachePricing5(pricing) {
|
|
3578
3863
|
if (!pricing) {
|
|
3579
3864
|
return;
|
|
3580
3865
|
}
|
|
@@ -3598,7 +3883,7 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
|
|
|
3598
3883
|
if (obj["type"] !== "message") {
|
|
3599
3884
|
return null;
|
|
3600
3885
|
}
|
|
3601
|
-
const entryTimestamp =
|
|
3886
|
+
const entryTimestamp = toIsoTimestamp3(obj["timestamp"]);
|
|
3602
3887
|
const message = obj["message"];
|
|
3603
3888
|
if (typeof message !== "object" || message === null) {
|
|
3604
3889
|
return null;
|
|
@@ -3624,16 +3909,16 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
|
|
|
3624
3909
|
if (totalTokens === 0) {
|
|
3625
3910
|
return null;
|
|
3626
3911
|
}
|
|
3627
|
-
const timestamp = entryTimestamp ??
|
|
3912
|
+
const timestamp = entryTimestamp ?? toIsoTimestamp3(msg["timestamp"]);
|
|
3628
3913
|
if (!timestamp) {
|
|
3629
3914
|
return null;
|
|
3630
3915
|
}
|
|
3631
|
-
const date =
|
|
3916
|
+
const date = extractDate4(timestamp);
|
|
3632
3917
|
if (!date) {
|
|
3633
3918
|
return null;
|
|
3634
3919
|
}
|
|
3635
3920
|
const cost = typeof usageObj["cost"] === "object" && usageObj["cost"] !== null ? usageObj["cost"]["total"] : undefined;
|
|
3636
|
-
const normalizedModel = normalizeModelName2(
|
|
3921
|
+
const normalizedModel = normalizeModelName2(compactModelDateSuffix3(model));
|
|
3637
3922
|
return {
|
|
3638
3923
|
date,
|
|
3639
3924
|
timestamp,
|
|
@@ -3648,17 +3933,17 @@ function parseUsageRecord2(record, fallbackProjectId, fallbackSessionId) {
|
|
|
3648
3933
|
projectId: fallbackProjectId
|
|
3649
3934
|
};
|
|
3650
3935
|
}
|
|
3651
|
-
function
|
|
3936
|
+
function getRecordCost3(record) {
|
|
3652
3937
|
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
3653
3938
|
return record.explicitCost;
|
|
3654
3939
|
}
|
|
3655
3940
|
return estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).totalCost;
|
|
3656
3941
|
}
|
|
3657
|
-
function
|
|
3942
|
+
function toUsageEvent3(record) {
|
|
3658
3943
|
const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
3659
|
-
const pricing =
|
|
3944
|
+
const pricing = toCachePricing5(estimateCostBreakdown(record.normalizedModel, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens).pricing);
|
|
3660
3945
|
return {
|
|
3661
|
-
provider:
|
|
3946
|
+
provider: PROVIDER_NAME3,
|
|
3662
3947
|
timestamp: record.timestamp,
|
|
3663
3948
|
date: record.date,
|
|
3664
3949
|
model: record.normalizedModel,
|
|
@@ -3667,15 +3952,15 @@ function toUsageEvent2(record) {
|
|
|
3667
3952
|
cacheReadTokens: record.cacheReadTokens,
|
|
3668
3953
|
cacheWriteTokens: record.cacheWriteTokens,
|
|
3669
3954
|
totalTokens,
|
|
3670
|
-
cost:
|
|
3955
|
+
cost: getRecordCost3(record),
|
|
3671
3956
|
pricing,
|
|
3672
3957
|
sessionId: record.sessionId,
|
|
3673
3958
|
projectId: record.projectId
|
|
3674
3959
|
};
|
|
3675
3960
|
}
|
|
3676
|
-
function
|
|
3961
|
+
function buildProviderData3(records) {
|
|
3677
3962
|
const byDate = new Map;
|
|
3678
|
-
const events = records.map(
|
|
3963
|
+
const events = records.map(toUsageEvent3);
|
|
3679
3964
|
for (const event of events) {
|
|
3680
3965
|
let dateMap = byDate.get(event.date);
|
|
3681
3966
|
if (!dateMap) {
|
|
@@ -3730,8 +4015,8 @@ function buildProviderData2(records) {
|
|
|
3730
4015
|
};
|
|
3731
4016
|
});
|
|
3732
4017
|
return {
|
|
3733
|
-
provider:
|
|
3734
|
-
displayName:
|
|
4018
|
+
provider: PROVIDER_NAME3,
|
|
4019
|
+
displayName: DISPLAY_NAME3,
|
|
3735
4020
|
daily,
|
|
3736
4021
|
totalTokens,
|
|
3737
4022
|
totalCost,
|
|
@@ -3741,8 +4026,8 @@ function buildProviderData2(records) {
|
|
|
3741
4026
|
}
|
|
3742
4027
|
|
|
3743
4028
|
class PiProvider {
|
|
3744
|
-
name =
|
|
3745
|
-
displayName =
|
|
4029
|
+
name = PROVIDER_NAME3;
|
|
4030
|
+
displayName = DISPLAY_NAME3;
|
|
3746
4031
|
colors = PI_COLORS;
|
|
3747
4032
|
agentDir;
|
|
3748
4033
|
constructor(baseDir) {
|
|
@@ -3750,7 +4035,7 @@ class PiProvider {
|
|
|
3750
4035
|
}
|
|
3751
4036
|
async isAvailable() {
|
|
3752
4037
|
try {
|
|
3753
|
-
return
|
|
4038
|
+
return existsSync5(getSessionsDir(this.agentDir));
|
|
3754
4039
|
} catch {
|
|
3755
4040
|
return false;
|
|
3756
4041
|
}
|
|
@@ -3773,7 +4058,7 @@ class PiProvider {
|
|
|
3773
4058
|
}
|
|
3774
4059
|
}
|
|
3775
4060
|
}
|
|
3776
|
-
return
|
|
4061
|
+
return buildProviderData3(records);
|
|
3777
4062
|
}
|
|
3778
4063
|
}
|
|
3779
4064
|
// packages/renderers/dist/json/json-renderer.js
|
|
@@ -4430,198 +4715,7 @@ class SvgRenderer {
|
|
|
4430
4715
|
});
|
|
4431
4716
|
}
|
|
4432
4717
|
}
|
|
4433
|
-
// packages/renderers/dist/svg/theme.js
|
|
4434
|
-
var DARK_THEME = {
|
|
4435
|
-
background: "#0d1117",
|
|
4436
|
-
foreground: "#e6edf3",
|
|
4437
|
-
muted: "#7d8590",
|
|
4438
|
-
border: "#30363d",
|
|
4439
|
-
cardBackground: "#161b22",
|
|
4440
|
-
heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
|
|
4441
|
-
accent: "#58a6ff",
|
|
4442
|
-
accentSecondary: "#bc8cff",
|
|
4443
|
-
barFill: "#3b82f6",
|
|
4444
|
-
barBackground: "#21262d"
|
|
4445
|
-
};
|
|
4446
|
-
var LIGHT_THEME = {
|
|
4447
|
-
background: "#ffffff",
|
|
4448
|
-
foreground: "#1a1a2e",
|
|
4449
|
-
muted: "#8b8fa3",
|
|
4450
|
-
border: "#e5e7eb",
|
|
4451
|
-
cardBackground: "#f8f9fc",
|
|
4452
|
-
heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
|
|
4453
|
-
accent: "#3b5bdb",
|
|
4454
|
-
accentSecondary: "#7048e8",
|
|
4455
|
-
barFill: "#5b6abf",
|
|
4456
|
-
barBackground: "#ebedf0"
|
|
4457
|
-
};
|
|
4458
|
-
function getTheme(mode) {
|
|
4459
|
-
return mode === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
4460
|
-
}
|
|
4461
4718
|
// packages/renderers/dist/svg/wrapped-slides.js
|
|
4462
|
-
var WIDTH = 1200;
|
|
4463
|
-
var PAD = 80;
|
|
4464
|
-
var DISPLAY_FONT = "'SF Pro Display', 'Helvetica Neue', 'Segoe UI', -apple-system, sans-serif";
|
|
4465
|
-
var MONO_FONT = "'SF Mono', 'Menlo', 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace";
|
|
4466
|
-
var BODY_FONT = "'SF Pro Text', 'Helvetica Neue', 'Segoe UI', -apple-system, sans-serif";
|
|
4467
|
-
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
4468
|
-
var DAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
4469
|
-
var MONTH_NAMES2 = [
|
|
4470
|
-
"January",
|
|
4471
|
-
"February",
|
|
4472
|
-
"March",
|
|
4473
|
-
"April",
|
|
4474
|
-
"May",
|
|
4475
|
-
"June",
|
|
4476
|
-
"July",
|
|
4477
|
-
"August",
|
|
4478
|
-
"September",
|
|
4479
|
-
"October",
|
|
4480
|
-
"November",
|
|
4481
|
-
"December"
|
|
4482
|
-
];
|
|
4483
|
-
function getWrappedTheme(mode) {
|
|
4484
|
-
const base = getTheme(mode);
|
|
4485
|
-
if (mode === "dark") {
|
|
4486
|
-
return {
|
|
4487
|
-
base,
|
|
4488
|
-
mode,
|
|
4489
|
-
sectionBgs: [
|
|
4490
|
-
["#08080c", "#0c0c14"],
|
|
4491
|
-
["#0a0a12", "#0e0e18"],
|
|
4492
|
-
["#0c0a08", "#100e0c"],
|
|
4493
|
-
["#080c14", "#0c1018"],
|
|
4494
|
-
["#0a0a0e", "#0e0e12"],
|
|
4495
|
-
["#080c14", "#0c1018"],
|
|
4496
|
-
["#0c0814", "#100c18"],
|
|
4497
|
-
["#080e0c", "#0c1210"],
|
|
4498
|
-
["#0c0a06", "#100e0a"],
|
|
4499
|
-
["#08080c", "#0c0c14"],
|
|
4500
|
-
["#0a0c10", "#0e1014"],
|
|
4501
|
-
["#060608", "#060608"]
|
|
4502
|
-
],
|
|
4503
|
-
heroAccent: "#a78bfa",
|
|
4504
|
-
warmAccent: "#fb923c",
|
|
4505
|
-
coolAccent: "#38bdf8",
|
|
4506
|
-
greenAccent: "#4ade80",
|
|
4507
|
-
goldAccent: "#fbbf24",
|
|
4508
|
-
purpleAccent: "#c084fc",
|
|
4509
|
-
narrativeColor: "#d1d5db",
|
|
4510
|
-
subtitleColor: "#6b7280"
|
|
4511
|
-
};
|
|
4512
|
-
}
|
|
4513
|
-
return {
|
|
4514
|
-
base,
|
|
4515
|
-
mode,
|
|
4516
|
-
sectionBgs: [
|
|
4517
|
-
["#fafaf9", "#f5f5f4"],
|
|
4518
|
-
["#f5f5f4", "#fafaf9"],
|
|
4519
|
-
["#fefce8", "#fef9c3"],
|
|
4520
|
-
["#eff6ff", "#dbeafe"],
|
|
4521
|
-
["#fafaf9", "#f5f5f4"],
|
|
4522
|
-
["#eff6ff", "#dbeafe"],
|
|
4523
|
-
["#faf5ff", "#f3e8ff"],
|
|
4524
|
-
["#ecfdf5", "#d1fae5"],
|
|
4525
|
-
["#fffbeb", "#fef3c7"],
|
|
4526
|
-
["#faf5ff", "#f3e8ff"],
|
|
4527
|
-
["#eff6ff", "#dbeafe"],
|
|
4528
|
-
["#fafaf9", "#fafaf9"]
|
|
4529
|
-
],
|
|
4530
|
-
heroAccent: "#7c3aed",
|
|
4531
|
-
warmAccent: "#ea580c",
|
|
4532
|
-
coolAccent: "#2563eb",
|
|
4533
|
-
greenAccent: "#16a34a",
|
|
4534
|
-
goldAccent: "#ca8a04",
|
|
4535
|
-
purpleAccent: "#7c3aed",
|
|
4536
|
-
narrativeColor: "#1f2937",
|
|
4537
|
-
subtitleColor: "#6b7280"
|
|
4538
|
-
};
|
|
4539
|
-
}
|
|
4540
|
-
function svgIconFire(x, y, size, color) {
|
|
4541
|
-
const s = size / 24;
|
|
4542
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2C6 8 4 12 4 15.5C4 19.09 7.58 22 12 22C16.42 22 20 19.09 20 15.5C20 12 18 8 12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `<path d="M12 8C9 12 8 14 8 15.5C8 17.71 9.79 19.5 12 19.5C14.21 19.5 16 17.71 16 15.5C16 14 15 12 12 8Z" ` + `fill="${escapeXml(color)}" opacity="0.5"/>` + `</g>`;
|
|
4543
|
-
}
|
|
4544
|
-
function svgIconStar(x, y, size, color) {
|
|
4545
|
-
const s = size / 24;
|
|
4546
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4547
|
-
}
|
|
4548
|
-
function svgIconCircle(x, y, size, color) {
|
|
4549
|
-
const r = size / 2;
|
|
4550
|
-
return `<circle cx="${x + r}" cy="${y + r}" r="${r}" fill="${escapeXml(color)}" opacity="0.85"/>`;
|
|
4551
|
-
}
|
|
4552
|
-
function svgIconDiamond(x, y, size, color) {
|
|
4553
|
-
const s = size / 2;
|
|
4554
|
-
const cx = x + s;
|
|
4555
|
-
const cy = y + s;
|
|
4556
|
-
return `<path d="M${cx} ${cy - s} L${cx + s} ${cy} L${cx} ${cy + s} L${cx - s} ${cy} Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>`;
|
|
4557
|
-
}
|
|
4558
|
-
function svgIconBolt(x, y, size, color) {
|
|
4559
|
-
const s = size / 24;
|
|
4560
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4561
|
-
}
|
|
4562
|
-
function svgIconTrophy(x, y, size, color) {
|
|
4563
|
-
const s = size / 24;
|
|
4564
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M7 4V2H17V4H20V8C20 9.1 19.1 10 18 10H16.76C16.34 11.8 14.84 13.17 13 13.44V16H16V18H8V16H11V13.44C9.16 13.17 7.66 11.8 7.24 10H6C4.9 10 4 9.1 4 8V4H7Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4565
|
-
}
|
|
4566
|
-
function svgIconTarget(x, y, size, color) {
|
|
4567
|
-
const cx = x + size / 2;
|
|
4568
|
-
const cy = y + size / 2;
|
|
4569
|
-
const r = size / 2;
|
|
4570
|
-
return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${escapeXml(color)}" stroke-width="2" opacity="0.6"/>` + `<circle cx="${cx}" cy="${cy}" r="${r * 0.6}" fill="none" stroke="${escapeXml(color)}" stroke-width="2" opacity="0.7"/>` + `<circle cx="${cx}" cy="${cy}" r="${r * 0.25}" fill="${escapeXml(color)}" opacity="0.85"/>`;
|
|
4571
|
-
}
|
|
4572
|
-
function svgIconMountain(x, y, size, color) {
|
|
4573
|
-
const s = size / 24;
|
|
4574
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M14 6L20 18H4L10 8L13 12.5L14 6Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4575
|
-
}
|
|
4576
|
-
function svgIconPalette(x, y, size, color) {
|
|
4577
|
-
const s = size / 24;
|
|
4578
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C12.83 22 13.5 21.33 13.5 20.5C13.5 20.12 13.37 19.78 13.15 19.52C12.93 19.26 12.82 18.93 12.82 18.57C12.82 17.75 13.5 17.07 14.32 17.07H16.5C19.54 17.07 22 14.61 22 11.57C22 6.28 17.51 2 12 2Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4579
|
-
}
|
|
4580
|
-
function svgIconCalendar(x, y, size, color) {
|
|
4581
|
-
const s = size / 24;
|
|
4582
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M19 4H18V2H16V4H8V2H6V4H5C3.89 4 3 4.9 3 6V20C3 21.1 3.89 22 5 22H19C20.1 22 21 21.1 21 20V6C21 4.9 20.1 4 19 4ZM19 20H5V10H19V20ZM19 8H5V6H19V8Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4583
|
-
}
|
|
4584
|
-
function svgIconMoon(x, y, size, color) {
|
|
4585
|
-
const s = size / 24;
|
|
4586
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" fill="${escapeXml(color)}" opacity="0.85"/>` + `</g>`;
|
|
4587
|
-
}
|
|
4588
|
-
function svgIconSun(x, y, size, color) {
|
|
4589
|
-
const cx = x + size / 2;
|
|
4590
|
-
const cy = y + size / 2;
|
|
4591
|
-
const r = size * 0.3;
|
|
4592
|
-
let svg = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${escapeXml(color)}" opacity="0.85"/>`;
|
|
4593
|
-
for (let i = 0;i < 8; i++) {
|
|
4594
|
-
const angle = i * 45 * Math.PI / 180;
|
|
4595
|
-
const x1 = cx + r * 1.4 * Math.cos(angle);
|
|
4596
|
-
const y1 = cy + r * 1.4 * Math.sin(angle);
|
|
4597
|
-
const x2 = cx + r * 2 * Math.cos(angle);
|
|
4598
|
-
const y2 = cy + r * 2 * Math.sin(angle);
|
|
4599
|
-
svg += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${escapeXml(color)}" stroke-width="2" stroke-linecap="round" opacity="0.7"/>`;
|
|
4600
|
-
}
|
|
4601
|
-
return svg;
|
|
4602
|
-
}
|
|
4603
|
-
function svgIconRocket(x, y, size, color) {
|
|
4604
|
-
const s = size / 24;
|
|
4605
|
-
return `<g transform="translate(${x},${y}) scale(${s})">` + `<path d="M12 2.5C12 2.5 7 8 7 13.5C7 16.81 9.24 19.5 12 19.5C14.76 19.5 17 16.81 17 13.5C17 8 12 2.5 12 2.5Z" ` + `fill="${escapeXml(color)}" opacity="0.85"/>` + `<circle cx="12" cy="13" r="2" fill="${escapeXml(color)}" opacity="0.4"/>` + `</g>`;
|
|
4606
|
-
}
|
|
4607
|
-
function renderIcon(name, x, y, size, color) {
|
|
4608
|
-
const fns = {
|
|
4609
|
-
fire: svgIconFire,
|
|
4610
|
-
star: svgIconStar,
|
|
4611
|
-
circle: svgIconCircle,
|
|
4612
|
-
diamond: svgIconDiamond,
|
|
4613
|
-
bolt: svgIconBolt,
|
|
4614
|
-
trophy: svgIconTrophy,
|
|
4615
|
-
target: svgIconTarget,
|
|
4616
|
-
mountain: svgIconMountain,
|
|
4617
|
-
palette: svgIconPalette,
|
|
4618
|
-
calendar: svgIconCalendar,
|
|
4619
|
-
moon: svgIconMoon,
|
|
4620
|
-
sun: svgIconSun,
|
|
4621
|
-
rocket: svgIconRocket
|
|
4622
|
-
};
|
|
4623
|
-
return fns[name](x, y, size, color);
|
|
4624
|
-
}
|
|
4625
4719
|
function computeAchievements(output) {
|
|
4626
4720
|
const stats = output.aggregated;
|
|
4627
4721
|
const more = output.more;
|
|
@@ -4684,22 +4778,83 @@ function computeAchievements(output) {
|
|
|
4684
4778
|
}
|
|
4685
4779
|
return all.slice(0, 6);
|
|
4686
4780
|
}
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4781
|
+
// packages/renderers/dist/svg/wrapped-single-page.js
|
|
4782
|
+
var WIDTH = 1200;
|
|
4783
|
+
var PAD = 56;
|
|
4784
|
+
var INNER = WIDTH - PAD * 2;
|
|
4785
|
+
var DISPLAY = "'Bricolage Grotesque', 'SF Pro Display', 'Helvetica Neue', sans-serif";
|
|
4786
|
+
var MONO = "'Space Mono', 'SF Mono', 'Menlo', monospace";
|
|
4787
|
+
var BODY = "'Space Grotesk', 'SF Pro Text', 'Helvetica Neue', sans-serif";
|
|
4788
|
+
var DARK = {
|
|
4789
|
+
bg: "#09090b",
|
|
4790
|
+
surface: "#111114",
|
|
4791
|
+
surface2: "#16161a",
|
|
4792
|
+
border: "rgba(255,255,255,0.07)",
|
|
4793
|
+
borderHi: "rgba(212,175,95,0.24)",
|
|
4794
|
+
gold: "#d4af5f",
|
|
4795
|
+
goldDim: "#a08040",
|
|
4796
|
+
ivory: "#f0ead6",
|
|
4797
|
+
ivoryDim: "rgba(240,234,214,0.52)",
|
|
4798
|
+
text: "#e8e2cc",
|
|
4799
|
+
muted: "rgba(232,226,204,0.38)",
|
|
4800
|
+
muted2: "rgba(232,226,204,0.18)",
|
|
4801
|
+
providerDefault: "#555555",
|
|
4802
|
+
trackStroke: "rgba(255,255,255,0.05)",
|
|
4803
|
+
barTrack: "rgba(255,255,255,0.09)",
|
|
4804
|
+
dotInactive: "#16161a",
|
|
4805
|
+
dotInactiveBorder: "rgba(255,255,255,0.07)"
|
|
4806
|
+
};
|
|
4807
|
+
var LIGHT = {
|
|
4808
|
+
bg: "#fafaf9",
|
|
4809
|
+
surface: "#f0efed",
|
|
4810
|
+
surface2: "#e8e6e3",
|
|
4811
|
+
border: "rgba(0,0,0,0.08)",
|
|
4812
|
+
borderHi: "rgba(160,128,64,0.3)",
|
|
4813
|
+
gold: "#9a7b3a",
|
|
4814
|
+
goldDim: "#b8985a",
|
|
4815
|
+
ivory: "#1a1a18",
|
|
4816
|
+
ivoryDim: "rgba(26,26,24,0.6)",
|
|
4817
|
+
text: "#2c2c28",
|
|
4818
|
+
muted: "rgba(44,44,40,0.42)",
|
|
4819
|
+
muted2: "rgba(44,44,40,0.22)",
|
|
4820
|
+
providerDefault: "#888888",
|
|
4821
|
+
trackStroke: "rgba(0,0,0,0.06)",
|
|
4822
|
+
barTrack: "rgba(0,0,0,0.08)",
|
|
4823
|
+
dotInactive: "#e8e6e3",
|
|
4824
|
+
dotInactiveBorder: "rgba(0,0,0,0.08)"
|
|
4825
|
+
};
|
|
4826
|
+
var PROVIDER_COLORS = {
|
|
4827
|
+
anthropic: { dark: "#d4af5f", light: "#9a7b3a" },
|
|
4828
|
+
"claude-code": { dark: "#d4af5f", light: "#9a7b3a" },
|
|
4829
|
+
openai: { dark: "#3a5070", light: "#4a6a90" },
|
|
4830
|
+
codex: { dark: "#3a5070", light: "#4a6a90" },
|
|
4831
|
+
google: { dark: "#6a2535", light: "#8a3548" },
|
|
4832
|
+
cursor: { dark: "#7c5cbf", light: "#6a4aaa" },
|
|
4833
|
+
pi: { dark: "#5a4a70", light: "#706088" }
|
|
4834
|
+
};
|
|
4835
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
4836
|
+
var DAY_NAMES_FULL = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
4837
|
+
var MONTH_NAMES2 = [
|
|
4838
|
+
"January",
|
|
4839
|
+
"February",
|
|
4840
|
+
"March",
|
|
4841
|
+
"April",
|
|
4842
|
+
"May",
|
|
4843
|
+
"June",
|
|
4844
|
+
"July",
|
|
4845
|
+
"August",
|
|
4846
|
+
"September",
|
|
4847
|
+
"October",
|
|
4848
|
+
"November",
|
|
4849
|
+
"December"
|
|
4850
|
+
];
|
|
4851
|
+
function txt(x, y, content, opts = {}) {
|
|
4697
4852
|
const attrs = [
|
|
4698
4853
|
`x="${x}"`,
|
|
4699
4854
|
`y="${y}"`,
|
|
4700
|
-
`fill="${escapeXml(opts.fill ?? "#
|
|
4855
|
+
`fill="${escapeXml(opts.fill ?? "#e8e2cc")}"`,
|
|
4701
4856
|
`font-size="${opts.size ?? 14}"`,
|
|
4702
|
-
`font-family="${escapeXml(opts.family ??
|
|
4857
|
+
`font-family="${escapeXml(opts.family ?? BODY)}"`,
|
|
4703
4858
|
`font-weight="${opts.weight ?? 400}"`
|
|
4704
4859
|
];
|
|
4705
4860
|
if (opts.anchor)
|
|
@@ -4708,9 +4863,11 @@ function svgText(x, y, content, opts = {}) {
|
|
|
4708
4863
|
attrs.push(`letter-spacing="${opts.spacing}"`);
|
|
4709
4864
|
if (opts.opacity !== undefined)
|
|
4710
4865
|
attrs.push(`opacity="${opts.opacity}"`);
|
|
4866
|
+
if (opts.dominantBaseline)
|
|
4867
|
+
attrs.push(`dominant-baseline="${opts.dominantBaseline}"`);
|
|
4711
4868
|
return `<text ${attrs.join(" ")}>${escapeXml(content)}</text>`;
|
|
4712
4869
|
}
|
|
4713
|
-
function
|
|
4870
|
+
function box(x, y, w, h, fill, rx = 2, opts = {}) {
|
|
4714
4871
|
const extra = [];
|
|
4715
4872
|
if (opts.opacity !== undefined)
|
|
4716
4873
|
extra.push(`opacity="${opts.opacity}"`);
|
|
@@ -4718,781 +4875,658 @@ function rect2(x, y, w, h, fill, rx = 4, opts = {}) {
|
|
|
4718
4875
|
extra.push(`stroke="${escapeXml(opts.stroke)}" stroke-width="${opts.strokeWidth ?? 1}"`);
|
|
4719
4876
|
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${escapeXml(fill)}" ${extra.join(" ")}/>`;
|
|
4720
4877
|
}
|
|
4721
|
-
function
|
|
4722
|
-
|
|
4723
|
-
const end = polarToCartesian(cx, cy, radius, startAngle);
|
|
4724
|
-
const largeArc = endAngle - startAngle <= 180 ? "0" : "1";
|
|
4725
|
-
return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArc} 0 ${end.x} ${end.y}`;
|
|
4726
|
-
}
|
|
4727
|
-
function polarToCartesian(cx, cy, radius, angleDeg) {
|
|
4728
|
-
const rad = (angleDeg - 90) * Math.PI / 180;
|
|
4729
|
-
return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) };
|
|
4730
|
-
}
|
|
4731
|
-
function cornerMark(x, y, size, color, corner) {
|
|
4732
|
-
const s = size;
|
|
4733
|
-
const paths = {
|
|
4734
|
-
tl: `M${x} ${y + s} L${x} ${y} L${x + s} ${y}`,
|
|
4735
|
-
tr: `M${x - s} ${y} L${x} ${y} L${x} ${y + s}`,
|
|
4736
|
-
bl: `M${x} ${y - s} L${x} ${y} L${x + s} ${y}`,
|
|
4737
|
-
br: `M${x - s} ${y} L${x} ${y} L${x} ${y - s}`
|
|
4738
|
-
};
|
|
4739
|
-
return `<path d="${paths[corner]}" fill="none" stroke="${escapeXml(color)}" stroke-width="1.5" opacity="0.3"/>`;
|
|
4740
|
-
}
|
|
4741
|
-
function sectionLabel(x, y, text2, color, accent) {
|
|
4742
|
-
return [
|
|
4743
|
-
`<line x1="${x}" y1="${y - 4}" x2="${x + 24}" y2="${y - 4}" stroke="${escapeXml(accent)}" stroke-width="2" opacity="0.6"/>`,
|
|
4744
|
-
svgText(x + 32, y, text2, { fill: color, size: 11, weight: 600, spacing: 3, family: MONO_FONT })
|
|
4745
|
-
].join(`
|
|
4746
|
-
`);
|
|
4878
|
+
function line(x1, y1, x2, y2, color, width = 1, opacity = 1) {
|
|
4879
|
+
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${escapeXml(color)}" stroke-width="${width}" opacity="${opacity}"/>`;
|
|
4747
4880
|
}
|
|
4748
|
-
function
|
|
4749
|
-
|
|
4750
|
-
}
|
|
4751
|
-
function dotGrid(x, y, w, h, color, spacing = 24, radius = 1) {
|
|
4752
|
-
const dots = [];
|
|
4753
|
-
for (let gx = x;gx <= x + w; gx += spacing) {
|
|
4754
|
-
for (let gy = y;gy <= y + h; gy += spacing) {
|
|
4755
|
-
dots.push(`<circle cx="${gx}" cy="${gy}" r="${radius}" fill="${escapeXml(color)}" opacity="0.06"/>`);
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4758
|
-
return dots.join(`
|
|
4759
|
-
`);
|
|
4881
|
+
function formatDateShort(dateStr) {
|
|
4882
|
+
const d = new Date(dateStr + "T00:00:00Z");
|
|
4883
|
+
return `${MONTH_NAMES2[d.getUTCMonth()]?.slice(0, 3)} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
|
|
4760
4884
|
}
|
|
4761
4885
|
function formatDateLong(dateStr) {
|
|
4762
4886
|
const d = new Date(dateStr + "T00:00:00Z");
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4887
|
+
return `${MONTH_NAMES2[d.getUTCMonth()] ?? ""} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
|
|
4888
|
+
}
|
|
4889
|
+
function getProviderColor(provider, mode) {
|
|
4890
|
+
const entry = PROVIDER_COLORS[provider.toLowerCase()];
|
|
4891
|
+
if (entry)
|
|
4892
|
+
return entry[mode];
|
|
4893
|
+
return mode === "dark" ? "#555555" : "#888888";
|
|
4894
|
+
}
|
|
4895
|
+
function describeArc(cx, cy, r, startDeg, endDeg) {
|
|
4896
|
+
const toRad = (deg) => (deg - 90) * Math.PI / 180;
|
|
4897
|
+
const start = { x: cx + r * Math.cos(toRad(endDeg)), y: cy + r * Math.sin(toRad(endDeg)) };
|
|
4898
|
+
const end = { x: cx + r * Math.cos(toRad(startDeg)), y: cy + r * Math.sin(toRad(startDeg)) };
|
|
4899
|
+
const large = endDeg - startDeg <= 180 ? "0" : "1";
|
|
4900
|
+
return `M ${start.x} ${start.y} A ${r} ${r} 0 ${large} 0 ${end.x} ${end.y}`;
|
|
4901
|
+
}
|
|
4902
|
+
function sectionTag(x, y, label, C) {
|
|
4903
|
+
return txt(x, y, label, {
|
|
4904
|
+
fill: C.muted,
|
|
4905
|
+
size: 10,
|
|
4906
|
+
weight: 400,
|
|
4907
|
+
family: MONO,
|
|
4908
|
+
spacing: 2.5
|
|
4909
|
+
});
|
|
4767
4910
|
}
|
|
4768
|
-
function
|
|
4769
|
-
const
|
|
4911
|
+
function noiseDef(isDark) {
|
|
4912
|
+
const opacity = isDark ? 0.03 : 0.02;
|
|
4770
4913
|
return [
|
|
4771
4914
|
"<defs>",
|
|
4772
4915
|
'<filter id="grain" x="0" y="0" width="100%" height="100%">',
|
|
4773
|
-
'<feTurbulence type="fractalNoise" baseFrequency="0.
|
|
4916
|
+
'<feTurbulence type="fractalNoise" baseFrequency="0.78" numOctaves="4" stitchTiles="stitch" result="noise"/>',
|
|
4774
4917
|
'<feColorMatrix type="saturate" values="0" in="noise" result="mono"/>',
|
|
4775
|
-
|
|
4918
|
+
'<feBlend in="SourceGraphic" in2="mono" mode="multiply"/>',
|
|
4776
4919
|
"</filter>",
|
|
4777
4920
|
"</defs>",
|
|
4778
|
-
|
|
4921
|
+
`<!-- noise opacity: ${opacity} -->`
|
|
4779
4922
|
].join(`
|
|
4780
4923
|
`);
|
|
4781
4924
|
}
|
|
4782
|
-
function
|
|
4783
|
-
const
|
|
4784
|
-
const
|
|
4785
|
-
const
|
|
4786
|
-
|
|
4787
|
-
const
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4925
|
+
function renderWrappedSinglePageSvg(output, options = { theme: "dark" }) {
|
|
4926
|
+
const stats = output.aggregated;
|
|
4927
|
+
const more = output.more;
|
|
4928
|
+
const providers = output.providers;
|
|
4929
|
+
const { since, until } = output.dateRange;
|
|
4930
|
+
const achievements = computeAchievements(output);
|
|
4931
|
+
const isDark = options.theme === "dark";
|
|
4932
|
+
const C = isDark ? DARK : LIGHT;
|
|
4933
|
+
const parts = [];
|
|
4934
|
+
let y = 0;
|
|
4935
|
+
const headerH = 200;
|
|
4936
|
+
parts.push(box(0, y, WIDTH, headerH, C.bg));
|
|
4937
|
+
if (isDark) {
|
|
4938
|
+
parts.push(`<defs><radialGradient id="amb1" cx="10%" cy="10%" r="60%"><stop offset="0%" stop-color="rgba(44,70,120,0.07)"/><stop offset="100%" stop-color="transparent"/></radialGradient></defs>`);
|
|
4939
|
+
parts.push(`<rect x="0" y="${y}" width="${WIDTH}" height="${headerH}" fill="url(#amb1)"/>`);
|
|
4940
|
+
}
|
|
4941
|
+
parts.push(line(0, y, WIDTH, y, C.gold, 1, 0.5));
|
|
4942
|
+
const year = until.slice(0, 4);
|
|
4943
|
+
parts.push(txt(PAD, y + 68, "AI Wrapped", {
|
|
4944
|
+
fill: C.ivory,
|
|
4945
|
+
size: 68,
|
|
4946
|
+
weight: 800,
|
|
4947
|
+
family: DISPLAY,
|
|
4948
|
+
spacing: -3
|
|
4798
4949
|
}));
|
|
4799
|
-
|
|
4800
|
-
fill:
|
|
4801
|
-
size:
|
|
4950
|
+
parts.push(txt(PAD + 460, y + 68, `'${year.slice(2)}`, {
|
|
4951
|
+
fill: C.gold,
|
|
4952
|
+
size: 68,
|
|
4802
4953
|
weight: 800,
|
|
4803
|
-
family:
|
|
4954
|
+
family: DISPLAY,
|
|
4804
4955
|
spacing: -3
|
|
4805
4956
|
}));
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
p.push(svgText(PAD, 210, rangeText, {
|
|
4810
|
-
fill: theme.subtitleColor,
|
|
4811
|
-
size: 14,
|
|
4957
|
+
parts.push(txt(PAD, y + 96, `${formatDateShort(since)} \u2014 ${formatDateShort(until)}`, {
|
|
4958
|
+
fill: C.muted,
|
|
4959
|
+
size: 13,
|
|
4812
4960
|
weight: 400,
|
|
4813
|
-
family:
|
|
4961
|
+
family: MONO,
|
|
4962
|
+
spacing: 1
|
|
4814
4963
|
}));
|
|
4815
|
-
|
|
4816
|
-
fill:
|
|
4817
|
-
size:
|
|
4818
|
-
weight:
|
|
4819
|
-
family:
|
|
4820
|
-
opacity: 0.4
|
|
4964
|
+
parts.push(txt(PAD, y + 126, "Every prompt has a price. Here's yours.", {
|
|
4965
|
+
fill: C.ivoryDim,
|
|
4966
|
+
size: 15,
|
|
4967
|
+
weight: 400,
|
|
4968
|
+
family: BODY
|
|
4821
4969
|
}));
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
const p = [];
|
|
4829
|
-
const stats = output.aggregated;
|
|
4830
|
-
const gc = theme.sectionBgs[1] ?? ["#0a0a12", "#0e0e18"];
|
|
4831
|
-
p.push(sectionBg(0, height, gc, "bignums-bg"));
|
|
4832
|
-
p.push(sectionLabel(PAD, 40, "THE BIG NUMBERS", theme.subtitleColor, theme.heroAccent));
|
|
4833
|
-
const numColor = theme.mode === "dark" ? "#f9fafb" : "#111827";
|
|
4834
|
-
p.push(svgText(PAD, 130, formatNumber(stats.totalTokens), {
|
|
4835
|
-
fill: numColor,
|
|
4836
|
-
size: 96,
|
|
4970
|
+
const stampW = 140;
|
|
4971
|
+
const stampX = WIDTH - PAD - stampW;
|
|
4972
|
+
parts.push(box(stampX, y + 78, stampW, 36, "transparent", 2, { stroke: C.borderHi, strokeWidth: 1 }));
|
|
4973
|
+
parts.push(txt(stampX + stampW / 2, y + 101, "TokenLeak", {
|
|
4974
|
+
fill: C.gold,
|
|
4975
|
+
size: 16,
|
|
4837
4976
|
weight: 800,
|
|
4838
|
-
family:
|
|
4839
|
-
spacing: -
|
|
4977
|
+
family: DISPLAY,
|
|
4978
|
+
spacing: -0.5,
|
|
4979
|
+
anchor: "middle"
|
|
4840
4980
|
}));
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4981
|
+
const totalDays = Math.round((new Date(until + "T00:00:00Z").getTime() - new Date(since + "T00:00:00Z").getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
4982
|
+
parts.push(`<circle cx="${PAD + 4}" cy="${y + 156}" r="3" fill="${C.gold}"/>`);
|
|
4983
|
+
parts.push(txt(PAD + 14, y + 160, `${totalDays} DAYS OF DATA`, {
|
|
4984
|
+
fill: C.muted,
|
|
4985
|
+
size: 9,
|
|
4986
|
+
weight: 400,
|
|
4987
|
+
family: MONO,
|
|
4988
|
+
spacing: 2
|
|
4847
4989
|
}));
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
const
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4990
|
+
parts.push(line(PAD, y + headerH - 1, WIDTH - PAD, y + headerH - 1, C.gold, 1, 0.15));
|
|
4991
|
+
y += headerH;
|
|
4992
|
+
const bigNumH = 140;
|
|
4993
|
+
parts.push(box(0, y, WIDTH, bigNumH, C.surface));
|
|
4994
|
+
parts.push(sectionTag(PAD, y + 24, "THE BIG NUMBERS", C));
|
|
4995
|
+
const cardW = (INNER - 30) / 4;
|
|
4996
|
+
const cardH = 82;
|
|
4997
|
+
const cardY = y + 38;
|
|
4998
|
+
const bigStats = [
|
|
4999
|
+
{ label: "TOTAL TOKENS", value: formatNumber(stats.totalTokens), unit: "TOKENS", gold: false },
|
|
5000
|
+
{ label: "TOTAL COST", value: formatCost2(stats.totalCost), unit: "USD", gold: true },
|
|
5001
|
+
{ label: "ACTIVE DAYS", value: `${stats.activeDays}`, unit: `OF ${stats.totalDays}`, gold: false },
|
|
5002
|
+
{ label: "AVG / DAY", value: formatNumber(stats.averageDailyTokens), unit: "TOKENS", gold: false }
|
|
4854
5003
|
];
|
|
4855
|
-
for (let i = 0;i <
|
|
4856
|
-
const
|
|
4857
|
-
const
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
family: MONO_FONT
|
|
5004
|
+
for (let i = 0;i < bigStats.length; i++) {
|
|
5005
|
+
const stat = bigStats[i];
|
|
5006
|
+
const cx = PAD + i * (cardW + 10);
|
|
5007
|
+
parts.push(box(cx, cardY, cardW, cardH, C.surface2, 2, { stroke: C.border, strokeWidth: 1 }));
|
|
5008
|
+
parts.push(txt(cx + 16, cardY + 20, stat.label, {
|
|
5009
|
+
fill: C.muted,
|
|
5010
|
+
size: 9,
|
|
5011
|
+
weight: 400,
|
|
5012
|
+
family: MONO,
|
|
5013
|
+
spacing: 2
|
|
4866
5014
|
}));
|
|
4867
|
-
|
|
4868
|
-
fill:
|
|
4869
|
-
size:
|
|
4870
|
-
weight:
|
|
4871
|
-
|
|
4872
|
-
|
|
5015
|
+
parts.push(txt(cx + 16, cardY + 52, stat.value, {
|
|
5016
|
+
fill: stat.gold ? C.gold : C.ivory,
|
|
5017
|
+
size: 30,
|
|
5018
|
+
weight: 800,
|
|
5019
|
+
family: DISPLAY,
|
|
5020
|
+
spacing: -1
|
|
5021
|
+
}));
|
|
5022
|
+
parts.push(txt(cx + 16, cardY + 68, stat.unit, {
|
|
5023
|
+
fill: C.gold,
|
|
5024
|
+
size: 9,
|
|
5025
|
+
weight: 400,
|
|
5026
|
+
family: MONO,
|
|
5027
|
+
spacing: 1.5
|
|
4873
5028
|
}));
|
|
4874
5029
|
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
function renderStreakSlide(output, theme) {
|
|
4880
|
-
const height = 250;
|
|
4881
|
-
const p = [];
|
|
4882
|
-
const stats = output.aggregated;
|
|
4883
|
-
const gc = theme.sectionBgs[2] ?? ["#0c0a08", "#100e0c"];
|
|
4884
|
-
p.push(sectionBg(0, height, gc, "streak-bg"));
|
|
4885
|
-
p.push(sectionLabel(PAD, 40, "STREAK STORY", theme.subtitleColor, theme.warmAccent));
|
|
4886
|
-
const narrative = stats.longestStreak > 0 ? `Your longest coding streak was ${stats.longestStreak} days` : "Start your first coding streak!";
|
|
4887
|
-
p.push(svgText(PAD, 85, narrative, {
|
|
4888
|
-
fill: theme.narrativeColor,
|
|
4889
|
-
size: 20,
|
|
4890
|
-
weight: 500,
|
|
4891
|
-
family: DISPLAY_FONT
|
|
4892
|
-
}));
|
|
4893
|
-
p.push(svgText(PAD, 170, `${stats.longestStreak}`, {
|
|
4894
|
-
fill: theme.warmAccent,
|
|
4895
|
-
size: 88,
|
|
4896
|
-
weight: 800,
|
|
4897
|
-
family: MONO_FONT,
|
|
4898
|
-
spacing: -4
|
|
4899
|
-
}));
|
|
4900
|
-
const numWidth = Math.max(60, `${stats.longestStreak}`.length * 50);
|
|
4901
|
-
p.push(svgText(PAD + numWidth + 8, 170, "days", {
|
|
4902
|
-
fill: theme.subtitleColor,
|
|
4903
|
-
size: 20,
|
|
4904
|
-
weight: 400,
|
|
4905
|
-
family: DISPLAY_FONT
|
|
4906
|
-
}));
|
|
4907
|
-
p.push(svgIconFire(PAD + numWidth + 8, 120, 40, theme.warmAccent));
|
|
4908
|
-
p.push(svgText(WIDTH - PAD, 170, `Current: ${stats.currentStreak}`, {
|
|
4909
|
-
fill: theme.subtitleColor,
|
|
4910
|
-
size: 14,
|
|
4911
|
-
weight: 500,
|
|
4912
|
-
family: MONO_FONT,
|
|
4913
|
-
anchor: "end"
|
|
4914
|
-
}));
|
|
4915
|
-
const barY = 198;
|
|
4916
|
-
const barW = WIDTH - PAD * 2;
|
|
4917
|
-
const barH = 6;
|
|
4918
|
-
const streakRatio = Math.min(stats.longestStreak / 30, 1);
|
|
4919
|
-
const trackColor = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
|
|
4920
|
-
p.push(rect2(PAD, barY, barW, barH, trackColor, 3));
|
|
4921
|
-
if (streakRatio > 0) {
|
|
4922
|
-
p.push(rect2(PAD, barY, Math.max(8, streakRatio * barW), barH, theme.warmAccent, 3, { opacity: 0.7 }));
|
|
4923
|
-
}
|
|
4924
|
-
for (let i = 0;i <= 30 && i <= stats.longestStreak; i += 5) {
|
|
4925
|
-
if (i === 0)
|
|
4926
|
-
continue;
|
|
4927
|
-
const tx = PAD + i / 30 * barW;
|
|
4928
|
-
p.push(`<line x1="${tx}" y1="${barY + barH + 2}" x2="${tx}" y2="${barY + barH + 6}" stroke="${escapeXml(theme.subtitleColor)}" stroke-width="1" opacity="0.3"/>`);
|
|
4929
|
-
}
|
|
4930
|
-
p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
|
|
4931
|
-
return { svg: p.join(`
|
|
4932
|
-
`), height };
|
|
4933
|
-
}
|
|
4934
|
-
function renderTopModelSlide(output, theme) {
|
|
4935
|
-
const height = 300;
|
|
4936
|
-
const p = [];
|
|
4937
|
-
const stats = output.aggregated;
|
|
5030
|
+
y += bigNumH;
|
|
5031
|
+
const colW = (INNER - 24) / 2;
|
|
5032
|
+
const leftX = PAD;
|
|
5033
|
+
const rightX = PAD + colW + 24;
|
|
4938
5034
|
const topModels2 = stats.topModels.slice(0, 3);
|
|
4939
|
-
const
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
p.
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5035
|
+
const totalProviderTokens = providers.reduce((s, p) => s + p.totalTokens, 0);
|
|
5036
|
+
const providerMix = providers.map((p) => ({
|
|
5037
|
+
name: p.displayName,
|
|
5038
|
+
pct: totalProviderTokens > 0 ? p.totalTokens / totalProviderTokens * 100 : 0,
|
|
5039
|
+
color: getProviderColor(p.provider, options.theme)
|
|
5040
|
+
})).sort((a, b) => b.pct - a.pct);
|
|
5041
|
+
const modelSectionH = 28 + 22 + topModels2.length * 42;
|
|
5042
|
+
const providerSectionH = 18 + 22 + Math.max(150, providerMix.length * 30 + 50);
|
|
5043
|
+
const leftColH = modelSectionH + providerSectionH;
|
|
5044
|
+
const dow = stats.dayOfWeek;
|
|
5045
|
+
const dowH = dow.length > 0 ? 28 + 22 + 26 + 100 + 28 : 60;
|
|
5046
|
+
const todH = 20 + 22 + 65 + 8 + 65;
|
|
5047
|
+
const rightColH = dowH + todH;
|
|
5048
|
+
const midH = Math.max(leftColH, rightColH) + 20;
|
|
5049
|
+
parts.push(box(0, y, WIDTH, midH, C.bg));
|
|
5050
|
+
let ly = y + 28;
|
|
5051
|
+
parts.push(sectionTag(leftX, ly, "YOUR TOP MODELS", C));
|
|
5052
|
+
ly += 22;
|
|
5053
|
+
if (topModels2.length > 0) {
|
|
5054
|
+
const maxPct = Math.max(...topModels2.map((m) => m.percentage), 1);
|
|
5055
|
+
for (let i = 0;i < topModels2.length; i++) {
|
|
5056
|
+
const m = topModels2[i];
|
|
5057
|
+
const barMaxW = colW - 80;
|
|
5058
|
+
const barW = Math.max(4, m.percentage / maxPct * barMaxW);
|
|
5059
|
+
const opacity = i === 0 ? 1 : i === 1 ? 0.6 : 0.35;
|
|
5060
|
+
parts.push(txt(leftX, ly + 14, m.model, {
|
|
5061
|
+
fill: C.text,
|
|
5062
|
+
size: 14,
|
|
5063
|
+
weight: 500,
|
|
5064
|
+
family: BODY
|
|
5065
|
+
}));
|
|
5066
|
+
parts.push(txt(leftX + colW, ly + 14, `${m.percentage.toFixed(1)}%`, {
|
|
5067
|
+
fill: C.gold,
|
|
5068
|
+
size: 13,
|
|
5069
|
+
weight: 700,
|
|
5070
|
+
family: MONO,
|
|
5071
|
+
anchor: "end"
|
|
5072
|
+
}));
|
|
5073
|
+
parts.push(line(leftX, ly + 26, leftX + colW, ly + 26, C.barTrack, 1));
|
|
5074
|
+
parts.push(line(leftX, ly + 26, leftX + barW, ly + 26, C.gold, 1, opacity));
|
|
5075
|
+
ly += 42;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
ly += 18;
|
|
5079
|
+
parts.push(sectionTag(leftX, ly, "PROVIDER MIX", C));
|
|
5080
|
+
ly += 22;
|
|
5081
|
+
const donutCx = leftX + 70;
|
|
5082
|
+
const donutCy = ly + 70;
|
|
5083
|
+
const donutR = 50;
|
|
5084
|
+
const donutStroke = 14;
|
|
5085
|
+
parts.push(`<circle cx="${donutCx}" cy="${donutCy}" r="${donutR}" fill="none" stroke="${C.trackStroke}" stroke-width="${donutStroke}"/>`);
|
|
5086
|
+
let startAngle = 0;
|
|
5087
|
+
for (const p of providerMix) {
|
|
5088
|
+
const sweep = p.pct / 100 * 360;
|
|
5089
|
+
if (sweep < 0.1)
|
|
5090
|
+
continue;
|
|
5091
|
+
const endAngle = Math.min(startAngle + sweep, 360);
|
|
5092
|
+
if (sweep >= 359.9) {
|
|
5093
|
+
parts.push(`<circle cx="${donutCx}" cy="${donutCy}" r="${donutR}" fill="none" stroke="${escapeXml(p.color)}" stroke-width="${donutStroke}"/>`);
|
|
5094
|
+
} else {
|
|
5095
|
+
const arc = describeArc(donutCx, donutCy, donutR, startAngle, endAngle);
|
|
5096
|
+
parts.push(`<path d="${arc}" fill="none" stroke="${escapeXml(p.color)}" stroke-width="${donutStroke}" stroke-linecap="butt"/>`);
|
|
5097
|
+
}
|
|
5098
|
+
startAngle = endAngle;
|
|
4950
5099
|
}
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
family: MONO_FONT,
|
|
4958
|
-
spacing: -1
|
|
5100
|
+
parts.push(txt(donutCx, donutCy + 5, `${providers.length}`, {
|
|
5101
|
+
fill: C.ivory,
|
|
5102
|
+
size: 22,
|
|
5103
|
+
weight: 800,
|
|
5104
|
+
family: DISPLAY,
|
|
5105
|
+
anchor: "middle"
|
|
4959
5106
|
}));
|
|
4960
|
-
|
|
4961
|
-
fill:
|
|
4962
|
-
size:
|
|
4963
|
-
weight:
|
|
5107
|
+
parts.push(txt(donutCx, donutCy + 20, "providers", {
|
|
5108
|
+
fill: C.muted,
|
|
5109
|
+
size: 8,
|
|
5110
|
+
weight: 400,
|
|
5111
|
+
family: MONO,
|
|
5112
|
+
anchor: "middle",
|
|
5113
|
+
spacing: 1
|
|
4964
5114
|
}));
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
p.push(rect2(segX + gap, segBarY, w - gap, segBarH, arcColors[i % arcColors.length], 4, { opacity: 0.85 }));
|
|
4976
|
-
segX += w;
|
|
4977
|
-
}
|
|
4978
|
-
const listY = 195;
|
|
4979
|
-
for (let i = 0;i < topModels2.length; i++) {
|
|
4980
|
-
const model = topModels2[i];
|
|
4981
|
-
const my = listY + i * 32;
|
|
4982
|
-
p.push(`<rect x="${PAD}" y="${my + 2}" width="4" height="16" rx="2" fill="${escapeXml(arcColors[i % arcColors.length])}" opacity="0.9"/>`);
|
|
4983
|
-
p.push(svgText(PAD + 16, my + 14, `${model.model}`, {
|
|
4984
|
-
fill: theme.mode === "dark" ? "#e5e7eb" : "#1f2937",
|
|
4985
|
-
size: 14,
|
|
4986
|
-
weight: 600,
|
|
4987
|
-
family: MONO_FONT
|
|
5115
|
+
const legendX = leftX + 155;
|
|
5116
|
+
for (let i = 0;i < providerMix.length; i++) {
|
|
5117
|
+
const p = providerMix[i];
|
|
5118
|
+
const py = ly + 35 + i * 28;
|
|
5119
|
+
parts.push(box(legendX, py, 8, 8, p.color, 1));
|
|
5120
|
+
parts.push(txt(legendX + 16, py + 8, p.name, {
|
|
5121
|
+
fill: C.text,
|
|
5122
|
+
size: 13,
|
|
5123
|
+
weight: 500,
|
|
5124
|
+
family: BODY
|
|
4988
5125
|
}));
|
|
4989
|
-
p.
|
|
4990
|
-
|
|
4991
|
-
|
|
5126
|
+
const pctLabel = p.pct > 0 && p.pct < 1 ? "<1%" : `${p.pct.toFixed(0)}%`;
|
|
5127
|
+
parts.push(txt(leftX + colW, py + 8, pctLabel, {
|
|
5128
|
+
fill: C.gold,
|
|
5129
|
+
size: 12,
|
|
4992
5130
|
weight: 700,
|
|
4993
|
-
family:
|
|
5131
|
+
family: MONO,
|
|
4994
5132
|
anchor: "end"
|
|
4995
5133
|
}));
|
|
4996
|
-
const barW = Math.max(4, model.percentage / 100 * (segBarW - 200));
|
|
4997
|
-
p.push(rect2(PAD + 16, my + 20, barW, 4, arcColors[i % arcColors.length], 2, { opacity: 0.4 }));
|
|
4998
5134
|
}
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
const
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
`), height: 180 };
|
|
5015
|
-
}
|
|
5016
|
-
const totalTokens = providers.reduce((s, pv) => s + pv.totalTokens, 0);
|
|
5017
|
-
const topProvider = providers.reduce((a, b) => a.totalTokens >= b.totalTokens ? a : b);
|
|
5018
|
-
const topPct = totalTokens > 0 ? (topProvider.totalTokens / totalTokens * 100).toFixed(0) : "0";
|
|
5019
|
-
p.push(svgText(PAD, 80, `${topProvider.displayName} \u2014 ${topPct}%`, {
|
|
5020
|
-
fill: theme.narrativeColor,
|
|
5021
|
-
size: 20,
|
|
5022
|
-
weight: 600,
|
|
5023
|
-
family: DISPLAY_FONT
|
|
5024
|
-
}));
|
|
5025
|
-
const barMaxWidth = WIDTH - PAD * 2 - 160;
|
|
5026
|
-
for (let i = 0;i < providers.length; i++) {
|
|
5027
|
-
const pv = providers[i];
|
|
5028
|
-
const py = 105 + i * perRow;
|
|
5029
|
-
const pct = totalTokens > 0 ? pv.totalTokens / totalTokens * 100 : 0;
|
|
5030
|
-
p.push(`<rect x="${PAD}" y="${py + 4}" width="4" height="14" rx="2" fill="${escapeXml(pv.colors.primary)}"/>`);
|
|
5031
|
-
p.push(svgText(PAD + 16, py + 15, pv.displayName, {
|
|
5032
|
-
fill: theme.mode === "dark" ? "#e5e7eb" : "#1f2937",
|
|
5033
|
-
size: 14,
|
|
5034
|
-
weight: 600,
|
|
5035
|
-
family: MONO_FONT
|
|
5036
|
-
}));
|
|
5037
|
-
p.push(svgText(WIDTH - PAD, py + 15, `${pct.toFixed(0)}%`, {
|
|
5038
|
-
fill: theme.subtitleColor,
|
|
5039
|
-
size: 13,
|
|
5135
|
+
let ry = y + 28;
|
|
5136
|
+
parts.push(sectionTag(rightX, ry, "CODING DAYS", C));
|
|
5137
|
+
ry += 22;
|
|
5138
|
+
const dowOrder = [1, 2, 3, 4, 5, 6, 0];
|
|
5139
|
+
const dowEntries = dowOrder.map((dayNum) => {
|
|
5140
|
+
const entry = dow.find((e) => e.day === dayNum);
|
|
5141
|
+
return { label: DAY_NAMES[dayNum] ?? "", tokens: entry?.tokens ?? 0 };
|
|
5142
|
+
});
|
|
5143
|
+
const maxDowTokens = Math.max(...dowEntries.map((e) => e.tokens), 1);
|
|
5144
|
+
if (dow.length > 0) {
|
|
5145
|
+
const peakDow = dowEntries.reduce((a, b) => b.tokens > a.tokens ? b : a);
|
|
5146
|
+
const peakDowFull = DAY_NAMES_FULL[dowOrder[dowEntries.indexOf(peakDow)] ?? 0] ?? "";
|
|
5147
|
+
parts.push(txt(rightX, ry + 14, `${peakDowFull} is your peak`, {
|
|
5148
|
+
fill: C.ivory,
|
|
5149
|
+
size: 18,
|
|
5040
5150
|
weight: 700,
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
}));
|
|
5044
|
-
const trackBg = theme.mode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.04)";
|
|
5045
|
-
p.push(rect2(PAD + 16, py + 24, barMaxWidth, 8, trackBg, 4));
|
|
5046
|
-
const barW = Math.max(4, pct / 100 * barMaxWidth);
|
|
5047
|
-
p.push(rect2(PAD + 16, py + 24, barW, 8, pv.colors.primary, 4, { opacity: 0.75 }));
|
|
5048
|
-
}
|
|
5049
|
-
p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
|
|
5050
|
-
return { svg: p.join(`
|
|
5051
|
-
`), height };
|
|
5052
|
-
}
|
|
5053
|
-
function renderDayOfWeekSlide(output, theme) {
|
|
5054
|
-
const height = 340;
|
|
5055
|
-
const p = [];
|
|
5056
|
-
const dow = output.aggregated.dayOfWeek;
|
|
5057
|
-
const gc = theme.sectionBgs[5] ?? ["#080c14", "#0c1018"];
|
|
5058
|
-
p.push(sectionBg(0, height, gc, "dow-bg"));
|
|
5059
|
-
p.push(sectionLabel(PAD, 40, "CODING DAYS", theme.subtitleColor, theme.coolAccent));
|
|
5060
|
-
if (dow.length === 0) {
|
|
5061
|
-
p.push(svgText(PAD, 170, "No day-of-week data", { fill: theme.subtitleColor, size: 16, weight: 500 }));
|
|
5062
|
-
return { svg: p.join(`
|
|
5063
|
-
`), height };
|
|
5064
|
-
}
|
|
5065
|
-
const peak = dow.reduce((a, b) => a.tokens >= b.tokens ? a : b);
|
|
5066
|
-
const peakName = DAY_NAMES[peak.day] ?? "Unknown";
|
|
5067
|
-
const maxTokens = Math.max(...dow.map((d) => d.tokens), 1);
|
|
5068
|
-
p.push(svgText(PAD, 80, `${peakName}s are your power day`, {
|
|
5069
|
-
fill: theme.narrativeColor,
|
|
5070
|
-
size: 20,
|
|
5071
|
-
weight: 600,
|
|
5072
|
-
family: DISPLAY_FONT
|
|
5073
|
-
}));
|
|
5074
|
-
const chartX = PAD;
|
|
5075
|
-
const chartY = 110;
|
|
5076
|
-
const barAreaWidth = WIDTH - PAD * 2;
|
|
5077
|
-
const gapSize = 12;
|
|
5078
|
-
const barWidth = Math.floor((barAreaWidth - 6 * gapSize) / 7);
|
|
5079
|
-
const barMaxHeight = 170;
|
|
5080
|
-
for (let i = 0;i < 7 && i < dow.length; i++) {
|
|
5081
|
-
const entry = dow[i];
|
|
5082
|
-
const bx = chartX + i * (barWidth + gapSize);
|
|
5083
|
-
const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
|
|
5084
|
-
const barH = Math.max(4, ratio * barMaxHeight);
|
|
5085
|
-
const by = chartY + barMaxHeight - barH;
|
|
5086
|
-
const isPeak = entry.day === peak.day;
|
|
5087
|
-
const barColor = isPeak ? theme.coolAccent : theme.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)";
|
|
5088
|
-
p.push(rect2(bx, by, barWidth, barH, barColor, 4, { opacity: isPeak ? 1 : 0.7 }));
|
|
5089
|
-
if (isPeak) {
|
|
5090
|
-
p.push(`<line x1="${bx}" y1="${by}" x2="${bx + barWidth}" y2="${by}" stroke="${escapeXml(theme.coolAccent)}" stroke-width="3" opacity="0.8"/>`);
|
|
5091
|
-
}
|
|
5092
|
-
const dayLabel = DAY_SHORT[i] ?? "";
|
|
5093
|
-
p.push(svgText(bx + barWidth / 2, chartY + barMaxHeight + 20, dayLabel, {
|
|
5094
|
-
fill: isPeak ? theme.coolAccent : theme.subtitleColor,
|
|
5095
|
-
size: 11,
|
|
5096
|
-
weight: isPeak ? 700 : 500,
|
|
5097
|
-
anchor: "middle",
|
|
5098
|
-
family: MONO_FONT
|
|
5151
|
+
family: DISPLAY,
|
|
5152
|
+
spacing: -0.5
|
|
5099
5153
|
}));
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5154
|
+
ry += 30;
|
|
5155
|
+
const barGap = 8;
|
|
5156
|
+
const barW = (colW - 6 * barGap) / 7;
|
|
5157
|
+
const barMaxH = 100;
|
|
5158
|
+
for (let i = 0;i < 7; i++) {
|
|
5159
|
+
const entry = dowEntries[i];
|
|
5160
|
+
const bx = rightX + i * (barW + barGap);
|
|
5161
|
+
const ratio = entry.tokens / maxDowTokens;
|
|
5162
|
+
const barH = Math.max(3, ratio * barMaxH);
|
|
5163
|
+
const by = ry + barMaxH - barH;
|
|
5164
|
+
const isPeak = entry.tokens === maxDowTokens;
|
|
5165
|
+
const opacity = ratio < 0.35 ? 0.35 : ratio < 0.55 ? 0.6 : 1;
|
|
5166
|
+
parts.push(box(bx, by, barW, barH, C.gold, 1, { opacity }));
|
|
5167
|
+
parts.push(txt(bx + barW / 2, ry + barMaxH + 16, entry.label, {
|
|
5168
|
+
fill: isPeak ? C.gold : C.muted,
|
|
5169
|
+
size: 9,
|
|
5170
|
+
weight: isPeak ? 700 : 400,
|
|
5171
|
+
family: MONO,
|
|
5105
5172
|
anchor: "middle",
|
|
5106
|
-
|
|
5173
|
+
spacing: 0.5
|
|
5107
5174
|
}));
|
|
5108
5175
|
}
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
if (totalTokens === 0)
|
|
5121
|
-
return { svg: "", height: 0 };
|
|
5122
|
-
const height = 320;
|
|
5123
|
-
const p = [];
|
|
5124
|
-
const gc = theme.sectionBgs[6] ?? ["#0c0814", "#100c18"];
|
|
5125
|
-
p.push(sectionBg(0, height, gc, "tod-bg"));
|
|
5126
|
-
p.push(sectionLabel(PAD, 40, "WHEN YOU CODE", theme.subtitleColor, theme.purpleAccent));
|
|
5127
|
-
const morning = hourOfDay.filter((e) => e.hour >= 6 && e.hour < 12).reduce((s, e) => s + e.tokens, 0);
|
|
5128
|
-
const afternoon = hourOfDay.filter((e) => e.hour >= 12 && e.hour < 18).reduce((s, e) => s + e.tokens, 0);
|
|
5129
|
-
const evening = hourOfDay.filter((e) => e.hour >= 18 && e.hour < 22).reduce((s, e) => s + e.tokens, 0);
|
|
5130
|
-
const night = hourOfDay.filter((e) => e.hour >= 22 || e.hour < 6).reduce((s, e) => s + e.tokens, 0);
|
|
5131
|
-
const periods = [
|
|
5132
|
-
{ label: "Morning", icon: "sun", tokens: morning, color: theme.warmAccent, hours: "6am\u201312pm" },
|
|
5133
|
-
{ label: "Afternoon", icon: "star", tokens: afternoon, color: theme.goldAccent, hours: "12\u20136pm" },
|
|
5134
|
-
{ label: "Evening", icon: "fire", tokens: evening, color: theme.purpleAccent, hours: "6\u201310pm" },
|
|
5135
|
-
{ label: "Night", icon: "moon", tokens: night, color: theme.coolAccent, hours: "10pm\u20136am" }
|
|
5176
|
+
ry += barMaxH + 28;
|
|
5177
|
+
}
|
|
5178
|
+
ry += 20;
|
|
5179
|
+
parts.push(sectionTag(rightX, ry, "WHEN YOU CODE", C));
|
|
5180
|
+
ry += 22;
|
|
5181
|
+
const hourOfDay = more?.hourOfDay;
|
|
5182
|
+
let todPeriods = [
|
|
5183
|
+
{ label: "Morning", range: "6am-12pm", pct: 0 },
|
|
5184
|
+
{ label: "Afternoon", range: "12-6pm", pct: 0 },
|
|
5185
|
+
{ label: "Evening", range: "6-10pm", pct: 0 },
|
|
5186
|
+
{ label: "Night", range: "10pm-6am", pct: 0 }
|
|
5136
5187
|
];
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
const
|
|
5156
|
-
const
|
|
5157
|
-
const
|
|
5158
|
-
for (let i = 0;i <
|
|
5159
|
-
const period =
|
|
5160
|
-
const
|
|
5161
|
-
const
|
|
5162
|
-
const
|
|
5163
|
-
const
|
|
5164
|
-
const
|
|
5165
|
-
|
|
5166
|
-
stroke:
|
|
5167
|
-
strokeWidth:
|
|
5168
|
-
opacity: 1
|
|
5188
|
+
if (hourOfDay) {
|
|
5189
|
+
const total = hourOfDay.reduce((s, e) => s + e.tokens, 0);
|
|
5190
|
+
if (total > 0) {
|
|
5191
|
+
const morning = hourOfDay.filter((e) => e.hour >= 6 && e.hour < 12).reduce((s, e) => s + e.tokens, 0);
|
|
5192
|
+
const afternoon = hourOfDay.filter((e) => e.hour >= 12 && e.hour < 18).reduce((s, e) => s + e.tokens, 0);
|
|
5193
|
+
const evening = hourOfDay.filter((e) => e.hour >= 18 && e.hour < 22).reduce((s, e) => s + e.tokens, 0);
|
|
5194
|
+
const morningPct = Math.round(morning / total * 100);
|
|
5195
|
+
const afternoonPct = Math.round(afternoon / total * 100);
|
|
5196
|
+
const eveningPct = Math.round(evening / total * 100);
|
|
5197
|
+
const nightPct = Math.max(0, 100 - morningPct - afternoonPct - eveningPct);
|
|
5198
|
+
todPeriods = [
|
|
5199
|
+
{ label: "Morning", range: "6am-12pm", pct: morningPct },
|
|
5200
|
+
{ label: "Afternoon", range: "12-6pm", pct: afternoonPct },
|
|
5201
|
+
{ label: "Evening", range: "6-10pm", pct: eveningPct },
|
|
5202
|
+
{ label: "Night", range: "10pm-6am", pct: nightPct }
|
|
5203
|
+
];
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
const peakPeriod = todPeriods.reduce((a, b) => b.pct > a.pct ? b : a);
|
|
5207
|
+
const todCellW = (colW - 8) / 2;
|
|
5208
|
+
const todCellH = 65;
|
|
5209
|
+
for (let i = 0;i < 4; i++) {
|
|
5210
|
+
const period = todPeriods[i];
|
|
5211
|
+
const col = i % 2;
|
|
5212
|
+
const row = Math.floor(i / 2);
|
|
5213
|
+
const cx = rightX + col * (todCellW + 8);
|
|
5214
|
+
const cy = ry + row * (todCellH + 8);
|
|
5215
|
+
const isPeak = period.label === peakPeriod.label;
|
|
5216
|
+
parts.push(box(cx, cy, todCellW, todCellH, C.surface2, 2, {
|
|
5217
|
+
stroke: isPeak ? C.borderHi : C.border,
|
|
5218
|
+
strokeWidth: isPeak ? 1.5 : 1
|
|
5169
5219
|
}));
|
|
5170
|
-
if (
|
|
5171
|
-
|
|
5220
|
+
if (isPeak) {
|
|
5221
|
+
parts.push(line(cx + 2, cy, cx + todCellW - 2, cy, C.gold, 2, 0.6));
|
|
5172
5222
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
family: MONO_FONT,
|
|
5180
|
-
spacing: 1
|
|
5223
|
+
parts.push(txt(cx + 14, cy + 20, period.label.toUpperCase(), {
|
|
5224
|
+
fill: isPeak ? C.gold : C.muted,
|
|
5225
|
+
size: 8,
|
|
5226
|
+
weight: 400,
|
|
5227
|
+
family: MONO,
|
|
5228
|
+
spacing: 1.5
|
|
5181
5229
|
}));
|
|
5182
|
-
|
|
5183
|
-
fill:
|
|
5184
|
-
size:
|
|
5230
|
+
parts.push(txt(cx + 14, cy + 44, `${period.pct}%`, {
|
|
5231
|
+
fill: isPeak ? C.gold : C.ivory,
|
|
5232
|
+
size: 22,
|
|
5185
5233
|
weight: 800,
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
}));
|
|
5189
|
-
p.push(svgText(cx + cardWidth / 2, cardsY + 135, formatNumber(period.tokens), {
|
|
5190
|
-
fill: theme.subtitleColor,
|
|
5191
|
-
size: 11,
|
|
5192
|
-
weight: 400,
|
|
5193
|
-
anchor: "middle",
|
|
5194
|
-
family: MONO_FONT
|
|
5234
|
+
family: DISPLAY,
|
|
5235
|
+
spacing: -0.5
|
|
5195
5236
|
}));
|
|
5196
|
-
|
|
5197
|
-
fill:
|
|
5198
|
-
size:
|
|
5237
|
+
parts.push(txt(cx + todCellW - 12, cy + 44, period.range, {
|
|
5238
|
+
fill: C.muted,
|
|
5239
|
+
size: 9,
|
|
5199
5240
|
weight: 400,
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
opacity: 0.5
|
|
5241
|
+
family: MONO,
|
|
5242
|
+
anchor: "end"
|
|
5203
5243
|
}));
|
|
5204
5244
|
}
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
const
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5245
|
+
y += midH;
|
|
5246
|
+
const bottomH = 260;
|
|
5247
|
+
parts.push(box(0, y, WIDTH, bottomH, C.surface));
|
|
5248
|
+
parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
|
|
5249
|
+
const col3W = (INNER - 32) / 3;
|
|
5250
|
+
const c1x = PAD;
|
|
5251
|
+
let c1y = y + 24;
|
|
5252
|
+
parts.push(sectionTag(c1x, c1y, "STREAK", C));
|
|
5253
|
+
c1y += 20;
|
|
5254
|
+
parts.push(txt(c1x, c1y + 42, `${stats.longestStreak}`, {
|
|
5255
|
+
fill: C.gold,
|
|
5256
|
+
size: 60,
|
|
5257
|
+
weight: 800,
|
|
5258
|
+
family: DISPLAY,
|
|
5259
|
+
spacing: -2
|
|
5260
|
+
}));
|
|
5261
|
+
parts.push(txt(c1x, c1y + 62, "DAY LONGEST STREAK", {
|
|
5262
|
+
fill: C.muted,
|
|
5263
|
+
size: 9,
|
|
5264
|
+
weight: 400,
|
|
5265
|
+
family: MONO,
|
|
5266
|
+
spacing: 2
|
|
5267
|
+
}));
|
|
5268
|
+
parts.push(txt(c1x, c1y + 86, `Current: ${stats.currentStreak} days`, {
|
|
5269
|
+
fill: C.muted,
|
|
5270
|
+
size: 11,
|
|
5271
|
+
weight: 400,
|
|
5272
|
+
family: MONO,
|
|
5273
|
+
spacing: 0.5
|
|
5274
|
+
}));
|
|
5275
|
+
c1y += 102;
|
|
5276
|
+
const activeDates = new Set;
|
|
5277
|
+
for (const prov of providers) {
|
|
5278
|
+
for (const d of prov.daily) {
|
|
5279
|
+
if (d.totalTokens > 0)
|
|
5280
|
+
activeDates.add(d.date);
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
const dotSize = 10;
|
|
5284
|
+
const dotGap = 4;
|
|
5285
|
+
const dotsPerRow = Math.floor(col3W / (dotSize + dotGap));
|
|
5286
|
+
for (let i = 0;i < 30; i++) {
|
|
5287
|
+
const dDate = new Date(new Date(until + "T00:00:00Z").getTime() - (29 - i) * 86400000);
|
|
5288
|
+
const dateStr = dDate.toISOString().slice(0, 10);
|
|
5289
|
+
const isActive = activeDates.has(dateStr);
|
|
5290
|
+
const isCurrentStreak = i >= 30 - stats.currentStreak;
|
|
5291
|
+
const col = i % dotsPerRow;
|
|
5292
|
+
const row = Math.floor(i / dotsPerRow);
|
|
5293
|
+
const dx = c1x + col * (dotSize + dotGap);
|
|
5294
|
+
const dy = c1y + row * (dotSize + dotGap);
|
|
5295
|
+
let dotColor = C.dotInactive;
|
|
5296
|
+
let dotBorder = C.dotInactiveBorder;
|
|
5297
|
+
if (isCurrentStreak) {
|
|
5298
|
+
dotColor = C.gold;
|
|
5299
|
+
dotBorder = C.gold;
|
|
5300
|
+
} else if (isActive) {
|
|
5301
|
+
dotColor = C.goldDim;
|
|
5302
|
+
dotBorder = C.goldDim;
|
|
5303
|
+
}
|
|
5304
|
+
parts.push(box(dx, dy, dotSize, dotSize, dotColor, 2, { stroke: dotBorder, strokeWidth: 1 }));
|
|
5305
|
+
}
|
|
5306
|
+
const c2x = PAD + col3W + 16;
|
|
5307
|
+
let c2y = y + 24;
|
|
5308
|
+
parts.push(sectionTag(c2x, c2y, "CACHE", C));
|
|
5309
|
+
c2y += 20;
|
|
5216
5310
|
const hitRate = stats.cacheHitRate;
|
|
5217
|
-
const hitPct = (hitRate * 100)
|
|
5218
|
-
const
|
|
5219
|
-
const
|
|
5220
|
-
const
|
|
5221
|
-
const
|
|
5222
|
-
|
|
5223
|
-
const bgArc = describeArc(gaugeCx, gaugeCy, gaugeR, -90, 270);
|
|
5224
|
-
p.push(`<path d="${bgArc}" fill="none" stroke="${trackColor}" stroke-width="${gaugeWidth}" stroke-linecap="round"/>`);
|
|
5311
|
+
const hitPct = Math.round(hitRate * 100);
|
|
5312
|
+
const ringCx = c2x + col3W / 2;
|
|
5313
|
+
const ringCy = c2y + 68;
|
|
5314
|
+
const ringR = 48;
|
|
5315
|
+
const ringStroke = 10;
|
|
5316
|
+
parts.push(`<circle cx="${ringCx}" cy="${ringCy}" r="${ringR}" fill="none" stroke="${C.trackStroke}" stroke-width="${ringStroke}"/>`);
|
|
5225
5317
|
if (hitRate > 0) {
|
|
5226
|
-
const
|
|
5227
|
-
const
|
|
5228
|
-
|
|
5318
|
+
const sweep = Math.min(hitRate * 360, 359);
|
|
5319
|
+
const arc = describeArc(ringCx, ringCy, ringR, 0, sweep);
|
|
5320
|
+
parts.push(`<path d="${arc}" fill="none" stroke="${C.gold}" stroke-width="${ringStroke}" stroke-linecap="butt"/>`);
|
|
5229
5321
|
}
|
|
5230
|
-
|
|
5231
|
-
fill:
|
|
5322
|
+
parts.push(txt(ringCx, ringCy + 5, `${hitPct}%`, {
|
|
5323
|
+
fill: C.gold,
|
|
5232
5324
|
size: 28,
|
|
5233
5325
|
weight: 800,
|
|
5234
|
-
|
|
5235
|
-
|
|
5326
|
+
family: DISPLAY,
|
|
5327
|
+
anchor: "middle"
|
|
5236
5328
|
}));
|
|
5237
|
-
|
|
5238
|
-
fill:
|
|
5239
|
-
size:
|
|
5240
|
-
weight:
|
|
5329
|
+
parts.push(txt(ringCx, ringCy + 20, "HIT RATE", {
|
|
5330
|
+
fill: C.muted,
|
|
5331
|
+
size: 8,
|
|
5332
|
+
weight: 400,
|
|
5333
|
+
family: MONO,
|
|
5241
5334
|
anchor: "middle",
|
|
5242
|
-
|
|
5335
|
+
spacing: 1.5
|
|
5243
5336
|
}));
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
size: 22,
|
|
5247
|
-
weight: 600,
|
|
5248
|
-
family: DISPLAY_FONT
|
|
5249
|
-
}));
|
|
5250
|
-
const cacheEcon = output.more?.cacheEconomics;
|
|
5337
|
+
c2y += 136;
|
|
5338
|
+
const cacheEcon = more?.cacheEconomics;
|
|
5251
5339
|
if (cacheEcon) {
|
|
5252
|
-
const
|
|
5253
|
-
{ label: "
|
|
5254
|
-
{ label: "
|
|
5340
|
+
const cacheStatItems = [
|
|
5341
|
+
{ label: "READS", value: formatNumber(cacheEcon.readTokens), highlight: false },
|
|
5342
|
+
{ label: "WRITES", value: formatNumber(cacheEcon.writeTokens), highlight: false }
|
|
5255
5343
|
];
|
|
5256
5344
|
if (cacheEcon.reuseRatio !== null && Number.isFinite(cacheEcon.reuseRatio)) {
|
|
5257
|
-
|
|
5258
|
-
}
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5345
|
+
cacheStatItems.push({ label: "REUSE", value: `${cacheEcon.reuseRatio.toFixed(1)}x`, highlight: true });
|
|
5346
|
+
}
|
|
5347
|
+
const cacheCardW = (col3W - (cacheStatItems.length - 1) * 6) / cacheStatItems.length;
|
|
5348
|
+
const cacheCardH = 52;
|
|
5349
|
+
for (let i = 0;i < cacheStatItems.length; i++) {
|
|
5350
|
+
const item = cacheStatItems[i];
|
|
5351
|
+
const ix = c2x + i * (cacheCardW + 6);
|
|
5352
|
+
const iy = c2y;
|
|
5353
|
+
parts.push(box(ix, iy, cacheCardW, cacheCardH, C.surface2, 2, {
|
|
5354
|
+
stroke: item.highlight ? C.borderHi : C.border,
|
|
5355
|
+
strokeWidth: 1
|
|
5267
5356
|
}));
|
|
5268
|
-
|
|
5269
|
-
fill:
|
|
5270
|
-
size:
|
|
5271
|
-
weight:
|
|
5272
|
-
family:
|
|
5357
|
+
parts.push(txt(ix + cacheCardW / 2, iy + 16, item.label, {
|
|
5358
|
+
fill: C.muted,
|
|
5359
|
+
size: 8,
|
|
5360
|
+
weight: 400,
|
|
5361
|
+
family: MONO,
|
|
5362
|
+
anchor: "middle",
|
|
5363
|
+
spacing: 1.5
|
|
5364
|
+
}));
|
|
5365
|
+
parts.push(txt(ix + cacheCardW / 2, iy + 38, item.value, {
|
|
5366
|
+
fill: item.highlight ? C.gold : C.ivory,
|
|
5367
|
+
size: 16,
|
|
5368
|
+
weight: 700,
|
|
5369
|
+
family: DISPLAY,
|
|
5370
|
+
anchor: "middle",
|
|
5371
|
+
spacing: -0.5
|
|
5273
5372
|
}));
|
|
5274
5373
|
}
|
|
5275
5374
|
}
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
fill: theme.subtitleColor,
|
|
5290
|
-
size: 16,
|
|
5291
|
-
weight: 500
|
|
5292
|
-
}));
|
|
5293
|
-
return { svg: p.join(`
|
|
5294
|
-
`), height };
|
|
5295
|
-
}
|
|
5296
|
-
const formattedDate = formatDateLong(stats.peakDay.date);
|
|
5297
|
-
p.push(svgText(PAD, 85, `${formattedDate} was your biggest day`, {
|
|
5298
|
-
fill: theme.narrativeColor,
|
|
5299
|
-
size: 20,
|
|
5300
|
-
weight: 500,
|
|
5301
|
-
family: DISPLAY_FONT
|
|
5302
|
-
}));
|
|
5303
|
-
p.push(svgText(PAD, 160, formatNumber(stats.peakDay.tokens), {
|
|
5304
|
-
fill: theme.goldAccent,
|
|
5305
|
-
size: 72,
|
|
5306
|
-
weight: 800,
|
|
5307
|
-
family: MONO_FONT,
|
|
5308
|
-
spacing: -3
|
|
5309
|
-
}));
|
|
5310
|
-
p.push(svgText(PAD, 190, "tokens in a single day", {
|
|
5311
|
-
fill: theme.subtitleColor,
|
|
5312
|
-
size: 14,
|
|
5313
|
-
weight: 500
|
|
5314
|
-
}));
|
|
5315
|
-
const badgeCx = WIDTH - 180;
|
|
5316
|
-
const badgeCy = 140;
|
|
5317
|
-
p.push(rect2(badgeCx - 40, badgeCy - 36, 80, 72, theme.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)", 6));
|
|
5318
|
-
p.push(cornerMark(badgeCx - 40, badgeCy - 36, 12, theme.goldAccent, "tl"));
|
|
5319
|
-
p.push(cornerMark(badgeCx + 40, badgeCy + 36, 12, theme.goldAccent, "br"));
|
|
5320
|
-
p.push(svgIconTrophy(badgeCx - 20, badgeCy - 20, 40, theme.goldAccent));
|
|
5321
|
-
p.push(rule(PAD, height - 1, WIDTH - PAD * 2, theme.mode === "dark" ? "#ffffff" : "#000000", 0.06));
|
|
5322
|
-
return { svg: p.join(`
|
|
5323
|
-
`), height };
|
|
5324
|
-
}
|
|
5325
|
-
function renderAchievementsSlide(output, theme) {
|
|
5326
|
-
const achievements = computeAchievements(output);
|
|
5327
|
-
const rows = Math.ceil(achievements.length / 2);
|
|
5328
|
-
const rowHeight = 80;
|
|
5329
|
-
const height = Math.max(200, 90 + rows * rowHeight + 20);
|
|
5330
|
-
const p = [];
|
|
5331
|
-
const gc = theme.sectionBgs[9] ?? ["#08080c", "#0c0c14"];
|
|
5332
|
-
p.push(sectionBg(0, height, gc, "achieve-bg"));
|
|
5333
|
-
p.push(sectionLabel(PAD, 40, "ACHIEVEMENTS UNLOCKED", theme.subtitleColor, theme.purpleAccent));
|
|
5334
|
-
if (achievements.length === 0) {
|
|
5335
|
-
p.push(svgText(PAD, 120, "Keep coding to unlock achievements!", {
|
|
5336
|
-
fill: theme.subtitleColor,
|
|
5337
|
-
size: 16,
|
|
5338
|
-
weight: 500
|
|
5339
|
-
}));
|
|
5340
|
-
return { svg: p.join(`
|
|
5341
|
-
`), height: 200 };
|
|
5342
|
-
}
|
|
5343
|
-
const colWidth = (WIDTH - PAD * 2 - 20) / 2;
|
|
5344
|
-
for (let i = 0;i < achievements.length; i++) {
|
|
5345
|
-
const a = achievements[i];
|
|
5346
|
-
const col = i % 2;
|
|
5347
|
-
const row = Math.floor(i / 2);
|
|
5348
|
-
const ax = PAD + col * (colWidth + 20);
|
|
5349
|
-
const ay = 70 + row * rowHeight;
|
|
5350
|
-
const cardBg = theme.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)";
|
|
5351
|
-
p.push(rect2(ax, ay, colWidth, 66, cardBg, 6));
|
|
5352
|
-
p.push(`<rect x="${ax}" y="${ay}" width="3" height="66" rx="1.5" fill="${escapeXml(a.color)}" opacity="0.7"/>`);
|
|
5353
|
-
p.push(renderIcon(a.icon, ax + 16, ay + 17, 32, a.color));
|
|
5354
|
-
p.push(svgText(ax + 60, ay + 28, a.title, {
|
|
5355
|
-
fill: theme.mode === "dark" ? "#f3f4f6" : "#1f2937",
|
|
5356
|
-
size: 15,
|
|
5357
|
-
weight: 700,
|
|
5358
|
-
family: DISPLAY_FONT
|
|
5375
|
+
const c3x = PAD + 2 * (col3W + 16);
|
|
5376
|
+
let c3y = y + 24;
|
|
5377
|
+
parts.push(sectionTag(c3x, c3y, "PEAK DAY", C));
|
|
5378
|
+
c3y += 20;
|
|
5379
|
+
if (stats.peakDay) {
|
|
5380
|
+
parts.push(box(c3x, c3y, col3W, 130, C.surface2, 2, { stroke: C.borderHi, strokeWidth: 1 }));
|
|
5381
|
+
parts.push(txt(c3x + col3W / 2, c3y + 24, formatDateLong(stats.peakDay.date).toUpperCase(), {
|
|
5382
|
+
fill: C.muted,
|
|
5383
|
+
size: 9,
|
|
5384
|
+
weight: 400,
|
|
5385
|
+
family: MONO,
|
|
5386
|
+
anchor: "middle",
|
|
5387
|
+
spacing: 2.5
|
|
5359
5388
|
}));
|
|
5360
|
-
|
|
5361
|
-
fill:
|
|
5362
|
-
size:
|
|
5363
|
-
weight:
|
|
5364
|
-
family:
|
|
5389
|
+
parts.push(txt(c3x + col3W / 2, c3y + 72, formatNumber(stats.peakDay.tokens), {
|
|
5390
|
+
fill: C.gold,
|
|
5391
|
+
size: 46,
|
|
5392
|
+
weight: 800,
|
|
5393
|
+
family: DISPLAY,
|
|
5394
|
+
anchor: "middle",
|
|
5395
|
+
spacing: -2
|
|
5365
5396
|
}));
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
const p = [];
|
|
5374
|
-
const gc = theme.sectionBgs[10] ?? ["#0a0c10", "#0e1014"];
|
|
5375
|
-
p.push(sectionBg(0, height, gc, "burn-bg"));
|
|
5376
|
-
p.push(sectionLabel(PAD, 40, "MONTHLY PROJECTION", theme.subtitleColor, theme.coolAccent));
|
|
5377
|
-
const burn = output.more?.monthlyBurn;
|
|
5378
|
-
if (!burn) {
|
|
5379
|
-
const avgDailyCost = output.aggregated.averageDailyCost;
|
|
5380
|
-
const projected = avgDailyCost * 30;
|
|
5381
|
-
p.push(svgText(PAD, 90, "At this rate, you will spend about", {
|
|
5382
|
-
fill: theme.narrativeColor,
|
|
5383
|
-
size: 18,
|
|
5384
|
-
weight: 500,
|
|
5385
|
-
family: DISPLAY_FONT
|
|
5397
|
+
parts.push(txt(c3x + col3W / 2, c3y + 94, "TOKENS IN ONE DAY", {
|
|
5398
|
+
fill: C.muted,
|
|
5399
|
+
size: 9,
|
|
5400
|
+
weight: 400,
|
|
5401
|
+
family: MONO,
|
|
5402
|
+
anchor: "middle",
|
|
5403
|
+
spacing: 2
|
|
5386
5404
|
}));
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5405
|
+
const multiplier = stats.averageDailyTokens > 0 ? (stats.peakDay.tokens / stats.averageDailyTokens).toFixed(1) : "0";
|
|
5406
|
+
parts.push(txt(c3x + col3W / 2, c3y + 118, `${multiplier}x your daily average`, {
|
|
5407
|
+
fill: C.muted,
|
|
5408
|
+
size: 11,
|
|
5409
|
+
weight: 400,
|
|
5410
|
+
family: BODY,
|
|
5411
|
+
anchor: "middle"
|
|
5393
5412
|
}));
|
|
5394
|
-
|
|
5395
|
-
|
|
5413
|
+
} else {
|
|
5414
|
+
parts.push(txt(c3x, c3y + 60, "No peak day data", {
|
|
5415
|
+
fill: C.muted,
|
|
5396
5416
|
size: 14,
|
|
5397
|
-
weight:
|
|
5417
|
+
weight: 400,
|
|
5418
|
+
family: BODY
|
|
5398
5419
|
}));
|
|
5399
|
-
return { svg: p.join(`
|
|
5400
|
-
`), height };
|
|
5401
5420
|
}
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5421
|
+
c3y += 148;
|
|
5422
|
+
const projectedCost = more?.monthlyBurn?.projectedCost ?? stats.averageDailyCost * 30;
|
|
5423
|
+
parts.push(txt(c3x, c3y, "PROJECTED / MONTH", {
|
|
5424
|
+
fill: C.muted,
|
|
5425
|
+
size: 9,
|
|
5426
|
+
weight: 400,
|
|
5427
|
+
family: MONO,
|
|
5428
|
+
spacing: 2
|
|
5407
5429
|
}));
|
|
5408
|
-
|
|
5409
|
-
fill:
|
|
5410
|
-
size:
|
|
5430
|
+
parts.push(txt(c3x, c3y + 28, formatCost2(projectedCost), {
|
|
5431
|
+
fill: C.ivory,
|
|
5432
|
+
size: 30,
|
|
5411
5433
|
weight: 800,
|
|
5412
|
-
family:
|
|
5413
|
-
spacing: -
|
|
5414
|
-
}));
|
|
5415
|
-
p.push(svgText(PAD, 185, "this month", {
|
|
5416
|
-
fill: theme.subtitleColor,
|
|
5417
|
-
size: 14,
|
|
5418
|
-
weight: 500
|
|
5434
|
+
family: DISPLAY,
|
|
5435
|
+
spacing: -1
|
|
5419
5436
|
}));
|
|
5420
|
-
const
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
const progress = burn.calendarDays > 0 ? burn.observedDays / burn.calendarDays : 0;
|
|
5424
|
-
const trackBg = theme.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)";
|
|
5425
|
-
p.push(rect2(PAD, barY, barWidth, barHeight, trackBg, 5));
|
|
5426
|
-
p.push(rect2(PAD, barY, Math.max(6, progress * barWidth), barHeight, theme.coolAccent, 5, { opacity: 0.7 }));
|
|
5427
|
-
p.push(svgText(PAD, barY + 30, `Based on ${burn.observedDays} of ${burn.calendarDays} days`, {
|
|
5428
|
-
fill: theme.subtitleColor,
|
|
5437
|
+
const avgDailyCostStr = stats.averageDailyCost >= 1 ? `$${stats.averageDailyCost.toFixed(2)}` : `$${stats.averageDailyCost.toFixed(4)}`;
|
|
5438
|
+
parts.push(txt(c3x, c3y + 44, `${avgDailyCostStr} avg/day`, {
|
|
5439
|
+
fill: C.muted,
|
|
5429
5440
|
size: 11,
|
|
5430
5441
|
weight: 400,
|
|
5431
|
-
family:
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
const
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5442
|
+
family: MONO,
|
|
5443
|
+
spacing: 0.5
|
|
5444
|
+
}));
|
|
5445
|
+
y += bottomH;
|
|
5446
|
+
const achH = 180;
|
|
5447
|
+
parts.push(box(0, y, WIDTH, achH, C.bg));
|
|
5448
|
+
parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
|
|
5449
|
+
const ALL_BADGES = [
|
|
5450
|
+
{ title: "Streak Master", sub: ">30d streak" },
|
|
5451
|
+
{ title: "Night Owl", sub: ">40% night" },
|
|
5452
|
+
{ title: "Big Spender", sub: ">$100 total" },
|
|
5453
|
+
{ title: "Cache Master", sub: ">50% hit rate" },
|
|
5454
|
+
{ title: "Daily Driver", sub: ">80% active" },
|
|
5455
|
+
{ title: "Power User", sub: ">10k avg/day" },
|
|
5456
|
+
{ title: "Summit Day", sub: "Peak >50k" },
|
|
5457
|
+
{ title: "Multi-Tool", sub: "3+ providers" },
|
|
5458
|
+
{ title: "Early Bird", sub: ">40% morning" },
|
|
5459
|
+
{ title: "Model Hopper", sub: "4+ models" }
|
|
5460
|
+
];
|
|
5461
|
+
const badgeTitleSet = new Set(ALL_BADGES.map((b) => b.title));
|
|
5462
|
+
const earnedTitles = new Set(achievements.filter((a) => badgeTitleSet.has(a.title)).map((a) => a.title));
|
|
5463
|
+
const earnedCount = earnedTitles.size;
|
|
5464
|
+
parts.push(sectionTag(PAD, y + 24, `ACHIEVEMENTS \xB7 ${earnedCount} UNLOCKED`, C));
|
|
5465
|
+
const badgeCols = 5;
|
|
5466
|
+
const badgeGapX = 10;
|
|
5467
|
+
const badgeGapY = 8;
|
|
5468
|
+
const badgeW = (INNER - (badgeCols - 1) * badgeGapX) / badgeCols;
|
|
5469
|
+
const badgeH = 54;
|
|
5470
|
+
const badgeStartY = y + 40;
|
|
5471
|
+
for (let i = 0;i < ALL_BADGES.length; i++) {
|
|
5472
|
+
const badge = ALL_BADGES[i];
|
|
5473
|
+
const isEarned = earnedTitles.has(badge.title);
|
|
5474
|
+
const col = i % badgeCols;
|
|
5475
|
+
const row = Math.floor(i / badgeCols);
|
|
5476
|
+
const bx = PAD + col * (badgeW + badgeGapX);
|
|
5477
|
+
const by = badgeStartY + row * (badgeH + badgeGapY);
|
|
5478
|
+
parts.push(`<g opacity="${isEarned ? 1 : 0.2}">`);
|
|
5479
|
+
parts.push(box(bx, by, badgeW, badgeH, C.surface2, 2, {
|
|
5480
|
+
stroke: isEarned ? C.borderHi : C.border,
|
|
5481
|
+
strokeWidth: 1
|
|
5482
|
+
}));
|
|
5483
|
+
parts.push(txt(bx + badgeW / 2, by + 24, badge.title, {
|
|
5484
|
+
fill: isEarned ? C.ivory : C.muted,
|
|
5485
|
+
size: 11,
|
|
5486
|
+
weight: 600,
|
|
5487
|
+
family: BODY,
|
|
5488
|
+
anchor: "middle"
|
|
5489
|
+
}));
|
|
5490
|
+
parts.push(txt(bx + badgeW / 2, by + 42, badge.sub, {
|
|
5491
|
+
fill: C.muted,
|
|
5492
|
+
size: 9,
|
|
5493
|
+
weight: 400,
|
|
5494
|
+
family: MONO,
|
|
5495
|
+
anchor: "middle",
|
|
5496
|
+
spacing: 0.5
|
|
5497
|
+
}));
|
|
5498
|
+
parts.push("</g>");
|
|
5499
|
+
}
|
|
5500
|
+
y += achH;
|
|
5501
|
+
const footH = 44;
|
|
5502
|
+
parts.push(box(0, y, WIDTH, footH, C.bg));
|
|
5503
|
+
parts.push(line(PAD, y, WIDTH - PAD, y, C.gold, 1, 0.15));
|
|
5504
|
+
const generatedTs = output.generated ? output.generated.replace("T", " ").slice(0, 19) + " UTC" : new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
5505
|
+
parts.push(txt(PAD, y + 28, `Generated ${generatedTs}`, {
|
|
5506
|
+
fill: C.muted2,
|
|
5507
|
+
size: 9,
|
|
5508
|
+
weight: 400,
|
|
5509
|
+
family: MONO,
|
|
5510
|
+
spacing: 1
|
|
5448
5511
|
}));
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
fill: theme.subtitleColor,
|
|
5512
|
+
parts.push(txt(WIDTH - PAD, y + 28, "tokenleak.devaa.dev", {
|
|
5513
|
+
fill: C.gold,
|
|
5452
5514
|
size: 11,
|
|
5453
|
-
weight: 400,
|
|
5454
|
-
family: MONO_FONT,
|
|
5455
|
-
opacity: 0.5
|
|
5456
|
-
}));
|
|
5457
|
-
p.push(svgText(WIDTH - PAD, 50, "tokenleak", {
|
|
5458
|
-
fill: theme.heroAccent,
|
|
5459
|
-
size: 16,
|
|
5460
5515
|
weight: 700,
|
|
5516
|
+
family: MONO,
|
|
5461
5517
|
anchor: "end",
|
|
5462
|
-
|
|
5463
|
-
|
|
5518
|
+
opacity: 0.5,
|
|
5519
|
+
spacing: 0.5
|
|
5464
5520
|
}));
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
function renderWrappedSlidesSvg(output, options) {
|
|
5469
|
-
const theme = getWrappedTheme(options.theme);
|
|
5470
|
-
const slides = [
|
|
5471
|
-
renderTitleSlide(output, theme),
|
|
5472
|
-
renderBigNumbersSlide(output, theme),
|
|
5473
|
-
renderStreakSlide(output, theme),
|
|
5474
|
-
renderTopModelSlide(output, theme),
|
|
5475
|
-
renderProviderMixSlide(output, theme),
|
|
5476
|
-
renderDayOfWeekSlide(output, theme),
|
|
5477
|
-
renderTimeOfDaySlide(output, theme),
|
|
5478
|
-
renderCacheSlide(output, theme),
|
|
5479
|
-
renderPeakDaySlide(output, theme),
|
|
5480
|
-
renderAchievementsSlide(output, theme),
|
|
5481
|
-
renderMonthlyBurnSlide(output, theme),
|
|
5482
|
-
renderFooterSlide(output, theme)
|
|
5483
|
-
].filter((s) => s.height > 0);
|
|
5484
|
-
let totalHeight = 0;
|
|
5485
|
-
const stackedSections = [];
|
|
5486
|
-
for (const slide of slides) {
|
|
5487
|
-
stackedSections.push(`<g transform="translate(0, ${totalHeight})">`, slide.svg, "</g>");
|
|
5488
|
-
totalHeight += slide.height;
|
|
5489
|
-
}
|
|
5490
|
-
const bgColor = theme.mode === "dark" ? "#060608" : "#fafaf9";
|
|
5521
|
+
y += footH;
|
|
5522
|
+
const totalHeight = y;
|
|
5523
|
+
const noiseOpacity = isDark ? 0.03 : 0.02;
|
|
5491
5524
|
return [
|
|
5492
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${totalHeight}" viewBox="0 0 ${WIDTH} ${totalHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility"
|
|
5493
|
-
`<rect width="${WIDTH}" height="${totalHeight}" fill="${
|
|
5494
|
-
|
|
5495
|
-
|
|
5525
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${totalHeight}" viewBox="0 0 ${WIDTH} ${totalHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility">`,
|
|
5526
|
+
`<rect width="${WIDTH}" height="${totalHeight}" fill="${C.bg}"/>`,
|
|
5527
|
+
noiseDef(isDark),
|
|
5528
|
+
`<rect width="${WIDTH}" height="${totalHeight}" fill="transparent" filter="url(#grain)" opacity="${noiseOpacity}" pointer-events="none"/>`,
|
|
5529
|
+
...parts,
|
|
5496
5530
|
"</svg>"
|
|
5497
5531
|
].join(`
|
|
5498
5532
|
`);
|
|
@@ -5519,7 +5553,7 @@ class PngRenderer {
|
|
|
5519
5553
|
import sharp2 from "sharp";
|
|
5520
5554
|
var PNG_DENSITY2 = 216;
|
|
5521
5555
|
async function renderWrappedPng(output, options) {
|
|
5522
|
-
const svgString =
|
|
5556
|
+
const svgString = renderWrappedSinglePageSvg(output, { theme: options.theme });
|
|
5523
5557
|
const pngBuffer = await sharp2(Buffer.from(svgString), {
|
|
5524
5558
|
density: PNG_DENSITY2
|
|
5525
5559
|
}).png({
|
|
@@ -5687,7 +5721,7 @@ function boxedHeader(title, width, noColor2) {
|
|
|
5687
5721
|
function renderSummaryParts(parts, width, noColor2) {
|
|
5688
5722
|
const left = parts.filter((_, index) => index % 2 === 0).map((part) => colorize256(part, SEMANTIC.INPUT, noColor2));
|
|
5689
5723
|
const right = parts.filter((_, index) => index % 2 === 1).map((part) => colorize256(part, SEMANTIC.OUTPUT, noColor2));
|
|
5690
|
-
return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((
|
|
5724
|
+
return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((line2) => ` ${line2}`).join(`
|
|
5691
5725
|
`);
|
|
5692
5726
|
}
|
|
5693
5727
|
function renderCompactDashboard(model, options) {
|
|
@@ -5723,7 +5757,7 @@ function renderCompactDashboard(model, options) {
|
|
|
5723
5757
|
} else if (model.activeProviders.length === 0) {
|
|
5724
5758
|
lines.push(" No provider activity in the selected range.");
|
|
5725
5759
|
}
|
|
5726
|
-
return lines.filter((
|
|
5760
|
+
return lines.filter((line2, index, array) => !(line2 === "" && array[index - 1] === "")).join(`
|
|
5727
5761
|
`);
|
|
5728
5762
|
}
|
|
5729
5763
|
|
|
@@ -6139,10 +6173,10 @@ function buildMonthHeader(model, visibleStartWeek, displayWeekCount, mode) {
|
|
|
6139
6173
|
nextFreeIndex = startIndex + marker.label.length + 1;
|
|
6140
6174
|
placedLabels += 1;
|
|
6141
6175
|
}
|
|
6142
|
-
const
|
|
6176
|
+
const line2 = header.some((cell) => cell !== " ") ? `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}` : null;
|
|
6143
6177
|
const uniqueVisibleMonths = visibleMarkers.map((marker) => `${marker.label} ${String(marker.year)}`).filter((value, index, values) => values.indexOf(value) === index);
|
|
6144
6178
|
const caption = placedLabels === 0 && uniqueVisibleMonths.length === 1 ? `${" ".repeat(DAY_LABEL_WIDTH2)}${uniqueVisibleMonths[0]}` : null;
|
|
6145
|
-
return { caption, line };
|
|
6179
|
+
return { caption, line: line2 };
|
|
6146
6180
|
}
|
|
6147
6181
|
function buildIntensityLegend(family, mode, noColor2) {
|
|
6148
6182
|
const cellWidth = getCellWidth(mode);
|
|
@@ -6191,12 +6225,12 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
6191
6225
|
const maxWeeks = Math.max(1, Math.floor(availableColumns / weekColWidth));
|
|
6192
6226
|
const displayWeeks = model.weeks.slice(Math.max(0, model.weeks.length - maxWeeks));
|
|
6193
6227
|
const visibleStartWeek = model.weeks.length - displayWeeks.length;
|
|
6194
|
-
const { caption, line } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length, mode);
|
|
6228
|
+
const { caption, line: line2 } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length, mode);
|
|
6195
6229
|
const lines = [];
|
|
6196
6230
|
if (caption)
|
|
6197
6231
|
lines.push(caption);
|
|
6198
|
-
if (
|
|
6199
|
-
lines.push(
|
|
6232
|
+
if (line2)
|
|
6233
|
+
lines.push(line2);
|
|
6200
6234
|
const gap = getGap(mode);
|
|
6201
6235
|
for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
|
|
6202
6236
|
const label = LABELED_DAYS[dayIndex] ?? "";
|
|
@@ -6269,8 +6303,8 @@ function renderSummary(parts, width, noColor2) {
|
|
|
6269
6303
|
if (parts.length === 0)
|
|
6270
6304
|
return [];
|
|
6271
6305
|
const colored = parts.map((part, index) => colorize256(part, index % 2 === 0 ? SEMANTIC.INPUT : SEMANTIC.OUTPUT, noColor2));
|
|
6272
|
-
const
|
|
6273
|
-
return [truncateVisible(` ${
|
|
6306
|
+
const line2 = colored.join(dim(" \xB7 ", noColor2));
|
|
6307
|
+
return [truncateVisible(` ${line2}`, width)];
|
|
6274
6308
|
}
|
|
6275
6309
|
function renderTrend(trend, width, noColor2) {
|
|
6276
6310
|
if (!trend)
|
|
@@ -6312,8 +6346,8 @@ function renderPatternList(title, entries, width, noColor2) {
|
|
|
6312
6346
|
const fillLength = Math.max(1, Math.round(entry.share * barWidth));
|
|
6313
6347
|
const fill = colorize256(BAR_CHAR.repeat(fillLength), SEMANTIC.OUTPUT, noColor2);
|
|
6314
6348
|
const track = TRACK_CHAR.repeat(Math.max(0, barWidth - fillLength));
|
|
6315
|
-
const
|
|
6316
|
-
lines.push(truncateVisible(
|
|
6349
|
+
const line2 = ` ${colorize256(truncateVisible(entry.label, nameWidth).padEnd(nameWidth), SEMANTIC.ACCENT, noColor2)} ${fill}${track} ${entry.value.padStart(valueWidth)}`;
|
|
6350
|
+
lines.push(truncateVisible(line2, width));
|
|
6317
6351
|
}
|
|
6318
6352
|
return lines;
|
|
6319
6353
|
}
|
|
@@ -6321,7 +6355,7 @@ function renderPatternColumns(provider, width, noColor2) {
|
|
|
6321
6355
|
const dayLines = renderPatternList("Day of Week", provider.dayOfWeek, Math.floor((width - 3) / 2), noColor2);
|
|
6322
6356
|
const modelLines = renderPatternList("Top Models", provider.topModels, Math.floor((width - 3) / 2), noColor2);
|
|
6323
6357
|
if (dayLines.length > 0 && modelLines.length > 0 && width >= 96) {
|
|
6324
|
-
return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((
|
|
6358
|
+
return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((line2) => ` ${line2}`);
|
|
6325
6359
|
}
|
|
6326
6360
|
return [...dayLines, ...dayLines.length > 0 && modelLines.length > 0 ? [""] : [], ...modelLines];
|
|
6327
6361
|
}
|
|
@@ -7298,8 +7332,8 @@ function renderRecommendation(rec, width, noColor2) {
|
|
|
7298
7332
|
const title = bold256(`${icon} ${rec.title}`, COLOR_TITLE, noColor2);
|
|
7299
7333
|
lines.push(`${indent}${title}`);
|
|
7300
7334
|
const descLines = wrapText(rec.description, contentWidth - 2);
|
|
7301
|
-
for (const
|
|
7302
|
-
lines.push(`${indent} ${dim(
|
|
7335
|
+
for (const line2 of descLines) {
|
|
7336
|
+
lines.push(`${indent} ${dim(line2, noColor2)}`);
|
|
7303
7337
|
}
|
|
7304
7338
|
if (rec.currentCost > 0 || rec.projectedCost > 0) {
|
|
7305
7339
|
const current = bold2(`${formatDollars(rec.currentCost)}/mo`, noColor2);
|
|
@@ -7850,7 +7884,7 @@ function guessProvider(model) {
|
|
|
7850
7884
|
return "Google";
|
|
7851
7885
|
return "Other";
|
|
7852
7886
|
}
|
|
7853
|
-
var
|
|
7887
|
+
var PROVIDER_COLORS2 = {
|
|
7854
7888
|
anthropic: "#d4af5f",
|
|
7855
7889
|
"claude-code": "#d4af5f",
|
|
7856
7890
|
openai: "#3a5070",
|
|
@@ -7858,8 +7892,8 @@ var PROVIDER_COLORS = {
|
|
|
7858
7892
|
google: "#6a2535",
|
|
7859
7893
|
pi: "#5a4a70"
|
|
7860
7894
|
};
|
|
7861
|
-
function
|
|
7862
|
-
return
|
|
7895
|
+
function getProviderColor2(provider) {
|
|
7896
|
+
return PROVIDER_COLORS2[provider.toLowerCase()] ?? "#555555";
|
|
7863
7897
|
}
|
|
7864
7898
|
function costValue(cost) {
|
|
7865
7899
|
const raw = formatCost2(cost);
|
|
@@ -7939,7 +7973,7 @@ function generateWrappedLiveHtml(output) {
|
|
|
7939
7973
|
const providerMix = providers.map((p) => ({
|
|
7940
7974
|
name: p.displayName,
|
|
7941
7975
|
pct: totalProviderTokens > 0 ? Math.round(p.totalTokens / totalProviderTokens * 100) : 0,
|
|
7942
|
-
color:
|
|
7976
|
+
color: getProviderColor2(p.provider)
|
|
7943
7977
|
})).sort((a, b) => b.pct - a.pct);
|
|
7944
7978
|
const pctSum = providerMix.reduce((s, p) => s + p.pct, 0);
|
|
7945
7979
|
if (providerMix.length > 0 && pctSum !== 100) {
|
|
@@ -8523,14 +8557,14 @@ async function startWrappedLiveServer(output, options = {}) {
|
|
|
8523
8557
|
throw new Error(`Could not find a free port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
8524
8558
|
}
|
|
8525
8559
|
// packages/cli/src/config.ts
|
|
8526
|
-
import { readFileSync as
|
|
8527
|
-
import { join as
|
|
8528
|
-
import { homedir as
|
|
8560
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
8561
|
+
import { join as join6 } from "path";
|
|
8562
|
+
import { homedir as homedir6 } from "os";
|
|
8529
8563
|
var CONFIG_FILENAME = ".tokenleakrc";
|
|
8530
8564
|
function loadConfig() {
|
|
8531
8565
|
try {
|
|
8532
|
-
const configPath =
|
|
8533
|
-
const raw =
|
|
8566
|
+
const configPath = join6(homedir6(), CONFIG_FILENAME);
|
|
8567
|
+
const raw = readFileSync3(configPath, "utf-8");
|
|
8534
8568
|
const parsed = JSON.parse(raw);
|
|
8535
8569
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
8536
8570
|
return parsed;
|
|
@@ -8578,107 +8612,754 @@ function resolveCompareRange(compareStr, currentRange) {
|
|
|
8578
8612
|
if (compareStr === "auto" || compareStr === "true" || compareStr === "") {
|
|
8579
8613
|
return computePreviousPeriod(currentRange);
|
|
8580
8614
|
}
|
|
8581
|
-
const parsed = parseCompareRange(compareStr);
|
|
8582
|
-
if (!parsed) {
|
|
8583
|
-
throw new TokenleakError(`Invalid --compare format: "${compareStr}". Use YYYY-MM-DD..YYYY-MM-DD or "auto".`);
|
|
8615
|
+
const parsed = parseCompareRange(compareStr);
|
|
8616
|
+
if (!parsed) {
|
|
8617
|
+
throw new TokenleakError(`Invalid --compare format: "${compareStr}". Use YYYY-MM-DD..YYYY-MM-DD or "auto".`);
|
|
8618
|
+
}
|
|
8619
|
+
return parsed;
|
|
8620
|
+
}
|
|
8621
|
+
async function loadAndAggregate(providers, range, allowEmpty = false) {
|
|
8622
|
+
const results = await Promise.all(providers.map(async (p) => {
|
|
8623
|
+
try {
|
|
8624
|
+
return await p.load(range);
|
|
8625
|
+
} catch {
|
|
8626
|
+
return null;
|
|
8627
|
+
}
|
|
8628
|
+
}));
|
|
8629
|
+
const providerDataList = results.filter((r) => r !== null);
|
|
8630
|
+
if (!allowEmpty && providerDataList.length === 0) {
|
|
8631
|
+
throw new TokenleakError("No provider data found");
|
|
8632
|
+
}
|
|
8633
|
+
const mergedDaily = mergeProviderData(providerDataList);
|
|
8634
|
+
const stats = aggregate(mergedDaily, range.until);
|
|
8635
|
+
return { data: providerDataList, stats };
|
|
8636
|
+
}
|
|
8637
|
+
async function loadCompareTokenleakData(providers, currentRange, compareStr) {
|
|
8638
|
+
const previousRange = resolveCompareRange(compareStr, currentRange);
|
|
8639
|
+
const [currentResult, previousResult] = await Promise.all([
|
|
8640
|
+
loadAndAggregate(providers, currentRange),
|
|
8641
|
+
loadAndAggregate(providers, previousRange, true)
|
|
8642
|
+
]);
|
|
8643
|
+
const compareOutput = buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
|
|
8644
|
+
return {
|
|
8645
|
+
compareOutput,
|
|
8646
|
+
currentData: currentResult.data,
|
|
8647
|
+
previousData: previousResult.data,
|
|
8648
|
+
output: {
|
|
8649
|
+
schemaVersion: SCHEMA_VERSION,
|
|
8650
|
+
generated: new Date().toISOString(),
|
|
8651
|
+
dateRange: currentRange,
|
|
8652
|
+
providers: currentResult.data,
|
|
8653
|
+
aggregated: currentResult.stats,
|
|
8654
|
+
more: buildMoreStats(currentResult.data, currentRange, {
|
|
8655
|
+
previousRange,
|
|
8656
|
+
previousProviders: previousResult.data,
|
|
8657
|
+
previousStats: compareOutput.periodB.stats,
|
|
8658
|
+
deltas: compareOutput.deltas
|
|
8659
|
+
})
|
|
8660
|
+
}
|
|
8661
|
+
};
|
|
8662
|
+
}
|
|
8663
|
+
|
|
8664
|
+
// packages/cli/src/date-range.ts
|
|
8665
|
+
var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
|
8666
|
+
function isValidDate(dateStr) {
|
|
8667
|
+
if (!DATE_FORMAT.test(dateStr))
|
|
8668
|
+
return false;
|
|
8669
|
+
const d = new Date(dateStr + "T00:00:00Z");
|
|
8670
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
|
|
8671
|
+
}
|
|
8672
|
+
function computeDateRange(args) {
|
|
8673
|
+
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
8674
|
+
if (args.until && !isValidDate(args.until)) {
|
|
8675
|
+
throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
|
|
8676
|
+
}
|
|
8677
|
+
if (args.since && !isValidDate(args.since)) {
|
|
8678
|
+
throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
|
|
8679
|
+
}
|
|
8680
|
+
let since;
|
|
8681
|
+
if (args.since) {
|
|
8682
|
+
since = args.since;
|
|
8683
|
+
} else {
|
|
8684
|
+
const daysBack = args.days ?? DEFAULT_DAYS;
|
|
8685
|
+
const d = new Date(until);
|
|
8686
|
+
d.setDate(d.getDate() - daysBack);
|
|
8687
|
+
since = d.toISOString().slice(0, 10);
|
|
8688
|
+
}
|
|
8689
|
+
if (since > until) {
|
|
8690
|
+
throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
|
|
8691
|
+
}
|
|
8692
|
+
return { since, until };
|
|
8693
|
+
}
|
|
8694
|
+
|
|
8695
|
+
// packages/cli/src/env.ts
|
|
8696
|
+
var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
|
|
8697
|
+
var VALID_THEMES = new Set(["dark", "light"]);
|
|
8698
|
+
function loadEnvOverrides() {
|
|
8699
|
+
const overrides = {};
|
|
8700
|
+
const format = process.env["TOKENLEAK_FORMAT"];
|
|
8701
|
+
if (format && VALID_FORMATS.has(format)) {
|
|
8702
|
+
overrides.format = format;
|
|
8703
|
+
}
|
|
8704
|
+
const theme = process.env["TOKENLEAK_THEME"];
|
|
8705
|
+
if (theme && VALID_THEMES.has(theme)) {
|
|
8706
|
+
overrides.theme = theme;
|
|
8707
|
+
}
|
|
8708
|
+
const days = process.env["TOKENLEAK_DAYS"];
|
|
8709
|
+
if (days !== undefined && days !== "") {
|
|
8710
|
+
const parsed = Number(days);
|
|
8711
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
8712
|
+
overrides.days = parsed;
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
return overrides;
|
|
8716
|
+
}
|
|
8717
|
+
|
|
8718
|
+
// packages/cli/src/cursor.ts
|
|
8719
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync5, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
8720
|
+
import { createHash } from "crypto";
|
|
8721
|
+
import { homedir as homedir7 } from "os";
|
|
8722
|
+
import { basename as basename2, dirname as dirname4, join as join7 } from "path";
|
|
8723
|
+
import { createInterface } from "readline/promises";
|
|
8724
|
+
import { stdin as input, stdout as output } from "process";
|
|
8725
|
+
var CURSOR_USAGE_CSV_ENDPOINT = "https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens";
|
|
8726
|
+
var CURSOR_USAGE_SUMMARY_ENDPOINT = "https://cursor.com/api/usage-summary";
|
|
8727
|
+
function getCursorRootDir() {
|
|
8728
|
+
return process.env["TOKENLEAK_CURSOR_DIR"] ?? join7(homedir7(), ".config", "tokenleak");
|
|
8729
|
+
}
|
|
8730
|
+
function getCursorCredentialsPath() {
|
|
8731
|
+
return join7(getCursorRootDir(), "cursor-credentials.json");
|
|
8732
|
+
}
|
|
8733
|
+
function getCursorCacheDir() {
|
|
8734
|
+
return join7(getCursorRootDir(), "cursor-cache");
|
|
8735
|
+
}
|
|
8736
|
+
function ensureDir(dirPath, mode) {
|
|
8737
|
+
if (!existsSync6(dirPath)) {
|
|
8738
|
+
mkdirSync(dirPath, { recursive: true });
|
|
8739
|
+
}
|
|
8740
|
+
if (mode !== undefined && process.platform !== "win32") {
|
|
8741
|
+
chmodSync(dirPath, mode);
|
|
8742
|
+
}
|
|
8743
|
+
}
|
|
8744
|
+
function atomicWriteFile(path, contents, mode) {
|
|
8745
|
+
const dir = dirname4(path);
|
|
8746
|
+
ensureDir(dir);
|
|
8747
|
+
const tempPath = join7(dir, `.tmp-${basename2(path)}-${process.pid}`);
|
|
8748
|
+
writeFileSync(tempPath, contents, "utf8");
|
|
8749
|
+
if (mode !== undefined && process.platform !== "win32") {
|
|
8750
|
+
chmodSync(tempPath, mode);
|
|
8751
|
+
}
|
|
8752
|
+
renameSync(tempPath, path);
|
|
8753
|
+
}
|
|
8754
|
+
function buildCursorHeaders(sessionToken) {
|
|
8755
|
+
return {
|
|
8756
|
+
Accept: "*/*",
|
|
8757
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
8758
|
+
Cookie: `WorkosCursorSessionToken=${sessionToken}`,
|
|
8759
|
+
Referer: "https://www.cursor.com/settings",
|
|
8760
|
+
"User-Agent": `tokenleak/${VERSION} (+https://github.com/ya-nsh/tokenleak)`
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
function sanitizeAccountIdForFilename(accountId) {
|
|
8764
|
+
const sanitized = accountId.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8765
|
+
return sanitized.length > 0 ? sanitized.slice(0, 80) : "account";
|
|
8766
|
+
}
|
|
8767
|
+
function extractUserIdFromSessionToken(token) {
|
|
8768
|
+
const trimmed = token.trim();
|
|
8769
|
+
if (trimmed.includes("%3A%3A")) {
|
|
8770
|
+
return trimmed.split("%3A%3A")[0]?.trim() || undefined;
|
|
8771
|
+
}
|
|
8772
|
+
if (trimmed.includes("::")) {
|
|
8773
|
+
return trimmed.split("::")[0]?.trim() || undefined;
|
|
8774
|
+
}
|
|
8775
|
+
return;
|
|
8776
|
+
}
|
|
8777
|
+
function deriveAccountId(sessionToken) {
|
|
8778
|
+
const userId = extractUserIdFromSessionToken(sessionToken);
|
|
8779
|
+
if (userId) {
|
|
8780
|
+
return userId;
|
|
8781
|
+
}
|
|
8782
|
+
const hash = createHash("sha256").update(sessionToken).digest("hex");
|
|
8783
|
+
return `anon-${hash.slice(0, 12)}`;
|
|
8784
|
+
}
|
|
8785
|
+
function countCursorCsvRows(csvText) {
|
|
8786
|
+
const rows = csvText.split(/\r?\n/).map((line2) => line2.trim()).filter((line2) => line2.length > 0);
|
|
8787
|
+
return rows.length > 0 ? rows.length - 1 : 0;
|
|
8788
|
+
}
|
|
8789
|
+
function archiveCacheFile(path, label) {
|
|
8790
|
+
if (!existsSync6(path)) {
|
|
8791
|
+
return;
|
|
8792
|
+
}
|
|
8793
|
+
const archiveDir = join7(getCursorCacheDir(), "archive");
|
|
8794
|
+
ensureDir(archiveDir, 448);
|
|
8795
|
+
const timestamp = new Date().toISOString().replaceAll(":", "-");
|
|
8796
|
+
renameSync(path, join7(archiveDir, `${sanitizeAccountIdForFilename(label)}-${timestamp}.csv`));
|
|
8797
|
+
}
|
|
8798
|
+
function resolveAccountId(store, nameOrId) {
|
|
8799
|
+
const needle = nameOrId.trim();
|
|
8800
|
+
if (!needle) {
|
|
8801
|
+
return;
|
|
8802
|
+
}
|
|
8803
|
+
if (store.accounts[needle]) {
|
|
8804
|
+
return needle;
|
|
8805
|
+
}
|
|
8806
|
+
const lowered = needle.toLowerCase();
|
|
8807
|
+
for (const [id, account] of Object.entries(store.accounts)) {
|
|
8808
|
+
if (account.label?.toLowerCase() === lowered) {
|
|
8809
|
+
return id;
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8812
|
+
return;
|
|
8813
|
+
}
|
|
8814
|
+
async function readSecret(prompt) {
|
|
8815
|
+
if (!input.isTTY || !output.isTTY) {
|
|
8816
|
+
const rl = createInterface({ input, output });
|
|
8817
|
+
try {
|
|
8818
|
+
return (await rl.question(prompt)).trim();
|
|
8819
|
+
} finally {
|
|
8820
|
+
rl.close();
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
output.write(prompt);
|
|
8824
|
+
input.resume();
|
|
8825
|
+
input.setEncoding("utf8");
|
|
8826
|
+
if (typeof input.setRawMode === "function") {
|
|
8827
|
+
input.setRawMode(true);
|
|
8828
|
+
}
|
|
8829
|
+
return await new Promise((resolve, reject) => {
|
|
8830
|
+
let value = "";
|
|
8831
|
+
const cleanup = () => {
|
|
8832
|
+
input.off("data", onData);
|
|
8833
|
+
if (typeof input.setRawMode === "function") {
|
|
8834
|
+
input.setRawMode(false);
|
|
8835
|
+
}
|
|
8836
|
+
input.pause();
|
|
8837
|
+
output.write(`
|
|
8838
|
+
`);
|
|
8839
|
+
};
|
|
8840
|
+
const onData = (chunk) => {
|
|
8841
|
+
const text2 = String(chunk);
|
|
8842
|
+
for (const char of text2) {
|
|
8843
|
+
if (char === "\x03") {
|
|
8844
|
+
cleanup();
|
|
8845
|
+
reject(new TokenleakError("Cancelled"));
|
|
8846
|
+
return;
|
|
8847
|
+
}
|
|
8848
|
+
if (char === "\r" || char === `
|
|
8849
|
+
`) {
|
|
8850
|
+
cleanup();
|
|
8851
|
+
resolve(value.trim());
|
|
8852
|
+
return;
|
|
8853
|
+
}
|
|
8854
|
+
if (char === "\b" || char === "\x7F") {
|
|
8855
|
+
value = value.slice(0, -1);
|
|
8856
|
+
continue;
|
|
8857
|
+
}
|
|
8858
|
+
value += char;
|
|
8859
|
+
}
|
|
8860
|
+
};
|
|
8861
|
+
input.on("data", onData);
|
|
8862
|
+
});
|
|
8863
|
+
}
|
|
8864
|
+
function loadCursorCredentialsStore() {
|
|
8865
|
+
const path = getCursorCredentialsPath();
|
|
8866
|
+
if (!existsSync6(path)) {
|
|
8867
|
+
return null;
|
|
8868
|
+
}
|
|
8869
|
+
try {
|
|
8870
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
8871
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.activeAccountId !== "string" || typeof parsed.accounts !== "object" || parsed.accounts === null) {
|
|
8872
|
+
return null;
|
|
8873
|
+
}
|
|
8874
|
+
return {
|
|
8875
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
8876
|
+
activeAccountId: parsed.activeAccountId,
|
|
8877
|
+
accounts: parsed.accounts
|
|
8878
|
+
};
|
|
8879
|
+
} catch {
|
|
8880
|
+
return null;
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
function saveCursorCredentialsStore(store) {
|
|
8884
|
+
ensureDir(getCursorRootDir(), 448);
|
|
8885
|
+
atomicWriteFile(getCursorCredentialsPath(), `${JSON.stringify(store, null, 2)}
|
|
8886
|
+
`, process.platform === "win32" ? undefined : 384);
|
|
8887
|
+
}
|
|
8888
|
+
function listCursorAccounts() {
|
|
8889
|
+
const store = loadCursorCredentialsStore();
|
|
8890
|
+
if (!store) {
|
|
8891
|
+
return [];
|
|
8892
|
+
}
|
|
8893
|
+
return Object.entries(store.accounts).map(([id, account]) => ({
|
|
8894
|
+
id,
|
|
8895
|
+
label: account.label,
|
|
8896
|
+
userId: account.userId,
|
|
8897
|
+
createdAt: account.createdAt,
|
|
8898
|
+
isActive: id === store.activeAccountId
|
|
8899
|
+
})).sort((left, right) => {
|
|
8900
|
+
if (left.isActive !== right.isActive) {
|
|
8901
|
+
return left.isActive ? -1 : 1;
|
|
8902
|
+
}
|
|
8903
|
+
return (left.label ?? left.id).localeCompare(right.label ?? right.id);
|
|
8904
|
+
});
|
|
8905
|
+
}
|
|
8906
|
+
function hasCursorUsageCache() {
|
|
8907
|
+
const cacheDir = getCursorCacheDir();
|
|
8908
|
+
if (!existsSync6(cacheDir)) {
|
|
8909
|
+
return false;
|
|
8910
|
+
}
|
|
8911
|
+
return readdirSync5(cacheDir).some((entry) => {
|
|
8912
|
+
if (entry === "archive") {
|
|
8913
|
+
return false;
|
|
8914
|
+
}
|
|
8915
|
+
return entry === "usage.csv" || /^usage\.[^.].*\.csv$/.test(entry);
|
|
8916
|
+
});
|
|
8917
|
+
}
|
|
8918
|
+
function isCursorLoggedIn() {
|
|
8919
|
+
const store = loadCursorCredentialsStore();
|
|
8920
|
+
return store !== null && Object.keys(store.accounts).length > 0;
|
|
8921
|
+
}
|
|
8922
|
+
function saveCursorCredentials(token, label) {
|
|
8923
|
+
const accountId = deriveAccountId(token);
|
|
8924
|
+
const store = loadCursorCredentialsStore() ?? {
|
|
8925
|
+
version: 1,
|
|
8926
|
+
activeAccountId: accountId,
|
|
8927
|
+
accounts: {}
|
|
8928
|
+
};
|
|
8929
|
+
const normalizedLabel = label?.trim();
|
|
8930
|
+
if (normalizedLabel) {
|
|
8931
|
+
for (const [id, account] of Object.entries(store.accounts)) {
|
|
8932
|
+
if (id === accountId) {
|
|
8933
|
+
continue;
|
|
8934
|
+
}
|
|
8935
|
+
if (account.label?.trim().toLowerCase() === normalizedLabel.toLowerCase()) {
|
|
8936
|
+
throw new TokenleakError(`Cursor account label already exists: ${normalizedLabel}`);
|
|
8937
|
+
}
|
|
8938
|
+
}
|
|
8939
|
+
}
|
|
8940
|
+
store.accounts[accountId] = {
|
|
8941
|
+
sessionToken: token,
|
|
8942
|
+
userId: extractUserIdFromSessionToken(token),
|
|
8943
|
+
createdAt: new Date().toISOString(),
|
|
8944
|
+
label: normalizedLabel
|
|
8945
|
+
};
|
|
8946
|
+
store.activeAccountId = accountId;
|
|
8947
|
+
saveCursorCredentialsStore(store);
|
|
8948
|
+
return accountId;
|
|
8949
|
+
}
|
|
8950
|
+
function removeCursorAccount(nameOrId, purgeCache) {
|
|
8951
|
+
const store = loadCursorCredentialsStore();
|
|
8952
|
+
if (!store) {
|
|
8953
|
+
throw new TokenleakError("No saved Cursor accounts");
|
|
8954
|
+
}
|
|
8955
|
+
const accountId = resolveAccountId(store, nameOrId);
|
|
8956
|
+
if (!accountId) {
|
|
8957
|
+
throw new TokenleakError(`Account not found: ${nameOrId}`);
|
|
8958
|
+
}
|
|
8959
|
+
const wasActive = accountId === store.activeAccountId;
|
|
8960
|
+
const cacheDir = getCursorCacheDir();
|
|
8961
|
+
const accountCachePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
|
|
8962
|
+
const activeCachePath = join7(cacheDir, "usage.csv");
|
|
8963
|
+
if (existsSync6(accountCachePath)) {
|
|
8964
|
+
if (purgeCache) {
|
|
8965
|
+
unlinkSync(accountCachePath);
|
|
8966
|
+
} else {
|
|
8967
|
+
archiveCacheFile(accountCachePath, `usage.${accountId}`);
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
if (wasActive && existsSync6(activeCachePath)) {
|
|
8971
|
+
if (purgeCache) {
|
|
8972
|
+
unlinkSync(activeCachePath);
|
|
8973
|
+
} else {
|
|
8974
|
+
archiveCacheFile(activeCachePath, `usage.active.${accountId}`);
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
delete store.accounts[accountId];
|
|
8978
|
+
const remainingIds = Object.keys(store.accounts);
|
|
8979
|
+
if (remainingIds.length === 0) {
|
|
8980
|
+
if (existsSync6(getCursorCredentialsPath())) {
|
|
8981
|
+
unlinkSync(getCursorCredentialsPath());
|
|
8982
|
+
}
|
|
8983
|
+
return;
|
|
8984
|
+
}
|
|
8985
|
+
if (wasActive) {
|
|
8986
|
+
const nextAccountId = remainingIds[0];
|
|
8987
|
+
store.activeAccountId = nextAccountId;
|
|
8988
|
+
const nextCachePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(nextAccountId)}.csv`);
|
|
8989
|
+
if (existsSync6(nextCachePath)) {
|
|
8990
|
+
renameSync(nextCachePath, activeCachePath);
|
|
8991
|
+
}
|
|
8992
|
+
}
|
|
8993
|
+
saveCursorCredentialsStore(store);
|
|
8994
|
+
}
|
|
8995
|
+
function removeAllCursorAccounts(purgeCache) {
|
|
8996
|
+
const cacheDir = getCursorCacheDir();
|
|
8997
|
+
if (existsSync6(cacheDir)) {
|
|
8998
|
+
for (const entry of readdirSync5(cacheDir)) {
|
|
8999
|
+
if (entry === "archive") {
|
|
9000
|
+
continue;
|
|
9001
|
+
}
|
|
9002
|
+
if (entry === "usage.csv" || /^usage\.[^.].*\.csv$/.test(entry)) {
|
|
9003
|
+
const fullPath = join7(cacheDir, entry);
|
|
9004
|
+
if (purgeCache) {
|
|
9005
|
+
rmSync(fullPath, { force: true });
|
|
9006
|
+
} else {
|
|
9007
|
+
archiveCacheFile(fullPath, `usage.all.${entry}`);
|
|
9008
|
+
}
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
if (existsSync6(getCursorCredentialsPath())) {
|
|
9013
|
+
unlinkSync(getCursorCredentialsPath());
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
function setActiveCursorAccount(nameOrId) {
|
|
9017
|
+
const store = loadCursorCredentialsStore();
|
|
9018
|
+
if (!store) {
|
|
9019
|
+
throw new TokenleakError("No saved Cursor accounts");
|
|
9020
|
+
}
|
|
9021
|
+
const accountId = resolveAccountId(store, nameOrId);
|
|
9022
|
+
if (!accountId) {
|
|
9023
|
+
throw new TokenleakError(`Account not found: ${nameOrId}`);
|
|
9024
|
+
}
|
|
9025
|
+
if (accountId === store.activeAccountId) {
|
|
9026
|
+
return;
|
|
9027
|
+
}
|
|
9028
|
+
const cacheDir = getCursorCacheDir();
|
|
9029
|
+
const activeCachePath = join7(cacheDir, "usage.csv");
|
|
9030
|
+
const oldActivePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(store.activeAccountId)}.csv`);
|
|
9031
|
+
const newActivePath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
|
|
9032
|
+
if (existsSync6(activeCachePath)) {
|
|
9033
|
+
if (existsSync6(oldActivePath)) {
|
|
9034
|
+
archiveCacheFile(oldActivePath, `usage.${store.activeAccountId}`);
|
|
9035
|
+
}
|
|
9036
|
+
renameSync(activeCachePath, oldActivePath);
|
|
9037
|
+
}
|
|
9038
|
+
if (existsSync6(newActivePath)) {
|
|
9039
|
+
renameSync(newActivePath, activeCachePath);
|
|
9040
|
+
}
|
|
9041
|
+
store.activeAccountId = accountId;
|
|
9042
|
+
saveCursorCredentialsStore(store);
|
|
9043
|
+
}
|
|
9044
|
+
function getActiveCursorCredentials() {
|
|
9045
|
+
const store = loadCursorCredentialsStore();
|
|
9046
|
+
if (!store) {
|
|
9047
|
+
return null;
|
|
9048
|
+
}
|
|
9049
|
+
return store.accounts[store.activeAccountId] ?? null;
|
|
9050
|
+
}
|
|
9051
|
+
function getCursorCredentialsFor(nameOrId) {
|
|
9052
|
+
const store = loadCursorCredentialsStore();
|
|
9053
|
+
if (!store) {
|
|
9054
|
+
return null;
|
|
9055
|
+
}
|
|
9056
|
+
const accountId = resolveAccountId(store, nameOrId);
|
|
9057
|
+
return accountId ? store.accounts[accountId] ?? null : null;
|
|
9058
|
+
}
|
|
9059
|
+
async function validateCursorSession(sessionToken) {
|
|
9060
|
+
let response;
|
|
9061
|
+
try {
|
|
9062
|
+
response = await fetch(CURSOR_USAGE_SUMMARY_ENDPOINT, {
|
|
9063
|
+
headers: buildCursorHeaders(sessionToken)
|
|
9064
|
+
});
|
|
9065
|
+
} catch (error) {
|
|
9066
|
+
return {
|
|
9067
|
+
valid: false,
|
|
9068
|
+
error: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`
|
|
9069
|
+
};
|
|
9070
|
+
}
|
|
9071
|
+
if (response.status === 401 || response.status === 403) {
|
|
9072
|
+
return {
|
|
9073
|
+
valid: false,
|
|
9074
|
+
error: "Session token expired or invalid"
|
|
9075
|
+
};
|
|
9076
|
+
}
|
|
9077
|
+
if (!response.ok) {
|
|
9078
|
+
return {
|
|
9079
|
+
valid: false,
|
|
9080
|
+
error: `API returned status ${response.status}`
|
|
9081
|
+
};
|
|
9082
|
+
}
|
|
9083
|
+
try {
|
|
9084
|
+
const payload = await response.json();
|
|
9085
|
+
const billingCycleStart = payload["billingCycleStart"];
|
|
9086
|
+
const billingCycleEnd = payload["billingCycleEnd"];
|
|
9087
|
+
if (typeof billingCycleStart !== "string" || typeof billingCycleEnd !== "string") {
|
|
9088
|
+
return {
|
|
9089
|
+
valid: false,
|
|
9090
|
+
error: "Invalid response format"
|
|
9091
|
+
};
|
|
9092
|
+
}
|
|
9093
|
+
return {
|
|
9094
|
+
valid: true,
|
|
9095
|
+
membershipType: typeof payload["membershipType"] === "string" ? payload["membershipType"] : undefined
|
|
9096
|
+
};
|
|
9097
|
+
} catch (error) {
|
|
9098
|
+
return {
|
|
9099
|
+
valid: false,
|
|
9100
|
+
error: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`
|
|
9101
|
+
};
|
|
9102
|
+
}
|
|
9103
|
+
}
|
|
9104
|
+
async function fetchCursorUsageCsv(sessionToken) {
|
|
9105
|
+
const response = await fetch(CURSOR_USAGE_CSV_ENDPOINT, {
|
|
9106
|
+
headers: buildCursorHeaders(sessionToken)
|
|
9107
|
+
});
|
|
9108
|
+
if (response.status === 401 || response.status === 403) {
|
|
9109
|
+
throw new TokenleakError("Cursor session expired. Please run 'tokenleak cursor login' to re-authenticate.");
|
|
9110
|
+
}
|
|
9111
|
+
if (!response.ok) {
|
|
9112
|
+
throw new TokenleakError(`Cursor API returned status ${response.status}`);
|
|
9113
|
+
}
|
|
9114
|
+
const text2 = await response.text();
|
|
9115
|
+
if (!text2.startsWith("Date,")) {
|
|
9116
|
+
throw new TokenleakError("Invalid response from Cursor API - expected CSV format");
|
|
8584
9117
|
}
|
|
8585
|
-
return
|
|
9118
|
+
return text2;
|
|
8586
9119
|
}
|
|
8587
|
-
async function
|
|
8588
|
-
const
|
|
9120
|
+
async function syncCursorCache() {
|
|
9121
|
+
const store = loadCursorCredentialsStore();
|
|
9122
|
+
if (!store || Object.keys(store.accounts).length === 0) {
|
|
9123
|
+
return { synced: false, rows: 0, error: "Not authenticated" };
|
|
9124
|
+
}
|
|
9125
|
+
const cacheDir = getCursorCacheDir();
|
|
9126
|
+
ensureDir(cacheDir, 448);
|
|
9127
|
+
let totalRows = 0;
|
|
9128
|
+
let successCount = 0;
|
|
9129
|
+
const errors = [];
|
|
9130
|
+
for (const [accountId, credentials] of Object.entries(store.accounts)) {
|
|
8589
9131
|
try {
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
9132
|
+
const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
|
|
9133
|
+
const filePath = accountId === store.activeAccountId ? join7(cacheDir, "usage.csv") : join7(cacheDir, `usage.${sanitizeAccountIdForFilename(accountId)}.csv`);
|
|
9134
|
+
atomicWriteFile(filePath, csvText, process.platform === "win32" ? undefined : 384);
|
|
9135
|
+
if (accountId === store.activeAccountId) {
|
|
9136
|
+
const activeDupPath = join7(cacheDir, `usage.${sanitizeAccountIdForFilename(store.activeAccountId)}.csv`);
|
|
9137
|
+
if (existsSync6(activeDupPath)) {
|
|
9138
|
+
unlinkSync(activeDupPath);
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
successCount += 1;
|
|
9142
|
+
totalRows += countCursorCsvRows(csvText);
|
|
9143
|
+
} catch (error) {
|
|
9144
|
+
errors.push(`${accountId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8593
9145
|
}
|
|
8594
|
-
}));
|
|
8595
|
-
const providerDataList = results.filter((r) => r !== null);
|
|
8596
|
-
if (!allowEmpty && providerDataList.length === 0) {
|
|
8597
|
-
throw new TokenleakError("No provider data found");
|
|
8598
9146
|
}
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
9147
|
+
if (successCount === 0) {
|
|
9148
|
+
return {
|
|
9149
|
+
synced: false,
|
|
9150
|
+
rows: 0,
|
|
9151
|
+
error: errors[0] ?? "Cursor sync failed"
|
|
9152
|
+
};
|
|
9153
|
+
}
|
|
9154
|
+
return {
|
|
9155
|
+
synced: true,
|
|
9156
|
+
rows: totalRows,
|
|
9157
|
+
error: errors.length > 0 ? `Some accounts failed to sync (${errors.length}/${Object.keys(store.accounts).length})` : undefined
|
|
9158
|
+
};
|
|
8602
9159
|
}
|
|
8603
|
-
async function
|
|
8604
|
-
const
|
|
8605
|
-
const
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
9160
|
+
async function shouldSyncCursorForRun(config) {
|
|
9161
|
+
const hasProviderFilter = Boolean(config.provider || config.cursor || config.claude || config.codex || config.pi || config.openCode);
|
|
9162
|
+
const requestedCursor = config.cursor || (config.provider?.split(",").some((token) => {
|
|
9163
|
+
const normalized = token.trim().toLowerCase().replace(/\s+/g, "-");
|
|
9164
|
+
return normalized === "cursor" || normalized === "cursor-ide" || normalized === "cursoride";
|
|
9165
|
+
}) ?? false);
|
|
9166
|
+
if (!isCursorLoggedIn()) {
|
|
9167
|
+
return { attempted: false };
|
|
9168
|
+
}
|
|
9169
|
+
if (!requestedCursor && hasProviderFilter && !config.allProviders) {
|
|
9170
|
+
return { attempted: false };
|
|
9171
|
+
}
|
|
9172
|
+
const result = await syncCursorCache();
|
|
8610
9173
|
return {
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
previousData: previousResult.data,
|
|
8614
|
-
output: {
|
|
8615
|
-
schemaVersion: SCHEMA_VERSION,
|
|
8616
|
-
generated: new Date().toISOString(),
|
|
8617
|
-
dateRange: currentRange,
|
|
8618
|
-
providers: currentResult.data,
|
|
8619
|
-
aggregated: currentResult.stats,
|
|
8620
|
-
more: buildMoreStats(currentResult.data, currentRange, {
|
|
8621
|
-
previousRange,
|
|
8622
|
-
previousProviders: previousResult.data,
|
|
8623
|
-
previousStats: compareOutput.periodB.stats,
|
|
8624
|
-
deltas: compareOutput.deltas
|
|
8625
|
-
})
|
|
8626
|
-
}
|
|
9174
|
+
attempted: true,
|
|
9175
|
+
error: result.error
|
|
8627
9176
|
};
|
|
8628
9177
|
}
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
9178
|
+
function buildCursorHelpText() {
|
|
9179
|
+
return [
|
|
9180
|
+
"tokenleak cursor",
|
|
9181
|
+
"Manage Cursor authentication and cache sync.",
|
|
9182
|
+
"",
|
|
9183
|
+
"Usage:",
|
|
9184
|
+
" tokenleak cursor login [--name <label>]",
|
|
9185
|
+
" tokenleak cursor status [--name <label>]",
|
|
9186
|
+
" tokenleak cursor accounts [--json]",
|
|
9187
|
+
" tokenleak cursor switch <name-or-id>",
|
|
9188
|
+
" tokenleak cursor logout [--name <label> | --all] [--purge-cache]",
|
|
9189
|
+
"",
|
|
9190
|
+
"Notes:",
|
|
9191
|
+
" Session tokens come from https://www.cursor.com/settings",
|
|
9192
|
+
" Session tokens are stored in plaintext with local-only file permissions.",
|
|
9193
|
+
` Credentials: ${getCursorCredentialsPath()}`,
|
|
9194
|
+
` Cache: ${getCursorCacheDir()}`,
|
|
9195
|
+
""
|
|
9196
|
+
].join(`
|
|
9197
|
+
`);
|
|
8637
9198
|
}
|
|
8638
|
-
function
|
|
8639
|
-
const
|
|
8640
|
-
if (
|
|
8641
|
-
|
|
9199
|
+
function printCursorAccounts(json) {
|
|
9200
|
+
const accounts = listCursorAccounts();
|
|
9201
|
+
if (json) {
|
|
9202
|
+
process.stdout.write(`${JSON.stringify({ accounts }, null, 2)}
|
|
9203
|
+
`);
|
|
9204
|
+
return;
|
|
8642
9205
|
}
|
|
8643
|
-
if (
|
|
8644
|
-
|
|
9206
|
+
if (accounts.length === 0) {
|
|
9207
|
+
process.stdout.write(`No saved Cursor accounts.
|
|
9208
|
+
`);
|
|
9209
|
+
return;
|
|
8645
9210
|
}
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
const
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
since = d.toISOString().slice(0, 10);
|
|
9211
|
+
process.stdout.write(`Cursor accounts:
|
|
9212
|
+
`);
|
|
9213
|
+
for (const account of accounts) {
|
|
9214
|
+
const marker = account.isActive ? "*" : "-";
|
|
9215
|
+
const label = account.label ? `${account.label} (${account.id})` : account.id;
|
|
9216
|
+
process.stdout.write(` ${marker} ${label}
|
|
9217
|
+
`);
|
|
8654
9218
|
}
|
|
8655
|
-
|
|
8656
|
-
|
|
9219
|
+
}
|
|
9220
|
+
async function runCursorLogin(name) {
|
|
9221
|
+
if (name && listCursorAccounts().some((account) => account.label?.toLowerCase() === name.toLowerCase())) {
|
|
9222
|
+
throw new TokenleakError(`Cursor account label already exists: ${name}`);
|
|
8657
9223
|
}
|
|
8658
|
-
|
|
9224
|
+
process.stdout.write(`Session tokens are stored in plaintext at ${getCursorCredentialsPath()} with local-only file permissions.
|
|
9225
|
+
`);
|
|
9226
|
+
const token = await readSecret("Enter Cursor session token: ");
|
|
9227
|
+
if (!token) {
|
|
9228
|
+
throw new TokenleakError("No token provided");
|
|
9229
|
+
}
|
|
9230
|
+
process.stdout.write(`Validating session token...
|
|
9231
|
+
`);
|
|
9232
|
+
const result = await validateCursorSession(token);
|
|
9233
|
+
if (!result.valid) {
|
|
9234
|
+
throw new TokenleakError(result.error ?? "Invalid session token");
|
|
9235
|
+
}
|
|
9236
|
+
const accountId = saveCursorCredentials(token, name);
|
|
9237
|
+
const display = name ?? accountId;
|
|
9238
|
+
process.stdout.write(`Saved Cursor account ${display}.
|
|
9239
|
+
`);
|
|
8659
9240
|
}
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
function loadEnvOverrides() {
|
|
8665
|
-
const overrides = {};
|
|
8666
|
-
const format = process.env["TOKENLEAK_FORMAT"];
|
|
8667
|
-
if (format && VALID_FORMATS.has(format)) {
|
|
8668
|
-
overrides.format = format;
|
|
9241
|
+
async function runCursorStatus(name) {
|
|
9242
|
+
const credentials = name ? getCursorCredentialsFor(name) : getActiveCursorCredentials();
|
|
9243
|
+
if (!credentials) {
|
|
9244
|
+
throw new TokenleakError(name ? `Account not found: ${name}` : "No saved Cursor accounts");
|
|
8669
9245
|
}
|
|
8670
|
-
const
|
|
8671
|
-
if (
|
|
8672
|
-
|
|
9246
|
+
const result = await validateCursorSession(credentials.sessionToken);
|
|
9247
|
+
if (!result.valid) {
|
|
9248
|
+
throw new TokenleakError(result.error ?? "Invalid / expired session");
|
|
8673
9249
|
}
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
9250
|
+
process.stdout.write(`Cursor session is valid.
|
|
9251
|
+
`);
|
|
9252
|
+
if (result.membershipType) {
|
|
9253
|
+
process.stdout.write(`Membership: ${result.membershipType}
|
|
9254
|
+
`);
|
|
9255
|
+
}
|
|
9256
|
+
}
|
|
9257
|
+
function runCursorLogout(name, all, purgeCache) {
|
|
9258
|
+
if (all) {
|
|
9259
|
+
removeAllCursorAccounts(purgeCache);
|
|
9260
|
+
process.stdout.write(`Logged out from all Cursor accounts.
|
|
9261
|
+
`);
|
|
9262
|
+
return;
|
|
9263
|
+
}
|
|
9264
|
+
if (name) {
|
|
9265
|
+
removeCursorAccount(name, purgeCache);
|
|
9266
|
+
process.stdout.write(`Logged out from Cursor account '${name}'.
|
|
9267
|
+
`);
|
|
9268
|
+
return;
|
|
9269
|
+
}
|
|
9270
|
+
const store = loadCursorCredentialsStore();
|
|
9271
|
+
if (!store) {
|
|
9272
|
+
throw new TokenleakError("No saved Cursor accounts");
|
|
9273
|
+
}
|
|
9274
|
+
removeCursorAccount(store.activeAccountId, purgeCache);
|
|
9275
|
+
process.stdout.write(`Logged out from Cursor account '${store.activeAccountId}'.
|
|
9276
|
+
`);
|
|
9277
|
+
}
|
|
9278
|
+
function parseNameFlag(argv, index) {
|
|
9279
|
+
const value = argv[index + 1];
|
|
9280
|
+
if (!value) {
|
|
9281
|
+
throw new TokenleakError(`${argv[index]} requires a value`);
|
|
9282
|
+
}
|
|
9283
|
+
return [value, index + 2];
|
|
9284
|
+
}
|
|
9285
|
+
async function runCursorCommand(argv) {
|
|
9286
|
+
const command = argv[0];
|
|
9287
|
+
if (!command || command === "--help" || command === "-h") {
|
|
9288
|
+
process.stdout.write(buildCursorHelpText());
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
9291
|
+
if (command === "login") {
|
|
9292
|
+
let name;
|
|
9293
|
+
for (let index = 1;index < argv.length; ) {
|
|
9294
|
+
const arg = argv[index];
|
|
9295
|
+
if (arg === "--name") {
|
|
9296
|
+
[name, index] = parseNameFlag(argv, index);
|
|
9297
|
+
} else {
|
|
9298
|
+
throw new TokenleakError(`Unknown cursor flag "${arg}"`);
|
|
9299
|
+
}
|
|
8679
9300
|
}
|
|
9301
|
+
await runCursorLogin(name);
|
|
9302
|
+
return;
|
|
8680
9303
|
}
|
|
8681
|
-
|
|
9304
|
+
if (command === "status") {
|
|
9305
|
+
let name;
|
|
9306
|
+
for (let index = 1;index < argv.length; ) {
|
|
9307
|
+
const arg = argv[index];
|
|
9308
|
+
if (arg === "--name") {
|
|
9309
|
+
[name, index] = parseNameFlag(argv, index);
|
|
9310
|
+
} else {
|
|
9311
|
+
throw new TokenleakError(`Unknown cursor flag "${arg}"`);
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
await runCursorStatus(name);
|
|
9315
|
+
return;
|
|
9316
|
+
}
|
|
9317
|
+
if (command === "accounts") {
|
|
9318
|
+
if (argv.length > 2 || argv[1] && argv[1] !== "--json") {
|
|
9319
|
+
throw new TokenleakError(`Unknown cursor flag "${argv[1]}"`);
|
|
9320
|
+
}
|
|
9321
|
+
printCursorAccounts(argv.includes("--json"));
|
|
9322
|
+
return;
|
|
9323
|
+
}
|
|
9324
|
+
if (command === "switch") {
|
|
9325
|
+
const target = argv[1];
|
|
9326
|
+
if (!target) {
|
|
9327
|
+
throw new TokenleakError("tokenleak cursor switch requires a name or account id");
|
|
9328
|
+
}
|
|
9329
|
+
setActiveCursorAccount(target);
|
|
9330
|
+
process.stdout.write(`Active Cursor account set to ${target}.
|
|
9331
|
+
`);
|
|
9332
|
+
return;
|
|
9333
|
+
}
|
|
9334
|
+
if (command === "logout") {
|
|
9335
|
+
let name;
|
|
9336
|
+
let all = false;
|
|
9337
|
+
let purgeCache = false;
|
|
9338
|
+
for (let index = 1;index < argv.length; ) {
|
|
9339
|
+
const arg = argv[index];
|
|
9340
|
+
if (arg === "--name") {
|
|
9341
|
+
[name, index] = parseNameFlag(argv, index);
|
|
9342
|
+
continue;
|
|
9343
|
+
}
|
|
9344
|
+
if (arg === "--all") {
|
|
9345
|
+
all = true;
|
|
9346
|
+
index += 1;
|
|
9347
|
+
continue;
|
|
9348
|
+
}
|
|
9349
|
+
if (arg === "--purge-cache") {
|
|
9350
|
+
purgeCache = true;
|
|
9351
|
+
index += 1;
|
|
9352
|
+
continue;
|
|
9353
|
+
}
|
|
9354
|
+
throw new TokenleakError(`Unknown cursor flag "${arg}"`);
|
|
9355
|
+
}
|
|
9356
|
+
if (all && name) {
|
|
9357
|
+
throw new TokenleakError("tokenleak cursor logout cannot combine --all with --name");
|
|
9358
|
+
}
|
|
9359
|
+
runCursorLogout(name, all, purgeCache);
|
|
9360
|
+
return;
|
|
9361
|
+
}
|
|
9362
|
+
throw new TokenleakError(`Unknown cursor command "${command}"`);
|
|
8682
9363
|
}
|
|
8683
9364
|
|
|
8684
9365
|
// packages/cli/src/explain.ts
|
|
@@ -8718,7 +9399,7 @@ function renderExplainTerminal(report, width = 80) {
|
|
|
8718
9399
|
`Explain ${report.date}`,
|
|
8719
9400
|
report.headline,
|
|
8720
9401
|
"",
|
|
8721
|
-
...report.summary.map((
|
|
9402
|
+
...report.summary.map((line2) => `- ${line2}`),
|
|
8722
9403
|
"",
|
|
8723
9404
|
...renderEvidenceTable("Providers", report.topProviders, width),
|
|
8724
9405
|
"",
|
|
@@ -8752,6 +9433,7 @@ function buildExplainHelpText() {
|
|
|
8752
9433
|
" -p, --provider <list> Provider filter list, comma-separated",
|
|
8753
9434
|
" --claude Only include Claude Code",
|
|
8754
9435
|
" --codex Only include Codex",
|
|
9436
|
+
" --cursor Only include Cursor",
|
|
8755
9437
|
" --pi Only include Pi",
|
|
8756
9438
|
" --open-code Only include OpenCode",
|
|
8757
9439
|
" --all-providers Ignore provider filters and use every available provider",
|
|
@@ -8781,6 +9463,7 @@ var CLI_FLAG_ORDER = [
|
|
|
8781
9463
|
"upload",
|
|
8782
9464
|
"claude",
|
|
8783
9465
|
"codex",
|
|
9466
|
+
"cursor",
|
|
8784
9467
|
"pi",
|
|
8785
9468
|
"openCode",
|
|
8786
9469
|
"allProviders",
|
|
@@ -8807,6 +9490,7 @@ var CLI_FLAG_NAMES = {
|
|
|
8807
9490
|
upload: "--upload",
|
|
8808
9491
|
claude: "--claude",
|
|
8809
9492
|
codex: "--codex",
|
|
9493
|
+
cursor: "--cursor",
|
|
8810
9494
|
pi: "--pi",
|
|
8811
9495
|
openCode: "--open-code",
|
|
8812
9496
|
allProviders: "--all-providers",
|
|
@@ -8844,7 +9528,7 @@ function buildCliPreview(cliArgs) {
|
|
|
8844
9528
|
|
|
8845
9529
|
// packages/cli/src/interactive.ts
|
|
8846
9530
|
import { emitKeypressEvents } from "readline";
|
|
8847
|
-
import { createInterface } from "readline/promises";
|
|
9531
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
8848
9532
|
var INTERACTIVE_FLAG_LINES = [
|
|
8849
9533
|
" explain <date> explain one day of usage",
|
|
8850
9534
|
" focus rank deep-work sessions",
|
|
@@ -8858,6 +9542,7 @@ var INTERACTIVE_FLAG_LINES = [
|
|
|
8858
9542
|
"-p, --provider <list> comma-separated providers",
|
|
8859
9543
|
" --claude shortcut for Claude Code",
|
|
8860
9544
|
" --codex shortcut for Codex",
|
|
9545
|
+
" --cursor shortcut for Cursor",
|
|
8861
9546
|
" --pi shortcut for Pi",
|
|
8862
9547
|
" --open-code shortcut for Open Code",
|
|
8863
9548
|
" --all-providers ignore provider filters",
|
|
@@ -8958,7 +9643,7 @@ function renderRule(width) {
|
|
|
8958
9643
|
return color("-".repeat(width), DIM);
|
|
8959
9644
|
}
|
|
8960
9645
|
function describeRequest(args) {
|
|
8961
|
-
const
|
|
9646
|
+
const output2 = typeof args["output"] === "string" ? args["output"] : null;
|
|
8962
9647
|
if (args["liveServer"]) {
|
|
8963
9648
|
return {
|
|
8964
9649
|
title: "Live Dashboard",
|
|
@@ -8987,7 +9672,7 @@ function describeRequest(args) {
|
|
|
8987
9672
|
return {
|
|
8988
9673
|
title: "Compare Report",
|
|
8989
9674
|
loadingTitle: "Building compare report",
|
|
8990
|
-
loadingDetail:
|
|
9675
|
+
loadingDetail: output2 ? `Computing period deltas and writing the report to ${output2}.` : "Computing period deltas for the current and previous windows.",
|
|
8991
9676
|
executionMode: "capture"
|
|
8992
9677
|
};
|
|
8993
9678
|
}
|
|
@@ -8996,21 +9681,21 @@ function describeRequest(args) {
|
|
|
8996
9681
|
return {
|
|
8997
9682
|
title: "JSON Export",
|
|
8998
9683
|
loadingTitle: "Generating JSON report",
|
|
8999
|
-
loadingDetail:
|
|
9684
|
+
loadingDetail: output2 ? `Collecting token usage and writing JSON to ${output2}.` : "Collecting token usage and building structured JSON output.",
|
|
9000
9685
|
executionMode: "capture"
|
|
9001
9686
|
};
|
|
9002
9687
|
case "svg":
|
|
9003
9688
|
return {
|
|
9004
9689
|
title: "SVG Export",
|
|
9005
9690
|
loadingTitle: "Rendering SVG",
|
|
9006
|
-
loadingDetail:
|
|
9691
|
+
loadingDetail: output2 ? `Rendering a vector card and writing it to ${output2}.` : "Rendering a vector card from your usage data.",
|
|
9007
9692
|
executionMode: "capture"
|
|
9008
9693
|
};
|
|
9009
9694
|
case "png":
|
|
9010
9695
|
return {
|
|
9011
9696
|
title: "PNG Export",
|
|
9012
9697
|
loadingTitle: "Rendering PNG",
|
|
9013
|
-
loadingDetail:
|
|
9698
|
+
loadingDetail: output2 ? `Rendering the PNG card and writing it to ${output2}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
|
|
9014
9699
|
executionMode: "capture"
|
|
9015
9700
|
};
|
|
9016
9701
|
default:
|
|
@@ -9086,9 +9771,18 @@ function renderFlagPanel() {
|
|
|
9086
9771
|
color("All Flags", WHITE + BOLD),
|
|
9087
9772
|
color("Every flag remains available while using the launcher.", DIM),
|
|
9088
9773
|
"",
|
|
9089
|
-
...INTERACTIVE_FLAG_LINES.map((
|
|
9774
|
+
...INTERACTIVE_FLAG_LINES.map((line2) => color(line2, CYAN))
|
|
9090
9775
|
];
|
|
9091
9776
|
}
|
|
9777
|
+
function sliceVisibleWindow(items, selectedIndex, maxVisible) {
|
|
9778
|
+
if (items.length <= maxVisible) {
|
|
9779
|
+
return [...items];
|
|
9780
|
+
}
|
|
9781
|
+
const visibleCount = Math.max(1, maxVisible);
|
|
9782
|
+
const maxStart = items.length - visibleCount;
|
|
9783
|
+
const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(visibleCount / 2)));
|
|
9784
|
+
return items.slice(start, start + visibleCount);
|
|
9785
|
+
}
|
|
9092
9786
|
function renderMenuPanel(context, options, selectedIndex) {
|
|
9093
9787
|
const selected = options[selectedIndex];
|
|
9094
9788
|
return [
|
|
@@ -9109,7 +9803,55 @@ function renderMenuPanel(context, options, selectedIndex) {
|
|
|
9109
9803
|
renderRule(44)
|
|
9110
9804
|
];
|
|
9111
9805
|
}
|
|
9806
|
+
function buildCompactLauncherBody(context, options, selectedIndex, width, rows) {
|
|
9807
|
+
const selected = options[selectedIndex];
|
|
9808
|
+
const menuLines = renderMenu(options, selectedIndex);
|
|
9809
|
+
const compact = rows < 18;
|
|
9810
|
+
const header = compact ? [
|
|
9811
|
+
color("Tokenleak Interactive Launcher", WHITE + BOLD),
|
|
9812
|
+
color("Narrow pane detected. Press H for the full flag reference.", DIM)
|
|
9813
|
+
] : [
|
|
9814
|
+
color("Tokenleak Interactive Launcher", WHITE + BOLD),
|
|
9815
|
+
`${color(`v${context.version}`, YELLOW)} ${color("adaptive launcher view", CYAN)}`,
|
|
9816
|
+
color("Narrow pane detected. Press H for the full flag reference.", DIM),
|
|
9817
|
+
""
|
|
9818
|
+
];
|
|
9819
|
+
const footer = compact ? [
|
|
9820
|
+
"",
|
|
9821
|
+
color(truncateVisible2(selected.preview, width), GREEN),
|
|
9822
|
+
`${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`
|
|
9823
|
+
] : [
|
|
9824
|
+
"",
|
|
9825
|
+
color("Preview", WHITE + BOLD),
|
|
9826
|
+
color(truncateVisible2(selected.preview, width), GREEN),
|
|
9827
|
+
"",
|
|
9828
|
+
`${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
|
|
9829
|
+
renderRule(44)
|
|
9830
|
+
];
|
|
9831
|
+
const meta = [
|
|
9832
|
+
color("Actions", WHITE + BOLD),
|
|
9833
|
+
color("Use arrow keys to move through the launcher menu.", DIM)
|
|
9834
|
+
];
|
|
9835
|
+
const fixedLineCount = header.length + meta.length + footer.length;
|
|
9836
|
+
const menuViewport = Math.max(1, rows - fixedLineCount);
|
|
9837
|
+
const visibleMenu = sliceVisibleWindow(menuLines, selectedIndex, menuViewport);
|
|
9838
|
+
return [...header, ...meta, ...visibleMenu, ...footer].slice(0, Math.max(1, rows));
|
|
9839
|
+
}
|
|
9840
|
+
function buildLauncherBody(context, options, selectedIndex, width, rows) {
|
|
9841
|
+
const menuPanel = renderMenuPanel(context, options, selectedIndex);
|
|
9842
|
+
const flagPanel = renderFlagPanel();
|
|
9843
|
+
const wideBody = joinColumns(menuPanel, flagPanel, width);
|
|
9844
|
+
if (width >= 118 && wideBody.length <= rows) {
|
|
9845
|
+
return wideBody;
|
|
9846
|
+
}
|
|
9847
|
+
const stackedBody = [...menuPanel, "", ...flagPanel];
|
|
9848
|
+
if (stackedBody.length <= rows) {
|
|
9849
|
+
return stackedBody;
|
|
9850
|
+
}
|
|
9851
|
+
return buildCompactLauncherBody(context, options, selectedIndex, width, rows);
|
|
9852
|
+
}
|
|
9112
9853
|
function renderHelpOverlay(helpText, width) {
|
|
9854
|
+
const rows = Math.max(6, process.stdout.rows ?? 40);
|
|
9113
9855
|
const lines = helpText.trimEnd().split(`
|
|
9114
9856
|
`);
|
|
9115
9857
|
const header = [
|
|
@@ -9117,14 +9859,18 @@ function renderHelpOverlay(helpText, width) {
|
|
|
9117
9859
|
color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
|
|
9118
9860
|
""
|
|
9119
9861
|
];
|
|
9120
|
-
|
|
9862
|
+
const availableHeight = Math.max(1, rows - header.length - 1);
|
|
9863
|
+
const visibleLines = lines.slice(0, availableHeight).map((line2) => truncateVisible2(line2, width));
|
|
9864
|
+
const truncatedNotice = lines.length > visibleLines.length ? [
|
|
9865
|
+
color("Help truncated to fit this pane. Resize taller or run `tokenleak --help` for the full output.", DIM)
|
|
9866
|
+
] : [];
|
|
9867
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...visibleLines, ...truncatedNotice].join(`
|
|
9121
9868
|
`)}`;
|
|
9122
9869
|
}
|
|
9123
9870
|
function renderLauncher(context, options, selectedIndex) {
|
|
9124
|
-
const width = process.stdout.columns ?? 120;
|
|
9125
|
-
const
|
|
9126
|
-
const
|
|
9127
|
-
const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
|
|
9871
|
+
const width = Math.max(40, process.stdout.columns ?? 120);
|
|
9872
|
+
const rows = Math.max(8, process.stdout.rows ?? 40);
|
|
9873
|
+
const body = buildLauncherBody(context, options, selectedIndex, width, rows);
|
|
9128
9874
|
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
9129
9875
|
`)}`;
|
|
9130
9876
|
}
|
|
@@ -9171,12 +9917,8 @@ function buildOutputSectionLines(title, content, width) {
|
|
|
9171
9917
|
if (!normalized)
|
|
9172
9918
|
return [];
|
|
9173
9919
|
const lines = normalized.split(`
|
|
9174
|
-
`).map((
|
|
9175
|
-
return [
|
|
9176
|
-
color(title, WHITE + BOLD),
|
|
9177
|
-
...lines,
|
|
9178
|
-
""
|
|
9179
|
-
];
|
|
9920
|
+
`).map((line2) => truncateVisible2(line2, width));
|
|
9921
|
+
return [color(title, WHITE + BOLD), ...lines, ""];
|
|
9180
9922
|
}
|
|
9181
9923
|
function renderResult(request, result, scrollOffset = 0) {
|
|
9182
9924
|
const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
|
@@ -9205,13 +9947,7 @@ function renderResult(request, result, scrollOffset = 0) {
|
|
|
9205
9947
|
const visibleBody = body.slice(effectiveOffset, effectiveOffset + viewportHeight);
|
|
9206
9948
|
const padding = Array.from({ length: Math.max(0, viewportHeight - visibleBody.length) }, () => "");
|
|
9207
9949
|
const scrollStatus = body.length > viewportHeight ? color(`Lines ${effectiveOffset + 1}-${Math.min(body.length, effectiveOffset + viewportHeight)} of ${body.length}`, DIM) : color("All command output is visible.", DIM);
|
|
9208
|
-
const lines = [
|
|
9209
|
-
...header,
|
|
9210
|
-
...visibleBody,
|
|
9211
|
-
...padding,
|
|
9212
|
-
scrollStatus,
|
|
9213
|
-
...footer
|
|
9214
|
-
];
|
|
9950
|
+
const lines = [...header, ...visibleBody, ...padding, scrollStatus, ...footer];
|
|
9215
9951
|
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
9216
9952
|
`)}`;
|
|
9217
9953
|
}
|
|
@@ -9248,6 +9984,7 @@ class InteractiveExitError extends Error {
|
|
|
9248
9984
|
var PROVIDER_CHOICES = [
|
|
9249
9985
|
{ value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
|
|
9250
9986
|
{ value: "codex", label: "Codex", description: "OpenAI session logs" },
|
|
9987
|
+
{ value: "cursor", label: "Cursor", description: "Cursor API cache exports" },
|
|
9251
9988
|
{ value: "pi", label: "Pi", description: "pi-mono local session logs" },
|
|
9252
9989
|
{ value: "open-code", label: "Open Code", description: "Open Code storage and database" }
|
|
9253
9990
|
];
|
|
@@ -9287,7 +10024,7 @@ function renderChoiceScreen(title, description, options, selectedIndex, selected
|
|
|
9287
10024
|
`)}`;
|
|
9288
10025
|
}
|
|
9289
10026
|
async function ask(prompt, initialValue = "") {
|
|
9290
|
-
const readline =
|
|
10027
|
+
const readline = createInterface2({
|
|
9291
10028
|
input: process.stdin,
|
|
9292
10029
|
output: process.stdout
|
|
9293
10030
|
});
|
|
@@ -9495,8 +10232,16 @@ Press Enter to try again.`);
|
|
|
9495
10232
|
async function promptCompareSetting() {
|
|
9496
10233
|
const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
|
|
9497
10234
|
{ value: "off", label: "No compare", description: "Render a standard single-period report" },
|
|
9498
|
-
{
|
|
9499
|
-
|
|
10235
|
+
{
|
|
10236
|
+
value: "auto",
|
|
10237
|
+
label: "Auto compare",
|
|
10238
|
+
description: "Split the selected window automatically"
|
|
10239
|
+
},
|
|
10240
|
+
{
|
|
10241
|
+
value: "custom",
|
|
10242
|
+
label: "Custom compare range",
|
|
10243
|
+
description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range"
|
|
10244
|
+
}
|
|
9500
10245
|
]);
|
|
9501
10246
|
if (choice === "off")
|
|
9502
10247
|
return null;
|
|
@@ -9629,13 +10374,13 @@ async function buildImagePreset(format) {
|
|
|
9629
10374
|
const rangeArgs = await promptDateWindow();
|
|
9630
10375
|
const providers = await promptProviderSelection("Provider Filter");
|
|
9631
10376
|
const compare = await promptCompareSetting();
|
|
9632
|
-
const
|
|
10377
|
+
const output2 = await promptOutputPath(`tokenleak.${format}`);
|
|
9633
10378
|
const shouldOpen = await askYesNo("Open the file when done", true);
|
|
9634
10379
|
const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
|
|
9635
10380
|
const args = {
|
|
9636
10381
|
format,
|
|
9637
10382
|
theme,
|
|
9638
|
-
output,
|
|
10383
|
+
output: output2,
|
|
9639
10384
|
open: shouldOpen,
|
|
9640
10385
|
more,
|
|
9641
10386
|
...rangeArgs
|
|
@@ -9649,12 +10394,12 @@ async function buildWrappedPreset() {
|
|
|
9649
10394
|
const theme = await promptTheme();
|
|
9650
10395
|
const rangeArgs = await promptDateWindow();
|
|
9651
10396
|
const providers = await promptProviderSelection("Provider Filter");
|
|
9652
|
-
const
|
|
10397
|
+
const output2 = await promptOutputPath("tokenleak-wrapped.png");
|
|
9653
10398
|
const shouldOpen = await askYesNo("Open the image when done", true);
|
|
9654
10399
|
const args = {
|
|
9655
10400
|
format: "wrapped",
|
|
9656
10401
|
theme,
|
|
9657
|
-
output,
|
|
10402
|
+
output: output2,
|
|
9658
10403
|
open: shouldOpen,
|
|
9659
10404
|
...rangeArgs
|
|
9660
10405
|
};
|
|
@@ -9665,8 +10410,16 @@ async function buildComparePreset() {
|
|
|
9665
10410
|
const rangeArgs = await promptDateWindow();
|
|
9666
10411
|
const providers = await promptProviderSelection();
|
|
9667
10412
|
const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
|
|
9668
|
-
{
|
|
9669
|
-
|
|
10413
|
+
{
|
|
10414
|
+
value: "auto",
|
|
10415
|
+
label: "Auto compare",
|
|
10416
|
+
description: "Split the chosen window automatically"
|
|
10417
|
+
},
|
|
10418
|
+
{
|
|
10419
|
+
value: "custom",
|
|
10420
|
+
label: "Custom compare range",
|
|
10421
|
+
description: "Enter an explicit prior range manually"
|
|
10422
|
+
}
|
|
9670
10423
|
]);
|
|
9671
10424
|
const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
|
|
9672
10425
|
const saveToFile = await askYesNo("Write compare output to a file", false);
|
|
@@ -9684,20 +10437,24 @@ async function buildComparePreset() {
|
|
|
9684
10437
|
async function buildExplainPreset() {
|
|
9685
10438
|
const date = await ask("Date to explain (YYYY-MM-DD)");
|
|
9686
10439
|
const format = await promptSingleChoice("Explain Format", "Choose how the explain report should be rendered.", [
|
|
9687
|
-
{
|
|
10440
|
+
{
|
|
10441
|
+
value: "terminal",
|
|
10442
|
+
label: "Terminal",
|
|
10443
|
+
description: "Narrative report in the current terminal"
|
|
10444
|
+
},
|
|
9688
10445
|
{ value: "json", label: "JSON", description: "Structured explain payload" }
|
|
9689
10446
|
]);
|
|
9690
10447
|
const providers = await promptProviderSelection();
|
|
9691
10448
|
const width = format === "terminal" ? await promptWidth() : null;
|
|
9692
|
-
const
|
|
10449
|
+
const output2 = await ask("Output file (blank keeps stdout)");
|
|
9693
10450
|
const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
|
|
9694
10451
|
const args = {
|
|
9695
10452
|
format
|
|
9696
10453
|
};
|
|
9697
10454
|
if (width)
|
|
9698
10455
|
args["width"] = width;
|
|
9699
|
-
if (
|
|
9700
|
-
args["output"] =
|
|
10456
|
+
if (output2)
|
|
10457
|
+
args["output"] = output2;
|
|
9701
10458
|
if (noColor2)
|
|
9702
10459
|
args["noColor"] = true;
|
|
9703
10460
|
applySelectedProviders(args, providers);
|
|
@@ -9709,13 +10466,17 @@ async function buildExplainPreset() {
|
|
|
9709
10466
|
}
|
|
9710
10467
|
async function buildFocusPreset() {
|
|
9711
10468
|
const format = await promptSingleChoice("Focus Format", "Choose how the focus report should be rendered.", [
|
|
9712
|
-
{
|
|
10469
|
+
{
|
|
10470
|
+
value: "terminal",
|
|
10471
|
+
label: "Terminal",
|
|
10472
|
+
description: "Ranked session report in the current terminal"
|
|
10473
|
+
},
|
|
9713
10474
|
{ value: "json", label: "JSON", description: "Structured focus payload" }
|
|
9714
10475
|
]);
|
|
9715
10476
|
const rangeArgs = await promptDateWindow();
|
|
9716
10477
|
const providers = await promptProviderSelection();
|
|
9717
10478
|
const width = format === "terminal" ? await promptWidth() : null;
|
|
9718
|
-
const
|
|
10479
|
+
const output2 = await ask("Output file (blank keeps stdout)");
|
|
9719
10480
|
const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
|
|
9720
10481
|
const args = {
|
|
9721
10482
|
format,
|
|
@@ -9723,8 +10484,8 @@ async function buildFocusPreset() {
|
|
|
9723
10484
|
};
|
|
9724
10485
|
if (width)
|
|
9725
10486
|
args["width"] = width;
|
|
9726
|
-
if (
|
|
9727
|
-
args["output"] =
|
|
10487
|
+
if (output2)
|
|
10488
|
+
args["output"] = output2;
|
|
9728
10489
|
if (noColor2)
|
|
9729
10490
|
args["noColor"] = true;
|
|
9730
10491
|
applySelectedProviders(args, providers);
|
|
@@ -9736,20 +10497,24 @@ async function buildFocusPreset() {
|
|
|
9736
10497
|
}
|
|
9737
10498
|
async function buildAdvisorPreset() {
|
|
9738
10499
|
const format = await promptSingleChoice("Advisor Format", "Choose how the advisor report should be rendered.", [
|
|
9739
|
-
{
|
|
10500
|
+
{
|
|
10501
|
+
value: "terminal",
|
|
10502
|
+
label: "Terminal",
|
|
10503
|
+
description: "Efficiency recommendations in the current terminal"
|
|
10504
|
+
},
|
|
9740
10505
|
{ value: "json", label: "JSON", description: "Structured advisor payload" }
|
|
9741
10506
|
]);
|
|
9742
10507
|
const rangeArgs = await promptDateWindow();
|
|
9743
10508
|
const providers = await promptProviderSelection();
|
|
9744
|
-
const
|
|
10509
|
+
const output2 = await ask("Output file (blank keeps stdout)");
|
|
9745
10510
|
const noColor2 = format === "terminal" ? await askYesNo("Disable ANSI colors", false) : false;
|
|
9746
10511
|
const args = {
|
|
9747
10512
|
format,
|
|
9748
10513
|
advisor: true,
|
|
9749
10514
|
...rangeArgs
|
|
9750
10515
|
};
|
|
9751
|
-
if (
|
|
9752
|
-
args["output"] =
|
|
10516
|
+
if (output2)
|
|
10517
|
+
args["output"] = output2;
|
|
9753
10518
|
if (noColor2)
|
|
9754
10519
|
args["noColor"] = true;
|
|
9755
10520
|
applySelectedProviders(args, providers);
|
|
@@ -9796,14 +10561,30 @@ async function askFormatChoice() {
|
|
|
9796
10561
|
{ value: "json", label: "JSON", description: "Structured machine-readable output" },
|
|
9797
10562
|
{ value: "svg", label: "SVG", description: "Shareable vector export" },
|
|
9798
10563
|
{ value: "png", label: "PNG", description: "Raster export for social and docs" },
|
|
9799
|
-
{
|
|
10564
|
+
{
|
|
10565
|
+
value: "wrapped",
|
|
10566
|
+
label: "\uD83C\uDF89 Wrapped",
|
|
10567
|
+
description: "Your AI coding story card (PNG)"
|
|
10568
|
+
}
|
|
9800
10569
|
]);
|
|
9801
10570
|
}
|
|
9802
10571
|
async function buildCustomCommand() {
|
|
9803
10572
|
const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
|
|
9804
|
-
{
|
|
9805
|
-
|
|
9806
|
-
|
|
10573
|
+
{
|
|
10574
|
+
value: "run",
|
|
10575
|
+
label: "Standard command",
|
|
10576
|
+
description: "Render terminal, JSON, SVG, or PNG output"
|
|
10577
|
+
},
|
|
10578
|
+
{
|
|
10579
|
+
value: "live-server",
|
|
10580
|
+
label: "Live server",
|
|
10581
|
+
description: "Launch the browser dashboard locally"
|
|
10582
|
+
},
|
|
10583
|
+
{
|
|
10584
|
+
value: "list-providers",
|
|
10585
|
+
label: "List providers",
|
|
10586
|
+
description: "Inspect registered provider backends"
|
|
10587
|
+
}
|
|
9807
10588
|
]);
|
|
9808
10589
|
if (mode === "live-server") {
|
|
9809
10590
|
return buildLivePreset();
|
|
@@ -9817,7 +10598,7 @@ async function buildCustomCommand() {
|
|
|
9817
10598
|
const providers = await promptProviderSelection();
|
|
9818
10599
|
const compare = await promptCompareSetting();
|
|
9819
10600
|
const width = format === "terminal" ? await promptWidth() : null;
|
|
9820
|
-
const
|
|
10601
|
+
const output2 = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
|
|
9821
10602
|
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
9822
10603
|
const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
|
|
9823
10604
|
const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
|
|
@@ -9834,8 +10615,8 @@ async function buildCustomCommand() {
|
|
|
9834
10615
|
args["compare"] = compare;
|
|
9835
10616
|
if (width)
|
|
9836
10617
|
args["width"] = width;
|
|
9837
|
-
if (
|
|
9838
|
-
args["output"] =
|
|
10618
|
+
if (output2)
|
|
10619
|
+
args["output"] = output2;
|
|
9839
10620
|
if (noColor2)
|
|
9840
10621
|
args["noColor"] = true;
|
|
9841
10622
|
if (noInsights)
|
|
@@ -10250,11 +11031,11 @@ async function uploadToGist(content, filename, description) {
|
|
|
10250
11031
|
if (!available) {
|
|
10251
11032
|
throw new Error("GitHub CLI (gh) is not installed or not authenticated. " + "Install it from https://cli.github.com and run `gh auth login`.");
|
|
10252
11033
|
}
|
|
10253
|
-
const { join:
|
|
11034
|
+
const { join: join8 } = await import("path");
|
|
10254
11035
|
const { tmpdir } = await import("os");
|
|
10255
|
-
const { writeFileSync, unlinkSync } = await import("fs");
|
|
10256
|
-
const tmpPath =
|
|
10257
|
-
|
|
11036
|
+
const { writeFileSync: writeFileSync2, unlinkSync: unlinkSync2 } = await import("fs");
|
|
11037
|
+
const tmpPath = join8(tmpdir(), `tokenleak-gist-${Date.now()}-${filename}`);
|
|
11038
|
+
writeFileSync2(tmpPath, content, "utf-8");
|
|
10258
11039
|
try {
|
|
10259
11040
|
const proc = Bun.spawn(["gh", "gist", "create", tmpPath, "--desc", description, "--public"], {
|
|
10260
11041
|
stdout: "pipe",
|
|
@@ -10272,7 +11053,7 @@ async function uploadToGist(content, filename, description) {
|
|
|
10272
11053
|
return stdout;
|
|
10273
11054
|
} finally {
|
|
10274
11055
|
try {
|
|
10275
|
-
|
|
11056
|
+
unlinkSync2(tmpPath);
|
|
10276
11057
|
} catch {}
|
|
10277
11058
|
}
|
|
10278
11059
|
}
|
|
@@ -10323,9 +11104,9 @@ async function loadForRange(state, providers, timeRange) {
|
|
|
10323
11104
|
if (inflight)
|
|
10324
11105
|
return inflight;
|
|
10325
11106
|
const range = resolveRange(state, timeRange);
|
|
10326
|
-
const loadPromise = (state.compare ? loadCompareTokenleakData(providers, range, state.compare).then((result) => result.output) : loadTokenleakData(providers, range)).then((
|
|
10327
|
-
state.dataCache.set(timeRange,
|
|
10328
|
-
return
|
|
11107
|
+
const loadPromise = (state.compare ? loadCompareTokenleakData(providers, range, state.compare).then((result) => result.output) : loadTokenleakData(providers, range)).then((output2) => {
|
|
11108
|
+
state.dataCache.set(timeRange, output2);
|
|
11109
|
+
return output2;
|
|
10329
11110
|
}).finally(() => {
|
|
10330
11111
|
state.inflightLoads.delete(timeRange);
|
|
10331
11112
|
});
|
|
@@ -10345,7 +11126,7 @@ function getViewportHeight(state, width, rows) {
|
|
|
10345
11126
|
const footerLines = 1;
|
|
10346
11127
|
return Math.max(4, rows - headerLines - footerLines - 1);
|
|
10347
11128
|
}
|
|
10348
|
-
function renderActiveView(
|
|
11129
|
+
function renderActiveView(output2, tab, width, noColor2, noInsights) {
|
|
10349
11130
|
const options = {
|
|
10350
11131
|
format: "terminal",
|
|
10351
11132
|
theme: "dark",
|
|
@@ -10357,38 +11138,38 @@ function renderActiveView(output, tab, width, noColor2, noInsights) {
|
|
|
10357
11138
|
};
|
|
10358
11139
|
switch (tab) {
|
|
10359
11140
|
case "overview":
|
|
10360
|
-
return renderOverviewView(
|
|
11141
|
+
return renderOverviewView(output2, options);
|
|
10361
11142
|
case "delta":
|
|
10362
|
-
return renderCompareView(
|
|
11143
|
+
return renderCompareView(output2, width, noColor2);
|
|
10363
11144
|
case "provider":
|
|
10364
|
-
return renderProviderView(
|
|
11145
|
+
return renderProviderView(output2, width, noColor2);
|
|
10365
11146
|
case "sess":
|
|
10366
|
-
return renderSessionView(
|
|
11147
|
+
return renderSessionView(output2, width, noColor2);
|
|
10367
11148
|
case "tok":
|
|
10368
|
-
return renderTokenView(
|
|
11149
|
+
return renderTokenView(output2, width, noColor2);
|
|
10369
11150
|
case "model":
|
|
10370
|
-
return renderModelView(
|
|
11151
|
+
return renderModelView(output2, width, noColor2);
|
|
10371
11152
|
case "cwd":
|
|
10372
|
-
return renderCwdView(
|
|
11153
|
+
return renderCwdView(output2, width, noColor2);
|
|
10373
11154
|
case "dow":
|
|
10374
|
-
return renderDowView(
|
|
11155
|
+
return renderDowView(output2, width, noColor2);
|
|
10375
11156
|
case "tod":
|
|
10376
|
-
return renderTodView(
|
|
11157
|
+
return renderTodView(output2, width, noColor2);
|
|
10377
11158
|
default:
|
|
10378
|
-
return renderOverviewView(
|
|
11159
|
+
return renderOverviewView(output2, options);
|
|
10379
11160
|
}
|
|
10380
11161
|
}
|
|
10381
|
-
function renderScreen(
|
|
11162
|
+
function renderScreen(output2, state) {
|
|
10382
11163
|
const width = getRenderWidth(state);
|
|
10383
11164
|
const rows = process.stdout.rows ?? 40;
|
|
10384
11165
|
const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
|
|
10385
11166
|
const tabBarLines = tabBar.split(`
|
|
10386
11167
|
`);
|
|
10387
|
-
const rangeLabel = state.noColor ? ` ${
|
|
11168
|
+
const rangeLabel = state.noColor ? ` ${output2.dateRange.since} \u2192 ${output2.dateRange.until}` : ` ${DIM2}${output2.dateRange.since} \u2192 ${output2.dateRange.until}${RESET3}`;
|
|
10388
11169
|
const headerLines = [...tabBarLines, rangeLabel, ""];
|
|
10389
11170
|
const footerLines = [""];
|
|
10390
11171
|
const viewportHeight = getViewportHeight(state, width, rows);
|
|
10391
|
-
const viewContent = renderActiveView(
|
|
11172
|
+
const viewContent = renderActiveView(output2, state.metricTab, width, state.noColor, state.noInsights);
|
|
10392
11173
|
const contentLines = viewContent.split(`
|
|
10393
11174
|
`);
|
|
10394
11175
|
const effectiveOffset = clampScrollOffset(state.scrollOffset, contentLines.length, viewportHeight);
|
|
@@ -10458,9 +11239,9 @@ async function startTabbedDashboard(providers, options) {
|
|
|
10458
11239
|
const loadAndRender = async (timeRange) => {
|
|
10459
11240
|
const loadId = ++activeLoadId;
|
|
10460
11241
|
paint2(renderLoading2(state));
|
|
10461
|
-
let
|
|
11242
|
+
let output2;
|
|
10462
11243
|
try {
|
|
10463
|
-
|
|
11244
|
+
output2 = await loadForRange(state, providers, timeRange);
|
|
10464
11245
|
} catch (error) {
|
|
10465
11246
|
if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
|
|
10466
11247
|
return;
|
|
@@ -10470,7 +11251,7 @@ async function startTabbedDashboard(providers, options) {
|
|
|
10470
11251
|
if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
|
|
10471
11252
|
return;
|
|
10472
11253
|
}
|
|
10473
|
-
currentOutput =
|
|
11254
|
+
currentOutput = output2;
|
|
10474
11255
|
paint2(renderScreen(currentOutput, state));
|
|
10475
11256
|
};
|
|
10476
11257
|
const rerender = () => {
|
|
@@ -10598,6 +11379,7 @@ var THEME_VALUES = ["dark", "light"];
|
|
|
10598
11379
|
var PROVIDER_SHORTCUTS = {
|
|
10599
11380
|
claude: "claude-code",
|
|
10600
11381
|
codex: "codex",
|
|
11382
|
+
cursor: "cursor",
|
|
10601
11383
|
pi: "pi",
|
|
10602
11384
|
openCode: "open-code"
|
|
10603
11385
|
};
|
|
@@ -10607,6 +11389,9 @@ var PROVIDER_ALIASES = {
|
|
|
10607
11389
|
"claude-code": "claude-code",
|
|
10608
11390
|
claudecode: "claude-code",
|
|
10609
11391
|
codex: "codex",
|
|
11392
|
+
cursor: "cursor",
|
|
11393
|
+
"cursor-ide": "cursor",
|
|
11394
|
+
cursoride: "cursor",
|
|
10610
11395
|
openai: "codex",
|
|
10611
11396
|
pi: "pi",
|
|
10612
11397
|
"pi-mono": "pi",
|
|
@@ -10617,6 +11402,7 @@ var PROVIDER_ALIASES = {
|
|
|
10617
11402
|
var PROVIDER_ALIAS_GROUPS = {
|
|
10618
11403
|
"claude-code": ["anthropic", "claude", "claudecode"],
|
|
10619
11404
|
codex: ["openai"],
|
|
11405
|
+
cursor: ["cursor-ide", "cursoride"],
|
|
10620
11406
|
pi: ["pi-mono"],
|
|
10621
11407
|
"open-code": ["opencode", "open_code"]
|
|
10622
11408
|
};
|
|
@@ -10638,6 +11424,8 @@ function getRequestedProviders(config) {
|
|
|
10638
11424
|
requested.add(PROVIDER_SHORTCUTS.claude);
|
|
10639
11425
|
if (config.codex)
|
|
10640
11426
|
requested.add(PROVIDER_SHORTCUTS.codex);
|
|
11427
|
+
if (config.cursor)
|
|
11428
|
+
requested.add(PROVIDER_SHORTCUTS.cursor);
|
|
10641
11429
|
if (config.pi)
|
|
10642
11430
|
requested.add(PROVIDER_SHORTCUTS.pi);
|
|
10643
11431
|
if (config.openCode)
|
|
@@ -10663,14 +11451,17 @@ function buildHelpText() {
|
|
|
10663
11451
|
" tokenleak [flags]",
|
|
10664
11452
|
" tokenleak explain <date> [flags]",
|
|
10665
11453
|
" tokenleak focus [flags]",
|
|
11454
|
+
" tokenleak cursor <command>",
|
|
10666
11455
|
"",
|
|
10667
11456
|
"Subcommands:",
|
|
10668
11457
|
" explain <date> Explain what drove usage on one day",
|
|
10669
11458
|
" focus Rank sessions by deep-work score",
|
|
11459
|
+
" cursor Manage Cursor auth and cache sync",
|
|
10670
11460
|
"",
|
|
10671
11461
|
"Provider Shortcuts:",
|
|
10672
11462
|
" --claude Only include Claude Code",
|
|
10673
11463
|
" --codex Only include Codex",
|
|
11464
|
+
" --cursor Only include Cursor",
|
|
10674
11465
|
" --pi Only include Pi",
|
|
10675
11466
|
" --open-code Only include OpenCode",
|
|
10676
11467
|
" --all-providers Ignore provider filters and use every available provider",
|
|
@@ -10739,6 +11530,7 @@ function buildFocusHelpText() {
|
|
|
10739
11530
|
" -p, --provider <list> Provider filter list, comma-separated",
|
|
10740
11531
|
" --claude Only include Claude Code",
|
|
10741
11532
|
" --codex Only include Codex",
|
|
11533
|
+
" --cursor Only include Cursor",
|
|
10742
11534
|
" --pi Only include Pi",
|
|
10743
11535
|
" --open-code Only include OpenCode",
|
|
10744
11536
|
" --all-providers Ignore provider filters and use every available provider",
|
|
@@ -10787,6 +11579,9 @@ function buildInteractiveSummary(cliArgs, ok, exitCode) {
|
|
|
10787
11579
|
if (cliArgs["subcommand"] === "focus") {
|
|
10788
11580
|
return "Focus report generated.";
|
|
10789
11581
|
}
|
|
11582
|
+
if (cliArgs["subcommand"] === "cursor") {
|
|
11583
|
+
return "Cursor command completed.";
|
|
11584
|
+
}
|
|
10790
11585
|
if (cliArgs["listProviders"]) {
|
|
10791
11586
|
return "Provider registry loaded.";
|
|
10792
11587
|
}
|
|
@@ -10885,6 +11680,7 @@ function normalizeCliArgv(argv) {
|
|
|
10885
11680
|
function registerBuiltInProviders(registry) {
|
|
10886
11681
|
registry.register(new ClaudeCodeProvider);
|
|
10887
11682
|
registry.register(new CodexProvider);
|
|
11683
|
+
registry.register(new CursorProvider);
|
|
10888
11684
|
registry.register(new PiProvider);
|
|
10889
11685
|
registry.register(new OpenCodeProvider);
|
|
10890
11686
|
}
|
|
@@ -10908,17 +11704,33 @@ function createRegistry() {
|
|
|
10908
11704
|
return registry;
|
|
10909
11705
|
}
|
|
10910
11706
|
function validateProviderSelection(config) {
|
|
10911
|
-
if (config.allProviders && (config.provider || config.claude || config.codex || config.pi || config.openCode)) {
|
|
11707
|
+
if (config.allProviders && (config.provider || config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
|
|
10912
11708
|
throw new TokenleakError("--all-providers cannot be combined with provider filters");
|
|
10913
11709
|
}
|
|
10914
11710
|
}
|
|
10915
11711
|
async function selectAvailableProviders(config) {
|
|
10916
11712
|
validateProviderSelection(config);
|
|
11713
|
+
const requestedProviders = getRequestedProviders(config);
|
|
11714
|
+
const requestedCursor = requestedProviders.has(PROVIDER_SHORTCUTS.cursor);
|
|
11715
|
+
if (requestedCursor && !isCursorLoggedIn() && !hasCursorUsageCache()) {
|
|
11716
|
+
throw new TokenleakError("Cursor is selected but not authenticated. Run `tokenleak cursor login` first.");
|
|
11717
|
+
}
|
|
11718
|
+
const cursorSync = await shouldSyncCursorForRun(config);
|
|
11719
|
+
if (cursorSync.attempted && cursorSync.error) {
|
|
11720
|
+
if (hasCursorUsageCache()) {
|
|
11721
|
+
process.stderr.write(`Cursor sync failed, using cached data: ${cursorSync.error}
|
|
11722
|
+
`);
|
|
11723
|
+
} else if (requestedCursor) {
|
|
11724
|
+
throw new TokenleakError(cursorSync.error);
|
|
11725
|
+
} else {
|
|
11726
|
+
process.stderr.write(`Cursor sync skipped: ${cursorSync.error}
|
|
11727
|
+
`);
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
10917
11730
|
const registry = createRegistry();
|
|
10918
11731
|
let available = await registry.getAvailable();
|
|
10919
|
-
const requestedProviders = getRequestedProviders(config);
|
|
10920
11732
|
if (!config.allProviders && requestedProviders.size > 0) {
|
|
10921
|
-
if (config.provider && (config.claude || config.codex || config.pi || config.openCode)) {
|
|
11733
|
+
if (config.provider && (config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
|
|
10922
11734
|
process.stderr.write(`Combining provider filters: ${Array.from(requestedProviders).join(", ")}
|
|
10923
11735
|
`);
|
|
10924
11736
|
}
|
|
@@ -10926,6 +11738,20 @@ async function selectAvailableProviders(config) {
|
|
|
10926
11738
|
}
|
|
10927
11739
|
return available;
|
|
10928
11740
|
}
|
|
11741
|
+
function resolveTabbedDashboardProviderConfig(opts) {
|
|
11742
|
+
return {
|
|
11743
|
+
provider: opts.providerNames && opts.providerNames.length > 0 ? opts.providerNames.join(",") : undefined,
|
|
11744
|
+
claude: false,
|
|
11745
|
+
codex: false,
|
|
11746
|
+
cursor: false,
|
|
11747
|
+
pi: false,
|
|
11748
|
+
openCode: false,
|
|
11749
|
+
allProviders: false
|
|
11750
|
+
};
|
|
11751
|
+
}
|
|
11752
|
+
async function resolveTabbedDashboardProviders(opts) {
|
|
11753
|
+
return selectAvailableProviders(resolveTabbedDashboardProviderConfig(opts));
|
|
11754
|
+
}
|
|
10929
11755
|
async function loadProviderData(config) {
|
|
10930
11756
|
const dateRange = computeDateRange({
|
|
10931
11757
|
since: config.since,
|
|
@@ -10980,6 +11806,7 @@ function resolveConfig(cliArgs) {
|
|
|
10980
11806
|
more: false,
|
|
10981
11807
|
claude: false,
|
|
10982
11808
|
codex: false,
|
|
11809
|
+
cursor: false,
|
|
10983
11810
|
pi: false,
|
|
10984
11811
|
openCode: false,
|
|
10985
11812
|
allProviders: false,
|
|
@@ -11062,6 +11889,9 @@ function resolveConfig(cliArgs) {
|
|
|
11062
11889
|
if (cliArgs["codex"] !== undefined) {
|
|
11063
11890
|
result.codex = cliArgs["codex"];
|
|
11064
11891
|
}
|
|
11892
|
+
if (cliArgs["cursor"] !== undefined) {
|
|
11893
|
+
result.cursor = cliArgs["cursor"];
|
|
11894
|
+
}
|
|
11065
11895
|
if (cliArgs["pi"] !== undefined) {
|
|
11066
11896
|
result.pi = cliArgs["pi"];
|
|
11067
11897
|
}
|
|
@@ -11108,6 +11938,7 @@ function resolveFocusConfig(cliArgs) {
|
|
|
11108
11938
|
provider: undefined,
|
|
11109
11939
|
claude: false,
|
|
11110
11940
|
codex: false,
|
|
11941
|
+
cursor: false,
|
|
11111
11942
|
pi: false,
|
|
11112
11943
|
openCode: false,
|
|
11113
11944
|
allProviders: false,
|
|
@@ -11169,6 +12000,9 @@ function resolveFocusConfig(cliArgs) {
|
|
|
11169
12000
|
if (cliArgs["codex"] !== undefined) {
|
|
11170
12001
|
result.codex = cliArgs["codex"];
|
|
11171
12002
|
}
|
|
12003
|
+
if (cliArgs["cursor"] !== undefined) {
|
|
12004
|
+
result.cursor = cliArgs["cursor"];
|
|
12005
|
+
}
|
|
11172
12006
|
if (cliArgs["pi"] !== undefined) {
|
|
11173
12007
|
result.pi = cliArgs["pi"];
|
|
11174
12008
|
}
|
|
@@ -11215,9 +12049,10 @@ function formatFocusDuration(durationMs) {
|
|
|
11215
12049
|
function formatFocusDensity(tokensPerHour) {
|
|
11216
12050
|
return `${Math.round(tokensPerHour).toLocaleString("en-US")}/h`;
|
|
11217
12051
|
}
|
|
11218
|
-
var
|
|
12052
|
+
var PROVIDER_COLORS3 = {
|
|
11219
12053
|
"claude-code": 179,
|
|
11220
12054
|
codex: 71,
|
|
12055
|
+
cursor: 78,
|
|
11221
12056
|
pi: 73,
|
|
11222
12057
|
"open-code": 68
|
|
11223
12058
|
};
|
|
@@ -11243,7 +12078,7 @@ function colorDensity(tokensPerHour, text2, noColor2) {
|
|
|
11243
12078
|
return dim(text2, noColor2);
|
|
11244
12079
|
}
|
|
11245
12080
|
function colorProvider(provider, text2, noColor2) {
|
|
11246
|
-
const code =
|
|
12081
|
+
const code = PROVIDER_COLORS3[provider] ?? 246;
|
|
11247
12082
|
return colorize256(text2, code, noColor2);
|
|
11248
12083
|
}
|
|
11249
12084
|
function colorStreak(streak, text2, noColor2) {
|
|
@@ -11382,7 +12217,7 @@ async function runFocus(cliArgs) {
|
|
|
11382
12217
|
if (events.length === 0) {
|
|
11383
12218
|
const emptyMsg = config.format === "json" ? JSON.stringify({ method: "No event data", entries: [] }, null, 2) : renderFocusReport({ method: "No event-level data found for focus analysis.", entries: [] }, config.width, config.noColor);
|
|
11384
12219
|
if (config.output) {
|
|
11385
|
-
|
|
12220
|
+
writeFileSync2(config.output, emptyMsg);
|
|
11386
12221
|
} else {
|
|
11387
12222
|
process.stdout.write(`${emptyMsg}
|
|
11388
12223
|
`);
|
|
@@ -11392,7 +12227,7 @@ async function runFocus(cliArgs) {
|
|
|
11392
12227
|
const report = buildFocusReport(events);
|
|
11393
12228
|
const rendered = config.format === "json" ? JSON.stringify(report, null, 2) : renderFocusReport(report, config.width, config.noColor);
|
|
11394
12229
|
if (config.output) {
|
|
11395
|
-
|
|
12230
|
+
writeFileSync2(config.output, rendered);
|
|
11396
12231
|
} else {
|
|
11397
12232
|
process.stdout.write(`${rendered}
|
|
11398
12233
|
`);
|
|
@@ -11441,7 +12276,7 @@ async function run(cliArgs) {
|
|
|
11441
12276
|
const rendered3 = await renderer2.render(compareResult.output, renderOptions2);
|
|
11442
12277
|
if (config.output) {
|
|
11443
12278
|
const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
|
|
11444
|
-
|
|
12279
|
+
writeFileSync2(config.output, data);
|
|
11445
12280
|
} else {
|
|
11446
12281
|
const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
|
|
11447
12282
|
process.stdout.write(text2 + `
|
|
@@ -11462,7 +12297,7 @@ async function run(cliArgs) {
|
|
|
11462
12297
|
};
|
|
11463
12298
|
const rendered3 = await renderer2.render(compareResult.output, renderOptions2);
|
|
11464
12299
|
if (config.output) {
|
|
11465
|
-
|
|
12300
|
+
writeFileSync2(config.output, rendered3);
|
|
11466
12301
|
} else {
|
|
11467
12302
|
process.stdout.write(`${rendered3}
|
|
11468
12303
|
`);
|
|
@@ -11475,7 +12310,7 @@ async function run(cliArgs) {
|
|
|
11475
12310
|
}
|
|
11476
12311
|
const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
|
|
11477
12312
|
if (config.output) {
|
|
11478
|
-
|
|
12313
|
+
writeFileSync2(config.output, rendered2);
|
|
11479
12314
|
} else {
|
|
11480
12315
|
process.stdout.write(rendered2 + `
|
|
11481
12316
|
`);
|
|
@@ -11502,7 +12337,7 @@ async function run(cliArgs) {
|
|
|
11502
12337
|
process.stderr.write(`Computing extended analytics (hourOfDay, sessions, cache, projections)...
|
|
11503
12338
|
`);
|
|
11504
12339
|
}
|
|
11505
|
-
const
|
|
12340
|
+
const output2 = {
|
|
11506
12341
|
schemaVersion: SCHEMA_VERSION,
|
|
11507
12342
|
generated: new Date().toISOString(),
|
|
11508
12343
|
dateRange,
|
|
@@ -11514,11 +12349,11 @@ async function run(cliArgs) {
|
|
|
11514
12349
|
if (config.format !== "terminal" && config.format !== "json") {
|
|
11515
12350
|
throw new TokenleakError(`--advisor only supports terminal and json formats, got "${config.format}".`);
|
|
11516
12351
|
}
|
|
11517
|
-
const advisorReport = analyzeEfficiency(
|
|
12352
|
+
const advisorReport = analyzeEfficiency(output2, MODEL_PRICING);
|
|
11518
12353
|
if (config.format === "json") {
|
|
11519
12354
|
const rendered3 = JSON.stringify(advisorReport, null, 2);
|
|
11520
12355
|
if (config.output) {
|
|
11521
|
-
|
|
12356
|
+
writeFileSync2(config.output, rendered3);
|
|
11522
12357
|
} else {
|
|
11523
12358
|
process.stdout.write(rendered3 + `
|
|
11524
12359
|
`);
|
|
@@ -11530,7 +12365,7 @@ async function run(cliArgs) {
|
|
|
11530
12365
|
noColor: config.noColor
|
|
11531
12366
|
});
|
|
11532
12367
|
if (config.output) {
|
|
11533
|
-
|
|
12368
|
+
writeFileSync2(config.output, rendered2);
|
|
11534
12369
|
} else {
|
|
11535
12370
|
process.stdout.write(rendered2 + `
|
|
11536
12371
|
`);
|
|
@@ -11539,8 +12374,8 @@ async function run(cliArgs) {
|
|
|
11539
12374
|
}
|
|
11540
12375
|
if (config.format === "wrapped") {
|
|
11541
12376
|
const outputPath = config.output ?? "tokenleak-wrapped.png";
|
|
11542
|
-
const wrappedBuffer = await renderWrappedPng(
|
|
11543
|
-
|
|
12377
|
+
const wrappedBuffer = await renderWrappedPng(output2, { theme: config.theme });
|
|
12378
|
+
writeFileSync2(outputPath, wrappedBuffer);
|
|
11544
12379
|
process.stderr.write(`Wrapped PNG written to ${outputPath}
|
|
11545
12380
|
`);
|
|
11546
12381
|
if (config.clipboard) {
|
|
@@ -11587,7 +12422,7 @@ async function run(cliArgs) {
|
|
|
11587
12422
|
output: config.output,
|
|
11588
12423
|
more: config.more
|
|
11589
12424
|
};
|
|
11590
|
-
const { port } = await startLiveServer(
|
|
12425
|
+
const { port } = await startLiveServer(output2, renderOptions2);
|
|
11591
12426
|
await new Promise((resolve) => {
|
|
11592
12427
|
process.on("SIGINT", () => {
|
|
11593
12428
|
process.stderr.write(`
|
|
@@ -11617,7 +12452,7 @@ Shutting down server...
|
|
|
11617
12452
|
}
|
|
11618
12453
|
process.stderr.write(`Generating wrapped presentation...
|
|
11619
12454
|
`);
|
|
11620
|
-
const { stop } = await startWrappedLiveServer(
|
|
12455
|
+
const { stop } = await startWrappedLiveServer(output2);
|
|
11621
12456
|
process.stderr.write(`Press Ctrl+C to stop the server.
|
|
11622
12457
|
`);
|
|
11623
12458
|
await new Promise((resolve) => {
|
|
@@ -11646,10 +12481,10 @@ Shutting down wrapped live server...
|
|
|
11646
12481
|
output: config.output,
|
|
11647
12482
|
more: config.more
|
|
11648
12483
|
};
|
|
11649
|
-
const rendered = await renderer.render(
|
|
12484
|
+
const rendered = await renderer.render(output2, renderOptions);
|
|
11650
12485
|
if (config.output) {
|
|
11651
12486
|
const data = typeof rendered === "string" ? rendered : Buffer.from(rendered);
|
|
11652
|
-
|
|
12487
|
+
writeFileSync2(config.output, data);
|
|
11653
12488
|
} else {
|
|
11654
12489
|
const text2 = typeof rendered === "string" ? rendered : rendered.toString("utf-8");
|
|
11655
12490
|
process.stdout.write(text2 + `
|
|
@@ -11751,6 +12586,10 @@ function parseExplainArgs(argv) {
|
|
|
11751
12586
|
cliArgs["codex"] = true;
|
|
11752
12587
|
index += 1;
|
|
11753
12588
|
break;
|
|
12589
|
+
case "--cursor":
|
|
12590
|
+
cliArgs["cursor"] = true;
|
|
12591
|
+
index += 1;
|
|
12592
|
+
break;
|
|
11754
12593
|
case "--pi":
|
|
11755
12594
|
cliArgs["pi"] = true;
|
|
11756
12595
|
index += 1;
|
|
@@ -11795,17 +12634,11 @@ function resolveExplainFormat(cliArgs) {
|
|
|
11795
12634
|
async function runExplain(date, cliArgs) {
|
|
11796
12635
|
const config = resolveConfig(cliArgs);
|
|
11797
12636
|
const format = resolveExplainFormat(cliArgs);
|
|
11798
|
-
if (config.allProviders && (config.provider || config.claude || config.codex || config.pi || config.openCode)) {
|
|
12637
|
+
if (config.allProviders && (config.provider || config.claude || config.codex || config.cursor || config.pi || config.openCode)) {
|
|
11799
12638
|
throw new TokenleakError("--all-providers cannot be combined with provider filters");
|
|
11800
12639
|
}
|
|
11801
12640
|
const explainRange = computeDateRange({ until: date, days: 30 });
|
|
11802
|
-
const
|
|
11803
|
-
registerBuiltInProviders(registry);
|
|
11804
|
-
let available = await registry.getAvailable();
|
|
11805
|
-
const requestedProviders = getRequestedProviders(config);
|
|
11806
|
-
if (!config.allProviders && requestedProviders.size > 0) {
|
|
11807
|
-
available = available.filter((provider) => providerMatchesFilter(provider, requestedProviders));
|
|
11808
|
-
}
|
|
12641
|
+
const available = await selectAvailableProviders(config);
|
|
11809
12642
|
if (available.length === 0) {
|
|
11810
12643
|
throw new TokenleakError("No provider data found");
|
|
11811
12644
|
}
|
|
@@ -11813,7 +12646,7 @@ async function runExplain(date, cliArgs) {
|
|
|
11813
12646
|
const report = buildExplainReport(explainOutput.providers, date);
|
|
11814
12647
|
const rendered = format === "json" ? JSON.stringify(report, null, 2) : renderExplainTerminal(report, config.width);
|
|
11815
12648
|
if (config.output) {
|
|
11816
|
-
|
|
12649
|
+
writeFileSync2(config.output, rendered);
|
|
11817
12650
|
} else {
|
|
11818
12651
|
process.stdout.write(rendered + `
|
|
11819
12652
|
`);
|
|
@@ -11895,6 +12728,11 @@ var main = defineCommand({
|
|
|
11895
12728
|
description: "Shortcut for --provider codex",
|
|
11896
12729
|
default: false
|
|
11897
12730
|
},
|
|
12731
|
+
cursor: {
|
|
12732
|
+
type: "boolean",
|
|
12733
|
+
description: "Shortcut for --provider cursor",
|
|
12734
|
+
default: false
|
|
12735
|
+
},
|
|
11898
12736
|
pi: {
|
|
11899
12737
|
type: "boolean",
|
|
11900
12738
|
description: "Shortcut for --provider pi",
|
|
@@ -11977,6 +12815,8 @@ var main = defineCommand({
|
|
|
11977
12815
|
cliArgs["claude"] = true;
|
|
11978
12816
|
if (args.codex)
|
|
11979
12817
|
cliArgs["codex"] = true;
|
|
12818
|
+
if (args.cursor)
|
|
12819
|
+
cliArgs["cursor"] = true;
|
|
11980
12820
|
if (args.pi)
|
|
11981
12821
|
cliArgs["pi"] = true;
|
|
11982
12822
|
if (args.openCode)
|
|
@@ -12060,6 +12900,11 @@ var focusMain = defineCommand({
|
|
|
12060
12900
|
description: "Shortcut for --provider codex",
|
|
12061
12901
|
default: false
|
|
12062
12902
|
},
|
|
12903
|
+
cursor: {
|
|
12904
|
+
type: "boolean",
|
|
12905
|
+
description: "Shortcut for --provider cursor",
|
|
12906
|
+
default: false
|
|
12907
|
+
},
|
|
12063
12908
|
pi: {
|
|
12064
12909
|
type: "boolean",
|
|
12065
12910
|
description: "Shortcut for --provider pi",
|
|
@@ -12104,6 +12949,8 @@ var focusMain = defineCommand({
|
|
|
12104
12949
|
cliArgs["claude"] = true;
|
|
12105
12950
|
if (args.codex)
|
|
12106
12951
|
cliArgs["codex"] = true;
|
|
12952
|
+
if (args.cursor)
|
|
12953
|
+
cliArgs["cursor"] = true;
|
|
12107
12954
|
if (args.pi)
|
|
12108
12955
|
cliArgs["pi"] = true;
|
|
12109
12956
|
if (args.openCode)
|
|
@@ -12153,6 +13000,22 @@ if (isDirectExecution) {
|
|
|
12153
13000
|
await runMain(focusMain);
|
|
12154
13001
|
process.exit(0);
|
|
12155
13002
|
}
|
|
13003
|
+
if (argv[0] === "cursor") {
|
|
13004
|
+
try {
|
|
13005
|
+
if (argv[1] === "--help" || argv[1] === "-h" || argv.length === 1) {
|
|
13006
|
+
process.stdout.write(buildCursorHelpText());
|
|
13007
|
+
process.exit(0);
|
|
13008
|
+
}
|
|
13009
|
+
if (argv[1] === "--version" || argv[1] === "-v") {
|
|
13010
|
+
process.stdout.write(buildVersionText());
|
|
13011
|
+
process.exit(0);
|
|
13012
|
+
}
|
|
13013
|
+
await runCursorCommand(argv.slice(1));
|
|
13014
|
+
process.exit(0);
|
|
13015
|
+
} catch (error) {
|
|
13016
|
+
handleError(error);
|
|
13017
|
+
}
|
|
13018
|
+
}
|
|
12156
13019
|
process.argv = [...process.argv.slice(0, 2), ...normalizedArgv];
|
|
12157
13020
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
12158
13021
|
process.stdout.write(buildHelpText());
|
|
@@ -12163,11 +13026,8 @@ if (isDirectExecution) {
|
|
|
12163
13026
|
process.exit(0);
|
|
12164
13027
|
}
|
|
12165
13028
|
if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
12166
|
-
const registry = createRegistry();
|
|
12167
|
-
const available = await registry.getAvailable();
|
|
12168
13029
|
const launchTabbed = async (opts) => {
|
|
12169
|
-
const
|
|
12170
|
-
const scopedProviders = requested.size > 0 ? available.filter((provider) => providerMatchesFilter(provider, requested)) : available;
|
|
13030
|
+
const scopedProviders = await resolveTabbedDashboardProviders(opts);
|
|
12171
13031
|
if (scopedProviders.length === 0) {
|
|
12172
13032
|
throw new TokenleakError("No provider data found");
|
|
12173
13033
|
}
|
|
@@ -12184,6 +13044,8 @@ if (isDirectExecution) {
|
|
|
12184
13044
|
export {
|
|
12185
13045
|
runFocus,
|
|
12186
13046
|
run,
|
|
13047
|
+
resolveTabbedDashboardProviders,
|
|
13048
|
+
resolveTabbedDashboardProviderConfig,
|
|
12187
13049
|
resolveFocusConfig,
|
|
12188
13050
|
resolveConfig,
|
|
12189
13051
|
renderFocusReport,
|