tokenleak 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -14
- package/package.json +1 -1
- package/tokenleak +2321 -487
package/tokenleak
CHANGED
|
@@ -755,8 +755,236 @@ function computePreviousPeriod(current) {
|
|
|
755
755
|
until: formatDateStringUtc(prevUntil)
|
|
756
756
|
};
|
|
757
757
|
}
|
|
758
|
+
// packages/core/dist/aggregation/more.js
|
|
759
|
+
function daysInMonth(dateString) {
|
|
760
|
+
const [year, month] = dateString.split("-").map(Number);
|
|
761
|
+
if (!year || !month) {
|
|
762
|
+
return 30;
|
|
763
|
+
}
|
|
764
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
765
|
+
}
|
|
766
|
+
function buildInputOutput(providers) {
|
|
767
|
+
let inputTokens = 0;
|
|
768
|
+
let outputTokens = 0;
|
|
769
|
+
for (const provider of providers) {
|
|
770
|
+
for (const day of provider.daily) {
|
|
771
|
+
inputTokens += day.inputTokens;
|
|
772
|
+
outputTokens += day.outputTokens;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const nonCacheTokens = inputTokens + outputTokens;
|
|
776
|
+
return {
|
|
777
|
+
inputPerOutput: outputTokens > 0 ? inputTokens / outputTokens : null,
|
|
778
|
+
outputPerInput: inputTokens > 0 ? outputTokens / inputTokens : null,
|
|
779
|
+
outputShare: nonCacheTokens > 0 ? outputTokens / nonCacheTokens : 0
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function buildMonthlyBurn(providers, range) {
|
|
783
|
+
const monthPrefix = range.until.slice(0, 7);
|
|
784
|
+
const monthStart = `${monthPrefix}-01`;
|
|
785
|
+
const observedSince = range.since > monthStart ? range.since : monthStart;
|
|
786
|
+
const observedDays = Math.max(1, Math.round((Date.parse(`${range.until}T00:00:00Z`) - Date.parse(`${observedSince}T00:00:00Z`)) / 86400000) + 1);
|
|
787
|
+
let observedTokens = 0;
|
|
788
|
+
let observedCost = 0;
|
|
789
|
+
for (const provider of providers) {
|
|
790
|
+
for (const day of provider.daily) {
|
|
791
|
+
if (day.date >= observedSince && day.date <= range.until) {
|
|
792
|
+
observedTokens += day.totalTokens;
|
|
793
|
+
observedCost += day.cost;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const calendarDays = daysInMonth(range.until);
|
|
798
|
+
const tokensPerDay = observedTokens / observedDays;
|
|
799
|
+
const costPerDay = observedCost / observedDays;
|
|
800
|
+
return {
|
|
801
|
+
projectedTokens: tokensPerDay * calendarDays,
|
|
802
|
+
projectedCost: costPerDay * calendarDays,
|
|
803
|
+
observedDays,
|
|
804
|
+
calendarDays
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function buildCacheEconomics(providers) {
|
|
808
|
+
let readTokens = 0;
|
|
809
|
+
let writeTokens = 0;
|
|
810
|
+
let inputTokens = 0;
|
|
811
|
+
for (const provider of providers) {
|
|
812
|
+
for (const day of provider.daily) {
|
|
813
|
+
readTokens += day.cacheReadTokens;
|
|
814
|
+
writeTokens += day.cacheWriteTokens;
|
|
815
|
+
inputTokens += day.inputTokens;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const readCoverage = readTokens + inputTokens > 0 ? readTokens / (readTokens + inputTokens) : 0;
|
|
819
|
+
return {
|
|
820
|
+
readTokens,
|
|
821
|
+
writeTokens,
|
|
822
|
+
readCoverage,
|
|
823
|
+
reuseRatio: writeTokens > 0 ? readTokens / writeTokens : null
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function collectEvents(providers) {
|
|
827
|
+
return providers.flatMap((provider) => provider.events ?? []);
|
|
828
|
+
}
|
|
829
|
+
function buildHourOfDay(events) {
|
|
830
|
+
const buckets = Array.from({ length: 24 }, (_, hour) => ({
|
|
831
|
+
hour,
|
|
832
|
+
tokens: 0,
|
|
833
|
+
cost: 0,
|
|
834
|
+
count: 0
|
|
835
|
+
}));
|
|
836
|
+
for (const event of events) {
|
|
837
|
+
const date = new Date(event.timestamp);
|
|
838
|
+
if (Number.isNaN(date.getTime())) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const bucket = buckets[date.getUTCHours()];
|
|
842
|
+
if (!bucket) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
bucket.tokens += event.totalTokens;
|
|
846
|
+
bucket.cost += event.cost;
|
|
847
|
+
bucket.count += 1;
|
|
848
|
+
}
|
|
849
|
+
return buckets;
|
|
850
|
+
}
|
|
851
|
+
function buildSessionMetrics(events) {
|
|
852
|
+
const sessions = new Map;
|
|
853
|
+
const projects = new Map;
|
|
854
|
+
for (const event of events) {
|
|
855
|
+
const key = event.sessionId?.trim() || `${event.provider}:${event.timestamp}`;
|
|
856
|
+
const timestamp = Date.parse(event.timestamp);
|
|
857
|
+
const safeTime = Number.isFinite(timestamp) ? timestamp : 0;
|
|
858
|
+
let session = sessions.get(key);
|
|
859
|
+
if (!session) {
|
|
860
|
+
session = {
|
|
861
|
+
label: event.projectId?.trim() || event.sessionId?.trim() || key,
|
|
862
|
+
tokens: 0,
|
|
863
|
+
cost: 0,
|
|
864
|
+
count: 0,
|
|
865
|
+
projectId: event.projectId?.trim() || undefined,
|
|
866
|
+
firstTimestamp: safeTime,
|
|
867
|
+
lastTimestamp: safeTime,
|
|
868
|
+
explicitDurationMs: 0,
|
|
869
|
+
hasExplicitDuration: false
|
|
870
|
+
};
|
|
871
|
+
sessions.set(key, session);
|
|
872
|
+
}
|
|
873
|
+
session.tokens += event.totalTokens;
|
|
874
|
+
session.cost += event.cost;
|
|
875
|
+
session.count += 1;
|
|
876
|
+
session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
|
|
877
|
+
session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
|
|
878
|
+
if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
|
|
879
|
+
session.explicitDurationMs += Math.max(0, event.durationMs);
|
|
880
|
+
session.hasExplicitDuration = true;
|
|
881
|
+
}
|
|
882
|
+
if (event.projectId?.trim()) {
|
|
883
|
+
projects.set(event.projectId, (projects.get(event.projectId) ?? 0) + event.totalTokens);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const sessionEntries = [...sessions.values()];
|
|
887
|
+
const totalSessions = sessionEntries.length;
|
|
888
|
+
let totalTokens = 0;
|
|
889
|
+
let totalCost = 0;
|
|
890
|
+
let totalMessages = 0;
|
|
891
|
+
let durationTotal = 0;
|
|
892
|
+
let durationCount = 0;
|
|
893
|
+
let longestSession = null;
|
|
894
|
+
let longestSessionDuration = -1;
|
|
895
|
+
for (const session of sessionEntries) {
|
|
896
|
+
totalTokens += session.tokens;
|
|
897
|
+
totalCost += session.cost;
|
|
898
|
+
totalMessages += session.count;
|
|
899
|
+
const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
|
|
900
|
+
if (derivedDurationMs > 0) {
|
|
901
|
+
durationTotal += derivedDurationMs;
|
|
902
|
+
durationCount += 1;
|
|
903
|
+
}
|
|
904
|
+
if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
|
|
905
|
+
longestSessionDuration = derivedDurationMs;
|
|
906
|
+
longestSession = {
|
|
907
|
+
label: session.label,
|
|
908
|
+
tokens: session.tokens,
|
|
909
|
+
cost: session.cost,
|
|
910
|
+
count: session.count,
|
|
911
|
+
durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
let topProject = null;
|
|
916
|
+
for (const [name, tokens] of projects) {
|
|
917
|
+
if (!topProject || tokens > topProject.tokens) {
|
|
918
|
+
topProject = { name, tokens };
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
totalSessions,
|
|
923
|
+
averageTokens: totalSessions > 0 ? totalTokens / totalSessions : 0,
|
|
924
|
+
averageCost: totalSessions > 0 ? totalCost / totalSessions : 0,
|
|
925
|
+
averageMessages: totalSessions > 0 ? totalMessages / totalSessions : 0,
|
|
926
|
+
averageDurationMs: durationCount > 0 ? durationTotal / durationCount : null,
|
|
927
|
+
longestSession,
|
|
928
|
+
projectCount: projects.size,
|
|
929
|
+
topProject
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
|
|
933
|
+
const currentModelTokens = new Map;
|
|
934
|
+
const previousModelTokens = new Map;
|
|
935
|
+
let currentTotal = 0;
|
|
936
|
+
let previousTotal = 0;
|
|
937
|
+
for (const provider of currentProviders) {
|
|
938
|
+
for (const day of provider.daily) {
|
|
939
|
+
for (const model of day.models) {
|
|
940
|
+
currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
941
|
+
currentTotal += model.totalTokens;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const provider of previousProviders) {
|
|
946
|
+
for (const day of provider.daily) {
|
|
947
|
+
for (const model of day.models) {
|
|
948
|
+
previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
949
|
+
previousTotal += model.totalTokens;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const models = new Set([
|
|
954
|
+
...currentModelTokens.keys(),
|
|
955
|
+
...previousModelTokens.keys()
|
|
956
|
+
]);
|
|
957
|
+
return [...models].map((model) => {
|
|
958
|
+
const currentTokens = currentModelTokens.get(model) ?? 0;
|
|
959
|
+
const previousTokens = previousModelTokens.get(model) ?? 0;
|
|
960
|
+
const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
|
|
961
|
+
const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
|
|
962
|
+
return {
|
|
963
|
+
model,
|
|
964
|
+
currentShare,
|
|
965
|
+
previousShare,
|
|
966
|
+
deltaShare: currentShare - previousShare,
|
|
967
|
+
currentTokens,
|
|
968
|
+
previousTokens
|
|
969
|
+
};
|
|
970
|
+
}).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
|
|
971
|
+
}
|
|
972
|
+
function buildMoreStats(providers, range, compare = null) {
|
|
973
|
+
const events = collectEvents(providers);
|
|
974
|
+
return {
|
|
975
|
+
inputOutput: buildInputOutput(providers),
|
|
976
|
+
monthlyBurn: buildMonthlyBurn(providers, range),
|
|
977
|
+
cacheEconomics: buildCacheEconomics(providers),
|
|
978
|
+
hourOfDay: buildHourOfDay(events),
|
|
979
|
+
sessionMetrics: buildSessionMetrics(events),
|
|
980
|
+
compare: compare ? {
|
|
981
|
+
previousRange: compare.previousRange,
|
|
982
|
+
modelMixShift: computeModelMixShift(providers, compare.previousProviders)
|
|
983
|
+
} : null
|
|
984
|
+
};
|
|
985
|
+
}
|
|
758
986
|
// packages/core/dist/index.js
|
|
759
|
-
var VERSION = "1.0.
|
|
987
|
+
var VERSION = "1.0.2";
|
|
760
988
|
|
|
761
989
|
// packages/registry/dist/models/normalizer.js
|
|
762
990
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -1022,7 +1250,7 @@ async function* splitJsonlRecords(filePath) {
|
|
|
1022
1250
|
}
|
|
1023
1251
|
// packages/registry/dist/providers/claude-code.js
|
|
1024
1252
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1025
|
-
import { join } from "path";
|
|
1253
|
+
import { dirname, join, relative, sep } from "path";
|
|
1026
1254
|
import { homedir } from "os";
|
|
1027
1255
|
|
|
1028
1256
|
// packages/registry/dist/utils.js
|
|
@@ -1101,6 +1329,7 @@ function extractUsage(record) {
|
|
|
1101
1329
|
}
|
|
1102
1330
|
return {
|
|
1103
1331
|
date,
|
|
1332
|
+
timestamp,
|
|
1104
1333
|
model,
|
|
1105
1334
|
inputTokens,
|
|
1106
1335
|
outputTokens,
|
|
@@ -1181,13 +1410,18 @@ class ClaudeCodeProvider {
|
|
|
1181
1410
|
async load(range) {
|
|
1182
1411
|
const files = collectJsonlFiles(this.baseDir);
|
|
1183
1412
|
const allRecords = [];
|
|
1413
|
+
const allEvents = [];
|
|
1184
1414
|
for (const file of files) {
|
|
1185
1415
|
const latestRecordsByMessageId = new Map;
|
|
1186
1416
|
const anonymousRecords = [];
|
|
1417
|
+
const relativeFile = relative(this.baseDir, file).split(sep).join("/");
|
|
1418
|
+
const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
|
|
1187
1419
|
try {
|
|
1188
1420
|
for await (const record of splitJsonlRecords(file)) {
|
|
1189
1421
|
const usage = extractUsage(record);
|
|
1190
1422
|
if (usage !== null && isInRange(usage.date, range)) {
|
|
1423
|
+
usage.sessionId = relativeFile;
|
|
1424
|
+
usage.projectId = projectId;
|
|
1191
1425
|
if (usage.messageId) {
|
|
1192
1426
|
latestRecordsByMessageId.set(usage.messageId, usage);
|
|
1193
1427
|
} else {
|
|
@@ -1201,6 +1435,24 @@ class ClaudeCodeProvider {
|
|
|
1201
1435
|
allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
|
|
1202
1436
|
}
|
|
1203
1437
|
const daily = buildDailyUsage(allRecords);
|
|
1438
|
+
for (const record of allRecords) {
|
|
1439
|
+
const normalizedModel = normalizeModelName(record.model);
|
|
1440
|
+
const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1441
|
+
allEvents.push({
|
|
1442
|
+
provider: this.name,
|
|
1443
|
+
timestamp: record.timestamp,
|
|
1444
|
+
date: record.date,
|
|
1445
|
+
model: normalizedModel,
|
|
1446
|
+
inputTokens: record.inputTokens,
|
|
1447
|
+
outputTokens: record.outputTokens,
|
|
1448
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1449
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1450
|
+
totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
|
|
1451
|
+
cost,
|
|
1452
|
+
sessionId: record.sessionId,
|
|
1453
|
+
projectId: record.projectId
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1204
1456
|
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
1205
1457
|
const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
|
|
1206
1458
|
return {
|
|
@@ -1209,13 +1461,14 @@ class ClaudeCodeProvider {
|
|
|
1209
1461
|
daily,
|
|
1210
1462
|
totalTokens,
|
|
1211
1463
|
totalCost,
|
|
1212
|
-
colors: this.colors
|
|
1464
|
+
colors: this.colors,
|
|
1465
|
+
events: allEvents
|
|
1213
1466
|
};
|
|
1214
1467
|
}
|
|
1215
1468
|
}
|
|
1216
1469
|
// packages/registry/dist/providers/codex.js
|
|
1217
1470
|
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1218
|
-
import { join as join2 } from "path";
|
|
1471
|
+
import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
1219
1472
|
import { homedir as homedir2 } from "os";
|
|
1220
1473
|
var CODEX_COLORS = {
|
|
1221
1474
|
primary: "#10a37f",
|
|
@@ -1373,6 +1626,7 @@ function parseTokenCountUsage(record, context) {
|
|
|
1373
1626
|
const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
|
|
1374
1627
|
return {
|
|
1375
1628
|
date,
|
|
1629
|
+
timestamp,
|
|
1376
1630
|
model: context.model,
|
|
1377
1631
|
inputTokens,
|
|
1378
1632
|
outputTokens: usage.outputTokens,
|
|
@@ -1403,6 +1657,7 @@ function parseUsageRecord(record, context) {
|
|
|
1403
1657
|
}
|
|
1404
1658
|
return {
|
|
1405
1659
|
date,
|
|
1660
|
+
timestamp: legacyEvent.timestamp,
|
|
1406
1661
|
model: compactModelDateSuffix(legacyEvent.model),
|
|
1407
1662
|
inputTokens: legacyEvent.usage.input_tokens,
|
|
1408
1663
|
outputTokens: legacyEvent.usage.output_tokens,
|
|
@@ -1429,11 +1684,14 @@ class CodexProvider {
|
|
|
1429
1684
|
async load(range) {
|
|
1430
1685
|
const dailyMap = new Map;
|
|
1431
1686
|
const files = collectJsonlFiles2(this.sessionsDir);
|
|
1687
|
+
const events = [];
|
|
1432
1688
|
for (const file of files) {
|
|
1433
1689
|
const context = {
|
|
1434
1690
|
model: "gpt-5",
|
|
1435
1691
|
previousTotals: null
|
|
1436
1692
|
};
|
|
1693
|
+
const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
|
|
1694
|
+
const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
|
|
1437
1695
|
try {
|
|
1438
1696
|
for await (const record of splitJsonlRecords(file)) {
|
|
1439
1697
|
const usage = parseUsageRecord(record, context);
|
|
@@ -1443,12 +1701,28 @@ class CodexProvider {
|
|
|
1443
1701
|
if (!isInRange(usage.date, range)) {
|
|
1444
1702
|
continue;
|
|
1445
1703
|
}
|
|
1704
|
+
usage.sessionId = relativeFile;
|
|
1705
|
+
usage.projectId = projectDir === "." ? undefined : projectDir;
|
|
1446
1706
|
const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
|
|
1447
1707
|
const inputTokens = usage.inputTokens;
|
|
1448
1708
|
const outputTokens = usage.outputTokens;
|
|
1449
1709
|
const cacheReadTokens = usage.cacheReadTokens;
|
|
1450
1710
|
const cacheWriteTokens = usage.cacheWriteTokens;
|
|
1451
1711
|
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1712
|
+
events.push({
|
|
1713
|
+
provider: this.name,
|
|
1714
|
+
timestamp: usage.timestamp,
|
|
1715
|
+
date: usage.date,
|
|
1716
|
+
model: normalizedModel,
|
|
1717
|
+
inputTokens,
|
|
1718
|
+
outputTokens,
|
|
1719
|
+
cacheReadTokens,
|
|
1720
|
+
cacheWriteTokens,
|
|
1721
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
1722
|
+
cost,
|
|
1723
|
+
sessionId: usage.sessionId,
|
|
1724
|
+
projectId: usage.projectId
|
|
1725
|
+
});
|
|
1452
1726
|
if (!dailyMap.has(usage.date)) {
|
|
1453
1727
|
dailyMap.set(usage.date, new Map);
|
|
1454
1728
|
}
|
|
@@ -1503,7 +1777,8 @@ class CodexProvider {
|
|
|
1503
1777
|
daily,
|
|
1504
1778
|
totalTokens,
|
|
1505
1779
|
totalCost,
|
|
1506
|
-
colors: this.colors
|
|
1780
|
+
colors: this.colors,
|
|
1781
|
+
events
|
|
1507
1782
|
};
|
|
1508
1783
|
}
|
|
1509
1784
|
}
|
|
@@ -1513,7 +1788,7 @@ import { join as join3 } from "path";
|
|
|
1513
1788
|
import { homedir as homedir3 } from "os";
|
|
1514
1789
|
import { Database } from "bun:sqlite";
|
|
1515
1790
|
var PROVIDER_NAME = "open-code";
|
|
1516
|
-
var DISPLAY_NAME = "
|
|
1791
|
+
var DISPLAY_NAME = "OpenCode";
|
|
1517
1792
|
var COLORS = {
|
|
1518
1793
|
primary: "#6366f1",
|
|
1519
1794
|
secondary: "#a78bfa",
|
|
@@ -1549,12 +1824,46 @@ function extractDate2(createdAt) {
|
|
|
1549
1824
|
}
|
|
1550
1825
|
return date.toISOString().slice(0, 10);
|
|
1551
1826
|
}
|
|
1827
|
+
function toTimestampMillis(createdAt) {
|
|
1828
|
+
const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
|
|
1829
|
+
if (!Number.isFinite(timestamp)) {
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
|
|
1833
|
+
return Number.isFinite(millis) ? millis : null;
|
|
1834
|
+
}
|
|
1835
|
+
function toIsoTimestamp(createdAt) {
|
|
1836
|
+
const millis = toTimestampMillis(createdAt);
|
|
1837
|
+
if (millis === null) {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
const date = new Date(millis);
|
|
1841
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1842
|
+
}
|
|
1552
1843
|
function getRecordCost(record) {
|
|
1553
1844
|
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
1554
1845
|
return record.explicitCost;
|
|
1555
1846
|
}
|
|
1556
1847
|
return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1557
1848
|
}
|
|
1849
|
+
function toUsageEvent(record) {
|
|
1850
|
+
const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
1851
|
+
return {
|
|
1852
|
+
provider: PROVIDER_NAME,
|
|
1853
|
+
timestamp: record.timestamp,
|
|
1854
|
+
date: record.date,
|
|
1855
|
+
model: normalizeModelName(record.model),
|
|
1856
|
+
inputTokens: record.inputTokens,
|
|
1857
|
+
outputTokens: record.outputTokens,
|
|
1858
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1859
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1860
|
+
totalTokens,
|
|
1861
|
+
cost: getRecordCost(record),
|
|
1862
|
+
sessionId: record.sessionId,
|
|
1863
|
+
projectId: record.projectId,
|
|
1864
|
+
durationMs: record.durationMs
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1558
1867
|
function buildProviderData(records) {
|
|
1559
1868
|
const byDate = new Map;
|
|
1560
1869
|
for (const record of records) {
|
|
@@ -1614,7 +1923,8 @@ function buildProviderData(records) {
|
|
|
1614
1923
|
daily,
|
|
1615
1924
|
totalTokens,
|
|
1616
1925
|
totalCost,
|
|
1617
|
-
colors: COLORS
|
|
1926
|
+
colors: COLORS,
|
|
1927
|
+
events: records.map(toUsageEvent)
|
|
1618
1928
|
};
|
|
1619
1929
|
}
|
|
1620
1930
|
function loadFromSqlite(dbPath, range) {
|
|
@@ -1629,18 +1939,21 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1629
1939
|
if (tables.length === 0) {
|
|
1630
1940
|
return [];
|
|
1631
1941
|
}
|
|
1632
|
-
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1942
|
+
const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1633
1943
|
const records = [];
|
|
1634
1944
|
for (const row of rows) {
|
|
1635
1945
|
const date = extractDate2(row.created_at);
|
|
1636
|
-
|
|
1946
|
+
const timestamp = toIsoTimestamp(row.created_at);
|
|
1947
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1637
1948
|
records.push({
|
|
1638
1949
|
date,
|
|
1950
|
+
timestamp,
|
|
1639
1951
|
model: row.model,
|
|
1640
1952
|
inputTokens: row.input_tokens,
|
|
1641
1953
|
outputTokens: row.output_tokens,
|
|
1642
1954
|
cacheReadTokens: 0,
|
|
1643
|
-
cacheWriteTokens: 0
|
|
1955
|
+
cacheWriteTokens: 0,
|
|
1956
|
+
sessionId: row.session_id
|
|
1644
1957
|
});
|
|
1645
1958
|
}
|
|
1646
1959
|
}
|
|
@@ -1666,14 +1979,17 @@ function loadFromLegacyJson(sessionsDir, range) {
|
|
|
1666
1979
|
continue;
|
|
1667
1980
|
}
|
|
1668
1981
|
const date = extractDate2(msg.created_at);
|
|
1669
|
-
|
|
1982
|
+
const timestamp = toIsoTimestamp(msg.created_at);
|
|
1983
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1670
1984
|
records.push({
|
|
1671
1985
|
date,
|
|
1986
|
+
timestamp,
|
|
1672
1987
|
model: msg.model,
|
|
1673
1988
|
inputTokens: msg.usage.input_tokens,
|
|
1674
1989
|
outputTokens: msg.usage.output_tokens,
|
|
1675
1990
|
cacheReadTokens: 0,
|
|
1676
|
-
cacheWriteTokens: 0
|
|
1991
|
+
cacheWriteTokens: 0,
|
|
1992
|
+
sessionId: file
|
|
1677
1993
|
});
|
|
1678
1994
|
}
|
|
1679
1995
|
}
|
|
@@ -1711,7 +2027,8 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1711
2027
|
continue;
|
|
1712
2028
|
}
|
|
1713
2029
|
const date = extractDate2(createdAt);
|
|
1714
|
-
|
|
2030
|
+
const timestamp = toIsoTimestamp(createdAt);
|
|
2031
|
+
if (!date || !timestamp || !isInRange(date, range)) {
|
|
1715
2032
|
continue;
|
|
1716
2033
|
}
|
|
1717
2034
|
const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
|
|
@@ -1720,13 +2037,21 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1720
2037
|
const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
|
|
1721
2038
|
const record = {
|
|
1722
2039
|
date,
|
|
2040
|
+
timestamp,
|
|
1723
2041
|
model,
|
|
1724
2042
|
inputTokens,
|
|
1725
2043
|
outputTokens,
|
|
1726
2044
|
cacheReadTokens,
|
|
1727
2045
|
cacheWriteTokens,
|
|
1728
|
-
explicitCost: typeof message.cost === "number" ? message.cost : undefined
|
|
2046
|
+
explicitCost: typeof message.cost === "number" ? message.cost : undefined,
|
|
2047
|
+
sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
|
|
1729
2048
|
};
|
|
2049
|
+
const completedAt = message.time?.completed;
|
|
2050
|
+
const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
|
|
2051
|
+
const createdMs = Date.parse(record.timestamp);
|
|
2052
|
+
if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
|
|
2053
|
+
record.durationMs = completedMs - createdMs;
|
|
2054
|
+
}
|
|
1730
2055
|
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1731
2056
|
if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
|
|
1732
2057
|
continue;
|
|
@@ -1792,66 +2117,10 @@ class JsonRenderer {
|
|
|
1792
2117
|
return JSON.stringify(output, null, 2);
|
|
1793
2118
|
}
|
|
1794
2119
|
}
|
|
1795
|
-
// packages/renderers/dist/svg/theme.js
|
|
1796
|
-
var DARK_THEME = {
|
|
1797
|
-
background: "#0d1117",
|
|
1798
|
-
foreground: "#e6edf3",
|
|
1799
|
-
muted: "#7d8590",
|
|
1800
|
-
border: "#30363d",
|
|
1801
|
-
cardBackground: "#161b22",
|
|
1802
|
-
heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
|
|
1803
|
-
accent: "#58a6ff",
|
|
1804
|
-
accentSecondary: "#bc8cff",
|
|
1805
|
-
barFill: "#3b82f6",
|
|
1806
|
-
barBackground: "#21262d"
|
|
1807
|
-
};
|
|
1808
|
-
var LIGHT_THEME = {
|
|
1809
|
-
background: "#ffffff",
|
|
1810
|
-
foreground: "#1a1a2e",
|
|
1811
|
-
muted: "#8b8fa3",
|
|
1812
|
-
border: "#e5e7eb",
|
|
1813
|
-
cardBackground: "#f8f9fc",
|
|
1814
|
-
heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
|
|
1815
|
-
accent: "#3b5bdb",
|
|
1816
|
-
accentSecondary: "#7048e8",
|
|
1817
|
-
barFill: "#5b6abf",
|
|
1818
|
-
barBackground: "#ebedf0"
|
|
1819
|
-
};
|
|
1820
|
-
function getTheme(mode) {
|
|
1821
|
-
return mode === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// packages/renderers/dist/svg/layout.js
|
|
1825
|
-
var PADDING = 40;
|
|
1826
|
-
var CELL_SIZE = 16;
|
|
1827
|
-
var CELL_GAP = 4;
|
|
1828
|
-
var MONTH_LABEL_HEIGHT = 24;
|
|
1829
|
-
var DAY_LABEL_WIDTH = 44;
|
|
1830
|
-
var HEATMAP_ROWS = 7;
|
|
1831
|
-
var SECTION_GAP = 32;
|
|
1832
|
-
var FONT_SIZE_TITLE = 28;
|
|
1833
|
-
var FONT_SIZE_SUBTITLE = 14;
|
|
1834
|
-
var FONT_SIZE_SMALL = 11;
|
|
1835
|
-
var FONT_SIZE_STAT_VALUE = 32;
|
|
1836
|
-
var FONT_SIZE_STAT_LABEL = 11;
|
|
1837
|
-
var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
1838
|
-
|
|
1839
2120
|
// packages/renderers/dist/svg/utils.js
|
|
1840
2121
|
function escapeXml(str) {
|
|
1841
2122
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1842
2123
|
}
|
|
1843
|
-
function rect(x, y, w, h, fill, rx) {
|
|
1844
|
-
const rxAttr = rx !== undefined ? ` rx="${rx}"` : "";
|
|
1845
|
-
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${escapeXml(fill)}"${rxAttr}/>`;
|
|
1846
|
-
}
|
|
1847
|
-
function text(x, y, content, attrs) {
|
|
1848
|
-
const attrStr = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${typeof v === "string" ? escapeXml(v) : v}"`).join("") : "";
|
|
1849
|
-
return `<text x="${x}" y="${y}"${attrStr}>${escapeXml(content)}</text>`;
|
|
1850
|
-
}
|
|
1851
|
-
function group(children, transform) {
|
|
1852
|
-
const transformAttr = transform ? ` transform="${escapeXml(transform)}"` : "";
|
|
1853
|
-
return `<g${transformAttr}>${children.join("")}</g>`;
|
|
1854
|
-
}
|
|
1855
2124
|
function formatNumber(n) {
|
|
1856
2125
|
if (n >= 1e6) {
|
|
1857
2126
|
const millions = Number((n / 1e6).toFixed(1));
|
|
@@ -1879,282 +2148,45 @@ function formatCost(cost) {
|
|
|
1879
2148
|
return `$${cost.toFixed(4)}`;
|
|
1880
2149
|
}
|
|
1881
2150
|
|
|
1882
|
-
// packages/renderers/dist/
|
|
1883
|
-
var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
|
|
1884
|
-
var MONTH_NAMES = [
|
|
1885
|
-
"Jan",
|
|
1886
|
-
"Feb",
|
|
1887
|
-
"Mar",
|
|
1888
|
-
"Apr",
|
|
1889
|
-
"May",
|
|
1890
|
-
"Jun",
|
|
1891
|
-
"Jul",
|
|
1892
|
-
"Aug",
|
|
1893
|
-
"Sep",
|
|
1894
|
-
"Oct",
|
|
1895
|
-
"Nov",
|
|
1896
|
-
"Dec"
|
|
1897
|
-
];
|
|
1898
|
-
function getLevel(tokens, quantiles) {
|
|
1899
|
-
if (tokens <= 0)
|
|
1900
|
-
return 0;
|
|
1901
|
-
if (tokens <= quantiles[0])
|
|
1902
|
-
return 1;
|
|
1903
|
-
if (tokens <= quantiles[1])
|
|
1904
|
-
return 2;
|
|
1905
|
-
if (tokens <= quantiles[2])
|
|
1906
|
-
return 3;
|
|
1907
|
-
return 4;
|
|
1908
|
-
}
|
|
1909
|
-
function computeQuantiles(values) {
|
|
1910
|
-
const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
1911
|
-
if (nonZero.length === 0)
|
|
1912
|
-
return [0, 0, 0];
|
|
1913
|
-
const q = (p) => {
|
|
1914
|
-
const idx = Math.floor(p * (nonZero.length - 1));
|
|
1915
|
-
return nonZero[idx] ?? 0;
|
|
1916
|
-
};
|
|
1917
|
-
return [q(0.25), q(0.5), q(0.75)];
|
|
1918
|
-
}
|
|
1919
|
-
function renderHeatmap(daily, theme, options = {}) {
|
|
1920
|
-
const tokenMap = new Map;
|
|
1921
|
-
for (const d of daily) {
|
|
1922
|
-
const existing = tokenMap.get(d.date) ?? 0;
|
|
1923
|
-
tokenMap.set(d.date, existing + d.totalTokens);
|
|
1924
|
-
}
|
|
1925
|
-
const dates = daily.map((d) => d.date).sort();
|
|
1926
|
-
const endStr = options.endDate ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
1927
|
-
const startStr = options.startDate ?? dates[0] ?? endStr;
|
|
1928
|
-
const end = new Date(endStr + "T00:00:00Z");
|
|
1929
|
-
const start = new Date(startStr + "T00:00:00Z");
|
|
1930
|
-
const startDay = start.getUTCDay();
|
|
1931
|
-
start.setUTCDate(start.getUTCDate() - startDay);
|
|
1932
|
-
const cells = [];
|
|
1933
|
-
const allTokens = Array.from(tokenMap.values());
|
|
1934
|
-
const quantiles = computeQuantiles(allTokens);
|
|
1935
|
-
const current = new Date(start);
|
|
1936
|
-
let col = 0;
|
|
1937
|
-
const monthLabels = [];
|
|
1938
|
-
let lastMonth = -1;
|
|
1939
|
-
const cellRadius = 3;
|
|
1940
|
-
while (current <= end) {
|
|
1941
|
-
const row = current.getUTCDay();
|
|
1942
|
-
const dateStr = current.toISOString().slice(0, 10);
|
|
1943
|
-
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
1944
|
-
const level = getLevel(tokens, quantiles);
|
|
1945
|
-
const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
|
|
1946
|
-
const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
|
|
1947
|
-
const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
|
|
1948
|
-
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="${cellRadius}"><title>${escapeXml(title)}</title></rect>`);
|
|
1949
|
-
const month = current.getUTCMonth();
|
|
1950
|
-
if (month !== lastMonth && row === 0) {
|
|
1951
|
-
lastMonth = month;
|
|
1952
|
-
monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 8, MONTH_NAMES[month] ?? "", {
|
|
1953
|
-
fill: theme.muted,
|
|
1954
|
-
"font-size": FONT_SIZE_SMALL,
|
|
1955
|
-
"font-family": FONT_FAMILY
|
|
1956
|
-
}));
|
|
1957
|
-
}
|
|
1958
|
-
if (row === 6) {
|
|
1959
|
-
col++;
|
|
1960
|
-
}
|
|
1961
|
-
current.setUTCDate(current.getUTCDate() + 1);
|
|
1962
|
-
}
|
|
1963
|
-
const dayLabels = DAY_LABELS2.map((label, i) => {
|
|
1964
|
-
if (!label)
|
|
1965
|
-
return "";
|
|
1966
|
-
const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
|
|
1967
|
-
return text(0, y, label, {
|
|
1968
|
-
fill: theme.muted,
|
|
1969
|
-
"font-size": FONT_SIZE_SMALL,
|
|
1970
|
-
"font-family": FONT_FAMILY
|
|
1971
|
-
});
|
|
1972
|
-
});
|
|
1973
|
-
const totalCols = col + 1;
|
|
1974
|
-
const gridWidth = DAY_LABEL_WIDTH + totalCols * CELL_SIZE + Math.max(0, totalCols - 1) * CELL_GAP;
|
|
1975
|
-
const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * CELL_SIZE + (HEATMAP_ROWS - 1) * CELL_GAP;
|
|
1976
|
-
const legendY = height + 16;
|
|
1977
|
-
const legendItems = [];
|
|
1978
|
-
const legendStartX = 0;
|
|
1979
|
-
legendItems.push(text(legendStartX, legendY + CELL_SIZE - 2, "LESS", {
|
|
1980
|
-
fill: theme.muted,
|
|
1981
|
-
"font-size": 9,
|
|
1982
|
-
"font-family": FONT_FAMILY,
|
|
1983
|
-
"font-weight": "600",
|
|
1984
|
-
"letter-spacing": "0.5"
|
|
1985
|
-
}));
|
|
1986
|
-
const legendBoxStart = legendStartX + 40;
|
|
1987
|
-
for (let i = 0;i < 5; i++) {
|
|
1988
|
-
legendItems.push(`<rect x="${legendBoxStart + i * (CELL_SIZE + 3)}" y="${legendY}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[i])}" rx="${cellRadius}"/>`);
|
|
1989
|
-
}
|
|
1990
|
-
legendItems.push(text(legendBoxStart + 5 * (CELL_SIZE + 3) + 4, legendY + CELL_SIZE - 2, "MORE", {
|
|
1991
|
-
fill: theme.muted,
|
|
1992
|
-
"font-size": 9,
|
|
1993
|
-
"font-family": FONT_FAMILY,
|
|
1994
|
-
"font-weight": "600",
|
|
1995
|
-
"letter-spacing": "0.5"
|
|
1996
|
-
}));
|
|
1997
|
-
const totalHeight = legendY + CELL_SIZE + 8;
|
|
1998
|
-
const legendRightX = legendBoxStart + 5 * (CELL_SIZE + 3) + 4 + 40;
|
|
1999
|
-
const width = Math.max(gridWidth, legendRightX);
|
|
2000
|
-
const svg = group([...monthLabels, ...dayLabels, ...cells, ...legendItems]);
|
|
2001
|
-
return { svg, width, height: totalHeight };
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
// packages/renderers/dist/svg/svg-renderer.js
|
|
2005
|
-
var MIN_SVG_WIDTH = 1000;
|
|
2006
|
-
var MAX_STAT_VALUE_CHARS = 28;
|
|
2007
|
-
function truncateText(value, maxChars) {
|
|
2008
|
-
if (value.length <= maxChars)
|
|
2009
|
-
return value;
|
|
2010
|
-
return value.slice(0, maxChars - 1) + "\u2026";
|
|
2011
|
-
}
|
|
2012
|
-
function renderHeaderStat(x, y, label, value, theme, align = "end") {
|
|
2013
|
-
const anchor = align === "end" ? "end" : "start";
|
|
2014
|
-
return group([
|
|
2015
|
-
text(x, y, label, {
|
|
2016
|
-
fill: theme.muted,
|
|
2017
|
-
"font-size": FONT_SIZE_STAT_LABEL,
|
|
2018
|
-
"font-family": FONT_FAMILY,
|
|
2019
|
-
"font-weight": "700",
|
|
2020
|
-
"text-anchor": anchor,
|
|
2021
|
-
"letter-spacing": "1"
|
|
2022
|
-
}),
|
|
2023
|
-
text(x, y + 34, value, {
|
|
2024
|
-
fill: theme.foreground,
|
|
2025
|
-
"font-size": FONT_SIZE_STAT_VALUE,
|
|
2026
|
-
"font-family": FONT_FAMILY,
|
|
2027
|
-
"font-weight": "700",
|
|
2028
|
-
"text-anchor": anchor
|
|
2029
|
-
})
|
|
2030
|
-
]);
|
|
2031
|
-
}
|
|
2032
|
-
function renderBottomStat(x, y, label, value, theme) {
|
|
2033
|
-
return group([
|
|
2034
|
-
text(x, y, label, {
|
|
2035
|
-
fill: theme.muted,
|
|
2036
|
-
"font-size": FONT_SIZE_STAT_LABEL,
|
|
2037
|
-
"font-family": FONT_FAMILY,
|
|
2038
|
-
"font-weight": "700",
|
|
2039
|
-
"letter-spacing": "0.8"
|
|
2040
|
-
}),
|
|
2041
|
-
text(x, y + 32, value, {
|
|
2042
|
-
fill: theme.foreground,
|
|
2043
|
-
"font-size": 18,
|
|
2044
|
-
"font-family": FONT_FAMILY,
|
|
2045
|
-
"font-weight": "700"
|
|
2046
|
-
})
|
|
2047
|
-
]);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
class SvgRenderer {
|
|
2051
|
-
format = "svg";
|
|
2052
|
-
async render(output, options) {
|
|
2053
|
-
const theme = getTheme(options.theme);
|
|
2054
|
-
const sections = [];
|
|
2055
|
-
const sectionWidths = [];
|
|
2056
|
-
let y = PADDING;
|
|
2057
|
-
const providerName = output.providers.length > 0 ? output.providers.map((p) => p.displayName).join(" + ") : "Tokenleak";
|
|
2058
|
-
sections.push(text(PADDING, y + FONT_SIZE_TITLE, providerName, {
|
|
2059
|
-
fill: theme.foreground,
|
|
2060
|
-
"font-size": FONT_SIZE_TITLE,
|
|
2061
|
-
"font-family": FONT_FAMILY,
|
|
2062
|
-
"font-weight": "700"
|
|
2063
|
-
}));
|
|
2064
|
-
sections.push(text(PADDING, y + FONT_SIZE_TITLE + 22, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
|
|
2065
|
-
fill: theme.muted,
|
|
2066
|
-
"font-size": FONT_SIZE_SUBTITLE,
|
|
2067
|
-
"font-family": FONT_FAMILY
|
|
2068
|
-
}));
|
|
2069
|
-
const allDaily = output.providers.flatMap((p) => p.daily);
|
|
2070
|
-
const heatmap = renderHeatmap(allDaily, theme, {
|
|
2071
|
-
startDate: output.dateRange.since,
|
|
2072
|
-
endDate: output.dateRange.until
|
|
2073
|
-
});
|
|
2074
|
-
sectionWidths.push(heatmap.width);
|
|
2075
|
-
const contentWidth = Math.max(heatmap.width, MIN_SVG_WIDTH - PADDING * 2);
|
|
2076
|
-
const stats = output.aggregated;
|
|
2077
|
-
const headerStatSpacing = 180;
|
|
2078
|
-
const headerStatsX = PADDING + contentWidth;
|
|
2079
|
-
sections.push(renderHeaderStat(headerStatsX, y, "INPUT TOKENS", formatNumber(stats.totalInputTokens), theme));
|
|
2080
|
-
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing, y, "OUTPUT TOKENS", formatNumber(stats.totalOutputTokens), theme));
|
|
2081
|
-
sections.push(renderHeaderStat(headerStatsX - headerStatSpacing * 2, y, "TOTAL TOKENS", formatNumber(stats.totalTokens), theme));
|
|
2082
|
-
y += FONT_SIZE_TITLE + 22 + SECTION_GAP;
|
|
2083
|
-
if (allDaily.length > 0) {
|
|
2084
|
-
sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
|
|
2085
|
-
y += heatmap.height + SECTION_GAP + 8;
|
|
2086
|
-
}
|
|
2087
|
-
sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2088
|
-
y += SECTION_GAP;
|
|
2089
|
-
const col1Width = contentWidth * 0.35;
|
|
2090
|
-
const col2Width = contentWidth * 0.35;
|
|
2091
|
-
const col3Width = contentWidth * 0.15;
|
|
2092
|
-
const col4Width = contentWidth * 0.15;
|
|
2093
|
-
const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
|
|
2094
|
-
const topModelLabel = topModel ? truncateText(`${topModel.model} (${formatNumber(topModel.tokens)})`, MAX_STAT_VALUE_CHARS) : "N/A";
|
|
2095
|
-
sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
|
|
2096
|
-
const recent30Label = stats.rolling30dTopModel ? truncateText(`${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})`, MAX_STAT_VALUE_CHARS) : formatNumber(stats.rolling30dTokens);
|
|
2097
|
-
sections.push(renderBottomStat(PADDING + col1Width, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
|
|
2098
|
-
sections.push(renderBottomStat(PADDING + col1Width + col2Width, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
|
|
2099
|
-
sections.push(renderBottomStat(PADDING + col1Width + col2Width + col3Width, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
|
|
2100
|
-
y += 56 + SECTION_GAP;
|
|
2101
|
-
const evenCardWidth = contentWidth / 4;
|
|
2102
|
-
sections.push(`<line x1="${PADDING}" y1="${y - SECTION_GAP / 2}" x2="${PADDING + contentWidth}" y2="${y - SECTION_GAP / 2}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2103
|
-
sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
|
|
2104
|
-
sections.push(renderBottomStat(PADDING + evenCardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
|
|
2105
|
-
sections.push(renderBottomStat(PADDING + evenCardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
|
|
2106
|
-
sections.push(renderBottomStat(PADDING + evenCardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
|
|
2107
|
-
y += 56 + PADDING;
|
|
2108
|
-
const totalHeight = y;
|
|
2109
|
-
const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
2110
|
-
sections.push(text(svgWidth - PADDING, totalHeight - 16, "tokenleak", {
|
|
2111
|
-
fill: theme.muted,
|
|
2112
|
-
"font-size": 10,
|
|
2113
|
-
"font-family": FONT_FAMILY,
|
|
2114
|
-
"text-anchor": "end",
|
|
2115
|
-
opacity: "0.4"
|
|
2116
|
-
}));
|
|
2117
|
-
const svgContent = [
|
|
2118
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
|
|
2119
|
-
`<defs><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');</style></defs>`,
|
|
2120
|
-
rect(0, 0, svgWidth, totalHeight, theme.background, 12),
|
|
2121
|
-
...sections,
|
|
2122
|
-
"</svg>"
|
|
2123
|
-
].join(`
|
|
2124
|
-
`);
|
|
2125
|
-
return svgContent;
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
// packages/renderers/dist/png/terminal-card.js
|
|
2151
|
+
// packages/renderers/dist/card/layout.js
|
|
2129
2152
|
var CARD_PADDING = 48;
|
|
2130
2153
|
var TITLEBAR_HEIGHT = 48;
|
|
2131
2154
|
var DOT_RADIUS = 6;
|
|
2132
2155
|
var DOT_GAP = 8;
|
|
2133
|
-
var
|
|
2134
|
-
var
|
|
2156
|
+
var CELL_SIZE = 16;
|
|
2157
|
+
var CELL_GAP = 4;
|
|
2135
2158
|
var STAT_GRID_COLS = 3;
|
|
2136
|
-
var MODEL_BAR_HEIGHT =
|
|
2137
|
-
var
|
|
2138
|
-
var
|
|
2159
|
+
var MODEL_BAR_HEIGHT = 11;
|
|
2160
|
+
var DAY_LABEL_WIDTH = 44;
|
|
2161
|
+
var MONTH_LABEL_HEIGHT = 24;
|
|
2139
2162
|
var PROVIDER_SECTION_GAP = 36;
|
|
2140
|
-
var
|
|
2163
|
+
var MIN_CONTENT_WIDTH = 700;
|
|
2164
|
+
var MODEL_NAME_WIDTH = 220;
|
|
2165
|
+
var MODEL_BAR_GAP = 36;
|
|
2166
|
+
var MODEL_PERCENT_WIDTH = 40;
|
|
2167
|
+
|
|
2168
|
+
// packages/renderers/dist/png/terminal-card.js
|
|
2169
|
+
var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
2170
|
+
var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
|
|
2141
2171
|
function getCardTheme(mode) {
|
|
2142
2172
|
if (mode === "dark") {
|
|
2143
2173
|
return {
|
|
2144
|
-
bg: "#
|
|
2145
|
-
fg: "#
|
|
2146
|
-
muted: "#
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2174
|
+
bg: "#09090b",
|
|
2175
|
+
fg: "#f0f0f0",
|
|
2176
|
+
muted: "#6b7280",
|
|
2177
|
+
labelFg: "#b0b8c4",
|
|
2178
|
+
border: "rgba(255,255,255,0.08)",
|
|
2179
|
+
accent: "#34d399",
|
|
2180
|
+
heatmapEmpty: "#1a1a22",
|
|
2181
|
+
barTrack: "#151520",
|
|
2182
|
+
titlebarBorder: "rgba(255,255,255,0.08)"
|
|
2152
2183
|
};
|
|
2153
2184
|
}
|
|
2154
2185
|
return {
|
|
2155
2186
|
bg: "#fafafa",
|
|
2156
2187
|
fg: "#18181b",
|
|
2157
2188
|
muted: "#a1a1aa",
|
|
2189
|
+
labelFg: "#71717a",
|
|
2158
2190
|
border: "rgba(0,0,0,0.08)",
|
|
2159
2191
|
accent: "#059669",
|
|
2160
2192
|
heatmapEmpty: "#e4e4e7",
|
|
@@ -2166,7 +2198,7 @@ function buildHeatmapScale(colors, isDark) {
|
|
|
2166
2198
|
const [startHex, endHex] = colors.gradient;
|
|
2167
2199
|
const s = hexToRgb(startHex);
|
|
2168
2200
|
const e = hexToRgb(endHex);
|
|
2169
|
-
const opacities = isDark ? [0.
|
|
2201
|
+
const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
|
|
2170
2202
|
return [
|
|
2171
2203
|
"transparent",
|
|
2172
2204
|
...opacities.map((t) => {
|
|
@@ -2189,7 +2221,7 @@ function rgbToHex(r, g, b) {
|
|
|
2189
2221
|
const toHex = (n) => n.toString(16).padStart(2, "0");
|
|
2190
2222
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
2191
2223
|
}
|
|
2192
|
-
function
|
|
2224
|
+
function computeQuantiles(values) {
|
|
2193
2225
|
const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
2194
2226
|
if (nonZero.length === 0)
|
|
2195
2227
|
return [0, 0, 0];
|
|
@@ -2199,7 +2231,7 @@ function computeQuantiles2(values) {
|
|
|
2199
2231
|
};
|
|
2200
2232
|
return [q(0.25), q(0.5), q(0.75)];
|
|
2201
2233
|
}
|
|
2202
|
-
function
|
|
2234
|
+
function getLevel(tokens, quantiles) {
|
|
2203
2235
|
if (tokens <= 0)
|
|
2204
2236
|
return 0;
|
|
2205
2237
|
if (tokens <= quantiles[0])
|
|
@@ -2210,7 +2242,7 @@ function getLevel2(tokens, quantiles) {
|
|
|
2210
2242
|
return 3;
|
|
2211
2243
|
return 4;
|
|
2212
2244
|
}
|
|
2213
|
-
var
|
|
2245
|
+
var MONTH_NAMES = [
|
|
2214
2246
|
"Jan",
|
|
2215
2247
|
"Feb",
|
|
2216
2248
|
"Mar",
|
|
@@ -2253,6 +2285,41 @@ function formatPercentage(rate) {
|
|
|
2253
2285
|
function formatStreak(n) {
|
|
2254
2286
|
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
2255
2287
|
}
|
|
2288
|
+
function formatRatio(value, suffix = "x") {
|
|
2289
|
+
if (value === null || !Number.isFinite(value)) {
|
|
2290
|
+
return "n/a";
|
|
2291
|
+
}
|
|
2292
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
|
|
2293
|
+
}
|
|
2294
|
+
function formatPercentPoints(value) {
|
|
2295
|
+
const prefix = value >= 0 ? "+" : "";
|
|
2296
|
+
return `${prefix}${(value * 100).toFixed(1)}pp`;
|
|
2297
|
+
}
|
|
2298
|
+
function formatHour(hour) {
|
|
2299
|
+
return `${hour.toString().padStart(2, "0")}:00`;
|
|
2300
|
+
}
|
|
2301
|
+
function formatDuration(durationMs) {
|
|
2302
|
+
if (durationMs === null || durationMs === undefined || durationMs <= 0) {
|
|
2303
|
+
return "n/a";
|
|
2304
|
+
}
|
|
2305
|
+
const totalMinutes = Math.round(durationMs / 60000);
|
|
2306
|
+
if (totalMinutes < 60) {
|
|
2307
|
+
return `${totalMinutes}m`;
|
|
2308
|
+
}
|
|
2309
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
2310
|
+
const minutes = totalMinutes % 60;
|
|
2311
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
2312
|
+
}
|
|
2313
|
+
function truncateText(value, maxLength) {
|
|
2314
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
2315
|
+
}
|
|
2316
|
+
function formatSessionSummary(summary) {
|
|
2317
|
+
const duration = formatDuration(summary.durationMs);
|
|
2318
|
+
if (duration === "n/a") {
|
|
2319
|
+
return truncateText(summary.label, 20);
|
|
2320
|
+
}
|
|
2321
|
+
return truncateText(`${summary.label} \xB7 ${duration}`, 24);
|
|
2322
|
+
}
|
|
2256
2323
|
function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
2257
2324
|
const tokenMap = new Map;
|
|
2258
2325
|
for (const d of daily) {
|
|
@@ -2265,7 +2332,7 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
|
2265
2332
|
const start = new Date(startStr + "T00:00:00Z");
|
|
2266
2333
|
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
2267
2334
|
const allTokens = Array.from(tokenMap.values());
|
|
2268
|
-
const quantiles =
|
|
2335
|
+
const quantiles = computeQuantiles(allTokens);
|
|
2269
2336
|
const cells = [];
|
|
2270
2337
|
const monthLabels = [];
|
|
2271
2338
|
let lastMonth = -1;
|
|
@@ -2275,37 +2342,183 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
|
2275
2342
|
const row = current.getUTCDay();
|
|
2276
2343
|
const dateStr = current.toISOString().slice(0, 10);
|
|
2277
2344
|
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
2278
|
-
const level =
|
|
2279
|
-
const x =
|
|
2280
|
-
const y =
|
|
2345
|
+
const level = getLevel(tokens, quantiles);
|
|
2346
|
+
const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
|
|
2347
|
+
const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
|
|
2281
2348
|
const fill = level === 0 ? emptyColor : heatmapColors[level];
|
|
2282
2349
|
const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
|
|
2283
|
-
cells.push(`<rect x="${x}" y="${y}" width="${
|
|
2350
|
+
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(fill)}" rx="3"><title>${escapeXml(title)}</title></rect>`);
|
|
2284
2351
|
const month = current.getUTCMonth();
|
|
2285
2352
|
if (month !== lastMonth && row === 0) {
|
|
2286
2353
|
lastMonth = month;
|
|
2287
|
-
monthLabels.push(`<text x="${x}" y="${
|
|
2354
|
+
monthLabels.push(`<text x="${x}" y="${MONTH_LABEL_HEIGHT - 8}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY)}">${escapeXml(MONTH_NAMES[month] ?? "")}</text>`);
|
|
2288
2355
|
}
|
|
2289
2356
|
if (row === 6)
|
|
2290
2357
|
col++;
|
|
2291
2358
|
current.setUTCDate(current.getUTCDate() + 1);
|
|
2292
2359
|
}
|
|
2293
2360
|
const dayLabels = [
|
|
2361
|
+
{ label: "Sun", row: 0 },
|
|
2294
2362
|
{ label: "Mon", row: 1 },
|
|
2363
|
+
{ label: "Tue", row: 2 },
|
|
2295
2364
|
{ label: "Wed", row: 3 },
|
|
2365
|
+
{ label: "Thu", row: 4 },
|
|
2296
2366
|
{ label: "Fri", row: 5 },
|
|
2297
|
-
{ label: "
|
|
2367
|
+
{ label: "Sat", row: 6 }
|
|
2298
2368
|
].map((d) => {
|
|
2299
|
-
const y =
|
|
2300
|
-
return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(
|
|
2369
|
+
const y = MONTH_LABEL_HEIGHT + d.row * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
|
|
2370
|
+
return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY)}">${escapeXml(d.label)}</text>`;
|
|
2301
2371
|
}).join("");
|
|
2302
2372
|
const totalCols = col + 1;
|
|
2303
|
-
const gridWidth =
|
|
2304
|
-
const height =
|
|
2373
|
+
const gridWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
|
|
2374
|
+
const height = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
|
|
2305
2375
|
const svg = [dayLabels, ...monthLabels, ...cells].join(`
|
|
2306
2376
|
`);
|
|
2307
2377
|
return { svg, gridWidth, height };
|
|
2308
2378
|
}
|
|
2379
|
+
function renderSectionHeader(x, y, title, theme, cardAccent) {
|
|
2380
|
+
const parts = [];
|
|
2381
|
+
parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
|
|
2382
|
+
parts.push(`<text x="${x + 12}" y="${y}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.8">${escapeXml(title)}</text>`);
|
|
2383
|
+
return parts.join(`
|
|
2384
|
+
`);
|
|
2385
|
+
}
|
|
2386
|
+
function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
|
|
2387
|
+
const parts = [];
|
|
2388
|
+
const height = 38 + lines.length * 22;
|
|
2389
|
+
parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2390
|
+
parts.push(`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">${escapeXml(title)}</text>`);
|
|
2391
|
+
lines.forEach((line, index) => {
|
|
2392
|
+
const lineY = y + 48 + index * 22;
|
|
2393
|
+
parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(line.label)}</text>`);
|
|
2394
|
+
parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(line.accent ? cardAccent : theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(line.value)}</text>`);
|
|
2395
|
+
});
|
|
2396
|
+
return parts.join(`
|
|
2397
|
+
`);
|
|
2398
|
+
}
|
|
2399
|
+
function buildProviderHourBuckets(providers) {
|
|
2400
|
+
return providers.map((p) => {
|
|
2401
|
+
const hours = new Array(24).fill(0);
|
|
2402
|
+
for (const event of p.events ?? []) {
|
|
2403
|
+
const date = new Date(event.timestamp);
|
|
2404
|
+
if (!Number.isNaN(date.getTime())) {
|
|
2405
|
+
const h = date.getUTCHours();
|
|
2406
|
+
hours[h] += event.totalTokens;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return { provider: p.provider, color: p.colors.primary, hours };
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
|
|
2413
|
+
const chartHeight = 140;
|
|
2414
|
+
const innerHeight = 72;
|
|
2415
|
+
const baselineY = y + 92;
|
|
2416
|
+
const barAreaX = x + 18;
|
|
2417
|
+
const barAreaWidth = width - 36;
|
|
2418
|
+
const barGap = 4;
|
|
2419
|
+
const barWidth = (barAreaWidth - barGap * 23) / 24;
|
|
2420
|
+
const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
|
|
2421
|
+
const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
|
|
2422
|
+
const isMulti = providers.length > 1;
|
|
2423
|
+
const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
|
|
2424
|
+
let legendSvg = "";
|
|
2425
|
+
if (isMulti && providerBuckets.length > 0) {
|
|
2426
|
+
const titleWidth = 105;
|
|
2427
|
+
let legendX = x + 18 + titleWidth + 12;
|
|
2428
|
+
const legendY = y + 22;
|
|
2429
|
+
for (const bucket of providerBuckets) {
|
|
2430
|
+
const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
|
|
2431
|
+
legendSvg += `<rect x="${legendX}" y="${legendY - 8}" width="8" height="8" rx="2" fill="${escapeXml(bucket.color)}" opacity="0.85"/>` + `<text x="${legendX + 12}" y="${legendY - 1}" fill="${escapeXml(theme.fg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(displayName)}</text>`;
|
|
2432
|
+
legendX += 12 + displayName.length * 5.5 + 16;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
const bars = [
|
|
2436
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2437
|
+
`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">HOUR OF DAY</text>`,
|
|
2438
|
+
legendSvg,
|
|
2439
|
+
`<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(busiest ? `${formatHour(busiest.hour)} peak` : "No session events")}</text>`,
|
|
2440
|
+
`<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
|
|
2441
|
+
];
|
|
2442
|
+
bars.push(`<defs><filter id="peakGlow" x="-50%" y="-50%" width="200%" height="200%">` + `<feGaussianBlur stdDeviation="4" result="blur"/>` + `<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>` + `</filter></defs>`);
|
|
2443
|
+
if (isMulti) {
|
|
2444
|
+
const provBaseOpacity = isDark ? "0.45" : "0.3";
|
|
2445
|
+
const provMidOpacity = isDark ? "0.85" : "0.75";
|
|
2446
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2447
|
+
const gradId = `hod-prov-${bi}`;
|
|
2448
|
+
bars.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provBaseOpacity}"/>` + `<stop offset="60%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2449
|
+
}
|
|
2450
|
+
hourOfDay.forEach((entry, index) => {
|
|
2451
|
+
if (entry.tokens <= 0)
|
|
2452
|
+
return;
|
|
2453
|
+
const totalRatio = entry.tokens / maxTokens;
|
|
2454
|
+
const totalBarHeight = Math.max(4, totalRatio * innerHeight);
|
|
2455
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2456
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour;
|
|
2457
|
+
if (isPeak) {
|
|
2458
|
+
const topY2 = baselineY - totalBarHeight;
|
|
2459
|
+
bars.push(`<rect x="${colX - 2}" y="${topY2 - 2}" width="${barWidth + 4}" height="${totalBarHeight + 4}" rx="5" fill="${escapeXml(providerBuckets[0]?.color ?? cardAccent)}" opacity="0.12" filter="url(#peakGlow)"/>`);
|
|
2460
|
+
}
|
|
2461
|
+
const clipId = `hod-clip-${index}`;
|
|
2462
|
+
const topY = baselineY - totalBarHeight;
|
|
2463
|
+
bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
|
|
2464
|
+
let offsetY = 0;
|
|
2465
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2466
|
+
const tokens = providerBuckets[bi].hours[index] ?? 0;
|
|
2467
|
+
if (tokens <= 0)
|
|
2468
|
+
continue;
|
|
2469
|
+
const segHeight = tokens / entry.tokens * totalBarHeight;
|
|
2470
|
+
const segY = baselineY - offsetY - segHeight;
|
|
2471
|
+
bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
|
|
2472
|
+
offsetY += segHeight;
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
} else {
|
|
2476
|
+
const hodGradId = "hod-bar-grad";
|
|
2477
|
+
const hodBaseOpacity = isDark ? "0.25" : "0.1";
|
|
2478
|
+
const hodMidOpacity = isDark ? "0.75" : "0.6";
|
|
2479
|
+
bars.push(`<defs><linearGradient id="${escapeXml(hodGradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodBaseOpacity}"/>` + `<stop offset="40%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(cardAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2480
|
+
hourOfDay.forEach((entry, index) => {
|
|
2481
|
+
const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
|
|
2482
|
+
const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
|
|
2483
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2484
|
+
const colY = baselineY - barHeight;
|
|
2485
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
|
|
2486
|
+
if (isPeak) {
|
|
2487
|
+
bars.push(`<rect x="${colX - 2}" y="${colY - 2}" width="${barWidth + 4}" height="${barHeight + 4}" rx="5" fill="${escapeXml(cardAccent)}" opacity="0.15" filter="url(#peakGlow)"/>`);
|
|
2488
|
+
}
|
|
2489
|
+
bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
[0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
|
|
2493
|
+
const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
|
|
2494
|
+
bars.push(`<text x="${labelX}" y="${y + 116}" fill="${escapeXml(theme.labelFg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="middle">${escapeXml(hour.toString().padStart(2, "0"))}</text>`);
|
|
2495
|
+
});
|
|
2496
|
+
return {
|
|
2497
|
+
svg: bars.join(`
|
|
2498
|
+
`),
|
|
2499
|
+
height: chartHeight
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
function renderModelMixShift(x, y, width, more, theme, cardAccent) {
|
|
2503
|
+
if (!more.compare || more.compare.modelMixShift.length === 0) {
|
|
2504
|
+
return { svg: "", height: 0 };
|
|
2505
|
+
}
|
|
2506
|
+
const rows = more.compare.modelMixShift.slice(0, 4);
|
|
2507
|
+
const height = 38 + rows.length * 24;
|
|
2508
|
+
const parts = [
|
|
2509
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2510
|
+
`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">MODEL MIX SHIFT</text>`,
|
|
2511
|
+
`<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${more.compare.previousRange.since} \u2192 ${more.compare.previousRange.until}`)}</text>`
|
|
2512
|
+
];
|
|
2513
|
+
rows.forEach((row, index) => {
|
|
2514
|
+
const lineY = y + 48 + index * 24;
|
|
2515
|
+
parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(truncateText(row.model, 28))}</text>`);
|
|
2516
|
+
parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(row.deltaShare >= 0 ? cardAccent : "#f97316")}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(formatPercentPoints(row.deltaShare))}</text>`);
|
|
2517
|
+
parts.push(`<text x="${x + width - 110}" y="${lineY}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${(row.previousShare * 100).toFixed(1)}% \u2192 ${(row.currentShare * 100).toFixed(1)}%`)}</text>`);
|
|
2518
|
+
});
|
|
2519
|
+
return { svg: parts.join(`
|
|
2520
|
+
`), height };
|
|
2521
|
+
}
|
|
2309
2522
|
function renderTerminalCardSvg(output, options) {
|
|
2310
2523
|
const theme = getCardTheme(options.theme);
|
|
2311
2524
|
const isDark = options.theme === "dark";
|
|
@@ -2313,6 +2526,8 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2313
2526
|
const stats = output.aggregated;
|
|
2314
2527
|
const { since, until } = output.dateRange;
|
|
2315
2528
|
const providers = output.providers;
|
|
2529
|
+
const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
|
|
2530
|
+
const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
|
|
2316
2531
|
const providerHeatmaps = providers.map((p) => {
|
|
2317
2532
|
const heatmapColors = buildHeatmapScale(p.colors, isDark);
|
|
2318
2533
|
return {
|
|
@@ -2322,7 +2537,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2322
2537
|
};
|
|
2323
2538
|
});
|
|
2324
2539
|
const maxHeatmapWidth = providerHeatmaps.reduce((max, ph) => Math.max(max, ph.heatmap.gridWidth), 0);
|
|
2325
|
-
const minContentWidth = Math.max(maxHeatmapWidth,
|
|
2540
|
+
const minContentWidth = Math.max(maxHeatmapWidth, MIN_CONTENT_WIDTH);
|
|
2326
2541
|
const cardWidth = minContentWidth + pad * 2;
|
|
2327
2542
|
const contentWidth = cardWidth - pad * 2;
|
|
2328
2543
|
let y = 0;
|
|
@@ -2340,24 +2555,22 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2340
2555
|
for (const dot of dots) {
|
|
2341
2556
|
sections.push(`<circle cx="${dot.cx}" cy="${dotY}" r="${DOT_RADIUS}" fill="${escapeXml(dot.color)}"/>`);
|
|
2342
2557
|
}
|
|
2343
|
-
const titleX = dots[2].cx + DOT_RADIUS + 20;
|
|
2344
|
-
sections.push(`<text x="${titleX}" y="${dotY + 5}" fill="${escapeXml(theme.muted)}" font-size="13" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">${escapeXml("tokenleak")}</text>`);
|
|
2345
2558
|
sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
|
|
2346
2559
|
y = TITLEBAR_HEIGHT + pad * 0.6;
|
|
2347
|
-
sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(
|
|
2560
|
+
sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(MONO_FONT_FAMILY)}" font-weight="500">` + `<tspan fill="${escapeXml(cardAccent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(cardAccent)}">_</tspan>` + `</text>`);
|
|
2348
2561
|
y += 40;
|
|
2349
2562
|
const dateRangeText = formatDateRange(since, until);
|
|
2350
|
-
sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(
|
|
2563
|
+
sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
|
|
2351
2564
|
y += 40;
|
|
2352
2565
|
for (let pi = 0;pi < providerHeatmaps.length; pi++) {
|
|
2353
2566
|
const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
|
|
2354
|
-
const provDotRadius =
|
|
2567
|
+
const provDotRadius = 7;
|
|
2355
2568
|
const provColor = provider.colors.primary;
|
|
2356
|
-
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y +
|
|
2357
|
-
sections.push(`<text x="${pad + provDotRadius * 2 +
|
|
2569
|
+
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
|
|
2570
|
+
sections.push(`<text x="${pad + provDotRadius * 2 + 12}" y="${y + 15}" fill="${escapeXml(theme.fg)}" font-size="17" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(provider.displayName)}</text>`);
|
|
2358
2571
|
const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
|
|
2359
|
-
sections.push(`<text x="${cardWidth - pad}" y="${y +
|
|
2360
|
-
y +=
|
|
2572
|
+
sections.push(`<text x="${cardWidth - pad}" y="${y + 15}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
|
|
2573
|
+
y += 32;
|
|
2361
2574
|
const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
|
|
2362
2575
|
sections.push(`<g transform="translate(${pad}, ${y})">`);
|
|
2363
2576
|
sections.push(heatmapSvg);
|
|
@@ -2372,13 +2585,13 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2372
2585
|
}
|
|
2373
2586
|
}
|
|
2374
2587
|
if (providers.length === 0) {
|
|
2375
|
-
sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(
|
|
2588
|
+
sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml("No provider data")}</text>`);
|
|
2376
2589
|
y += 32;
|
|
2377
2590
|
}
|
|
2378
2591
|
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2379
2592
|
y += 28;
|
|
2380
2593
|
if (providers.length > 1) {
|
|
2381
|
-
sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(
|
|
2594
|
+
sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml("OVERALL")}</text>`);
|
|
2382
2595
|
y += 24;
|
|
2383
2596
|
}
|
|
2384
2597
|
const statColWidth = contentWidth / STAT_GRID_COLS;
|
|
@@ -2396,9 +2609,9 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2396
2609
|
for (let i = 0;i < row.length; i++) {
|
|
2397
2610
|
const stat = row[i];
|
|
2398
2611
|
const x = pad + i * statColWidth;
|
|
2399
|
-
sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(
|
|
2400
|
-
const valueColor = stat.accent
|
|
2401
|
-
sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(
|
|
2612
|
+
sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
|
|
2613
|
+
const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
|
|
2614
|
+
sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(stat.value)}</text>`);
|
|
2402
2615
|
}
|
|
2403
2616
|
}
|
|
2404
2617
|
renderStatRow(statsRow1, y);
|
|
@@ -2408,38 +2621,152 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2408
2621
|
y += 8;
|
|
2409
2622
|
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2410
2623
|
y += 28;
|
|
2411
|
-
sections.push(
|
|
2624
|
+
sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
|
|
2412
2625
|
y += 24;
|
|
2413
2626
|
const topModels2 = stats.topModels.slice(0, 3);
|
|
2414
|
-
const
|
|
2415
|
-
const
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2627
|
+
const rankWidth = 28;
|
|
2628
|
+
const modelNameWidth = MODEL_NAME_WIDTH;
|
|
2629
|
+
const barGap = MODEL_BAR_GAP;
|
|
2630
|
+
const percentX = cardWidth - pad;
|
|
2631
|
+
const barX = pad + rankWidth + modelNameWidth;
|
|
2632
|
+
const barMaxWidth = Math.max(48, percentX - barX - barGap);
|
|
2633
|
+
for (const [index, model] of topModels2.entries()) {
|
|
2418
2634
|
const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
|
|
2419
|
-
sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
sections.push(`<
|
|
2425
|
-
sections.push(`<
|
|
2635
|
+
sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(cardAccent)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" opacity="0.7">${escapeXml(String(index + 1))}</text>`);
|
|
2636
|
+
sections.push(`<text x="${pad + rankWidth}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml(model.model)}</text>`);
|
|
2637
|
+
const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
|
|
2638
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
|
|
2639
|
+
const gradId = `grad-${index}-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
2640
|
+
sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(barAccent)}" stop-opacity="${isDark ? "0.45" : "0.27"}"/>` + `<stop offset="100%" stop-color="${escapeXml(barAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2641
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
|
|
2642
|
+
sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
|
|
2426
2643
|
y += 32;
|
|
2427
2644
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2645
|
+
if (options.more && output.more) {
|
|
2646
|
+
const more = output.more;
|
|
2647
|
+
const cardGap = 16;
|
|
2648
|
+
const detailCardWidth = (contentWidth - cardGap) / 2;
|
|
2649
|
+
y += 8;
|
|
2650
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2651
|
+
y += 28;
|
|
2652
|
+
sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
|
|
2653
|
+
y += 24;
|
|
2654
|
+
const efficiencyLines = [
|
|
2655
|
+
{
|
|
2656
|
+
label: "Input / Output",
|
|
2657
|
+
value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
|
|
2658
|
+
accent: true
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
label: "Output / Input",
|
|
2662
|
+
value: formatRatio(more.inputOutput.outputPerInput)
|
|
2663
|
+
},
|
|
2664
|
+
{
|
|
2665
|
+
label: "Output Share",
|
|
2666
|
+
value: formatPercentage(more.inputOutput.outputShare)
|
|
2667
|
+
}
|
|
2668
|
+
];
|
|
2669
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
|
|
2670
|
+
const burnLines = [
|
|
2671
|
+
{
|
|
2672
|
+
label: "Projected Cost",
|
|
2673
|
+
value: formatCost(more.monthlyBurn.projectedCost),
|
|
2674
|
+
accent: true
|
|
2675
|
+
},
|
|
2676
|
+
{
|
|
2677
|
+
label: "Projected Tokens",
|
|
2678
|
+
value: formatNumber(more.monthlyBurn.projectedTokens)
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
label: "Based On",
|
|
2682
|
+
value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
|
|
2683
|
+
}
|
|
2684
|
+
];
|
|
2685
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
|
|
2686
|
+
y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
|
|
2687
|
+
const cacheLines = [
|
|
2688
|
+
{
|
|
2689
|
+
label: "Cache Reads",
|
|
2690
|
+
value: formatNumber(more.cacheEconomics.readTokens),
|
|
2691
|
+
accent: true
|
|
2692
|
+
},
|
|
2693
|
+
{
|
|
2694
|
+
label: "Cache Writes",
|
|
2695
|
+
value: formatNumber(more.cacheEconomics.writeTokens)
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
label: "Read Coverage",
|
|
2699
|
+
value: formatPercentage(more.cacheEconomics.readCoverage)
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
label: "Reuse Ratio",
|
|
2703
|
+
value: formatRatio(more.cacheEconomics.reuseRatio)
|
|
2704
|
+
}
|
|
2705
|
+
];
|
|
2706
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
|
|
2707
|
+
const sessionLines = [
|
|
2708
|
+
{
|
|
2709
|
+
label: "Sessions",
|
|
2710
|
+
value: String(more.sessionMetrics.totalSessions),
|
|
2711
|
+
accent: true
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
label: "Avg Tokens",
|
|
2715
|
+
value: formatNumber(more.sessionMetrics.averageTokens)
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
label: "Avg Messages",
|
|
2719
|
+
value: more.sessionMetrics.averageMessages.toFixed(1)
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
label: "Avg Duration",
|
|
2723
|
+
value: formatDuration(more.sessionMetrics.averageDurationMs)
|
|
2724
|
+
},
|
|
2725
|
+
{
|
|
2726
|
+
label: "Longest Session",
|
|
2727
|
+
value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
|
|
2728
|
+
},
|
|
2729
|
+
{
|
|
2730
|
+
label: "Top Project",
|
|
2731
|
+
value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
|
|
2732
|
+
}
|
|
2733
|
+
];
|
|
2734
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
|
|
2735
|
+
y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
|
|
2736
|
+
const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
|
|
2737
|
+
sections.push(hourChart.svg);
|
|
2738
|
+
y += hourChart.height + 16;
|
|
2739
|
+
const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
|
|
2740
|
+
if (mixShift.height > 0) {
|
|
2741
|
+
sections.push(mixShift.svg);
|
|
2742
|
+
y += mixShift.height + 12;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
y += pad * 0.5;
|
|
2746
|
+
const cardHeight = y;
|
|
2747
|
+
const svg = sections.join(`
|
|
2431
2748
|
`).replace("__CARD_HEIGHT__", String(cardHeight));
|
|
2432
2749
|
return [
|
|
2433
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}" shape-rendering="geometricPrecision" text-rendering="
|
|
2750
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility" color-rendering="optimizeQuality">`,
|
|
2434
2751
|
svg,
|
|
2435
2752
|
"</svg>"
|
|
2436
2753
|
].join(`
|
|
2437
2754
|
`);
|
|
2438
2755
|
}
|
|
2439
2756
|
|
|
2757
|
+
// packages/renderers/dist/svg/svg-renderer.js
|
|
2758
|
+
class SvgRenderer {
|
|
2759
|
+
format = "svg";
|
|
2760
|
+
async render(output, options) {
|
|
2761
|
+
return renderTerminalCardSvg(output, {
|
|
2762
|
+
...options,
|
|
2763
|
+
format: "svg"
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2440
2767
|
// packages/renderers/dist/png/png-renderer.js
|
|
2441
2768
|
import sharp from "sharp";
|
|
2442
|
-
var PNG_DENSITY =
|
|
2769
|
+
var PNG_DENSITY = 432;
|
|
2443
2770
|
|
|
2444
2771
|
class PngRenderer {
|
|
2445
2772
|
format = "png";
|
|
@@ -2448,7 +2775,9 @@ class PngRenderer {
|
|
|
2448
2775
|
const pngBuffer = await sharp(Buffer.from(svgString), {
|
|
2449
2776
|
density: PNG_DENSITY
|
|
2450
2777
|
}).png({
|
|
2451
|
-
|
|
2778
|
+
adaptiveFiltering: true,
|
|
2779
|
+
compressionLevel: 9,
|
|
2780
|
+
force: true
|
|
2452
2781
|
}).toBuffer();
|
|
2453
2782
|
return pngBuffer;
|
|
2454
2783
|
}
|
|
@@ -2475,20 +2804,8 @@ var HEATMAP_BLOCKS = {
|
|
|
2475
2804
|
DARK: "\u2593",
|
|
2476
2805
|
MEDIUM: "\u2592",
|
|
2477
2806
|
LIGHT: "\u2591",
|
|
2478
|
-
EMPTY: "
|
|
2807
|
+
EMPTY: "\xB7"
|
|
2479
2808
|
};
|
|
2480
|
-
function intensityBlock(value, max) {
|
|
2481
|
-
if (max <= 0 || value <= 0)
|
|
2482
|
-
return HEATMAP_BLOCKS.EMPTY;
|
|
2483
|
-
const ratio = value / max;
|
|
2484
|
-
if (ratio >= 0.75)
|
|
2485
|
-
return HEATMAP_BLOCKS.FULL;
|
|
2486
|
-
if (ratio >= 0.5)
|
|
2487
|
-
return HEATMAP_BLOCKS.DARK;
|
|
2488
|
-
if (ratio >= 0.25)
|
|
2489
|
-
return HEATMAP_BLOCKS.MEDIUM;
|
|
2490
|
-
return HEATMAP_BLOCKS.LIGHT;
|
|
2491
|
-
}
|
|
2492
2809
|
function intensityColor(value, max) {
|
|
2493
2810
|
if (max <= 0 || value <= 0)
|
|
2494
2811
|
return "dim";
|
|
@@ -2503,9 +2820,10 @@ function intensityColor(value, max) {
|
|
|
2503
2820
|
}
|
|
2504
2821
|
|
|
2505
2822
|
// packages/renderers/dist/terminal/heatmap.js
|
|
2506
|
-
var
|
|
2823
|
+
var DAY_LABELS2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2507
2824
|
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
2508
|
-
var
|
|
2825
|
+
var DAY_LABEL_WIDTH2 = 4;
|
|
2826
|
+
var WEEK_COLUMN_WIDTH = 2;
|
|
2509
2827
|
var LEGEND_TEXT = "Less";
|
|
2510
2828
|
var LEGEND_TEXT_MORE = "More";
|
|
2511
2829
|
function buildUsageMap(daily) {
|
|
@@ -2515,83 +2833,117 @@ function buildUsageMap(daily) {
|
|
|
2515
2833
|
}
|
|
2516
2834
|
return map;
|
|
2517
2835
|
}
|
|
2518
|
-
function
|
|
2519
|
-
|
|
2520
|
-
|
|
2836
|
+
function computeQuantiles2(values) {
|
|
2837
|
+
const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
|
|
2838
|
+
if (nonZero.length === 0)
|
|
2839
|
+
return [0, 0, 0];
|
|
2840
|
+
const quantile = (ratio) => {
|
|
2841
|
+
const index = Math.floor(ratio * (nonZero.length - 1));
|
|
2842
|
+
return nonZero[index] ?? 0;
|
|
2843
|
+
};
|
|
2844
|
+
return [quantile(0.25), quantile(0.5), quantile(0.75)];
|
|
2845
|
+
}
|
|
2846
|
+
function getHeatmapBlock(tokens, quantiles) {
|
|
2847
|
+
if (tokens <= 0)
|
|
2848
|
+
return HEATMAP_BLOCKS.EMPTY;
|
|
2849
|
+
if (tokens <= quantiles[0])
|
|
2850
|
+
return HEATMAP_BLOCKS.LIGHT;
|
|
2851
|
+
if (tokens <= quantiles[1])
|
|
2852
|
+
return HEATMAP_BLOCKS.MEDIUM;
|
|
2853
|
+
if (tokens <= quantiles[2])
|
|
2854
|
+
return HEATMAP_BLOCKS.DARK;
|
|
2855
|
+
return HEATMAP_BLOCKS.FULL;
|
|
2856
|
+
}
|
|
2857
|
+
function formatDate(date) {
|
|
2858
|
+
const year = date.getUTCFullYear();
|
|
2859
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2860
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
2861
|
+
return `${year}-${month}-${day}`;
|
|
2862
|
+
}
|
|
2863
|
+
function buildMonthHeader(weeks) {
|
|
2864
|
+
const header = Array.from({ length: weeks.length * WEEK_COLUMN_WIDTH }, () => " ");
|
|
2865
|
+
let lastMonth = -1;
|
|
2866
|
+
let nextFreeIndex = 0;
|
|
2867
|
+
for (let weekIndex = 0;weekIndex < weeks.length; weekIndex++) {
|
|
2868
|
+
const firstDay = weeks[weekIndex]?.[0];
|
|
2869
|
+
if (!firstDay)
|
|
2870
|
+
continue;
|
|
2871
|
+
const month = firstDay.getUTCMonth();
|
|
2872
|
+
if (month === lastMonth)
|
|
2873
|
+
continue;
|
|
2874
|
+
lastMonth = month;
|
|
2875
|
+
const desiredStart = weekIndex * WEEK_COLUMN_WIDTH;
|
|
2876
|
+
const startIndex = Math.max(desiredStart, nextFreeIndex);
|
|
2877
|
+
const remaining = header.length - startIndex;
|
|
2878
|
+
if (remaining <= 0)
|
|
2879
|
+
continue;
|
|
2880
|
+
const fullLabel = MONTH_LABELS[month] ?? "";
|
|
2881
|
+
const label = remaining >= fullLabel.length ? fullLabel : remaining >= 2 ? fullLabel.slice(0, 2) : fullLabel.slice(0, 1);
|
|
2882
|
+
for (let offset = 0;offset < label.length; offset++) {
|
|
2883
|
+
header[startIndex + offset] = label[offset] ?? " ";
|
|
2884
|
+
}
|
|
2885
|
+
nextFreeIndex = startIndex + label.length + 1;
|
|
2521
2886
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
const startDate = new Date(dates[0]);
|
|
2526
|
-
const endDate = new Date(dates[dates.length - 1]);
|
|
2887
|
+
return `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}`;
|
|
2888
|
+
}
|
|
2889
|
+
function buildWeeks(startDate, endDate) {
|
|
2527
2890
|
const alignedStart = new Date(startDate);
|
|
2528
|
-
alignedStart.
|
|
2891
|
+
alignedStart.setUTCDate(alignedStart.getUTCDate() - alignedStart.getUTCDay());
|
|
2529
2892
|
const weeks = [];
|
|
2530
|
-
const
|
|
2531
|
-
while (
|
|
2893
|
+
const cursor = new Date(alignedStart);
|
|
2894
|
+
while (cursor <= endDate) {
|
|
2532
2895
|
const week = [];
|
|
2533
|
-
for (let
|
|
2534
|
-
week.push(new Date(
|
|
2535
|
-
|
|
2896
|
+
for (let day = 0;day < 7; day++) {
|
|
2897
|
+
week.push(new Date(cursor));
|
|
2898
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
2536
2899
|
}
|
|
2537
2900
|
weeks.push(week);
|
|
2538
2901
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
let lastMonth = -1;
|
|
2545
|
-
for (const week of displayWeeks) {
|
|
2546
|
-
const month = week[0].getMonth();
|
|
2547
|
-
if (month !== lastMonth) {
|
|
2548
|
-
monthHeader += MONTH_LABELS[month];
|
|
2549
|
-
lastMonth = month;
|
|
2550
|
-
const labelLen = MONTH_LABELS[month].length;
|
|
2551
|
-
} else {
|
|
2552
|
-
monthHeader += " ";
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
if (monthHeader.length > options.width) {
|
|
2556
|
-
monthHeader = monthHeader.slice(0, options.width);
|
|
2902
|
+
return weeks;
|
|
2903
|
+
}
|
|
2904
|
+
function renderTerminalHeatmap(daily, options) {
|
|
2905
|
+
if (daily.length === 0) {
|
|
2906
|
+
return " No usage data available.";
|
|
2557
2907
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2908
|
+
const usageMap = buildUsageMap(daily);
|
|
2909
|
+
const quantiles = computeQuantiles2(Array.from(usageMap.values()));
|
|
2910
|
+
const maxTokens = Math.max(...usageMap.values(), 0);
|
|
2911
|
+
const dates = daily.map((entry) => entry.date).sort();
|
|
2912
|
+
const startDate = new Date(`${dates[0]}T00:00:00Z`);
|
|
2913
|
+
const endDate = new Date(`${dates[dates.length - 1]}T00:00:00Z`);
|
|
2914
|
+
const weeks = buildWeeks(startDate, endDate);
|
|
2915
|
+
const availableColumns = Math.max(WEEK_COLUMN_WIDTH, options.width - DAY_LABEL_WIDTH2);
|
|
2916
|
+
const maxWeeks = Math.max(1, Math.floor(availableColumns / WEEK_COLUMN_WIDTH));
|
|
2917
|
+
const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
|
|
2918
|
+
const lines = [buildMonthHeader(displayWeeks)];
|
|
2919
|
+
for (let dayIndex = 0;dayIndex < 7; dayIndex++) {
|
|
2920
|
+
const label = DAY_LABELS2[dayIndex] ?? " ";
|
|
2921
|
+
let line = `${label} `.slice(0, DAY_LABEL_WIDTH2);
|
|
2563
2922
|
for (const week of displayWeeks) {
|
|
2564
|
-
const date = week[
|
|
2565
|
-
if (!date || date
|
|
2566
|
-
line +=
|
|
2923
|
+
const date = week[dayIndex];
|
|
2924
|
+
if (!date || date < startDate || date > endDate) {
|
|
2925
|
+
line += `${HEATMAP_BLOCKS.EMPTY} `;
|
|
2567
2926
|
continue;
|
|
2568
2927
|
}
|
|
2569
|
-
const
|
|
2570
|
-
const tokens = usageMap.get(
|
|
2571
|
-
const block =
|
|
2928
|
+
const dateString = formatDate(date);
|
|
2929
|
+
const tokens = usageMap.get(dateString) ?? 0;
|
|
2930
|
+
const block = getHeatmapBlock(tokens, quantiles);
|
|
2572
2931
|
const color = intensityColor(tokens, maxTokens);
|
|
2573
|
-
line += colorize(block, color, options.noColor)
|
|
2932
|
+
line += `${colorize(block, color, options.noColor)} `;
|
|
2574
2933
|
}
|
|
2575
|
-
lines.push(line);
|
|
2934
|
+
lines.push(line.trimEnd());
|
|
2576
2935
|
}
|
|
2577
|
-
const
|
|
2936
|
+
const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${[
|
|
2578
2937
|
HEATMAP_BLOCKS.EMPTY,
|
|
2579
2938
|
HEATMAP_BLOCKS.LIGHT,
|
|
2580
2939
|
HEATMAP_BLOCKS.MEDIUM,
|
|
2581
2940
|
HEATMAP_BLOCKS.DARK,
|
|
2582
2941
|
HEATMAP_BLOCKS.FULL
|
|
2583
|
-
]
|
|
2584
|
-
const legend = `${" ".repeat(DAY_LABEL_WIDTH3)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
|
|
2942
|
+
].join("")} ${LEGEND_TEXT_MORE}`;
|
|
2585
2943
|
lines.push(legend);
|
|
2586
2944
|
return lines.join(`
|
|
2587
2945
|
`);
|
|
2588
2946
|
}
|
|
2589
|
-
function formatDate(date) {
|
|
2590
|
-
const y = date.getFullYear();
|
|
2591
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2592
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
2593
|
-
return `${y}-${m}-${d}`;
|
|
2594
|
-
}
|
|
2595
2947
|
|
|
2596
2948
|
// packages/renderers/dist/terminal/dashboard.js
|
|
2597
2949
|
var BOX_H = "\u2500";
|
|
@@ -2686,11 +3038,18 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2686
3038
|
}
|
|
2687
3039
|
function renderTopModels(stats, width, noColor2) {
|
|
2688
3040
|
const lines = [];
|
|
3041
|
+
const nameWidth = Math.min(28, Math.max(16, Math.floor(width * 0.35)));
|
|
3042
|
+
const pctWidth = 4;
|
|
3043
|
+
const barGap = 2;
|
|
3044
|
+
const barWidth = Math.max(8, width - nameWidth - pctWidth - 6);
|
|
2689
3045
|
for (const model of stats.topModels.slice(0, 5)) {
|
|
2690
3046
|
const pct = formatSharePercent(model.percentage);
|
|
2691
|
-
const
|
|
2692
|
-
const
|
|
2693
|
-
|
|
3047
|
+
const normalizedName = model.model.length > nameWidth ? `${model.model.slice(0, nameWidth - 1)}\u2026` : model.model;
|
|
3048
|
+
const fillLength = Math.max(1, Math.round(model.percentage / 100 * barWidth));
|
|
3049
|
+
const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
|
|
3050
|
+
const track = "\u2591".repeat(Math.max(0, barWidth - fillLength));
|
|
3051
|
+
const line = ` ${colorize(normalizedName.padEnd(nameWidth), "yellow", noColor2)}${" ".repeat(barGap)}${fill}${track}${" ".repeat(barGap)}${pct.padStart(pctWidth)}`;
|
|
3052
|
+
lines.push(line);
|
|
2694
3053
|
}
|
|
2695
3054
|
return lines.join(`
|
|
2696
3055
|
`);
|
|
@@ -2757,7 +3116,8 @@ function renderDashboard(output, options) {
|
|
|
2757
3116
|
}
|
|
2758
3117
|
for (let i = 0;i < output.providers.length; i++) {
|
|
2759
3118
|
const provider = output.providers[i];
|
|
2760
|
-
|
|
3119
|
+
const providerStats = aggregate(provider.daily, output.dateRange.until);
|
|
3120
|
+
sections.push(renderProviderSection(provider, providerStats, width, noColor2, options.showInsights));
|
|
2761
3121
|
if (i < output.providers.length - 1) {
|
|
2762
3122
|
sections.push("");
|
|
2763
3123
|
sections.push(divider(width));
|
|
@@ -2802,7 +3162,7 @@ class TerminalRenderer {
|
|
|
2802
3162
|
}
|
|
2803
3163
|
}
|
|
2804
3164
|
// packages/renderers/dist/live/template.js
|
|
2805
|
-
var
|
|
3165
|
+
var MONTH_NAMES2 = [
|
|
2806
3166
|
"Jan",
|
|
2807
3167
|
"Feb",
|
|
2808
3168
|
"Mar",
|
|
@@ -2858,7 +3218,7 @@ function computeQuantiles3(values) {
|
|
|
2858
3218
|
};
|
|
2859
3219
|
return [q(0.25), q(0.5), q(0.75)];
|
|
2860
3220
|
}
|
|
2861
|
-
function
|
|
3221
|
+
function getLevel2(tokens, quantiles) {
|
|
2862
3222
|
if (tokens <= 0)
|
|
2863
3223
|
return 0;
|
|
2864
3224
|
if (tokens <= quantiles[0])
|
|
@@ -2918,12 +3278,12 @@ function buildHeatmapCells(daily, since, until) {
|
|
|
2918
3278
|
const row = current.getUTCDay();
|
|
2919
3279
|
const dateStr = current.toISOString().slice(0, 10);
|
|
2920
3280
|
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
2921
|
-
const level =
|
|
3281
|
+
const level = getLevel2(tokens, quantiles);
|
|
2922
3282
|
cells.push({ date: dateStr, tokens, level, row, col });
|
|
2923
3283
|
const month = current.getUTCMonth();
|
|
2924
3284
|
if (month !== lastMonth && row === 0) {
|
|
2925
3285
|
lastMonth = month;
|
|
2926
|
-
months.push({ label:
|
|
3286
|
+
months.push({ label: MONTH_NAMES2[month] ?? "", col });
|
|
2927
3287
|
}
|
|
2928
3288
|
if (row === 6)
|
|
2929
3289
|
col++;
|
|
@@ -2934,21 +3294,17 @@ function buildHeatmapCells(daily, since, until) {
|
|
|
2934
3294
|
function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
|
|
2935
3295
|
const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
|
|
2936
3296
|
const { cells, months, totalCols } = buildHeatmapCells(provider.daily, since, until);
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
2939
|
-
const dayLabelWidth = 44;
|
|
2940
|
-
const monthLabelHeight = 24;
|
|
2941
|
-
const heatmapWidth = dayLabelWidth + totalCols * (cellSize + cellGap);
|
|
2942
|
-
const heatmapHeight = monthLabelHeight + 7 * (cellSize + cellGap);
|
|
3297
|
+
const heatmapWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
|
|
3298
|
+
const heatmapHeight = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
|
|
2943
3299
|
const cellsHtml = cells.map((c) => {
|
|
2944
|
-
const x =
|
|
2945
|
-
const y =
|
|
3300
|
+
const x = DAY_LABEL_WIDTH + c.col * (CELL_SIZE + CELL_GAP);
|
|
3301
|
+
const y = MONTH_LABEL_HEIGHT + c.row * (CELL_SIZE + CELL_GAP);
|
|
2946
3302
|
const fill = c.level === 0 ? emptyCell : heatmapColors[c.level];
|
|
2947
3303
|
return `<div class="heatmap-cell" style="left:${x}px;top:${y}px;background:${fill}" data-date="${esc(c.date)}" data-tokens="${c.tokens}"></div>`;
|
|
2948
3304
|
}).join(`
|
|
2949
3305
|
`);
|
|
2950
3306
|
const monthLabelsHtml = months.map((m) => {
|
|
2951
|
-
const x =
|
|
3307
|
+
const x = DAY_LABEL_WIDTH + m.col * (CELL_SIZE + CELL_GAP);
|
|
2952
3308
|
return `<span class="month-label" style="left:${x}px">${esc(m.label)}</span>`;
|
|
2953
3309
|
}).join(`
|
|
2954
3310
|
`);
|
|
@@ -2958,7 +3314,7 @@ function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
|
|
|
2958
3314
|
{ label: "Fri", row: 5 },
|
|
2959
3315
|
{ label: "Sun", row: 0 }
|
|
2960
3316
|
].map((d) => {
|
|
2961
|
-
const y =
|
|
3317
|
+
const y = MONTH_LABEL_HEIGHT + d.row * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
|
|
2962
3318
|
return `<span class="day-label" style="top:${y - 10}px">${d.label}</span>`;
|
|
2963
3319
|
}).join(`
|
|
2964
3320
|
`);
|
|
@@ -3080,8 +3436,8 @@ function generateHtml(output, options) {
|
|
|
3080
3436
|
.heatmap-container { position: relative; margin-bottom: 8px; }
|
|
3081
3437
|
.heatmap-cell {
|
|
3082
3438
|
position: absolute;
|
|
3083
|
-
width:
|
|
3084
|
-
height:
|
|
3439
|
+
width: ${CELL_SIZE}px;
|
|
3440
|
+
height: ${CELL_SIZE}px;
|
|
3085
3441
|
border-radius: 3px;
|
|
3086
3442
|
cursor: pointer;
|
|
3087
3443
|
}
|
|
@@ -3111,11 +3467,17 @@ function generateHtml(output, options) {
|
|
|
3111
3467
|
.stat-value.accent { color: ${accent}; }
|
|
3112
3468
|
.models-section { margin-top: 8px; }
|
|
3113
3469
|
.models-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
|
|
3114
|
-
.model-row {
|
|
3115
|
-
|
|
3116
|
-
|
|
3470
|
+
.model-row {
|
|
3471
|
+
display: grid;
|
|
3472
|
+
grid-template-columns: ${MODEL_NAME_WIDTH}px minmax(0, 1fr) ${MODEL_PERCENT_WIDTH}px;
|
|
3473
|
+
align-items: center;
|
|
3474
|
+
column-gap: ${MODEL_BAR_GAP}px;
|
|
3475
|
+
margin-bottom: 16px;
|
|
3476
|
+
}
|
|
3477
|
+
.model-name { color: ${muted}; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3478
|
+
.model-bar-track { width: 100%; height: 8px; background: ${barTrack}; border-radius: 4px; overflow: hidden; }
|
|
3117
3479
|
.model-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, ${accent}44, ${accent}); }
|
|
3118
|
-
.model-pct { color: ${muted}; font-size: 12px;
|
|
3480
|
+
.model-pct { color: ${muted}; font-size: 12px; text-align: right; }
|
|
3119
3481
|
.refresh-btn {
|
|
3120
3482
|
display: inline-flex;
|
|
3121
3483
|
align-items: center;
|
|
@@ -3299,6 +3661,1097 @@ function handleError(error) {
|
|
|
3299
3661
|
process.exit(1);
|
|
3300
3662
|
}
|
|
3301
3663
|
|
|
3664
|
+
// packages/cli/src/flags.ts
|
|
3665
|
+
var CLI_FLAG_ORDER = [
|
|
3666
|
+
"format",
|
|
3667
|
+
"theme",
|
|
3668
|
+
"since",
|
|
3669
|
+
"until",
|
|
3670
|
+
"days",
|
|
3671
|
+
"output",
|
|
3672
|
+
"width",
|
|
3673
|
+
"provider",
|
|
3674
|
+
"compare",
|
|
3675
|
+
"upload",
|
|
3676
|
+
"claude",
|
|
3677
|
+
"codex",
|
|
3678
|
+
"openCode",
|
|
3679
|
+
"allProviders",
|
|
3680
|
+
"listProviders",
|
|
3681
|
+
"more",
|
|
3682
|
+
"clipboard",
|
|
3683
|
+
"open",
|
|
3684
|
+
"liveServer",
|
|
3685
|
+
"noColor",
|
|
3686
|
+
"noInsights"
|
|
3687
|
+
];
|
|
3688
|
+
var CLI_FLAG_NAMES = {
|
|
3689
|
+
format: "--format",
|
|
3690
|
+
theme: "--theme",
|
|
3691
|
+
since: "--since",
|
|
3692
|
+
until: "--until",
|
|
3693
|
+
days: "--days",
|
|
3694
|
+
output: "--output",
|
|
3695
|
+
width: "--width",
|
|
3696
|
+
provider: "--provider",
|
|
3697
|
+
compare: "--compare",
|
|
3698
|
+
upload: "--upload",
|
|
3699
|
+
claude: "--claude",
|
|
3700
|
+
codex: "--codex",
|
|
3701
|
+
openCode: "--open-code",
|
|
3702
|
+
allProviders: "--all-providers",
|
|
3703
|
+
listProviders: "--list-providers",
|
|
3704
|
+
more: "--more",
|
|
3705
|
+
clipboard: "--clipboard",
|
|
3706
|
+
open: "--open",
|
|
3707
|
+
liveServer: "--live-server",
|
|
3708
|
+
noColor: "--no-color",
|
|
3709
|
+
noInsights: "--no-insights"
|
|
3710
|
+
};
|
|
3711
|
+
function buildCliArgTokens(cliArgs) {
|
|
3712
|
+
const tokens = [];
|
|
3713
|
+
for (const key of CLI_FLAG_ORDER) {
|
|
3714
|
+
const value = cliArgs[key];
|
|
3715
|
+
if (value === undefined || value === false || value === null) {
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
const flag = CLI_FLAG_NAMES[key];
|
|
3719
|
+
if (!flag)
|
|
3720
|
+
continue;
|
|
3721
|
+
tokens.push(flag);
|
|
3722
|
+
if (value !== true) {
|
|
3723
|
+
tokens.push(String(value));
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return tokens;
|
|
3727
|
+
}
|
|
3728
|
+
function buildCliPreview(cliArgs) {
|
|
3729
|
+
const tokens = buildCliArgTokens(cliArgs);
|
|
3730
|
+
return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
// packages/cli/src/interactive.ts
|
|
3734
|
+
import { emitKeypressEvents } from "readline";
|
|
3735
|
+
import { createInterface } from "readline/promises";
|
|
3736
|
+
var INTERACTIVE_FLAG_LINES = [
|
|
3737
|
+
"-f, --format <format> terminal | png | svg | json",
|
|
3738
|
+
"-t, --theme <theme> dark | light",
|
|
3739
|
+
"-s, --since <date> YYYY-MM-DD start date",
|
|
3740
|
+
"-u, --until <date> YYYY-MM-DD end date",
|
|
3741
|
+
"-d, --days <number> trailing days window",
|
|
3742
|
+
"-o, --output <path> write output to a file",
|
|
3743
|
+
"-w, --width <number> terminal render width",
|
|
3744
|
+
"-p, --provider <list> comma-separated providers",
|
|
3745
|
+
" --claude shortcut for Claude Code",
|
|
3746
|
+
" --codex shortcut for Codex",
|
|
3747
|
+
" --open-code shortcut for Open Code",
|
|
3748
|
+
" --all-providers ignore provider filters",
|
|
3749
|
+
" --list-providers show provider registry",
|
|
3750
|
+
" --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
|
|
3751
|
+
" --more richer PNG/SVG stats",
|
|
3752
|
+
" --clipboard copy rendered output",
|
|
3753
|
+
" --open open generated file",
|
|
3754
|
+
" --upload <target> gist",
|
|
3755
|
+
"-L, --live-server local interactive dashboard",
|
|
3756
|
+
" --no-color disable ANSI colors",
|
|
3757
|
+
" --no-insights hide terminal insights",
|
|
3758
|
+
" --help print help",
|
|
3759
|
+
" --version print version"
|
|
3760
|
+
];
|
|
3761
|
+
var ESC2 = "\x1B[";
|
|
3762
|
+
var RESET = `${ESC2}0m`;
|
|
3763
|
+
var BOLD = `${ESC2}1m`;
|
|
3764
|
+
var DIM = `${ESC2}2m`;
|
|
3765
|
+
var CYAN = `${ESC2}36m`;
|
|
3766
|
+
var GREEN = `${ESC2}32m`;
|
|
3767
|
+
var YELLOW = `${ESC2}33m`;
|
|
3768
|
+
var RED = `${ESC2}31m`;
|
|
3769
|
+
var WHITE = `${ESC2}97m`;
|
|
3770
|
+
var HOME_CLEAR = "\x1B[H\x1B[J";
|
|
3771
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
3772
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
3773
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
3774
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
3775
|
+
var LOADING_TICK_MS = 120;
|
|
3776
|
+
function color(text2, code) {
|
|
3777
|
+
return `${code}${text2}${RESET}`;
|
|
3778
|
+
}
|
|
3779
|
+
function stripAnsi(text2) {
|
|
3780
|
+
return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
3781
|
+
}
|
|
3782
|
+
function visibleLength(text2) {
|
|
3783
|
+
return stripAnsi(text2).length;
|
|
3784
|
+
}
|
|
3785
|
+
function padVisible(text2, width) {
|
|
3786
|
+
const padding = Math.max(0, width - visibleLength(text2));
|
|
3787
|
+
return text2 + " ".repeat(padding);
|
|
3788
|
+
}
|
|
3789
|
+
function truncateVisible(text2, width) {
|
|
3790
|
+
if (width <= 0)
|
|
3791
|
+
return "";
|
|
3792
|
+
const plain = stripAnsi(text2);
|
|
3793
|
+
if (plain.length <= width)
|
|
3794
|
+
return text2;
|
|
3795
|
+
const limit = width <= 3 ? width : width - 3;
|
|
3796
|
+
let visibleCount = 0;
|
|
3797
|
+
let index = 0;
|
|
3798
|
+
let result = "";
|
|
3799
|
+
let sawAnsi = false;
|
|
3800
|
+
while (index < text2.length && visibleCount < limit) {
|
|
3801
|
+
if (text2[index] === "\x1B") {
|
|
3802
|
+
const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
|
|
3803
|
+
if (match) {
|
|
3804
|
+
result += match[0];
|
|
3805
|
+
index += match[0].length;
|
|
3806
|
+
sawAnsi = true;
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
result += text2[index];
|
|
3811
|
+
index += 1;
|
|
3812
|
+
visibleCount += 1;
|
|
3813
|
+
}
|
|
3814
|
+
if (width <= 3) {
|
|
3815
|
+
return sawAnsi ? `${result}${RESET}` : result;
|
|
3816
|
+
}
|
|
3817
|
+
return sawAnsi ? `${result}...${RESET}` : `${result}...`;
|
|
3818
|
+
}
|
|
3819
|
+
function joinColumns(left, right, totalWidth) {
|
|
3820
|
+
const gutter = 3;
|
|
3821
|
+
const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
|
|
3822
|
+
const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
|
|
3823
|
+
const rows = Math.max(left.length, right.length);
|
|
3824
|
+
const lines = [];
|
|
3825
|
+
for (let index = 0;index < rows; index++) {
|
|
3826
|
+
const leftLine = truncateVisible(left[index] ?? "", leftWidth);
|
|
3827
|
+
const rightLine = truncateVisible(right[index] ?? "", rightWidth);
|
|
3828
|
+
lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
|
|
3829
|
+
}
|
|
3830
|
+
return lines;
|
|
3831
|
+
}
|
|
3832
|
+
function renderRule(width) {
|
|
3833
|
+
return color("-".repeat(width), DIM);
|
|
3834
|
+
}
|
|
3835
|
+
function describeRequest(args) {
|
|
3836
|
+
const output = typeof args["output"] === "string" ? args["output"] : null;
|
|
3837
|
+
if (args["liveServer"]) {
|
|
3838
|
+
return {
|
|
3839
|
+
title: "Live Dashboard",
|
|
3840
|
+
loadingTitle: "Starting live dashboard",
|
|
3841
|
+
loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
|
|
3842
|
+
executionMode: "inherit"
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
if (args["listProviders"]) {
|
|
3846
|
+
return {
|
|
3847
|
+
title: "Provider Registry",
|
|
3848
|
+
loadingTitle: "Loading provider registry",
|
|
3849
|
+
loadingDetail: "Checking registered providers and current availability.",
|
|
3850
|
+
executionMode: "capture"
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
if (args["compare"]) {
|
|
3854
|
+
return {
|
|
3855
|
+
title: "Compare Report",
|
|
3856
|
+
loadingTitle: "Building compare report",
|
|
3857
|
+
loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
|
|
3858
|
+
executionMode: "capture"
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
switch (args["format"]) {
|
|
3862
|
+
case "json":
|
|
3863
|
+
return {
|
|
3864
|
+
title: "JSON Export",
|
|
3865
|
+
loadingTitle: "Generating JSON report",
|
|
3866
|
+
loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
|
|
3867
|
+
executionMode: "capture"
|
|
3868
|
+
};
|
|
3869
|
+
case "svg":
|
|
3870
|
+
return {
|
|
3871
|
+
title: "SVG Export",
|
|
3872
|
+
loadingTitle: "Rendering SVG",
|
|
3873
|
+
loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
|
|
3874
|
+
executionMode: "capture"
|
|
3875
|
+
};
|
|
3876
|
+
case "png":
|
|
3877
|
+
return {
|
|
3878
|
+
title: "PNG Export",
|
|
3879
|
+
loadingTitle: "Rendering PNG",
|
|
3880
|
+
loadingDetail: output ? `Rendering the PNG card and writing it to ${output}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
|
|
3881
|
+
executionMode: "capture"
|
|
3882
|
+
};
|
|
3883
|
+
default:
|
|
3884
|
+
return {
|
|
3885
|
+
title: "Terminal Dashboard",
|
|
3886
|
+
loadingTitle: "Generating terminal dashboard",
|
|
3887
|
+
loadingDetail: "Reading provider logs and aggregating token usage.",
|
|
3888
|
+
executionMode: "capture"
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function finalizeCliArgs(args) {
|
|
3893
|
+
const finalized = { ...args };
|
|
3894
|
+
const format = finalized["format"];
|
|
3895
|
+
if (finalized["compare"] && (format === "png" || format === "svg")) {
|
|
3896
|
+
finalized["more"] = true;
|
|
3897
|
+
}
|
|
3898
|
+
if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
|
|
3899
|
+
if (format === "png" || format === "svg" || format === "json") {
|
|
3900
|
+
finalized["output"] = `tokenleak.${format}`;
|
|
3901
|
+
} else {
|
|
3902
|
+
delete finalized["open"];
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (format === "png") {
|
|
3906
|
+
delete finalized["clipboard"];
|
|
3907
|
+
delete finalized["upload"];
|
|
3908
|
+
}
|
|
3909
|
+
return finalized;
|
|
3910
|
+
}
|
|
3911
|
+
function createRunCommand(args) {
|
|
3912
|
+
const finalizedArgs = finalizeCliArgs(args);
|
|
3913
|
+
return {
|
|
3914
|
+
type: "run",
|
|
3915
|
+
request: {
|
|
3916
|
+
args: finalizedArgs,
|
|
3917
|
+
preview: buildCliPreview(finalizedArgs),
|
|
3918
|
+
...describeRequest(finalizedArgs)
|
|
3919
|
+
}
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function renderMenu(options, selectedIndex) {
|
|
3923
|
+
return options.map((option, index) => {
|
|
3924
|
+
const isSelected = index === selectedIndex;
|
|
3925
|
+
const prefix = isSelected ? color(">", GREEN) : " ";
|
|
3926
|
+
const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
|
|
3927
|
+
const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
|
|
3928
|
+
const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
|
|
3929
|
+
return `${prefix} [${number}] ${title} ${description}`;
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
function renderFlagPanel() {
|
|
3933
|
+
return [
|
|
3934
|
+
color("All Flags", WHITE + BOLD),
|
|
3935
|
+
color("Every flag remains available while using the launcher.", DIM),
|
|
3936
|
+
"",
|
|
3937
|
+
...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
|
|
3938
|
+
];
|
|
3939
|
+
}
|
|
3940
|
+
function renderMenuPanel(context, options, selectedIndex) {
|
|
3941
|
+
const selected = options[selectedIndex];
|
|
3942
|
+
return [
|
|
3943
|
+
color("Tokenleak Interactive Launcher", WHITE + BOLD),
|
|
3944
|
+
`${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
|
|
3945
|
+
"",
|
|
3946
|
+
color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
|
|
3947
|
+
color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
|
|
3948
|
+
"",
|
|
3949
|
+
...renderMenu(options, selectedIndex),
|
|
3950
|
+
"",
|
|
3951
|
+
color("Preview", WHITE + BOLD),
|
|
3952
|
+
color(selected.preview, GREEN),
|
|
3953
|
+
"",
|
|
3954
|
+
color("Keys", WHITE + BOLD),
|
|
3955
|
+
`${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
|
|
3956
|
+
"",
|
|
3957
|
+
renderRule(44)
|
|
3958
|
+
];
|
|
3959
|
+
}
|
|
3960
|
+
function renderHelpOverlay(helpText, width) {
|
|
3961
|
+
const lines = helpText.trimEnd().split(`
|
|
3962
|
+
`);
|
|
3963
|
+
const header = [
|
|
3964
|
+
color("Tokenleak Help", WHITE + BOLD),
|
|
3965
|
+
color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
|
|
3966
|
+
""
|
|
3967
|
+
];
|
|
3968
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible(line, width))].join(`
|
|
3969
|
+
`)}`;
|
|
3970
|
+
}
|
|
3971
|
+
function renderLauncher(context, options, selectedIndex) {
|
|
3972
|
+
const width = process.stdout.columns ?? 120;
|
|
3973
|
+
const menuPanel = renderMenuPanel(context, options, selectedIndex);
|
|
3974
|
+
const flagPanel = renderFlagPanel();
|
|
3975
|
+
const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
|
|
3976
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
3977
|
+
`)}`;
|
|
3978
|
+
}
|
|
3979
|
+
function renderProgressBar(frame, width) {
|
|
3980
|
+
const innerWidth = Math.max(12, width - 2);
|
|
3981
|
+
const headSize = Math.max(4, Math.floor(innerWidth / 5));
|
|
3982
|
+
const travel = Math.max(1, innerWidth - headSize);
|
|
3983
|
+
const cycle = travel * 2;
|
|
3984
|
+
const offset = frame % cycle;
|
|
3985
|
+
const start = offset <= travel ? offset : cycle - offset;
|
|
3986
|
+
const cells = Array.from({ length: innerWidth }, (_, index) => {
|
|
3987
|
+
if (index >= start && index < start + headSize) {
|
|
3988
|
+
return "=";
|
|
3989
|
+
}
|
|
3990
|
+
return "-";
|
|
3991
|
+
}).join("");
|
|
3992
|
+
return color(`[${cells}]`, CYAN);
|
|
3993
|
+
}
|
|
3994
|
+
function renderLoading(request, frame = 0, startedAt = Date.now()) {
|
|
3995
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
|
|
3996
|
+
const progressBar = renderProgressBar(frame, 34);
|
|
3997
|
+
const lines = [
|
|
3998
|
+
color(request.loadingTitle, WHITE + BOLD),
|
|
3999
|
+
color(request.preview, CYAN),
|
|
4000
|
+
"",
|
|
4001
|
+
color(request.loadingDetail, DIM),
|
|
4002
|
+
"",
|
|
4003
|
+
color("Status", WHITE + BOLD),
|
|
4004
|
+
color("Working... stay in tokenleak while this finishes.", YELLOW),
|
|
4005
|
+
"",
|
|
4006
|
+
color("Progress", WHITE + BOLD),
|
|
4007
|
+
progressBar,
|
|
4008
|
+
color(`Elapsed ${elapsedSeconds}s`, DIM),
|
|
4009
|
+
"",
|
|
4010
|
+
renderRule(44)
|
|
4011
|
+
];
|
|
4012
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4013
|
+
`)}`;
|
|
4014
|
+
}
|
|
4015
|
+
function clipOutputLines(lines, limit) {
|
|
4016
|
+
if (limit <= 0)
|
|
4017
|
+
return [];
|
|
4018
|
+
if (lines.length <= limit)
|
|
4019
|
+
return lines;
|
|
4020
|
+
const visible = lines.slice(0, Math.max(0, limit - 1));
|
|
4021
|
+
visible.push(color(`... ${lines.length - visible.length} more lines hidden`, DIM));
|
|
4022
|
+
return visible;
|
|
4023
|
+
}
|
|
4024
|
+
function renderOutputSection(title, content, width, maxLines) {
|
|
4025
|
+
const normalized = content.trimEnd();
|
|
4026
|
+
if (!normalized)
|
|
4027
|
+
return [];
|
|
4028
|
+
const lines = normalized.split(`
|
|
4029
|
+
`).map((line) => truncateVisible(line, width));
|
|
4030
|
+
return [
|
|
4031
|
+
color(title, WHITE + BOLD),
|
|
4032
|
+
...clipOutputLines(lines, maxLines),
|
|
4033
|
+
""
|
|
4034
|
+
];
|
|
4035
|
+
}
|
|
4036
|
+
function renderResult(request, result) {
|
|
4037
|
+
const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
|
4038
|
+
const rows = process.stdout.rows ?? 40;
|
|
4039
|
+
const statusColor = result.ok ? GREEN : RED;
|
|
4040
|
+
const statusLabel = result.ok ? "Completed" : "Failed";
|
|
4041
|
+
const fixedLines = 10;
|
|
4042
|
+
const outputBudget = Math.max(8, rows - fixedLines);
|
|
4043
|
+
const firstSectionLines = result.stdout.trim() && result.stderr.trim() ? Math.max(4, Math.floor(outputBudget * 0.6)) : outputBudget;
|
|
4044
|
+
const secondSectionLines = Math.max(4, outputBudget - firstSectionLines);
|
|
4045
|
+
const body = [
|
|
4046
|
+
color(request.title, WHITE + BOLD),
|
|
4047
|
+
color(request.preview, CYAN),
|
|
4048
|
+
"",
|
|
4049
|
+
`${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
|
|
4050
|
+
color(result.summary, DIM),
|
|
4051
|
+
"",
|
|
4052
|
+
...renderOutputSection("Output", result.stdout, width, firstSectionLines),
|
|
4053
|
+
...renderOutputSection("Messages", result.stderr, width, secondSectionLines),
|
|
4054
|
+
renderRule(44),
|
|
4055
|
+
`${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
|
|
4056
|
+
];
|
|
4057
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
4058
|
+
`)}`;
|
|
4059
|
+
}
|
|
4060
|
+
function enterAltScreen() {
|
|
4061
|
+
process.stdout.write(`${ALT_SCREEN_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
|
|
4062
|
+
}
|
|
4063
|
+
function leaveAltScreen() {
|
|
4064
|
+
process.stdout.write(`${SHOW_CURSOR}${ALT_SCREEN_OFF}`);
|
|
4065
|
+
}
|
|
4066
|
+
function paint(content) {
|
|
4067
|
+
process.stdout.write(content);
|
|
4068
|
+
}
|
|
4069
|
+
function suspendRawMode() {
|
|
4070
|
+
if (process.stdin.isTTY) {
|
|
4071
|
+
process.stdin.setRawMode(false);
|
|
4072
|
+
}
|
|
4073
|
+
process.stdin.pause();
|
|
4074
|
+
process.stdout.write(SHOW_CURSOR);
|
|
4075
|
+
}
|
|
4076
|
+
function resumeRawMode() {
|
|
4077
|
+
if (process.stdin.isTTY) {
|
|
4078
|
+
process.stdin.setRawMode(true);
|
|
4079
|
+
}
|
|
4080
|
+
process.stdin.resume();
|
|
4081
|
+
process.stdout.write(HIDE_CURSOR);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
class InteractiveExitError extends Error {
|
|
4085
|
+
constructor() {
|
|
4086
|
+
super("Interactive session cancelled");
|
|
4087
|
+
this.name = "InteractiveExitError";
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
var PROVIDER_CHOICES = [
|
|
4091
|
+
{ value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
|
|
4092
|
+
{ value: "codex", label: "Codex", description: "OpenAI session logs" },
|
|
4093
|
+
{ value: "open-code", label: "Open Code", description: "Open Code storage and database" }
|
|
4094
|
+
];
|
|
4095
|
+
function isInteractiveExitError(error) {
|
|
4096
|
+
return error instanceof InteractiveExitError;
|
|
4097
|
+
}
|
|
4098
|
+
function parsePositiveInteger(value) {
|
|
4099
|
+
const trimmed = value.trim();
|
|
4100
|
+
if (trimmed === "")
|
|
4101
|
+
return null;
|
|
4102
|
+
const parsed = Number(trimmed);
|
|
4103
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4104
|
+
throw new Error(`Expected a positive whole number, received "${value}".`);
|
|
4105
|
+
}
|
|
4106
|
+
return parsed;
|
|
4107
|
+
}
|
|
4108
|
+
function renderChoiceScreen(title, description, options, selectedIndex, selectedValues, footer = `${color("Up/Down", YELLOW)} move ${color("Space", YELLOW)} toggle ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`) {
|
|
4109
|
+
const optionLines = options.map((option, index) => {
|
|
4110
|
+
const isSelected = index === selectedIndex;
|
|
4111
|
+
const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
|
|
4112
|
+
const pointer = isSelected ? color(">", GREEN) : " ";
|
|
4113
|
+
const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
|
|
4114
|
+
const titleColor = isSelected ? WHITE + BOLD : WHITE;
|
|
4115
|
+
const descriptionColor = isSelected ? CYAN : DIM;
|
|
4116
|
+
return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
|
|
4117
|
+
});
|
|
4118
|
+
const lines = [
|
|
4119
|
+
color(title, WHITE + BOLD),
|
|
4120
|
+
color(description, DIM),
|
|
4121
|
+
"",
|
|
4122
|
+
...optionLines,
|
|
4123
|
+
"",
|
|
4124
|
+
renderRule(44),
|
|
4125
|
+
footer
|
|
4126
|
+
];
|
|
4127
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4128
|
+
`)}`;
|
|
4129
|
+
}
|
|
4130
|
+
async function ask(prompt, initialValue = "") {
|
|
4131
|
+
const readline = createInterface({
|
|
4132
|
+
input: process.stdin,
|
|
4133
|
+
output: process.stdout
|
|
4134
|
+
});
|
|
4135
|
+
let settled = false;
|
|
4136
|
+
return new Promise((resolve, reject) => {
|
|
4137
|
+
const finish = (fn) => {
|
|
4138
|
+
if (settled)
|
|
4139
|
+
return;
|
|
4140
|
+
settled = true;
|
|
4141
|
+
readline.off("SIGINT", onSigint);
|
|
4142
|
+
readline.close();
|
|
4143
|
+
fn();
|
|
4144
|
+
};
|
|
4145
|
+
const onSigint = () => {
|
|
4146
|
+
finish(() => reject(new InteractiveExitError));
|
|
4147
|
+
};
|
|
4148
|
+
readline.on("SIGINT", onSigint);
|
|
4149
|
+
const suffix = initialValue ? ` (${initialValue})` : "";
|
|
4150
|
+
readline.question(`${prompt}${suffix}: `).then((value) => {
|
|
4151
|
+
finish(() => resolve(value.trim() || initialValue));
|
|
4152
|
+
}).catch((error) => {
|
|
4153
|
+
finish(() => reject(error));
|
|
4154
|
+
});
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
async function askYesNo(prompt, defaultValue = false) {
|
|
4158
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
4159
|
+
const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
|
|
4160
|
+
if (value === "")
|
|
4161
|
+
return defaultValue;
|
|
4162
|
+
return value === "y" || value === "yes";
|
|
4163
|
+
}
|
|
4164
|
+
async function promptSingleChoice(title, description, options, initialIndex = 0) {
|
|
4165
|
+
return new Promise((resolve, reject) => {
|
|
4166
|
+
let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
|
|
4167
|
+
const onKeypress = (_input, key) => {
|
|
4168
|
+
if (key.ctrl && key.name === "c") {
|
|
4169
|
+
cleanup();
|
|
4170
|
+
reject(new InteractiveExitError);
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
if (key.name === "up") {
|
|
4174
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
4175
|
+
render();
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
if (key.name === "down") {
|
|
4179
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
4180
|
+
render();
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
4184
|
+
if (digit) {
|
|
4185
|
+
const index = Number(digit) - 1;
|
|
4186
|
+
if (index < options.length) {
|
|
4187
|
+
selectedIndex = index;
|
|
4188
|
+
cleanup();
|
|
4189
|
+
resolve(options[selectedIndex].value);
|
|
4190
|
+
}
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4194
|
+
cleanup();
|
|
4195
|
+
resolve(options[selectedIndex].value);
|
|
4196
|
+
}
|
|
4197
|
+
};
|
|
4198
|
+
function render() {
|
|
4199
|
+
paint(renderChoiceScreen(title, description, options, selectedIndex, undefined, `${color("Up/Down", YELLOW)} move ${color("1-9", YELLOW)} pick ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`));
|
|
4200
|
+
}
|
|
4201
|
+
function cleanup() {
|
|
4202
|
+
process.stdin.off("keypress", onKeypress);
|
|
4203
|
+
suspendRawMode();
|
|
4204
|
+
}
|
|
4205
|
+
render();
|
|
4206
|
+
resumeRawMode();
|
|
4207
|
+
process.stdin.on("keypress", onKeypress);
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
async function promptMultiChoice(title, description, options, initialValues = []) {
|
|
4211
|
+
return new Promise((resolve, reject) => {
|
|
4212
|
+
let selectedIndex = 0;
|
|
4213
|
+
const selectedValues = new Set(initialValues);
|
|
4214
|
+
const onKeypress = (_input, key) => {
|
|
4215
|
+
if (key.ctrl && key.name === "c") {
|
|
4216
|
+
cleanup();
|
|
4217
|
+
reject(new InteractiveExitError);
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (key.name === "up") {
|
|
4221
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
4222
|
+
render();
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
if (key.name === "down") {
|
|
4226
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
4227
|
+
render();
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
if (key.name === "space") {
|
|
4231
|
+
toggleSelected(selectedIndex);
|
|
4232
|
+
render();
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
4236
|
+
if (digit) {
|
|
4237
|
+
const index = Number(digit) - 1;
|
|
4238
|
+
if (index < options.length) {
|
|
4239
|
+
selectedIndex = index;
|
|
4240
|
+
toggleSelected(selectedIndex);
|
|
4241
|
+
render();
|
|
4242
|
+
}
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4246
|
+
cleanup();
|
|
4247
|
+
resolve(Array.from(selectedValues));
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
4250
|
+
function toggleSelected(index) {
|
|
4251
|
+
const value = options[index].value;
|
|
4252
|
+
if (selectedValues.has(value)) {
|
|
4253
|
+
selectedValues.delete(value);
|
|
4254
|
+
} else {
|
|
4255
|
+
selectedValues.add(value);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
function render() {
|
|
4259
|
+
paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
|
|
4260
|
+
}
|
|
4261
|
+
function cleanup() {
|
|
4262
|
+
process.stdin.off("keypress", onKeypress);
|
|
4263
|
+
suspendRawMode();
|
|
4264
|
+
}
|
|
4265
|
+
render();
|
|
4266
|
+
resumeRawMode();
|
|
4267
|
+
process.stdin.on("keypress", onKeypress);
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
function applySelectedProviders(args, providers) {
|
|
4271
|
+
if (providers.length === 0)
|
|
4272
|
+
return;
|
|
4273
|
+
args["provider"] = providers.join(",");
|
|
4274
|
+
}
|
|
4275
|
+
async function promptTheme(defaultTheme = "dark") {
|
|
4276
|
+
const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
|
|
4277
|
+
{ value: "dark", label: "Dark", description: "High-contrast dark canvas" },
|
|
4278
|
+
{ value: "light", label: "Light", description: "Bright export with light background" }
|
|
4279
|
+
], defaultTheme === "light" ? 1 : 0);
|
|
4280
|
+
return theme;
|
|
4281
|
+
}
|
|
4282
|
+
async function promptDateWindow() {
|
|
4283
|
+
const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
|
|
4284
|
+
{ value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
|
|
4285
|
+
{ value: "30", label: "Last 30 days", description: "Short-term trend window" },
|
|
4286
|
+
{ value: "90", label: "Last 90 days", description: "Default overview" },
|
|
4287
|
+
{ value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
|
|
4288
|
+
{ value: "custom", label: "Custom range", description: "Enter exact dates manually" }
|
|
4289
|
+
], 2);
|
|
4290
|
+
if (choice !== "custom") {
|
|
4291
|
+
return { days: Number(choice) };
|
|
4292
|
+
}
|
|
4293
|
+
const since = await ask("Since date YYYY-MM-DD");
|
|
4294
|
+
const until = await ask("Until date YYYY-MM-DD (blank for today)");
|
|
4295
|
+
const args = { since };
|
|
4296
|
+
if (until)
|
|
4297
|
+
args["until"] = until;
|
|
4298
|
+
return args;
|
|
4299
|
+
}
|
|
4300
|
+
async function promptProviderSelection(title = "Provider Filter") {
|
|
4301
|
+
return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
|
|
4302
|
+
}
|
|
4303
|
+
async function promptOutputPath(defaultPath) {
|
|
4304
|
+
return ask("Output file", defaultPath);
|
|
4305
|
+
}
|
|
4306
|
+
async function promptWidth() {
|
|
4307
|
+
const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
|
|
4308
|
+
{ value: "80", label: "80 columns", description: "Standard terminal width" },
|
|
4309
|
+
{ value: "100", label: "100 columns", description: "Balanced dashboard layout" },
|
|
4310
|
+
{ value: "120", label: "120 columns", description: "Wide dashboard layout" },
|
|
4311
|
+
{ value: "custom", label: "Custom width", description: "Enter an exact width" }
|
|
4312
|
+
], 1);
|
|
4313
|
+
if (choice !== "custom") {
|
|
4314
|
+
return Number(choice);
|
|
4315
|
+
}
|
|
4316
|
+
while (true) {
|
|
4317
|
+
try {
|
|
4318
|
+
const parsed = parsePositiveInteger(await ask("Custom width"));
|
|
4319
|
+
if (parsed !== null) {
|
|
4320
|
+
return parsed;
|
|
4321
|
+
}
|
|
4322
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
|
|
4323
|
+
${color("Enter a positive whole number to continue.", DIM)}
|
|
4324
|
+
|
|
4325
|
+
Press Enter to try again.`);
|
|
4326
|
+
await ask("");
|
|
4327
|
+
} catch (error) {
|
|
4328
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
|
|
4329
|
+
${color(error instanceof Error ? error.message : String(error), DIM)}
|
|
4330
|
+
|
|
4331
|
+
Press Enter to try again.`);
|
|
4332
|
+
await ask("");
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
async function promptCompareSetting() {
|
|
4337
|
+
const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
|
|
4338
|
+
{ value: "off", label: "No compare", description: "Render a standard single-period report" },
|
|
4339
|
+
{ value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
|
|
4340
|
+
{ value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
|
|
4341
|
+
]);
|
|
4342
|
+
if (choice === "off")
|
|
4343
|
+
return null;
|
|
4344
|
+
if (choice === "auto")
|
|
4345
|
+
return "auto";
|
|
4346
|
+
return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
|
|
4347
|
+
}
|
|
4348
|
+
async function buildDashboardPreset() {
|
|
4349
|
+
const rangeArgs = await promptDateWindow();
|
|
4350
|
+
const providers = await promptProviderSelection();
|
|
4351
|
+
const width = await promptWidth();
|
|
4352
|
+
const noInsights = await askYesNo("Hide insights panel", false);
|
|
4353
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
4354
|
+
const args = { ...rangeArgs };
|
|
4355
|
+
applySelectedProviders(args, providers);
|
|
4356
|
+
if (width)
|
|
4357
|
+
args["width"] = width;
|
|
4358
|
+
if (noInsights)
|
|
4359
|
+
args["noInsights"] = true;
|
|
4360
|
+
if (noColor2)
|
|
4361
|
+
args["noColor"] = true;
|
|
4362
|
+
return createRunCommand(args);
|
|
4363
|
+
}
|
|
4364
|
+
async function buildJsonPreset() {
|
|
4365
|
+
const rangeArgs = await promptDateWindow();
|
|
4366
|
+
const providers = await promptProviderSelection();
|
|
4367
|
+
const compare = await promptCompareSetting();
|
|
4368
|
+
const saveToFile = await askYesNo("Write JSON to a file", false);
|
|
4369
|
+
const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
|
|
4370
|
+
const args = {
|
|
4371
|
+
format: "json",
|
|
4372
|
+
...rangeArgs
|
|
4373
|
+
};
|
|
4374
|
+
applySelectedProviders(args, providers);
|
|
4375
|
+
if (compare)
|
|
4376
|
+
args["compare"] = compare;
|
|
4377
|
+
if (saveToFile) {
|
|
4378
|
+
args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
|
|
4379
|
+
}
|
|
4380
|
+
if (clipboard)
|
|
4381
|
+
args["clipboard"] = true;
|
|
4382
|
+
return createRunCommand(args);
|
|
4383
|
+
}
|
|
4384
|
+
async function buildImagePreset(format) {
|
|
4385
|
+
const theme = await promptTheme();
|
|
4386
|
+
const rangeArgs = await promptDateWindow();
|
|
4387
|
+
const providers = await promptProviderSelection("Provider Filter");
|
|
4388
|
+
const compare = await promptCompareSetting();
|
|
4389
|
+
const output = await promptOutputPath(`tokenleak.${format}`);
|
|
4390
|
+
const shouldOpen = await askYesNo("Open the file when done", true);
|
|
4391
|
+
const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
|
|
4392
|
+
const args = {
|
|
4393
|
+
format,
|
|
4394
|
+
theme,
|
|
4395
|
+
output,
|
|
4396
|
+
open: shouldOpen,
|
|
4397
|
+
more,
|
|
4398
|
+
...rangeArgs
|
|
4399
|
+
};
|
|
4400
|
+
applySelectedProviders(args, providers);
|
|
4401
|
+
if (compare)
|
|
4402
|
+
args["compare"] = compare;
|
|
4403
|
+
return createRunCommand(args);
|
|
4404
|
+
}
|
|
4405
|
+
async function buildComparePreset() {
|
|
4406
|
+
const rangeArgs = await promptDateWindow();
|
|
4407
|
+
const providers = await promptProviderSelection();
|
|
4408
|
+
const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
|
|
4409
|
+
{ value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
|
|
4410
|
+
{ value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
|
|
4411
|
+
]);
|
|
4412
|
+
const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
|
|
4413
|
+
const saveToFile = await askYesNo("Write compare output to a file", false);
|
|
4414
|
+
const args = {
|
|
4415
|
+
format: "json",
|
|
4416
|
+
compare,
|
|
4417
|
+
...rangeArgs
|
|
4418
|
+
};
|
|
4419
|
+
applySelectedProviders(args, providers);
|
|
4420
|
+
if (saveToFile) {
|
|
4421
|
+
args["output"] = await promptOutputPath("tokenleak-compare.json");
|
|
4422
|
+
}
|
|
4423
|
+
return createRunCommand(args);
|
|
4424
|
+
}
|
|
4425
|
+
async function buildLivePreset() {
|
|
4426
|
+
const theme = await promptTheme();
|
|
4427
|
+
const rangeArgs = await promptDateWindow();
|
|
4428
|
+
const providers = await promptProviderSelection();
|
|
4429
|
+
const more = await askYesNo("Enable --more stats", true);
|
|
4430
|
+
const args = {
|
|
4431
|
+
liveServer: true,
|
|
4432
|
+
theme,
|
|
4433
|
+
more,
|
|
4434
|
+
...rangeArgs
|
|
4435
|
+
};
|
|
4436
|
+
applySelectedProviders(args, providers);
|
|
4437
|
+
return createRunCommand(args);
|
|
4438
|
+
}
|
|
4439
|
+
async function askFormatChoice() {
|
|
4440
|
+
return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
|
|
4441
|
+
{ value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
|
|
4442
|
+
{ value: "json", label: "JSON", description: "Structured machine-readable output" },
|
|
4443
|
+
{ value: "svg", label: "SVG", description: "Shareable vector export" },
|
|
4444
|
+
{ value: "png", label: "PNG", description: "Raster export for social and docs" }
|
|
4445
|
+
]);
|
|
4446
|
+
}
|
|
4447
|
+
async function buildCustomCommand() {
|
|
4448
|
+
const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
|
|
4449
|
+
{ value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
|
|
4450
|
+
{ value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
|
|
4451
|
+
{ value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
|
|
4452
|
+
]);
|
|
4453
|
+
if (mode === "live-server") {
|
|
4454
|
+
return buildLivePreset();
|
|
4455
|
+
}
|
|
4456
|
+
if (mode === "list-providers") {
|
|
4457
|
+
return createRunCommand({ listProviders: true });
|
|
4458
|
+
}
|
|
4459
|
+
const format = await askFormatChoice();
|
|
4460
|
+
const theme = format === "terminal" ? null : await promptTheme();
|
|
4461
|
+
const rangeArgs = await promptDateWindow();
|
|
4462
|
+
const providers = await promptProviderSelection();
|
|
4463
|
+
const compare = await promptCompareSetting();
|
|
4464
|
+
const width = format === "terminal" ? await promptWidth() : null;
|
|
4465
|
+
const output = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
|
|
4466
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
4467
|
+
const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
|
|
4468
|
+
const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
|
|
4469
|
+
const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
|
|
4470
|
+
const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
|
|
4471
|
+
const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
|
|
4472
|
+
const args = {
|
|
4473
|
+
format,
|
|
4474
|
+
...rangeArgs
|
|
4475
|
+
};
|
|
4476
|
+
if (theme)
|
|
4477
|
+
args["theme"] = theme;
|
|
4478
|
+
if (compare)
|
|
4479
|
+
args["compare"] = compare;
|
|
4480
|
+
if (width)
|
|
4481
|
+
args["width"] = width;
|
|
4482
|
+
if (output)
|
|
4483
|
+
args["output"] = output;
|
|
4484
|
+
if (noColor2)
|
|
4485
|
+
args["noColor"] = true;
|
|
4486
|
+
if (noInsights)
|
|
4487
|
+
args["noInsights"] = true;
|
|
4488
|
+
if (more)
|
|
4489
|
+
args["more"] = true;
|
|
4490
|
+
if (clipboard)
|
|
4491
|
+
args["clipboard"] = true;
|
|
4492
|
+
if (open)
|
|
4493
|
+
args["open"] = true;
|
|
4494
|
+
if (upload)
|
|
4495
|
+
args["upload"] = upload;
|
|
4496
|
+
applySelectedProviders(args, providers);
|
|
4497
|
+
return createRunCommand(args);
|
|
4498
|
+
}
|
|
4499
|
+
function createMenuOptions() {
|
|
4500
|
+
return [
|
|
4501
|
+
{
|
|
4502
|
+
digit: "1",
|
|
4503
|
+
title: "Launch Dashboard",
|
|
4504
|
+
description: "guided terminal view",
|
|
4505
|
+
preview: "tokenleak --days 90",
|
|
4506
|
+
select: buildDashboardPreset
|
|
4507
|
+
},
|
|
4508
|
+
{
|
|
4509
|
+
digit: "2",
|
|
4510
|
+
title: "Export JSON",
|
|
4511
|
+
description: "structured output for scripts",
|
|
4512
|
+
preview: "tokenleak --format json",
|
|
4513
|
+
select: buildJsonPreset
|
|
4514
|
+
},
|
|
4515
|
+
{
|
|
4516
|
+
digit: "3",
|
|
4517
|
+
title: "Export SVG",
|
|
4518
|
+
description: "shareable vector card",
|
|
4519
|
+
preview: "tokenleak --format svg --output tokenleak.svg",
|
|
4520
|
+
select: async () => buildImagePreset("svg")
|
|
4521
|
+
},
|
|
4522
|
+
{
|
|
4523
|
+
digit: "4",
|
|
4524
|
+
title: "Export PNG",
|
|
4525
|
+
description: "social-ready raster image",
|
|
4526
|
+
preview: "tokenleak --format png --output tokenleak.png --more",
|
|
4527
|
+
select: async () => buildImagePreset("png")
|
|
4528
|
+
},
|
|
4529
|
+
{
|
|
4530
|
+
digit: "5",
|
|
4531
|
+
title: "Compare Periods",
|
|
4532
|
+
description: "diff current vs previous usage",
|
|
4533
|
+
preview: "tokenleak --compare auto --format json",
|
|
4534
|
+
select: buildComparePreset
|
|
4535
|
+
},
|
|
4536
|
+
{
|
|
4537
|
+
digit: "6",
|
|
4538
|
+
title: "Start Live Server",
|
|
4539
|
+
description: "browser dashboard on localhost",
|
|
4540
|
+
preview: "tokenleak --live-server --theme dark",
|
|
4541
|
+
select: buildLivePreset
|
|
4542
|
+
},
|
|
4543
|
+
{
|
|
4544
|
+
digit: "7",
|
|
4545
|
+
title: "Build Custom Command",
|
|
4546
|
+
description: "configure flags interactively",
|
|
4547
|
+
preview: "tokenleak --format terminal --days 90",
|
|
4548
|
+
select: buildCustomCommand
|
|
4549
|
+
},
|
|
4550
|
+
{
|
|
4551
|
+
digit: "8",
|
|
4552
|
+
title: "Full Help",
|
|
4553
|
+
description: "examples and complete usage",
|
|
4554
|
+
preview: "tokenleak --help",
|
|
4555
|
+
select: async () => ({ type: "show-help" })
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
digit: "9",
|
|
4559
|
+
title: "List Providers",
|
|
4560
|
+
description: "detect available registries",
|
|
4561
|
+
preview: "tokenleak --list-providers",
|
|
4562
|
+
select: async () => createRunCommand({ listProviders: true })
|
|
4563
|
+
},
|
|
4564
|
+
{
|
|
4565
|
+
digit: "0",
|
|
4566
|
+
title: "Exit",
|
|
4567
|
+
description: "close the launcher",
|
|
4568
|
+
preview: "exit",
|
|
4569
|
+
select: async () => ({ type: "exit" })
|
|
4570
|
+
}
|
|
4571
|
+
];
|
|
4572
|
+
}
|
|
4573
|
+
async function waitForSingleKey() {
|
|
4574
|
+
return new Promise((resolve) => {
|
|
4575
|
+
const onKeypress = (_input, key) => {
|
|
4576
|
+
process.stdin.off("keypress", onKeypress);
|
|
4577
|
+
suspendRawMode();
|
|
4578
|
+
resolve(key);
|
|
4579
|
+
};
|
|
4580
|
+
resumeRawMode();
|
|
4581
|
+
process.stdin.on("keypress", onKeypress);
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
async function promptForMenuCommand(context, options, state) {
|
|
4585
|
+
let showHelp = false;
|
|
4586
|
+
let resolving = false;
|
|
4587
|
+
return new Promise((resolve, reject) => {
|
|
4588
|
+
const onKeypress = async (_input, key) => {
|
|
4589
|
+
if (resolving) {
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
if (key.ctrl && key.name === "c") {
|
|
4593
|
+
cleanup();
|
|
4594
|
+
resolve({ type: "exit" });
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
if (showHelp) {
|
|
4598
|
+
if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
|
|
4599
|
+
showHelp = false;
|
|
4600
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4601
|
+
}
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (key.name === "up") {
|
|
4605
|
+
state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
|
|
4606
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
if (key.name === "down") {
|
|
4610
|
+
state.selectedIndex = (state.selectedIndex + 1) % options.length;
|
|
4611
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
if (key.name === "h") {
|
|
4615
|
+
showHelp = true;
|
|
4616
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
if (key.name === "q" || key.name === "escape") {
|
|
4620
|
+
cleanup();
|
|
4621
|
+
resolve({ type: "exit" });
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
const digit = key.sequence?.match(/^[0-9]$/)?.[0];
|
|
4625
|
+
if (digit) {
|
|
4626
|
+
const nextIndex = options.findIndex((option) => option.digit === digit);
|
|
4627
|
+
if (nextIndex >= 0) {
|
|
4628
|
+
state.selectedIndex = nextIndex;
|
|
4629
|
+
if (digit === "8") {
|
|
4630
|
+
showHelp = true;
|
|
4631
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
resolving = true;
|
|
4635
|
+
cleanup();
|
|
4636
|
+
try {
|
|
4637
|
+
const command = await options[nextIndex].select();
|
|
4638
|
+
resolve(command);
|
|
4639
|
+
} catch (error) {
|
|
4640
|
+
if (isInteractiveExitError(error)) {
|
|
4641
|
+
resolve({ type: "exit" });
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
reject(error);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4650
|
+
if (options[state.selectedIndex].digit === "8") {
|
|
4651
|
+
showHelp = true;
|
|
4652
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4653
|
+
return;
|
|
4654
|
+
}
|
|
4655
|
+
resolving = true;
|
|
4656
|
+
cleanup();
|
|
4657
|
+
try {
|
|
4658
|
+
const command = await options[state.selectedIndex].select();
|
|
4659
|
+
resolve(command);
|
|
4660
|
+
} catch (error) {
|
|
4661
|
+
if (isInteractiveExitError(error)) {
|
|
4662
|
+
resolve({ type: "exit" });
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
reject(error);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
function cleanup() {
|
|
4670
|
+
process.stdin.off("keypress", onKeypress);
|
|
4671
|
+
suspendRawMode();
|
|
4672
|
+
}
|
|
4673
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4674
|
+
resumeRawMode();
|
|
4675
|
+
process.stdin.on("keypress", onKeypress);
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
async function showExecutionResult(request, result) {
|
|
4679
|
+
paint(renderResult(request, result));
|
|
4680
|
+
const key = await waitForSingleKey();
|
|
4681
|
+
if (key.ctrl && key.name === "c")
|
|
4682
|
+
return "exit";
|
|
4683
|
+
if (key.name === "q" || key.name === "escape")
|
|
4684
|
+
return "exit";
|
|
4685
|
+
return "menu";
|
|
4686
|
+
}
|
|
4687
|
+
function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
|
|
4688
|
+
return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
|
|
4689
|
+
}
|
|
4690
|
+
async function startInteractiveCli(context, execute) {
|
|
4691
|
+
const options = createMenuOptions();
|
|
4692
|
+
const state = { selectedIndex: 0 };
|
|
4693
|
+
let interrupted = false;
|
|
4694
|
+
let ignoreSigint = false;
|
|
4695
|
+
const onSigint = () => {
|
|
4696
|
+
if (!ignoreSigint) {
|
|
4697
|
+
interrupted = true;
|
|
4698
|
+
}
|
|
4699
|
+
};
|
|
4700
|
+
emitKeypressEvents(process.stdin);
|
|
4701
|
+
enterAltScreen();
|
|
4702
|
+
process.on("SIGINT", onSigint);
|
|
4703
|
+
try {
|
|
4704
|
+
while (true) {
|
|
4705
|
+
if (interrupted) {
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const command = await promptForMenuCommand(context, options, state);
|
|
4709
|
+
if (command.type === "exit") {
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
if (command.type === "show-help") {
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
const startedAt = Date.now();
|
|
4716
|
+
let loadingFrame = 0;
|
|
4717
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
4718
|
+
const loadingTicker = setInterval(() => {
|
|
4719
|
+
loadingFrame += 1;
|
|
4720
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
4721
|
+
}, LOADING_TICK_MS);
|
|
4722
|
+
let result;
|
|
4723
|
+
try {
|
|
4724
|
+
if (command.request.executionMode === "inherit") {
|
|
4725
|
+
clearInterval(loadingTicker);
|
|
4726
|
+
leaveAltScreen();
|
|
4727
|
+
try {
|
|
4728
|
+
ignoreSigint = true;
|
|
4729
|
+
result = await execute(command.request);
|
|
4730
|
+
} finally {
|
|
4731
|
+
ignoreSigint = false;
|
|
4732
|
+
enterAltScreen();
|
|
4733
|
+
}
|
|
4734
|
+
} else {
|
|
4735
|
+
result = await execute(command.request);
|
|
4736
|
+
}
|
|
4737
|
+
} finally {
|
|
4738
|
+
clearInterval(loadingTicker);
|
|
4739
|
+
}
|
|
4740
|
+
if (interrupted) {
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
const next = await showExecutionResult(command.request, result);
|
|
4744
|
+
if (next === "exit") {
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
} finally {
|
|
4749
|
+
process.off("SIGINT", onSigint);
|
|
4750
|
+
suspendRawMode();
|
|
4751
|
+
leaveAltScreen();
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
3302
4755
|
// packages/cli/src/sharing/clipboard.ts
|
|
3303
4756
|
var PLATFORM_COMMANDS = {
|
|
3304
4757
|
darwin: ["pbcopy"],
|
|
@@ -3400,6 +4853,248 @@ async function uploadToGist(content, filename, description) {
|
|
|
3400
4853
|
// packages/cli/src/cli.ts
|
|
3401
4854
|
var FORMAT_VALUES = ["json", "svg", "png", "terminal"];
|
|
3402
4855
|
var THEME_VALUES = ["dark", "light"];
|
|
4856
|
+
var PROVIDER_SHORTCUTS = {
|
|
4857
|
+
claude: "claude-code",
|
|
4858
|
+
codex: "codex",
|
|
4859
|
+
openCode: "open-code"
|
|
4860
|
+
};
|
|
4861
|
+
var PROVIDER_ALIASES = {
|
|
4862
|
+
anthropic: "claude-code",
|
|
4863
|
+
claude: "claude-code",
|
|
4864
|
+
"claude-code": "claude-code",
|
|
4865
|
+
claudecode: "claude-code",
|
|
4866
|
+
codex: "codex",
|
|
4867
|
+
openai: "codex",
|
|
4868
|
+
"open-code": "open-code",
|
|
4869
|
+
open_code: "open-code",
|
|
4870
|
+
opencode: "open-code"
|
|
4871
|
+
};
|
|
4872
|
+
var PROVIDER_ALIAS_GROUPS = {
|
|
4873
|
+
"claude-code": ["anthropic", "claude", "claudecode"],
|
|
4874
|
+
codex: ["openai"],
|
|
4875
|
+
"open-code": ["opencode", "open_code"]
|
|
4876
|
+
};
|
|
4877
|
+
function normalizeProviderToken(token) {
|
|
4878
|
+
const normalized = token.trim().toLowerCase().replace(/\s+/g, "-");
|
|
4879
|
+
return PROVIDER_ALIASES[normalized] ?? normalized;
|
|
4880
|
+
}
|
|
4881
|
+
function getRequestedProviders(config) {
|
|
4882
|
+
const requested = new Set;
|
|
4883
|
+
if (config.provider) {
|
|
4884
|
+
for (const token of config.provider.split(",")) {
|
|
4885
|
+
const normalized = normalizeProviderToken(token);
|
|
4886
|
+
if (normalized) {
|
|
4887
|
+
requested.add(normalized);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
if (config.claude)
|
|
4892
|
+
requested.add(PROVIDER_SHORTCUTS.claude);
|
|
4893
|
+
if (config.codex)
|
|
4894
|
+
requested.add(PROVIDER_SHORTCUTS.codex);
|
|
4895
|
+
if (config.openCode)
|
|
4896
|
+
requested.add(PROVIDER_SHORTCUTS.openCode);
|
|
4897
|
+
return requested;
|
|
4898
|
+
}
|
|
4899
|
+
function providerMatchesFilter(provider, requested) {
|
|
4900
|
+
if (requested.size === 0)
|
|
4901
|
+
return true;
|
|
4902
|
+
const candidates = [
|
|
4903
|
+
normalizeProviderToken(provider.name),
|
|
4904
|
+
normalizeProviderToken(provider.displayName)
|
|
4905
|
+
];
|
|
4906
|
+
return candidates.some((candidate) => requested.has(candidate));
|
|
4907
|
+
}
|
|
4908
|
+
function buildHelpText() {
|
|
4909
|
+
return [
|
|
4910
|
+
`tokenleak ${VERSION}`,
|
|
4911
|
+
"Visualize AI coding assistant token usage across providers.",
|
|
4912
|
+
"Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
|
|
4913
|
+
"",
|
|
4914
|
+
"Usage:",
|
|
4915
|
+
" tokenleak [flags]",
|
|
4916
|
+
"",
|
|
4917
|
+
"Provider Shortcuts:",
|
|
4918
|
+
" --claude Only include Claude Code",
|
|
4919
|
+
" --codex Only include Codex",
|
|
4920
|
+
" --open-code Only include OpenCode",
|
|
4921
|
+
" --all-providers Ignore provider filters and use every available provider",
|
|
4922
|
+
" --list-providers Show registered providers and aliases",
|
|
4923
|
+
"",
|
|
4924
|
+
"Flags:",
|
|
4925
|
+
" -f, --format <format> Output format: terminal, png, svg, json",
|
|
4926
|
+
" -t, --theme <theme> Theme for png/svg/live output: dark, light",
|
|
4927
|
+
" -s, --since <date> Start date in YYYY-MM-DD format",
|
|
4928
|
+
" -u, --until <date> End date in YYYY-MM-DD format",
|
|
4929
|
+
` -d, --days <number> Number of trailing days to include (default: ${DEFAULT_DAYS})`,
|
|
4930
|
+
" -o, --output <path> Write output to a file and infer format from extension",
|
|
4931
|
+
" -w, --width <number> Terminal render width",
|
|
4932
|
+
" -p, --provider <list> Provider filter list, comma-separated",
|
|
4933
|
+
" --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
|
|
4934
|
+
" --more Add expanded PNG/SVG stats and unlock compare cards",
|
|
4935
|
+
" --clipboard Copy rendered output to the clipboard",
|
|
4936
|
+
" --open Open the generated output file",
|
|
4937
|
+
" --upload <target> Upload rendered output, currently: gist",
|
|
4938
|
+
" -L, --live-server Start the interactive local dashboard",
|
|
4939
|
+
" --no-color Disable ANSI colors",
|
|
4940
|
+
" --no-insights Hide insights in terminal mode",
|
|
4941
|
+
" --help Show this help",
|
|
4942
|
+
" --version Show version information",
|
|
4943
|
+
"",
|
|
4944
|
+
"Examples:",
|
|
4945
|
+
" tokenleak",
|
|
4946
|
+
" tokenleak --claude --days 30",
|
|
4947
|
+
" tokenleak --codex --format png --output codex.png",
|
|
4948
|
+
" tokenleak --open-code --since 2026-01-01 --until 2026-03-01",
|
|
4949
|
+
" tokenleak --provider claude,codex --format svg --output usage.svg",
|
|
4950
|
+
" tokenleak --provider anthropic,openai",
|
|
4951
|
+
" tokenleak --list-providers",
|
|
4952
|
+
" tokenleak --compare auto --format terminal",
|
|
4953
|
+
" tokenleak --live-server --theme light",
|
|
4954
|
+
"",
|
|
4955
|
+
"Version:",
|
|
4956
|
+
` CLI ${VERSION}`,
|
|
4957
|
+
` Schema ${SCHEMA_VERSION}`,
|
|
4958
|
+
""
|
|
4959
|
+
].join(`
|
|
4960
|
+
`);
|
|
4961
|
+
}
|
|
4962
|
+
function buildVersionText() {
|
|
4963
|
+
return `tokenleak ${VERSION}
|
|
4964
|
+
schema ${SCHEMA_VERSION}
|
|
4965
|
+
`;
|
|
4966
|
+
}
|
|
4967
|
+
function normalizeCliArg(arg) {
|
|
4968
|
+
const flagMap = {
|
|
4969
|
+
"--all-providers": "--allProviders",
|
|
4970
|
+
"--list-providers": "--listProviders",
|
|
4971
|
+
"--open-code": "--openCode",
|
|
4972
|
+
"--live-server": "--liveServer",
|
|
4973
|
+
"--no-color": "--noColor",
|
|
4974
|
+
"--no-insights": "--noInsights"
|
|
4975
|
+
};
|
|
4976
|
+
return flagMap[arg] ?? arg;
|
|
4977
|
+
}
|
|
4978
|
+
function buildInteractiveSummary(cliArgs, ok, exitCode) {
|
|
4979
|
+
if (!ok) {
|
|
4980
|
+
return `Command exited with code ${exitCode}.`;
|
|
4981
|
+
}
|
|
4982
|
+
if (typeof cliArgs["output"] === "string") {
|
|
4983
|
+
const outputPath = cliArgs["output"];
|
|
4984
|
+
const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
|
|
4985
|
+
return `${format2} written to ${outputPath}.`;
|
|
4986
|
+
}
|
|
4987
|
+
if (cliArgs["listProviders"]) {
|
|
4988
|
+
return "Provider registry loaded.";
|
|
4989
|
+
}
|
|
4990
|
+
if (cliArgs["liveServer"]) {
|
|
4991
|
+
return "Live dashboard stopped.";
|
|
4992
|
+
}
|
|
4993
|
+
if (cliArgs["compare"]) {
|
|
4994
|
+
return "Compare report generated.";
|
|
4995
|
+
}
|
|
4996
|
+
const format = String(cliArgs["format"] ?? "terminal");
|
|
4997
|
+
if (format === "terminal") {
|
|
4998
|
+
return "Terminal dashboard generated.";
|
|
4999
|
+
}
|
|
5000
|
+
return `${format.toUpperCase()} command finished successfully.`;
|
|
5001
|
+
}
|
|
5002
|
+
async function executeInteractiveCommand(request) {
|
|
5003
|
+
try {
|
|
5004
|
+
const cliPath = process.argv[1];
|
|
5005
|
+
if (!cliPath) {
|
|
5006
|
+
return {
|
|
5007
|
+
ok: false,
|
|
5008
|
+
summary: "Could not resolve the current tokenleak entrypoint.",
|
|
5009
|
+
stdout: "",
|
|
5010
|
+
stderr: "Error: process.argv[1] is missing."
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
|
|
5014
|
+
if (request.executionMode === "inherit") {
|
|
5015
|
+
const proc2 = Bun.spawn(command, {
|
|
5016
|
+
stdin: "inherit",
|
|
5017
|
+
stdout: "inherit",
|
|
5018
|
+
stderr: "inherit"
|
|
5019
|
+
});
|
|
5020
|
+
const exitCode2 = await proc2.exited;
|
|
5021
|
+
return {
|
|
5022
|
+
ok: exitCode2 === 0,
|
|
5023
|
+
summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
|
|
5024
|
+
stdout: "",
|
|
5025
|
+
stderr: ""
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
const proc = Bun.spawn(command, {
|
|
5029
|
+
stdin: "ignore",
|
|
5030
|
+
stdout: "pipe",
|
|
5031
|
+
stderr: "pipe"
|
|
5032
|
+
});
|
|
5033
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
5034
|
+
proc.exited,
|
|
5035
|
+
new Response(proc.stdout).text(),
|
|
5036
|
+
new Response(proc.stderr).text()
|
|
5037
|
+
]);
|
|
5038
|
+
return {
|
|
5039
|
+
ok: exitCode === 0,
|
|
5040
|
+
summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
|
|
5041
|
+
stdout,
|
|
5042
|
+
stderr
|
|
5043
|
+
};
|
|
5044
|
+
} catch (error) {
|
|
5045
|
+
return {
|
|
5046
|
+
ok: false,
|
|
5047
|
+
summary: "Interactive command failed before it could finish.",
|
|
5048
|
+
stdout: "",
|
|
5049
|
+
stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
function normalizeCliArgv(argv) {
|
|
5054
|
+
const normalized = argv.map(normalizeCliArg);
|
|
5055
|
+
const result = [];
|
|
5056
|
+
for (let i = 0;i < normalized.length; i++) {
|
|
5057
|
+
const arg = normalized[i];
|
|
5058
|
+
if (arg === "--provider" || arg === "-p") {
|
|
5059
|
+
result.push(arg);
|
|
5060
|
+
const providerParts = [];
|
|
5061
|
+
let j = i + 1;
|
|
5062
|
+
while (j < normalized.length) {
|
|
5063
|
+
const next = normalized[j];
|
|
5064
|
+
if (next.startsWith("-"))
|
|
5065
|
+
break;
|
|
5066
|
+
providerParts.push(next);
|
|
5067
|
+
j++;
|
|
5068
|
+
}
|
|
5069
|
+
if (providerParts.length > 0) {
|
|
5070
|
+
result.push(providerParts.join(" "));
|
|
5071
|
+
i = j - 1;
|
|
5072
|
+
}
|
|
5073
|
+
continue;
|
|
5074
|
+
}
|
|
5075
|
+
result.push(arg);
|
|
5076
|
+
}
|
|
5077
|
+
return result;
|
|
5078
|
+
}
|
|
5079
|
+
function registerBuiltInProviders(registry) {
|
|
5080
|
+
registry.register(new ClaudeCodeProvider);
|
|
5081
|
+
registry.register(new CodexProvider);
|
|
5082
|
+
registry.register(new OpenCodeProvider);
|
|
5083
|
+
}
|
|
5084
|
+
function buildProviderList(providers, availability) {
|
|
5085
|
+
const lines = ["Registered providers:", ""];
|
|
5086
|
+
for (const provider of providers) {
|
|
5087
|
+
const aliases = PROVIDER_ALIAS_GROUPS[provider.name] ?? [];
|
|
5088
|
+
const status = availability.get(provider.name) ? "available" : "unavailable";
|
|
5089
|
+
lines.push(`- ${provider.name} (${provider.displayName}) [${status}]`);
|
|
5090
|
+
if (aliases.length > 0) {
|
|
5091
|
+
lines.push(` aliases: ${aliases.join(", ")}`);
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
lines.push("");
|
|
5095
|
+
return lines.join(`
|
|
5096
|
+
`);
|
|
5097
|
+
}
|
|
3403
5098
|
function inferFormatFromPath(filePath) {
|
|
3404
5099
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
3405
5100
|
switch (ext) {
|
|
@@ -3453,6 +5148,12 @@ function resolveConfig(cliArgs) {
|
|
|
3453
5148
|
width: 80,
|
|
3454
5149
|
noColor: false,
|
|
3455
5150
|
noInsights: false,
|
|
5151
|
+
more: false,
|
|
5152
|
+
claude: false,
|
|
5153
|
+
codex: false,
|
|
5154
|
+
openCode: false,
|
|
5155
|
+
allProviders: false,
|
|
5156
|
+
listProviders: false,
|
|
3456
5157
|
clipboard: false,
|
|
3457
5158
|
open: false,
|
|
3458
5159
|
liveServer: false
|
|
@@ -3471,6 +5172,8 @@ function resolveConfig(cliArgs) {
|
|
|
3471
5172
|
merged.noColor = fileConfig.noColor;
|
|
3472
5173
|
if (fileConfig.noInsights !== undefined)
|
|
3473
5174
|
merged.noInsights = fileConfig.noInsights;
|
|
5175
|
+
if (fileConfig.more !== undefined)
|
|
5176
|
+
merged.more = fileConfig.more;
|
|
3474
5177
|
if (envConfig.format)
|
|
3475
5178
|
merged.format = envConfig.format;
|
|
3476
5179
|
if (envConfig.theme)
|
|
@@ -3512,12 +5215,30 @@ function resolveConfig(cliArgs) {
|
|
|
3512
5215
|
if (cliArgs["noInsights"] !== undefined) {
|
|
3513
5216
|
result.noInsights = cliArgs["noInsights"];
|
|
3514
5217
|
}
|
|
5218
|
+
if (cliArgs["more"] !== undefined) {
|
|
5219
|
+
result.more = cliArgs["more"];
|
|
5220
|
+
}
|
|
3515
5221
|
if (cliArgs["compare"] !== undefined) {
|
|
3516
5222
|
result.compare = cliArgs["compare"];
|
|
3517
5223
|
}
|
|
3518
5224
|
if (cliArgs["provider"] !== undefined) {
|
|
3519
5225
|
result.provider = cliArgs["provider"];
|
|
3520
5226
|
}
|
|
5227
|
+
if (cliArgs["claude"] !== undefined) {
|
|
5228
|
+
result.claude = cliArgs["claude"];
|
|
5229
|
+
}
|
|
5230
|
+
if (cliArgs["codex"] !== undefined) {
|
|
5231
|
+
result.codex = cliArgs["codex"];
|
|
5232
|
+
}
|
|
5233
|
+
if (cliArgs["openCode"] !== undefined) {
|
|
5234
|
+
result.openCode = cliArgs["openCode"];
|
|
5235
|
+
}
|
|
5236
|
+
if (cliArgs["allProviders"] !== undefined) {
|
|
5237
|
+
result.allProviders = cliArgs["allProviders"];
|
|
5238
|
+
}
|
|
5239
|
+
if (cliArgs["listProviders"] !== undefined) {
|
|
5240
|
+
result.listProviders = cliArgs["listProviders"];
|
|
5241
|
+
}
|
|
3521
5242
|
if (cliArgs["clipboard"] !== undefined) {
|
|
3522
5243
|
result.clipboard = cliArgs["clipboard"];
|
|
3523
5244
|
}
|
|
@@ -3574,34 +5295,82 @@ async function runCompare(compareStr, currentRange, _registry, available) {
|
|
|
3574
5295
|
loadAndAggregate(currentRange, available),
|
|
3575
5296
|
loadAndAggregate(previousRange, available)
|
|
3576
5297
|
]);
|
|
3577
|
-
return
|
|
5298
|
+
return {
|
|
5299
|
+
compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
|
|
5300
|
+
currentData: currentResult.data,
|
|
5301
|
+
previousData: previousResult.data
|
|
5302
|
+
};
|
|
3578
5303
|
}
|
|
3579
5304
|
async function run(cliArgs) {
|
|
3580
5305
|
const config = resolveConfig(cliArgs);
|
|
5306
|
+
if (config.allProviders && (config.provider || config.claude || config.codex || config.openCode)) {
|
|
5307
|
+
throw new TokenleakError("--all-providers cannot be combined with provider filters");
|
|
5308
|
+
}
|
|
5309
|
+
const registry = new ProviderRegistry;
|
|
5310
|
+
registerBuiltInProviders(registry);
|
|
5311
|
+
if (config.listProviders) {
|
|
5312
|
+
const providers = registry.getAll();
|
|
5313
|
+
const availabilityResults = await Promise.all(providers.map(async (provider) => [provider.name, await provider.isAvailable()]));
|
|
5314
|
+
process.stdout.write(buildProviderList(providers, new Map(availabilityResults)));
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
3581
5317
|
const dateRange = computeDateRange({
|
|
3582
5318
|
since: config.since,
|
|
3583
5319
|
until: config.until,
|
|
3584
5320
|
days: config.days
|
|
3585
5321
|
});
|
|
3586
|
-
const registry = new ProviderRegistry;
|
|
3587
|
-
registry.register(new ClaudeCodeProvider);
|
|
3588
|
-
registry.register(new CodexProvider);
|
|
3589
|
-
registry.register(new OpenCodeProvider);
|
|
3590
5322
|
let available = await registry.getAvailable();
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
5323
|
+
const requestedProviders = getRequestedProviders(config);
|
|
5324
|
+
if (!config.allProviders && requestedProviders.size > 0) {
|
|
5325
|
+
if (config.provider && (config.claude || config.codex || config.openCode)) {
|
|
5326
|
+
process.stderr.write(`Combining provider filters: ${Array.from(requestedProviders).join(", ")}
|
|
5327
|
+
`);
|
|
5328
|
+
}
|
|
5329
|
+
available = available.filter((provider) => providerMatchesFilter(provider, requestedProviders));
|
|
3594
5330
|
}
|
|
3595
5331
|
if (available.length === 0) {
|
|
3596
5332
|
throw new TokenleakError("No provider data found");
|
|
3597
5333
|
}
|
|
3598
5334
|
if (config.compare) {
|
|
5335
|
+
const compareResult = await runCompare(config.compare, dateRange, registry, available);
|
|
5336
|
+
if (config.more && (config.format === "png" || config.format === "svg")) {
|
|
5337
|
+
const compareOutput = {
|
|
5338
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5339
|
+
generated: new Date().toISOString(),
|
|
5340
|
+
dateRange,
|
|
5341
|
+
providers: compareResult.currentData,
|
|
5342
|
+
aggregated: compareResult.compareOutput.periodA.stats,
|
|
5343
|
+
more: buildMoreStats(compareResult.currentData, dateRange, {
|
|
5344
|
+
previousRange: compareResult.compareOutput.periodB.range,
|
|
5345
|
+
previousProviders: compareResult.previousData
|
|
5346
|
+
})
|
|
5347
|
+
};
|
|
5348
|
+
const renderer2 = getRenderer(config.format);
|
|
5349
|
+
const renderOptions2 = {
|
|
5350
|
+
format: config.format,
|
|
5351
|
+
theme: config.theme,
|
|
5352
|
+
width: config.width,
|
|
5353
|
+
showInsights: !config.noInsights,
|
|
5354
|
+
noColor: config.noColor,
|
|
5355
|
+
output: config.output,
|
|
5356
|
+
more: true
|
|
5357
|
+
};
|
|
5358
|
+
const rendered3 = await renderer2.render(compareOutput, renderOptions2);
|
|
5359
|
+
if (config.output) {
|
|
5360
|
+
const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
|
|
5361
|
+
writeFileSync(config.output, data);
|
|
5362
|
+
} else {
|
|
5363
|
+
const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
|
|
5364
|
+
process.stdout.write(text2 + `
|
|
5365
|
+
`);
|
|
5366
|
+
}
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
3599
5369
|
if (config.format !== "json" && config.format !== "terminal") {
|
|
3600
5370
|
process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
|
|
3601
5371
|
`);
|
|
3602
5372
|
}
|
|
3603
|
-
const
|
|
3604
|
-
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
5373
|
+
const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
|
|
3605
5374
|
if (config.output) {
|
|
3606
5375
|
writeFileSync(config.output, rendered2);
|
|
3607
5376
|
} else {
|
|
@@ -3628,7 +5397,8 @@ async function run(cliArgs) {
|
|
|
3628
5397
|
generated: new Date().toISOString(),
|
|
3629
5398
|
dateRange,
|
|
3630
5399
|
providers: providerDataList,
|
|
3631
|
-
aggregated: stats
|
|
5400
|
+
aggregated: stats,
|
|
5401
|
+
more: config.more ? buildMoreStats(providerDataList, dateRange) : null
|
|
3632
5402
|
};
|
|
3633
5403
|
if (config.liveServer) {
|
|
3634
5404
|
const ignoredFlags = [];
|
|
@@ -3650,7 +5420,8 @@ async function run(cliArgs) {
|
|
|
3650
5420
|
width: config.width,
|
|
3651
5421
|
showInsights: !config.noInsights,
|
|
3652
5422
|
noColor: config.noColor,
|
|
3653
|
-
output: config.output
|
|
5423
|
+
output: config.output,
|
|
5424
|
+
more: config.more
|
|
3654
5425
|
};
|
|
3655
5426
|
const { port } = await startLiveServer(output, renderOptions2);
|
|
3656
5427
|
await new Promise((resolve) => {
|
|
@@ -3673,7 +5444,8 @@ Shutting down server...
|
|
|
3673
5444
|
width: config.width,
|
|
3674
5445
|
showInsights: !config.noInsights,
|
|
3675
5446
|
noColor: config.noColor,
|
|
3676
|
-
output: config.output
|
|
5447
|
+
output: config.output,
|
|
5448
|
+
more: config.more
|
|
3677
5449
|
};
|
|
3678
5450
|
const rendered = await renderer.render(output, renderOptions);
|
|
3679
5451
|
if (config.output) {
|
|
@@ -3762,6 +5534,11 @@ var main = defineCommand({
|
|
|
3762
5534
|
description: "Hide insights panel",
|
|
3763
5535
|
default: false
|
|
3764
5536
|
},
|
|
5537
|
+
more: {
|
|
5538
|
+
type: "boolean",
|
|
5539
|
+
description: "Add expanded PNG/SVG stats and compare cards",
|
|
5540
|
+
default: false
|
|
5541
|
+
},
|
|
3765
5542
|
compare: {
|
|
3766
5543
|
type: "string",
|
|
3767
5544
|
description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
|
|
@@ -3771,6 +5548,31 @@ var main = defineCommand({
|
|
|
3771
5548
|
alias: "p",
|
|
3772
5549
|
description: "Filter to specific provider(s), comma-separated"
|
|
3773
5550
|
},
|
|
5551
|
+
claude: {
|
|
5552
|
+
type: "boolean",
|
|
5553
|
+
description: "Shortcut for --provider claude-code",
|
|
5554
|
+
default: false
|
|
5555
|
+
},
|
|
5556
|
+
codex: {
|
|
5557
|
+
type: "boolean",
|
|
5558
|
+
description: "Shortcut for --provider codex",
|
|
5559
|
+
default: false
|
|
5560
|
+
},
|
|
5561
|
+
openCode: {
|
|
5562
|
+
type: "boolean",
|
|
5563
|
+
description: "Shortcut for --provider open-code",
|
|
5564
|
+
default: false
|
|
5565
|
+
},
|
|
5566
|
+
allProviders: {
|
|
5567
|
+
type: "boolean",
|
|
5568
|
+
description: "Ignore provider filters and use every available provider",
|
|
5569
|
+
default: false
|
|
5570
|
+
},
|
|
5571
|
+
listProviders: {
|
|
5572
|
+
type: "boolean",
|
|
5573
|
+
description: "List registered providers and aliases",
|
|
5574
|
+
default: false
|
|
5575
|
+
},
|
|
3774
5576
|
clipboard: {
|
|
3775
5577
|
type: "boolean",
|
|
3776
5578
|
description: "Copy output to clipboard after rendering",
|
|
@@ -3813,10 +5615,22 @@ var main = defineCommand({
|
|
|
3813
5615
|
cliArgs["noColor"] = true;
|
|
3814
5616
|
if (args.noInsights)
|
|
3815
5617
|
cliArgs["noInsights"] = true;
|
|
5618
|
+
if (args.more)
|
|
5619
|
+
cliArgs["more"] = true;
|
|
3816
5620
|
if (args.compare !== undefined)
|
|
3817
5621
|
cliArgs["compare"] = args.compare;
|
|
3818
5622
|
if (args.provider !== undefined)
|
|
3819
5623
|
cliArgs["provider"] = args.provider;
|
|
5624
|
+
if (args.claude)
|
|
5625
|
+
cliArgs["claude"] = true;
|
|
5626
|
+
if (args.codex)
|
|
5627
|
+
cliArgs["codex"] = true;
|
|
5628
|
+
if (args.openCode)
|
|
5629
|
+
cliArgs["openCode"] = true;
|
|
5630
|
+
if (args.allProviders)
|
|
5631
|
+
cliArgs["allProviders"] = true;
|
|
5632
|
+
if (args.listProviders)
|
|
5633
|
+
cliArgs["listProviders"] = true;
|
|
3820
5634
|
if (args.clipboard)
|
|
3821
5635
|
cliArgs["clipboard"] = true;
|
|
3822
5636
|
if (args.open)
|
|
@@ -3833,11 +5647,31 @@ var main = defineCommand({
|
|
|
3833
5647
|
});
|
|
3834
5648
|
var isDirectExecution = typeof Bun !== "undefined" ? Bun.main === import.meta.path : process.argv[1] !== undefined && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
|
|
3835
5649
|
if (isDirectExecution) {
|
|
3836
|
-
|
|
5650
|
+
const normalizedArgv = normalizeCliArgv(process.argv.slice(2));
|
|
5651
|
+
process.argv = [...process.argv.slice(0, 2), ...normalizedArgv];
|
|
5652
|
+
const argv = normalizedArgv;
|
|
5653
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
5654
|
+
process.stdout.write(buildHelpText());
|
|
5655
|
+
process.exit(0);
|
|
5656
|
+
}
|
|
5657
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
5658
|
+
process.stdout.write(buildVersionText());
|
|
5659
|
+
process.exit(0);
|
|
5660
|
+
}
|
|
5661
|
+
if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
5662
|
+
await startInteractiveCli({
|
|
5663
|
+
version: VERSION,
|
|
5664
|
+
helpText: buildHelpText()
|
|
5665
|
+
}, executeInteractiveCommand);
|
|
5666
|
+
} else {
|
|
5667
|
+
await runMain(main);
|
|
5668
|
+
}
|
|
3837
5669
|
}
|
|
3838
5670
|
export {
|
|
3839
5671
|
run,
|
|
3840
5672
|
resolveConfig,
|
|
5673
|
+
normalizeCliArgv,
|
|
3841
5674
|
inferFormatFromPath,
|
|
3842
|
-
computeDateRange
|
|
5675
|
+
computeDateRange,
|
|
5676
|
+
buildInteractiveSummary
|
|
3843
5677
|
};
|