tokenleak 1.0.2 → 1.1.1
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 +3560 -464
package/tokenleak
CHANGED
|
@@ -755,6 +755,235 @@ 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
|
+
const projectId = event.projectId?.trim() || undefined;
|
|
859
|
+
let session = sessions.get(key);
|
|
860
|
+
if (!session) {
|
|
861
|
+
session = {
|
|
862
|
+
label: projectId || event.sessionId?.trim() || key,
|
|
863
|
+
tokens: 0,
|
|
864
|
+
cost: 0,
|
|
865
|
+
count: 0,
|
|
866
|
+
projectId,
|
|
867
|
+
firstTimestamp: safeTime,
|
|
868
|
+
lastTimestamp: safeTime,
|
|
869
|
+
explicitDurationMs: 0,
|
|
870
|
+
hasExplicitDuration: false
|
|
871
|
+
};
|
|
872
|
+
sessions.set(key, session);
|
|
873
|
+
} else if (!session.projectId && projectId) {
|
|
874
|
+
session.projectId = projectId;
|
|
875
|
+
session.label = projectId || event.sessionId?.trim() || key;
|
|
876
|
+
}
|
|
877
|
+
session.tokens += event.totalTokens;
|
|
878
|
+
session.cost += event.cost;
|
|
879
|
+
session.count += 1;
|
|
880
|
+
session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
|
|
881
|
+
session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
|
|
882
|
+
if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
|
|
883
|
+
session.explicitDurationMs += Math.max(0, event.durationMs);
|
|
884
|
+
session.hasExplicitDuration = true;
|
|
885
|
+
}
|
|
886
|
+
if (projectId) {
|
|
887
|
+
projects.set(projectId, (projects.get(projectId) ?? 0) + event.totalTokens);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const sessionEntries = [...sessions.values()];
|
|
891
|
+
const totalSessions = sessionEntries.length;
|
|
892
|
+
let totalTokens = 0;
|
|
893
|
+
let totalCost = 0;
|
|
894
|
+
let totalMessages = 0;
|
|
895
|
+
let durationTotal = 0;
|
|
896
|
+
let durationCount = 0;
|
|
897
|
+
let longestSession = null;
|
|
898
|
+
let longestSessionDuration = -1;
|
|
899
|
+
for (const session of sessionEntries) {
|
|
900
|
+
totalTokens += session.tokens;
|
|
901
|
+
totalCost += session.cost;
|
|
902
|
+
totalMessages += session.count;
|
|
903
|
+
const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
|
|
904
|
+
if (derivedDurationMs > 0) {
|
|
905
|
+
durationTotal += derivedDurationMs;
|
|
906
|
+
durationCount += 1;
|
|
907
|
+
}
|
|
908
|
+
if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
|
|
909
|
+
longestSessionDuration = derivedDurationMs;
|
|
910
|
+
longestSession = {
|
|
911
|
+
label: session.label,
|
|
912
|
+
tokens: session.tokens,
|
|
913
|
+
cost: session.cost,
|
|
914
|
+
count: session.count,
|
|
915
|
+
durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const projectBreakdown = [...projects.entries()].map(([name, tokens]) => ({ name, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
|
|
920
|
+
const topProject = projectBreakdown[0] ?? null;
|
|
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
|
+
projectBreakdown
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
|
|
934
|
+
const currentModelTokens = new Map;
|
|
935
|
+
const previousModelTokens = new Map;
|
|
936
|
+
let currentTotal = 0;
|
|
937
|
+
let previousTotal = 0;
|
|
938
|
+
for (const provider of currentProviders) {
|
|
939
|
+
for (const day of provider.daily) {
|
|
940
|
+
for (const model of day.models) {
|
|
941
|
+
currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
942
|
+
currentTotal += model.totalTokens;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const provider of previousProviders) {
|
|
947
|
+
for (const day of provider.daily) {
|
|
948
|
+
for (const model of day.models) {
|
|
949
|
+
previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
950
|
+
previousTotal += model.totalTokens;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const models = new Set([
|
|
955
|
+
...currentModelTokens.keys(),
|
|
956
|
+
...previousModelTokens.keys()
|
|
957
|
+
]);
|
|
958
|
+
return [...models].map((model) => {
|
|
959
|
+
const currentTokens = currentModelTokens.get(model) ?? 0;
|
|
960
|
+
const previousTokens = previousModelTokens.get(model) ?? 0;
|
|
961
|
+
const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
|
|
962
|
+
const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
|
|
963
|
+
return {
|
|
964
|
+
model,
|
|
965
|
+
currentShare,
|
|
966
|
+
previousShare,
|
|
967
|
+
deltaShare: currentShare - previousShare,
|
|
968
|
+
currentTokens,
|
|
969
|
+
previousTokens
|
|
970
|
+
};
|
|
971
|
+
}).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
|
|
972
|
+
}
|
|
973
|
+
function buildMoreStats(providers, range, compare = null) {
|
|
974
|
+
const events = collectEvents(providers);
|
|
975
|
+
return {
|
|
976
|
+
inputOutput: buildInputOutput(providers),
|
|
977
|
+
monthlyBurn: buildMonthlyBurn(providers, range),
|
|
978
|
+
cacheEconomics: buildCacheEconomics(providers),
|
|
979
|
+
hourOfDay: buildHourOfDay(events),
|
|
980
|
+
sessionMetrics: buildSessionMetrics(events),
|
|
981
|
+
compare: compare ? {
|
|
982
|
+
previousRange: compare.previousRange,
|
|
983
|
+
modelMixShift: computeModelMixShift(providers, compare.previousProviders)
|
|
984
|
+
} : null
|
|
985
|
+
};
|
|
986
|
+
}
|
|
758
987
|
// packages/core/dist/index.js
|
|
759
988
|
var VERSION = "1.0.2";
|
|
760
989
|
|
|
@@ -1022,7 +1251,7 @@ async function* splitJsonlRecords(filePath) {
|
|
|
1022
1251
|
}
|
|
1023
1252
|
// packages/registry/dist/providers/claude-code.js
|
|
1024
1253
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1025
|
-
import { join } from "path";
|
|
1254
|
+
import { dirname, join, relative, sep } from "path";
|
|
1026
1255
|
import { homedir } from "os";
|
|
1027
1256
|
|
|
1028
1257
|
// packages/registry/dist/utils.js
|
|
@@ -1101,6 +1330,7 @@ function extractUsage(record) {
|
|
|
1101
1330
|
}
|
|
1102
1331
|
return {
|
|
1103
1332
|
date,
|
|
1333
|
+
timestamp,
|
|
1104
1334
|
model,
|
|
1105
1335
|
inputTokens,
|
|
1106
1336
|
outputTokens,
|
|
@@ -1181,13 +1411,18 @@ class ClaudeCodeProvider {
|
|
|
1181
1411
|
async load(range) {
|
|
1182
1412
|
const files = collectJsonlFiles(this.baseDir);
|
|
1183
1413
|
const allRecords = [];
|
|
1414
|
+
const allEvents = [];
|
|
1184
1415
|
for (const file of files) {
|
|
1185
1416
|
const latestRecordsByMessageId = new Map;
|
|
1186
1417
|
const anonymousRecords = [];
|
|
1418
|
+
const relativeFile = relative(this.baseDir, file).split(sep).join("/");
|
|
1419
|
+
const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
|
|
1187
1420
|
try {
|
|
1188
1421
|
for await (const record of splitJsonlRecords(file)) {
|
|
1189
1422
|
const usage = extractUsage(record);
|
|
1190
1423
|
if (usage !== null && isInRange(usage.date, range)) {
|
|
1424
|
+
usage.sessionId = relativeFile;
|
|
1425
|
+
usage.projectId = projectId;
|
|
1191
1426
|
if (usage.messageId) {
|
|
1192
1427
|
latestRecordsByMessageId.set(usage.messageId, usage);
|
|
1193
1428
|
} else {
|
|
@@ -1201,6 +1436,24 @@ class ClaudeCodeProvider {
|
|
|
1201
1436
|
allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
|
|
1202
1437
|
}
|
|
1203
1438
|
const daily = buildDailyUsage(allRecords);
|
|
1439
|
+
for (const record of allRecords) {
|
|
1440
|
+
const normalizedModel = normalizeModelName(record.model);
|
|
1441
|
+
const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1442
|
+
allEvents.push({
|
|
1443
|
+
provider: this.name,
|
|
1444
|
+
timestamp: record.timestamp,
|
|
1445
|
+
date: record.date,
|
|
1446
|
+
model: normalizedModel,
|
|
1447
|
+
inputTokens: record.inputTokens,
|
|
1448
|
+
outputTokens: record.outputTokens,
|
|
1449
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1450
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1451
|
+
totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
|
|
1452
|
+
cost,
|
|
1453
|
+
sessionId: record.sessionId,
|
|
1454
|
+
projectId: record.projectId
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1204
1457
|
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
1205
1458
|
const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
|
|
1206
1459
|
return {
|
|
@@ -1209,13 +1462,14 @@ class ClaudeCodeProvider {
|
|
|
1209
1462
|
daily,
|
|
1210
1463
|
totalTokens,
|
|
1211
1464
|
totalCost,
|
|
1212
|
-
colors: this.colors
|
|
1465
|
+
colors: this.colors,
|
|
1466
|
+
events: allEvents
|
|
1213
1467
|
};
|
|
1214
1468
|
}
|
|
1215
1469
|
}
|
|
1216
1470
|
// packages/registry/dist/providers/codex.js
|
|
1217
1471
|
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1218
|
-
import { join as join2 } from "path";
|
|
1472
|
+
import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
1219
1473
|
import { homedir as homedir2 } from "os";
|
|
1220
1474
|
var CODEX_COLORS = {
|
|
1221
1475
|
primary: "#10a37f",
|
|
@@ -1373,6 +1627,7 @@ function parseTokenCountUsage(record, context) {
|
|
|
1373
1627
|
const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
|
|
1374
1628
|
return {
|
|
1375
1629
|
date,
|
|
1630
|
+
timestamp,
|
|
1376
1631
|
model: context.model,
|
|
1377
1632
|
inputTokens,
|
|
1378
1633
|
outputTokens: usage.outputTokens,
|
|
@@ -1403,6 +1658,7 @@ function parseUsageRecord(record, context) {
|
|
|
1403
1658
|
}
|
|
1404
1659
|
return {
|
|
1405
1660
|
date,
|
|
1661
|
+
timestamp: legacyEvent.timestamp,
|
|
1406
1662
|
model: compactModelDateSuffix(legacyEvent.model),
|
|
1407
1663
|
inputTokens: legacyEvent.usage.input_tokens,
|
|
1408
1664
|
outputTokens: legacyEvent.usage.output_tokens,
|
|
@@ -1429,11 +1685,14 @@ class CodexProvider {
|
|
|
1429
1685
|
async load(range) {
|
|
1430
1686
|
const dailyMap = new Map;
|
|
1431
1687
|
const files = collectJsonlFiles2(this.sessionsDir);
|
|
1688
|
+
const events = [];
|
|
1432
1689
|
for (const file of files) {
|
|
1433
1690
|
const context = {
|
|
1434
1691
|
model: "gpt-5",
|
|
1435
1692
|
previousTotals: null
|
|
1436
1693
|
};
|
|
1694
|
+
const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
|
|
1695
|
+
const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
|
|
1437
1696
|
try {
|
|
1438
1697
|
for await (const record of splitJsonlRecords(file)) {
|
|
1439
1698
|
const usage = parseUsageRecord(record, context);
|
|
@@ -1443,12 +1702,28 @@ class CodexProvider {
|
|
|
1443
1702
|
if (!isInRange(usage.date, range)) {
|
|
1444
1703
|
continue;
|
|
1445
1704
|
}
|
|
1705
|
+
usage.sessionId = relativeFile;
|
|
1706
|
+
usage.projectId = projectDir === "." ? undefined : projectDir;
|
|
1446
1707
|
const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
|
|
1447
1708
|
const inputTokens = usage.inputTokens;
|
|
1448
1709
|
const outputTokens = usage.outputTokens;
|
|
1449
1710
|
const cacheReadTokens = usage.cacheReadTokens;
|
|
1450
1711
|
const cacheWriteTokens = usage.cacheWriteTokens;
|
|
1451
1712
|
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1713
|
+
events.push({
|
|
1714
|
+
provider: this.name,
|
|
1715
|
+
timestamp: usage.timestamp,
|
|
1716
|
+
date: usage.date,
|
|
1717
|
+
model: normalizedModel,
|
|
1718
|
+
inputTokens,
|
|
1719
|
+
outputTokens,
|
|
1720
|
+
cacheReadTokens,
|
|
1721
|
+
cacheWriteTokens,
|
|
1722
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
1723
|
+
cost,
|
|
1724
|
+
sessionId: usage.sessionId,
|
|
1725
|
+
projectId: usage.projectId
|
|
1726
|
+
});
|
|
1452
1727
|
if (!dailyMap.has(usage.date)) {
|
|
1453
1728
|
dailyMap.set(usage.date, new Map);
|
|
1454
1729
|
}
|
|
@@ -1503,7 +1778,8 @@ class CodexProvider {
|
|
|
1503
1778
|
daily,
|
|
1504
1779
|
totalTokens,
|
|
1505
1780
|
totalCost,
|
|
1506
|
-
colors: this.colors
|
|
1781
|
+
colors: this.colors,
|
|
1782
|
+
events
|
|
1507
1783
|
};
|
|
1508
1784
|
}
|
|
1509
1785
|
}
|
|
@@ -1513,7 +1789,7 @@ import { join as join3 } from "path";
|
|
|
1513
1789
|
import { homedir as homedir3 } from "os";
|
|
1514
1790
|
import { Database } from "bun:sqlite";
|
|
1515
1791
|
var PROVIDER_NAME = "open-code";
|
|
1516
|
-
var DISPLAY_NAME = "
|
|
1792
|
+
var DISPLAY_NAME = "OpenCode";
|
|
1517
1793
|
var COLORS = {
|
|
1518
1794
|
primary: "#6366f1",
|
|
1519
1795
|
secondary: "#a78bfa",
|
|
@@ -1549,12 +1825,46 @@ function extractDate2(createdAt) {
|
|
|
1549
1825
|
}
|
|
1550
1826
|
return date.toISOString().slice(0, 10);
|
|
1551
1827
|
}
|
|
1828
|
+
function toTimestampMillis(createdAt) {
|
|
1829
|
+
const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
|
|
1830
|
+
if (!Number.isFinite(timestamp)) {
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
|
|
1834
|
+
return Number.isFinite(millis) ? millis : null;
|
|
1835
|
+
}
|
|
1836
|
+
function toIsoTimestamp(createdAt) {
|
|
1837
|
+
const millis = toTimestampMillis(createdAt);
|
|
1838
|
+
if (millis === null) {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
const date = new Date(millis);
|
|
1842
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1843
|
+
}
|
|
1552
1844
|
function getRecordCost(record) {
|
|
1553
1845
|
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
1554
1846
|
return record.explicitCost;
|
|
1555
1847
|
}
|
|
1556
1848
|
return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1557
1849
|
}
|
|
1850
|
+
function toUsageEvent(record) {
|
|
1851
|
+
const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
1852
|
+
return {
|
|
1853
|
+
provider: PROVIDER_NAME,
|
|
1854
|
+
timestamp: record.timestamp,
|
|
1855
|
+
date: record.date,
|
|
1856
|
+
model: normalizeModelName(record.model),
|
|
1857
|
+
inputTokens: record.inputTokens,
|
|
1858
|
+
outputTokens: record.outputTokens,
|
|
1859
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1860
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1861
|
+
totalTokens,
|
|
1862
|
+
cost: getRecordCost(record),
|
|
1863
|
+
sessionId: record.sessionId,
|
|
1864
|
+
projectId: record.projectId,
|
|
1865
|
+
durationMs: record.durationMs
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1558
1868
|
function buildProviderData(records) {
|
|
1559
1869
|
const byDate = new Map;
|
|
1560
1870
|
for (const record of records) {
|
|
@@ -1614,7 +1924,8 @@ function buildProviderData(records) {
|
|
|
1614
1924
|
daily,
|
|
1615
1925
|
totalTokens,
|
|
1616
1926
|
totalCost,
|
|
1617
|
-
colors: COLORS
|
|
1927
|
+
colors: COLORS,
|
|
1928
|
+
events: records.map(toUsageEvent)
|
|
1618
1929
|
};
|
|
1619
1930
|
}
|
|
1620
1931
|
function loadFromSqlite(dbPath, range) {
|
|
@@ -1629,18 +1940,21 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1629
1940
|
if (tables.length === 0) {
|
|
1630
1941
|
return [];
|
|
1631
1942
|
}
|
|
1632
|
-
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1943
|
+
const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1633
1944
|
const records = [];
|
|
1634
1945
|
for (const row of rows) {
|
|
1635
1946
|
const date = extractDate2(row.created_at);
|
|
1636
|
-
|
|
1947
|
+
const timestamp = toIsoTimestamp(row.created_at);
|
|
1948
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1637
1949
|
records.push({
|
|
1638
1950
|
date,
|
|
1951
|
+
timestamp,
|
|
1639
1952
|
model: row.model,
|
|
1640
1953
|
inputTokens: row.input_tokens,
|
|
1641
1954
|
outputTokens: row.output_tokens,
|
|
1642
1955
|
cacheReadTokens: 0,
|
|
1643
|
-
cacheWriteTokens: 0
|
|
1956
|
+
cacheWriteTokens: 0,
|
|
1957
|
+
sessionId: row.session_id
|
|
1644
1958
|
});
|
|
1645
1959
|
}
|
|
1646
1960
|
}
|
|
@@ -1666,14 +1980,17 @@ function loadFromLegacyJson(sessionsDir, range) {
|
|
|
1666
1980
|
continue;
|
|
1667
1981
|
}
|
|
1668
1982
|
const date = extractDate2(msg.created_at);
|
|
1669
|
-
|
|
1983
|
+
const timestamp = toIsoTimestamp(msg.created_at);
|
|
1984
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1670
1985
|
records.push({
|
|
1671
1986
|
date,
|
|
1987
|
+
timestamp,
|
|
1672
1988
|
model: msg.model,
|
|
1673
1989
|
inputTokens: msg.usage.input_tokens,
|
|
1674
1990
|
outputTokens: msg.usage.output_tokens,
|
|
1675
1991
|
cacheReadTokens: 0,
|
|
1676
|
-
cacheWriteTokens: 0
|
|
1992
|
+
cacheWriteTokens: 0,
|
|
1993
|
+
sessionId: file
|
|
1677
1994
|
});
|
|
1678
1995
|
}
|
|
1679
1996
|
}
|
|
@@ -1711,7 +2028,8 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1711
2028
|
continue;
|
|
1712
2029
|
}
|
|
1713
2030
|
const date = extractDate2(createdAt);
|
|
1714
|
-
|
|
2031
|
+
const timestamp = toIsoTimestamp(createdAt);
|
|
2032
|
+
if (!date || !timestamp || !isInRange(date, range)) {
|
|
1715
2033
|
continue;
|
|
1716
2034
|
}
|
|
1717
2035
|
const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
|
|
@@ -1720,13 +2038,21 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1720
2038
|
const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
|
|
1721
2039
|
const record = {
|
|
1722
2040
|
date,
|
|
2041
|
+
timestamp,
|
|
1723
2042
|
model,
|
|
1724
2043
|
inputTokens,
|
|
1725
2044
|
outputTokens,
|
|
1726
2045
|
cacheReadTokens,
|
|
1727
2046
|
cacheWriteTokens,
|
|
1728
|
-
explicitCost: typeof message.cost === "number" ? message.cost : undefined
|
|
2047
|
+
explicitCost: typeof message.cost === "number" ? message.cost : undefined,
|
|
2048
|
+
sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
|
|
1729
2049
|
};
|
|
2050
|
+
const completedAt = message.time?.completed;
|
|
2051
|
+
const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
|
|
2052
|
+
const createdMs = Date.parse(record.timestamp);
|
|
2053
|
+
if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
|
|
2054
|
+
record.durationMs = completedMs - createdMs;
|
|
2055
|
+
}
|
|
1730
2056
|
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1731
2057
|
if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
|
|
1732
2058
|
continue;
|
|
@@ -1831,7 +2157,7 @@ var DOT_GAP = 8;
|
|
|
1831
2157
|
var CELL_SIZE = 16;
|
|
1832
2158
|
var CELL_GAP = 4;
|
|
1833
2159
|
var STAT_GRID_COLS = 3;
|
|
1834
|
-
var MODEL_BAR_HEIGHT =
|
|
2160
|
+
var MODEL_BAR_HEIGHT = 11;
|
|
1835
2161
|
var DAY_LABEL_WIDTH = 44;
|
|
1836
2162
|
var MONTH_LABEL_HEIGHT = 24;
|
|
1837
2163
|
var PROVIDER_SECTION_GAP = 36;
|
|
@@ -1841,24 +2167,27 @@ var MODEL_BAR_GAP = 36;
|
|
|
1841
2167
|
var MODEL_PERCENT_WIDTH = 40;
|
|
1842
2168
|
|
|
1843
2169
|
// packages/renderers/dist/png/terminal-card.js
|
|
1844
|
-
var FONT_FAMILY = "'
|
|
2170
|
+
var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
2171
|
+
var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
|
|
1845
2172
|
function getCardTheme(mode) {
|
|
1846
2173
|
if (mode === "dark") {
|
|
1847
2174
|
return {
|
|
1848
2175
|
bg: "#09090b",
|
|
1849
|
-
fg: "#
|
|
1850
|
-
muted: "#
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2176
|
+
fg: "#f0f0f0",
|
|
2177
|
+
muted: "#6b7280",
|
|
2178
|
+
labelFg: "#b0b8c4",
|
|
2179
|
+
border: "rgba(255,255,255,0.08)",
|
|
2180
|
+
accent: "#34d399",
|
|
2181
|
+
heatmapEmpty: "#1a1a22",
|
|
2182
|
+
barTrack: "#151520",
|
|
2183
|
+
titlebarBorder: "rgba(255,255,255,0.08)"
|
|
1856
2184
|
};
|
|
1857
2185
|
}
|
|
1858
2186
|
return {
|
|
1859
2187
|
bg: "#fafafa",
|
|
1860
2188
|
fg: "#18181b",
|
|
1861
2189
|
muted: "#a1a1aa",
|
|
2190
|
+
labelFg: "#71717a",
|
|
1862
2191
|
border: "rgba(0,0,0,0.08)",
|
|
1863
2192
|
accent: "#059669",
|
|
1864
2193
|
heatmapEmpty: "#e4e4e7",
|
|
@@ -1870,7 +2199,7 @@ function buildHeatmapScale(colors, isDark) {
|
|
|
1870
2199
|
const [startHex, endHex] = colors.gradient;
|
|
1871
2200
|
const s = hexToRgb(startHex);
|
|
1872
2201
|
const e = hexToRgb(endHex);
|
|
1873
|
-
const opacities = isDark ? [0.
|
|
2202
|
+
const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
|
|
1874
2203
|
return [
|
|
1875
2204
|
"transparent",
|
|
1876
2205
|
...opacities.map((t) => {
|
|
@@ -1957,6 +2286,41 @@ function formatPercentage(rate) {
|
|
|
1957
2286
|
function formatStreak(n) {
|
|
1958
2287
|
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
1959
2288
|
}
|
|
2289
|
+
function formatRatio(value, suffix = "x") {
|
|
2290
|
+
if (value === null || !Number.isFinite(value)) {
|
|
2291
|
+
return "n/a";
|
|
2292
|
+
}
|
|
2293
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
|
|
2294
|
+
}
|
|
2295
|
+
function formatPercentPoints(value) {
|
|
2296
|
+
const prefix = value >= 0 ? "+" : "";
|
|
2297
|
+
return `${prefix}${(value * 100).toFixed(1)}pp`;
|
|
2298
|
+
}
|
|
2299
|
+
function formatHour(hour) {
|
|
2300
|
+
return `${hour.toString().padStart(2, "0")}:00`;
|
|
2301
|
+
}
|
|
2302
|
+
function formatDuration(durationMs) {
|
|
2303
|
+
if (durationMs === null || durationMs === undefined || durationMs <= 0) {
|
|
2304
|
+
return "n/a";
|
|
2305
|
+
}
|
|
2306
|
+
const totalMinutes = Math.round(durationMs / 60000);
|
|
2307
|
+
if (totalMinutes < 60) {
|
|
2308
|
+
return `${totalMinutes}m`;
|
|
2309
|
+
}
|
|
2310
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
2311
|
+
const minutes = totalMinutes % 60;
|
|
2312
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
2313
|
+
}
|
|
2314
|
+
function truncateText(value, maxLength) {
|
|
2315
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
2316
|
+
}
|
|
2317
|
+
function formatSessionSummary(summary) {
|
|
2318
|
+
const duration = formatDuration(summary.durationMs);
|
|
2319
|
+
if (duration === "n/a") {
|
|
2320
|
+
return truncateText(summary.label, 20);
|
|
2321
|
+
}
|
|
2322
|
+
return truncateText(`${summary.label} \xB7 ${duration}`, 24);
|
|
2323
|
+
}
|
|
1960
2324
|
function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
1961
2325
|
const tokenMap = new Map;
|
|
1962
2326
|
for (const d of daily) {
|
|
@@ -2013,6 +2377,149 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
|
2013
2377
|
`);
|
|
2014
2378
|
return { svg, gridWidth, height };
|
|
2015
2379
|
}
|
|
2380
|
+
function renderSectionHeader(x, y, title, theme, cardAccent) {
|
|
2381
|
+
const parts = [];
|
|
2382
|
+
parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
|
|
2383
|
+
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>`);
|
|
2384
|
+
return parts.join(`
|
|
2385
|
+
`);
|
|
2386
|
+
}
|
|
2387
|
+
function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
|
|
2388
|
+
const parts = [];
|
|
2389
|
+
const height = 38 + lines.length * 22;
|
|
2390
|
+
parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2391
|
+
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>`);
|
|
2392
|
+
lines.forEach((line, index) => {
|
|
2393
|
+
const lineY = y + 48 + index * 22;
|
|
2394
|
+
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>`);
|
|
2395
|
+
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>`);
|
|
2396
|
+
});
|
|
2397
|
+
return parts.join(`
|
|
2398
|
+
`);
|
|
2399
|
+
}
|
|
2400
|
+
function buildProviderHourBuckets(providers) {
|
|
2401
|
+
return providers.map((p) => {
|
|
2402
|
+
const hours = new Array(24).fill(0);
|
|
2403
|
+
for (const event of p.events ?? []) {
|
|
2404
|
+
const date = new Date(event.timestamp);
|
|
2405
|
+
if (!Number.isNaN(date.getTime())) {
|
|
2406
|
+
const h = date.getUTCHours();
|
|
2407
|
+
hours[h] += event.totalTokens;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
return { provider: p.provider, color: p.colors.primary, hours };
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
|
|
2414
|
+
const chartHeight = 140;
|
|
2415
|
+
const innerHeight = 72;
|
|
2416
|
+
const baselineY = y + 92;
|
|
2417
|
+
const barAreaX = x + 18;
|
|
2418
|
+
const barAreaWidth = width - 36;
|
|
2419
|
+
const barGap = 4;
|
|
2420
|
+
const barWidth = (barAreaWidth - barGap * 23) / 24;
|
|
2421
|
+
const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
|
|
2422
|
+
const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
|
|
2423
|
+
const isMulti = providers.length > 1;
|
|
2424
|
+
const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
|
|
2425
|
+
let legendSvg = "";
|
|
2426
|
+
if (isMulti && providerBuckets.length > 0) {
|
|
2427
|
+
const titleWidth = 105;
|
|
2428
|
+
let legendX = x + 18 + titleWidth + 12;
|
|
2429
|
+
const legendY = y + 22;
|
|
2430
|
+
for (const bucket of providerBuckets) {
|
|
2431
|
+
const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
|
|
2432
|
+
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>`;
|
|
2433
|
+
legendX += 12 + displayName.length * 5.5 + 16;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
const bars = [
|
|
2437
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2438
|
+
`<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>`,
|
|
2439
|
+
legendSvg,
|
|
2440
|
+
`<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>`,
|
|
2441
|
+
`<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
|
|
2442
|
+
];
|
|
2443
|
+
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>`);
|
|
2444
|
+
if (isMulti) {
|
|
2445
|
+
const provBaseOpacity = isDark ? "0.45" : "0.3";
|
|
2446
|
+
const provMidOpacity = isDark ? "0.85" : "0.75";
|
|
2447
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2448
|
+
const gradId = `hod-prov-${bi}`;
|
|
2449
|
+
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>`);
|
|
2450
|
+
}
|
|
2451
|
+
hourOfDay.forEach((entry, index) => {
|
|
2452
|
+
if (entry.tokens <= 0)
|
|
2453
|
+
return;
|
|
2454
|
+
const totalRatio = entry.tokens / maxTokens;
|
|
2455
|
+
const totalBarHeight = Math.max(4, totalRatio * innerHeight);
|
|
2456
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2457
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour;
|
|
2458
|
+
if (isPeak) {
|
|
2459
|
+
const topY2 = baselineY - totalBarHeight;
|
|
2460
|
+
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)"/>`);
|
|
2461
|
+
}
|
|
2462
|
+
const clipId = `hod-clip-${index}`;
|
|
2463
|
+
const topY = baselineY - totalBarHeight;
|
|
2464
|
+
bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
|
|
2465
|
+
let offsetY = 0;
|
|
2466
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2467
|
+
const tokens = providerBuckets[bi].hours[index] ?? 0;
|
|
2468
|
+
if (tokens <= 0)
|
|
2469
|
+
continue;
|
|
2470
|
+
const segHeight = tokens / entry.tokens * totalBarHeight;
|
|
2471
|
+
const segY = baselineY - offsetY - segHeight;
|
|
2472
|
+
bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
|
|
2473
|
+
offsetY += segHeight;
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
} else {
|
|
2477
|
+
const hodGradId = "hod-bar-grad";
|
|
2478
|
+
const hodBaseOpacity = isDark ? "0.25" : "0.1";
|
|
2479
|
+
const hodMidOpacity = isDark ? "0.75" : "0.6";
|
|
2480
|
+
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>`);
|
|
2481
|
+
hourOfDay.forEach((entry, index) => {
|
|
2482
|
+
const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
|
|
2483
|
+
const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
|
|
2484
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2485
|
+
const colY = baselineY - barHeight;
|
|
2486
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
|
|
2487
|
+
if (isPeak) {
|
|
2488
|
+
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)"/>`);
|
|
2489
|
+
}
|
|
2490
|
+
bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
[0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
|
|
2494
|
+
const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
|
|
2495
|
+
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>`);
|
|
2496
|
+
});
|
|
2497
|
+
return {
|
|
2498
|
+
svg: bars.join(`
|
|
2499
|
+
`),
|
|
2500
|
+
height: chartHeight
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
function renderModelMixShift(x, y, width, more, theme, cardAccent) {
|
|
2504
|
+
if (!more.compare || more.compare.modelMixShift.length === 0) {
|
|
2505
|
+
return { svg: "", height: 0 };
|
|
2506
|
+
}
|
|
2507
|
+
const rows = more.compare.modelMixShift.slice(0, 4);
|
|
2508
|
+
const height = 38 + rows.length * 24;
|
|
2509
|
+
const parts = [
|
|
2510
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2511
|
+
`<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>`,
|
|
2512
|
+
`<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>`
|
|
2513
|
+
];
|
|
2514
|
+
rows.forEach((row, index) => {
|
|
2515
|
+
const lineY = y + 48 + index * 24;
|
|
2516
|
+
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>`);
|
|
2517
|
+
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>`);
|
|
2518
|
+
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>`);
|
|
2519
|
+
});
|
|
2520
|
+
return { svg: parts.join(`
|
|
2521
|
+
`), height };
|
|
2522
|
+
}
|
|
2016
2523
|
function renderTerminalCardSvg(output, options) {
|
|
2017
2524
|
const theme = getCardTheme(options.theme);
|
|
2018
2525
|
const isDark = options.theme === "dark";
|
|
@@ -2021,6 +2528,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2021
2528
|
const { since, until } = output.dateRange;
|
|
2022
2529
|
const providers = output.providers;
|
|
2023
2530
|
const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
|
|
2531
|
+
const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
|
|
2024
2532
|
const providerHeatmaps = providers.map((p) => {
|
|
2025
2533
|
const heatmapColors = buildHeatmapScale(p.colors, isDark);
|
|
2026
2534
|
return {
|
|
@@ -2050,20 +2558,20 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2050
2558
|
}
|
|
2051
2559
|
sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
|
|
2052
2560
|
y = TITLEBAR_HEIGHT + pad * 0.6;
|
|
2053
|
-
sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(
|
|
2561
|
+
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>`);
|
|
2054
2562
|
y += 40;
|
|
2055
2563
|
const dateRangeText = formatDateRange(since, until);
|
|
2056
2564
|
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>`);
|
|
2057
2565
|
y += 40;
|
|
2058
2566
|
for (let pi = 0;pi < providerHeatmaps.length; pi++) {
|
|
2059
2567
|
const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
|
|
2060
|
-
const provDotRadius =
|
|
2568
|
+
const provDotRadius = 7;
|
|
2061
2569
|
const provColor = provider.colors.primary;
|
|
2062
|
-
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y +
|
|
2063
|
-
sections.push(`<text x="${pad + provDotRadius * 2 +
|
|
2570
|
+
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
|
|
2571
|
+
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>`);
|
|
2064
2572
|
const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
|
|
2065
|
-
sections.push(`<text x="${cardWidth - pad}" y="${y +
|
|
2066
|
-
y +=
|
|
2573
|
+
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>`);
|
|
2574
|
+
y += 32;
|
|
2067
2575
|
const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
|
|
2068
2576
|
sections.push(`<g transform="translate(${pad}, ${y})">`);
|
|
2069
2577
|
sections.push(heatmapSvg);
|
|
@@ -2103,7 +2611,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2103
2611
|
const stat = row[i];
|
|
2104
2612
|
const x = pad + i * statColWidth;
|
|
2105
2613
|
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>`);
|
|
2106
|
-
const valueColor = stat.accent ? cardAccent : theme.fg;
|
|
2614
|
+
const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
|
|
2107
2615
|
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>`);
|
|
2108
2616
|
}
|
|
2109
2617
|
}
|
|
@@ -2114,24 +2622,127 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2114
2622
|
y += 8;
|
|
2115
2623
|
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2116
2624
|
y += 28;
|
|
2117
|
-
sections.push(
|
|
2625
|
+
sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
|
|
2118
2626
|
y += 24;
|
|
2119
2627
|
const topModels2 = stats.topModels.slice(0, 3);
|
|
2628
|
+
const rankWidth = 28;
|
|
2120
2629
|
const modelNameWidth = MODEL_NAME_WIDTH;
|
|
2121
2630
|
const barGap = MODEL_BAR_GAP;
|
|
2122
2631
|
const percentX = cardWidth - pad;
|
|
2123
|
-
const barX = pad + modelNameWidth;
|
|
2632
|
+
const barX = pad + rankWidth + modelNameWidth;
|
|
2124
2633
|
const barMaxWidth = Math.max(48, percentX - barX - barGap);
|
|
2125
2634
|
for (const [index, model] of topModels2.entries()) {
|
|
2126
2635
|
const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
|
|
2127
|
-
sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(
|
|
2128
|
-
sections.push(`<
|
|
2636
|
+
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>`);
|
|
2637
|
+
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>`);
|
|
2638
|
+
const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
|
|
2639
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
|
|
2129
2640
|
const gradId = `grad-${index}-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
2130
|
-
sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(
|
|
2131
|
-
sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="
|
|
2132
|
-
sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.
|
|
2641
|
+
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>`);
|
|
2642
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
|
|
2643
|
+
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>`);
|
|
2133
2644
|
y += 32;
|
|
2134
2645
|
}
|
|
2646
|
+
if (options.more && output.more) {
|
|
2647
|
+
const more = output.more;
|
|
2648
|
+
const cardGap = 16;
|
|
2649
|
+
const detailCardWidth = (contentWidth - cardGap) / 2;
|
|
2650
|
+
y += 8;
|
|
2651
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2652
|
+
y += 28;
|
|
2653
|
+
sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
|
|
2654
|
+
y += 24;
|
|
2655
|
+
const efficiencyLines = [
|
|
2656
|
+
{
|
|
2657
|
+
label: "Input / Output",
|
|
2658
|
+
value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
|
|
2659
|
+
accent: true
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
label: "Output / Input",
|
|
2663
|
+
value: formatRatio(more.inputOutput.outputPerInput)
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
label: "Output Share",
|
|
2667
|
+
value: formatPercentage(more.inputOutput.outputShare)
|
|
2668
|
+
}
|
|
2669
|
+
];
|
|
2670
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
|
|
2671
|
+
const burnLines = [
|
|
2672
|
+
{
|
|
2673
|
+
label: "Projected Cost",
|
|
2674
|
+
value: formatCost(more.monthlyBurn.projectedCost),
|
|
2675
|
+
accent: true
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
label: "Projected Tokens",
|
|
2679
|
+
value: formatNumber(more.monthlyBurn.projectedTokens)
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
label: "Based On",
|
|
2683
|
+
value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
|
|
2684
|
+
}
|
|
2685
|
+
];
|
|
2686
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
|
|
2687
|
+
y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
|
|
2688
|
+
const cacheLines = [
|
|
2689
|
+
{
|
|
2690
|
+
label: "Cache Reads",
|
|
2691
|
+
value: formatNumber(more.cacheEconomics.readTokens),
|
|
2692
|
+
accent: true
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
label: "Cache Writes",
|
|
2696
|
+
value: formatNumber(more.cacheEconomics.writeTokens)
|
|
2697
|
+
},
|
|
2698
|
+
{
|
|
2699
|
+
label: "Read Coverage",
|
|
2700
|
+
value: formatPercentage(more.cacheEconomics.readCoverage)
|
|
2701
|
+
},
|
|
2702
|
+
{
|
|
2703
|
+
label: "Reuse Ratio",
|
|
2704
|
+
value: formatRatio(more.cacheEconomics.reuseRatio)
|
|
2705
|
+
}
|
|
2706
|
+
];
|
|
2707
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
|
|
2708
|
+
const sessionLines = [
|
|
2709
|
+
{
|
|
2710
|
+
label: "Sessions",
|
|
2711
|
+
value: String(more.sessionMetrics.totalSessions),
|
|
2712
|
+
accent: true
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
label: "Avg Tokens",
|
|
2716
|
+
value: formatNumber(more.sessionMetrics.averageTokens)
|
|
2717
|
+
},
|
|
2718
|
+
{
|
|
2719
|
+
label: "Avg Messages",
|
|
2720
|
+
value: more.sessionMetrics.averageMessages.toFixed(1)
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
label: "Avg Duration",
|
|
2724
|
+
value: formatDuration(more.sessionMetrics.averageDurationMs)
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
label: "Longest Session",
|
|
2728
|
+
value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
label: "Top Project",
|
|
2732
|
+
value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
|
|
2733
|
+
}
|
|
2734
|
+
];
|
|
2735
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
|
|
2736
|
+
y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
|
|
2737
|
+
const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
|
|
2738
|
+
sections.push(hourChart.svg);
|
|
2739
|
+
y += hourChart.height + 16;
|
|
2740
|
+
const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
|
|
2741
|
+
if (mixShift.height > 0) {
|
|
2742
|
+
sections.push(mixShift.svg);
|
|
2743
|
+
y += mixShift.height + 12;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2135
2746
|
y += pad * 0.5;
|
|
2136
2747
|
const cardHeight = y;
|
|
2137
2748
|
const svg = sections.join(`
|
|
@@ -2209,142 +2820,123 @@ function intensityColor(value, max) {
|
|
|
2209
2820
|
return "dim";
|
|
2210
2821
|
}
|
|
2211
2822
|
|
|
2212
|
-
// packages/renderers/dist/terminal/
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
var DAY_LABEL_WIDTH2 = 4;
|
|
2216
|
-
var WEEK_COLUMN_WIDTH = 2;
|
|
2217
|
-
var LEGEND_TEXT = "Less";
|
|
2218
|
-
var LEGEND_TEXT_MORE = "More";
|
|
2219
|
-
function buildUsageMap(daily) {
|
|
2220
|
-
const map = new Map;
|
|
2221
|
-
for (const entry of daily) {
|
|
2222
|
-
map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
|
|
2223
|
-
}
|
|
2224
|
-
return map;
|
|
2225
|
-
}
|
|
2226
|
-
function computeQuantiles2(values) {
|
|
2227
|
-
const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
|
|
2228
|
-
if (nonZero.length === 0)
|
|
2229
|
-
return [0, 0, 0];
|
|
2230
|
-
const quantile = (ratio) => {
|
|
2231
|
-
const index = Math.floor(ratio * (nonZero.length - 1));
|
|
2232
|
-
return nonZero[index] ?? 0;
|
|
2233
|
-
};
|
|
2234
|
-
return [quantile(0.25), quantile(0.5), quantile(0.75)];
|
|
2235
|
-
}
|
|
2236
|
-
function getHeatmapBlock(tokens, quantiles) {
|
|
2237
|
-
if (tokens <= 0)
|
|
2238
|
-
return HEATMAP_BLOCKS.EMPTY;
|
|
2239
|
-
if (tokens <= quantiles[0])
|
|
2240
|
-
return HEATMAP_BLOCKS.LIGHT;
|
|
2241
|
-
if (tokens <= quantiles[1])
|
|
2242
|
-
return HEATMAP_BLOCKS.MEDIUM;
|
|
2243
|
-
if (tokens <= quantiles[2])
|
|
2244
|
-
return HEATMAP_BLOCKS.DARK;
|
|
2245
|
-
return HEATMAP_BLOCKS.FULL;
|
|
2823
|
+
// packages/renderers/dist/terminal/layout.js
|
|
2824
|
+
function stripAnsi(text2) {
|
|
2825
|
+
return text2.replace(/\u001B\[[0-9;?]*[A-Za-z]/g, "");
|
|
2246
2826
|
}
|
|
2247
|
-
function
|
|
2248
|
-
|
|
2249
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2250
|
-
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
2251
|
-
return `${year}-${month}-${day}`;
|
|
2252
|
-
}
|
|
2253
|
-
function buildMonthHeader(weeks) {
|
|
2254
|
-
const header = Array.from({ length: weeks.length * WEEK_COLUMN_WIDTH }, () => " ");
|
|
2255
|
-
let lastMonth = -1;
|
|
2256
|
-
let nextFreeIndex = 0;
|
|
2257
|
-
for (let weekIndex = 0;weekIndex < weeks.length; weekIndex++) {
|
|
2258
|
-
const firstDay = weeks[weekIndex]?.[0];
|
|
2259
|
-
if (!firstDay)
|
|
2260
|
-
continue;
|
|
2261
|
-
const month = firstDay.getUTCMonth();
|
|
2262
|
-
if (month === lastMonth)
|
|
2263
|
-
continue;
|
|
2264
|
-
lastMonth = month;
|
|
2265
|
-
const desiredStart = weekIndex * WEEK_COLUMN_WIDTH;
|
|
2266
|
-
const startIndex = Math.max(desiredStart, nextFreeIndex);
|
|
2267
|
-
const remaining = header.length - startIndex;
|
|
2268
|
-
if (remaining <= 0)
|
|
2269
|
-
continue;
|
|
2270
|
-
const fullLabel = MONTH_LABELS[month] ?? "";
|
|
2271
|
-
const label = remaining >= fullLabel.length ? fullLabel : remaining >= 2 ? fullLabel.slice(0, 2) : fullLabel.slice(0, 1);
|
|
2272
|
-
for (let offset = 0;offset < label.length; offset++) {
|
|
2273
|
-
header[startIndex + offset] = label[offset] ?? " ";
|
|
2274
|
-
}
|
|
2275
|
-
nextFreeIndex = startIndex + label.length + 1;
|
|
2276
|
-
}
|
|
2277
|
-
return `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}`;
|
|
2827
|
+
function visibleLength(text2) {
|
|
2828
|
+
return stripAnsi(text2).length;
|
|
2278
2829
|
}
|
|
2279
|
-
function
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
const weeks = [];
|
|
2283
|
-
const cursor = new Date(alignedStart);
|
|
2284
|
-
while (cursor <= endDate) {
|
|
2285
|
-
const week = [];
|
|
2286
|
-
for (let day = 0;day < 7; day++) {
|
|
2287
|
-
week.push(new Date(cursor));
|
|
2288
|
-
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
2289
|
-
}
|
|
2290
|
-
weeks.push(week);
|
|
2291
|
-
}
|
|
2292
|
-
return weeks;
|
|
2830
|
+
function padVisible(text2, width) {
|
|
2831
|
+
const padding = Math.max(0, width - visibleLength(text2));
|
|
2832
|
+
return text2 + " ".repeat(padding);
|
|
2293
2833
|
}
|
|
2294
|
-
function
|
|
2295
|
-
if (
|
|
2296
|
-
return "
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
for (const week of displayWeeks) {
|
|
2313
|
-
const date = week[dayIndex];
|
|
2314
|
-
if (!date || date < startDate || date > endDate) {
|
|
2315
|
-
line += `${HEATMAP_BLOCKS.EMPTY} `;
|
|
2834
|
+
function truncateVisible(text2, width) {
|
|
2835
|
+
if (width <= 0)
|
|
2836
|
+
return "";
|
|
2837
|
+
const plain = stripAnsi(text2);
|
|
2838
|
+
if (plain.length <= width)
|
|
2839
|
+
return text2;
|
|
2840
|
+
const limit = width <= 1 ? width : width - 1;
|
|
2841
|
+
let visibleCount = 0;
|
|
2842
|
+
let index = 0;
|
|
2843
|
+
let result = "";
|
|
2844
|
+
let sawAnsi = false;
|
|
2845
|
+
while (index < text2.length && visibleCount < limit) {
|
|
2846
|
+
if (text2[index] === "\x1B") {
|
|
2847
|
+
const match = text2.slice(index).match(/^\u001B\[[0-9;?]*[A-Za-z]/);
|
|
2848
|
+
if (match) {
|
|
2849
|
+
result += match[0];
|
|
2850
|
+
index += match[0].length;
|
|
2851
|
+
sawAnsi = true;
|
|
2316
2852
|
continue;
|
|
2317
2853
|
}
|
|
2318
|
-
const dateString = formatDate(date);
|
|
2319
|
-
const tokens = usageMap.get(dateString) ?? 0;
|
|
2320
|
-
const block = getHeatmapBlock(tokens, quantiles);
|
|
2321
|
-
const color = intensityColor(tokens, maxTokens);
|
|
2322
|
-
line += `${colorize(block, color, options.noColor)} `;
|
|
2323
2854
|
}
|
|
2324
|
-
|
|
2855
|
+
result += text2[index];
|
|
2856
|
+
index += 1;
|
|
2857
|
+
visibleCount += 1;
|
|
2325
2858
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
lines
|
|
2334
|
-
|
|
2335
|
-
|
|
2859
|
+
return sawAnsi ? `${result}\u2026\x1B[0m` : `${result}\u2026`;
|
|
2860
|
+
}
|
|
2861
|
+
function renderColumns(left, right, totalWidth, leftRatio = 0.5, gutter = 3) {
|
|
2862
|
+
const safeWidth = Math.max(12, totalWidth);
|
|
2863
|
+
const leftWidth = Math.max(18, Math.floor((safeWidth - gutter) * leftRatio));
|
|
2864
|
+
const rightWidth = Math.max(18, safeWidth - leftWidth - gutter);
|
|
2865
|
+
const rows = Math.max(left.length, right.length);
|
|
2866
|
+
const lines = [];
|
|
2867
|
+
for (let index = 0;index < rows; index += 1) {
|
|
2868
|
+
const leftLine = truncateVisible(left[index] ?? "", leftWidth);
|
|
2869
|
+
const rightLine = truncateVisible(right[index] ?? "", rightWidth);
|
|
2870
|
+
lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
|
|
2871
|
+
}
|
|
2872
|
+
return lines;
|
|
2336
2873
|
}
|
|
2337
2874
|
|
|
2338
|
-
// packages/renderers/dist/terminal/
|
|
2875
|
+
// packages/renderers/dist/terminal/compact.js
|
|
2339
2876
|
var BOX_H = "\u2500";
|
|
2340
2877
|
var BOX_V = "\u2502";
|
|
2341
2878
|
var BOX_TL = "\u250C";
|
|
2342
2879
|
var BOX_TR = "\u2510";
|
|
2343
2880
|
var BOX_BL = "\u2514";
|
|
2344
2881
|
var BOX_BR = "\u2518";
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2882
|
+
function boxedHeader(title, width, noColor2) {
|
|
2883
|
+
const inner = Math.max(1, width - 2);
|
|
2884
|
+
const padded = ` ${title} `;
|
|
2885
|
+
const remaining = Math.max(0, inner - padded.length);
|
|
2886
|
+
const left = Math.floor(remaining / 2);
|
|
2887
|
+
const right = remaining - left;
|
|
2888
|
+
const titleLine = `${BOX_V}${" ".repeat(left)}${colorize(padded, "bold", noColor2)}${" ".repeat(right)}${BOX_V}`;
|
|
2889
|
+
return [
|
|
2890
|
+
`${BOX_TL}${BOX_H.repeat(inner)}${BOX_TR}`,
|
|
2891
|
+
titleLine,
|
|
2892
|
+
`${BOX_BL}${BOX_H.repeat(inner)}${BOX_BR}`
|
|
2893
|
+
].join(`
|
|
2894
|
+
`);
|
|
2895
|
+
}
|
|
2896
|
+
function renderSummaryParts(parts, width, noColor2) {
|
|
2897
|
+
const left = parts.filter((_, index) => index % 2 === 0).map((part) => colorize(part, "cyan", noColor2));
|
|
2898
|
+
const right = parts.filter((_, index) => index % 2 === 1).map((part) => colorize(part, "green", noColor2));
|
|
2899
|
+
return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((line) => ` ${line}`).join(`
|
|
2900
|
+
`);
|
|
2901
|
+
}
|
|
2902
|
+
function renderCompactDashboard(model, options) {
|
|
2903
|
+
const width = options.width;
|
|
2904
|
+
const noColor2 = options.noColor;
|
|
2905
|
+
const lines = [
|
|
2906
|
+
boxedHeader("Tokenleak", width, noColor2),
|
|
2907
|
+
"",
|
|
2908
|
+
truncateVisible(` Range ${model.rangeLabel}`, width),
|
|
2909
|
+
"",
|
|
2910
|
+
renderSummaryParts(model.overview.summary, width, noColor2),
|
|
2911
|
+
...model.overview.trend ? ["", truncateVisible(` Recent Trend ${model.overview.trend}`, width)] : [],
|
|
2912
|
+
""
|
|
2913
|
+
];
|
|
2914
|
+
if (model.overview.metrics.length > 0) {
|
|
2915
|
+
const keyMetrics = model.overview.metrics.slice(0, 4).map((metric) => {
|
|
2916
|
+
const text2 = `${metric.label}: ${metric.value}`;
|
|
2917
|
+
return ` ${truncateVisible(text2, width - 2)}`;
|
|
2918
|
+
});
|
|
2919
|
+
lines.push(...keyMetrics, "");
|
|
2920
|
+
}
|
|
2921
|
+
if (model.activeProviders.length > 0) {
|
|
2922
|
+
lines.push(colorize(" Providers", "bold", noColor2));
|
|
2923
|
+
for (const provider of model.activeProviders.slice(0, 4)) {
|
|
2924
|
+
const tokensMetric = provider.metrics.find((entry) => entry.label === "Total Tokens");
|
|
2925
|
+
const summary = `${provider.provider.displayName} ${tokensMetric?.value ?? ""} ${provider.lastActiveDate ? `| ${provider.lastActiveDate}` : ""}`.trim();
|
|
2926
|
+
lines.push(truncateVisible(` ${summary}`, width));
|
|
2927
|
+
}
|
|
2928
|
+
lines.push("");
|
|
2929
|
+
}
|
|
2930
|
+
if (model.inactiveProviders.length > 0) {
|
|
2931
|
+
lines.push(truncateVisible(` No activity: ${model.inactiveProviders.join(", ")}`, width));
|
|
2932
|
+
} else if (model.activeProviders.length === 0) {
|
|
2933
|
+
lines.push(" No provider activity in the selected range.");
|
|
2934
|
+
}
|
|
2935
|
+
return lines.filter((line, index, array) => !(line === "" && array[index - 1] === "")).join(`
|
|
2936
|
+
`);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// packages/renderers/dist/terminal/dashboard-model.js
|
|
2348
2940
|
function formatTokens(count) {
|
|
2349
2941
|
if (count >= 1e6) {
|
|
2350
2942
|
return `${(count / 1e6).toFixed(1)}M`;
|
|
@@ -2363,180 +2955,527 @@ function formatPercent(rate) {
|
|
|
2363
2955
|
function formatSharePercent(percentage) {
|
|
2364
2956
|
return `${percentage.toFixed(0)}%`;
|
|
2365
2957
|
}
|
|
2366
|
-
function
|
|
2367
|
-
return
|
|
2958
|
+
function buildRangeLabel(output) {
|
|
2959
|
+
return `${output.dateRange.since} -> ${output.dateRange.until}`;
|
|
2368
2960
|
}
|
|
2369
|
-
function
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
const
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2961
|
+
function buildSparkline(daily, points = 14) {
|
|
2962
|
+
const blocks = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2963
|
+
if (daily.length === 0)
|
|
2964
|
+
return "\xB7".repeat(points);
|
|
2965
|
+
const values = daily.slice().sort((left, right) => left.date.localeCompare(right.date)).slice(-points).map((entry) => entry.totalTokens);
|
|
2966
|
+
const max = Math.max(...values, 0);
|
|
2967
|
+
if (max <= 0) {
|
|
2968
|
+
return "\xB7".repeat(values.length);
|
|
2969
|
+
}
|
|
2970
|
+
return values.map((value) => {
|
|
2971
|
+
const index = Math.min(blocks.length - 1, Math.round(value / max * (blocks.length - 1)));
|
|
2972
|
+
return blocks[index] ?? blocks[0];
|
|
2973
|
+
}).join("");
|
|
2381
2974
|
}
|
|
2382
|
-
function
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
const length = Math.round(tokens / maxTokens * MAX_BAR_LENGTH);
|
|
2386
|
-
const bar = BAR_CHAR.repeat(length);
|
|
2387
|
-
return colorize(bar, "green", noColor2);
|
|
2975
|
+
function getLastActiveDate(provider) {
|
|
2976
|
+
const activeDays = provider.daily.filter((entry) => entry.totalTokens > 0);
|
|
2977
|
+
return activeDays.at(-1)?.date ?? null;
|
|
2388
2978
|
}
|
|
2389
|
-
function
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
["Total Tokens", formatTokens(stats.totalTokens)],
|
|
2396
|
-
["Total Cost", formatCost2(stats.totalCost)],
|
|
2397
|
-
["30d Tokens", formatTokens(stats.rolling30dTokens)],
|
|
2398
|
-
["30d Cost", formatCost2(stats.rolling30dCost)],
|
|
2399
|
-
["7d Tokens", formatTokens(stats.rolling7dTokens)],
|
|
2400
|
-
["7d Cost", formatCost2(stats.rolling7dCost)],
|
|
2401
|
-
["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
|
|
2402
|
-
["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
|
|
2403
|
-
["Cache Hit Rate", formatPercent(stats.cacheHitRate)],
|
|
2404
|
-
["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
|
|
2979
|
+
function buildMetricEntries(stats) {
|
|
2980
|
+
const metrics = [
|
|
2981
|
+
{ label: "Current Streak", value: `${stats.currentStreak}d` },
|
|
2982
|
+
{ label: "Longest Streak", value: `${stats.longestStreak}d` },
|
|
2983
|
+
{ label: "Total Tokens", value: formatTokens(stats.totalTokens) },
|
|
2984
|
+
{ label: "Total Cost", value: formatCost2(stats.totalCost) }
|
|
2405
2985
|
];
|
|
2406
|
-
if (stats.
|
|
2407
|
-
|
|
2986
|
+
if (stats.rolling30dTokens > 0 || stats.rolling30dCost > 0) {
|
|
2987
|
+
metrics.push({ label: "30d Tokens", value: formatTokens(stats.rolling30dTokens) });
|
|
2988
|
+
metrics.push({ label: "30d Cost", value: formatCost2(stats.rolling30dCost) });
|
|
2408
2989
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2990
|
+
if (stats.rolling7dTokens > 0 || stats.rolling7dCost > 0) {
|
|
2991
|
+
metrics.push({ label: "7d Tokens", value: formatTokens(stats.rolling7dTokens) });
|
|
2992
|
+
metrics.push({ label: "7d Cost", value: formatCost2(stats.rolling7dCost) });
|
|
2412
2993
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
}
|
|
2416
|
-
function renderDayOfWeek(stats, width, noColor2) {
|
|
2417
|
-
const lines = [];
|
|
2418
|
-
const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
|
|
2419
|
-
for (const entry of stats.dayOfWeek) {
|
|
2420
|
-
const label = DAY_NAMES[entry.day] ?? `Day${entry.day}`;
|
|
2421
|
-
const bar = dayBar(entry.tokens, maxTokens, noColor2);
|
|
2422
|
-
const tokenStr = formatTokens(entry.tokens);
|
|
2423
|
-
const line = ` ${label} ${bar} ${tokenStr}`;
|
|
2424
|
-
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2994
|
+
if (stats.activeDays > 0) {
|
|
2995
|
+
metrics.push({ label: "Avg Daily Tokens", value: formatTokens(stats.averageDailyTokens) });
|
|
2996
|
+
metrics.push({ label: "Avg Daily Cost", value: formatCost2(stats.averageDailyCost) });
|
|
2425
2997
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
}
|
|
2429
|
-
function renderTopModels(stats, width, noColor2) {
|
|
2430
|
-
const lines = [];
|
|
2431
|
-
const nameWidth = Math.min(28, Math.max(16, Math.floor(width * 0.35)));
|
|
2432
|
-
const pctWidth = 4;
|
|
2433
|
-
const barGap = 2;
|
|
2434
|
-
const barWidth = Math.max(8, width - nameWidth - pctWidth - 6);
|
|
2435
|
-
for (const model of stats.topModels.slice(0, 5)) {
|
|
2436
|
-
const pct = formatSharePercent(model.percentage);
|
|
2437
|
-
const normalizedName = model.model.length > nameWidth ? `${model.model.slice(0, nameWidth - 1)}\u2026` : model.model;
|
|
2438
|
-
const fillLength = Math.max(1, Math.round(model.percentage / 100 * barWidth));
|
|
2439
|
-
const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
|
|
2440
|
-
const track = "\u2591".repeat(Math.max(0, barWidth - fillLength));
|
|
2441
|
-
const line = ` ${colorize(normalizedName.padEnd(nameWidth), "yellow", noColor2)}${" ".repeat(barGap)}${fill}${track}${" ".repeat(barGap)}${pct.padStart(pctWidth)}`;
|
|
2442
|
-
lines.push(line);
|
|
2998
|
+
if (stats.totalTokens > 0) {
|
|
2999
|
+
metrics.push({ label: "Cache Hit Rate", value: formatPercent(stats.cacheHitRate) });
|
|
2443
3000
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
3001
|
+
metrics.push({ label: "Active Days", value: `${stats.activeDays} / ${stats.totalDays}` });
|
|
3002
|
+
if (stats.peakDay) {
|
|
3003
|
+
metrics.push({
|
|
3004
|
+
label: "Peak Day",
|
|
3005
|
+
value: `${stats.peakDay.date} (${formatTokens(stats.peakDay.tokens)})`
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
return metrics;
|
|
2446
3009
|
}
|
|
2447
|
-
function
|
|
2448
|
-
const
|
|
2449
|
-
|
|
2450
|
-
|
|
3010
|
+
function buildProviderSummary(provider, stats) {
|
|
3011
|
+
const parts = [
|
|
3012
|
+
`${formatTokens(stats.totalTokens)} tokens`,
|
|
3013
|
+
formatCost2(stats.totalCost),
|
|
3014
|
+
`${stats.activeDays} active day${stats.activeDays === 1 ? "" : "s"}`
|
|
3015
|
+
];
|
|
3016
|
+
const lastActiveDate = getLastActiveDate(provider);
|
|
3017
|
+
if (lastActiveDate) {
|
|
3018
|
+
parts.push(`last active ${lastActiveDate}`);
|
|
3019
|
+
}
|
|
3020
|
+
if (stats.topModels[0]) {
|
|
3021
|
+
parts.push(`${stats.topModels[0].model} ${formatSharePercent(stats.topModels[0].percentage)}`);
|
|
2451
3022
|
}
|
|
2452
|
-
|
|
2453
|
-
|
|
3023
|
+
return parts;
|
|
3024
|
+
}
|
|
3025
|
+
function buildInsights(stats) {
|
|
3026
|
+
const insights = [];
|
|
3027
|
+
if (stats.currentStreak >= 7) {
|
|
3028
|
+
insights.push(`${stats.currentStreak}-day streak is still alive.`);
|
|
2454
3029
|
}
|
|
2455
|
-
if (stats.cacheHitRate
|
|
2456
|
-
insights.push(
|
|
3030
|
+
if (stats.cacheHitRate >= 0.5) {
|
|
3031
|
+
insights.push(`Cache reuse is strong at ${formatPercent(stats.cacheHitRate)}.`);
|
|
3032
|
+
} else if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
|
|
3033
|
+
insights.push("Cache reuse is low. There is room to improve prompt caching.");
|
|
2457
3034
|
}
|
|
2458
3035
|
if (stats.peakDay) {
|
|
2459
|
-
insights.push(`Peak usage
|
|
3036
|
+
insights.push(`Peak usage hit ${formatTokens(stats.peakDay.tokens)} on ${stats.peakDay.date}.`);
|
|
2460
3037
|
}
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
3038
|
+
if (stats.topModels[0] && stats.topModels[0].percentage >= 70) {
|
|
3039
|
+
insights.push(`${stats.topModels[0].model} dominates at ${formatSharePercent(stats.topModels[0].percentage)}.`);
|
|
3040
|
+
}
|
|
3041
|
+
return insights;
|
|
2465
3042
|
}
|
|
2466
|
-
function
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
sections.push(colorize(" Heatmap", "bold", noColor2));
|
|
2471
|
-
sections.push(renderTerminalHeatmap(provider.daily, { width, noColor: noColor2 }));
|
|
2472
|
-
sections.push("");
|
|
2473
|
-
sections.push(colorize(" Stats", "bold", noColor2));
|
|
2474
|
-
sections.push(renderStats(stats, width, noColor2));
|
|
2475
|
-
if (stats.dayOfWeek.length > 0) {
|
|
2476
|
-
sections.push("");
|
|
2477
|
-
sections.push(colorize(" Day of Week", "bold", noColor2));
|
|
2478
|
-
sections.push(renderDayOfWeek(stats, width, noColor2));
|
|
3043
|
+
function buildDayOfWeekPatterns(stats) {
|
|
3044
|
+
const maxTokens = Math.max(...stats.dayOfWeek.map((entry) => entry.tokens), 0);
|
|
3045
|
+
if (maxTokens <= 0) {
|
|
3046
|
+
return [];
|
|
2479
3047
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
3048
|
+
return stats.dayOfWeek.filter((entry) => entry.tokens > 0).map((entry) => ({
|
|
3049
|
+
label: entry.label,
|
|
3050
|
+
value: formatTokens(entry.tokens),
|
|
3051
|
+
share: entry.tokens / maxTokens
|
|
3052
|
+
}));
|
|
3053
|
+
}
|
|
3054
|
+
function buildTopModelPatterns(stats) {
|
|
3055
|
+
return stats.topModels.slice(0, 5).filter((entry) => entry.tokens > 0).map((entry) => ({
|
|
3056
|
+
label: entry.model,
|
|
3057
|
+
value: formatSharePercent(entry.percentage),
|
|
3058
|
+
share: Math.max(0.03, entry.percentage / 100)
|
|
3059
|
+
}));
|
|
3060
|
+
}
|
|
3061
|
+
function buildProviderModel(provider, until) {
|
|
3062
|
+
const stats = aggregate(provider.daily, until);
|
|
3063
|
+
if (stats.totalTokens <= 0) {
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
return {
|
|
3067
|
+
provider,
|
|
3068
|
+
stats,
|
|
3069
|
+
summary: buildProviderSummary(provider, stats),
|
|
3070
|
+
trend: buildSparkline(provider.daily),
|
|
3071
|
+
metrics: buildMetricEntries(stats),
|
|
3072
|
+
dayOfWeek: buildDayOfWeekPatterns(stats),
|
|
3073
|
+
topModels: buildTopModelPatterns(stats),
|
|
3074
|
+
insights: buildInsights(stats),
|
|
3075
|
+
lastActiveDate: getLastActiveDate(provider)
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
function buildOverview(output, activeProviders) {
|
|
3079
|
+
const mergedDaily = activeProviders.length > 0 ? mergeProviderData(activeProviders.map((entry) => entry.provider)) : [];
|
|
3080
|
+
const summary = [
|
|
3081
|
+
`${formatTokens(output.aggregated.totalTokens)} tokens`,
|
|
3082
|
+
formatCost2(output.aggregated.totalCost),
|
|
3083
|
+
`${activeProviders.length} active provider${activeProviders.length === 1 ? "" : "s"}`,
|
|
3084
|
+
`${output.aggregated.activeDays} active day${output.aggregated.activeDays === 1 ? "" : "s"}`
|
|
3085
|
+
];
|
|
3086
|
+
const metrics = buildMetricEntries(output.aggregated);
|
|
3087
|
+
const maxProviderTokens = Math.max(...activeProviders.map((entry) => entry.stats.totalTokens), 0);
|
|
3088
|
+
const providerLeaders = activeProviders.slice().sort((left, right) => right.stats.totalTokens - left.stats.totalTokens).map((entry) => ({
|
|
3089
|
+
label: entry.provider.displayName,
|
|
3090
|
+
value: formatTokens(entry.stats.totalTokens),
|
|
3091
|
+
share: maxProviderTokens > 0 ? entry.stats.totalTokens / maxProviderTokens : 0
|
|
3092
|
+
}));
|
|
3093
|
+
return {
|
|
3094
|
+
summary,
|
|
3095
|
+
trend: buildSparkline(mergedDaily),
|
|
3096
|
+
metrics,
|
|
3097
|
+
providerLeaders
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function resolveDashboardMode(width) {
|
|
3101
|
+
if (width < 32)
|
|
3102
|
+
return "summary";
|
|
3103
|
+
if (width < 56)
|
|
3104
|
+
return "compact";
|
|
3105
|
+
return "full";
|
|
3106
|
+
}
|
|
3107
|
+
function buildDashboardModel(output, options) {
|
|
3108
|
+
const activeProviders = [];
|
|
3109
|
+
const inactiveProviders = [];
|
|
3110
|
+
for (const provider of output.providers) {
|
|
3111
|
+
const model = buildProviderModel(provider, output.dateRange.until);
|
|
3112
|
+
if (model) {
|
|
3113
|
+
activeProviders.push(model);
|
|
3114
|
+
} else {
|
|
3115
|
+
inactiveProviders.push(provider.displayName);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
return {
|
|
3119
|
+
mode: resolveDashboardMode(options.width),
|
|
3120
|
+
rangeLabel: buildRangeLabel(output),
|
|
3121
|
+
activeProviders,
|
|
3122
|
+
inactiveProviders,
|
|
3123
|
+
overview: buildOverview(output, activeProviders)
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// packages/renderers/dist/shared/heatmap-model.js
|
|
3128
|
+
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
3129
|
+
function formatDate(date) {
|
|
3130
|
+
const year = date.getUTCFullYear();
|
|
3131
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
3132
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
3133
|
+
return `${year}-${month}-${day}`;
|
|
3134
|
+
}
|
|
3135
|
+
function buildUsageMap(daily) {
|
|
3136
|
+
const map = new Map;
|
|
3137
|
+
for (const entry of daily) {
|
|
3138
|
+
map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
|
|
3139
|
+
}
|
|
3140
|
+
return map;
|
|
3141
|
+
}
|
|
3142
|
+
function computeQuantiles2(values) {
|
|
3143
|
+
const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
|
|
3144
|
+
if (nonZero.length === 0)
|
|
3145
|
+
return [0, 0, 0];
|
|
3146
|
+
const quantile = (ratio) => {
|
|
3147
|
+
const index = Math.floor(ratio * (nonZero.length - 1));
|
|
3148
|
+
return nonZero[index] ?? 0;
|
|
3149
|
+
};
|
|
3150
|
+
return [quantile(0.25), quantile(0.5), quantile(0.75)];
|
|
3151
|
+
}
|
|
3152
|
+
function getLevel2(tokens, quantiles) {
|
|
3153
|
+
if (tokens <= 0)
|
|
3154
|
+
return 0;
|
|
3155
|
+
if (tokens <= quantiles[0])
|
|
3156
|
+
return 1;
|
|
3157
|
+
if (tokens <= quantiles[1])
|
|
3158
|
+
return 2;
|
|
3159
|
+
if (tokens <= quantiles[2])
|
|
3160
|
+
return 3;
|
|
3161
|
+
return 4;
|
|
3162
|
+
}
|
|
3163
|
+
function alignToSunday(date) {
|
|
3164
|
+
const aligned = new Date(date);
|
|
3165
|
+
aligned.setUTCDate(aligned.getUTCDate() - aligned.getUTCDay());
|
|
3166
|
+
return aligned;
|
|
3167
|
+
}
|
|
3168
|
+
function buildHeatmapModel(daily, range) {
|
|
3169
|
+
if (daily.length === 0) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
const dates = daily.map((entry) => entry.date).sort();
|
|
3173
|
+
const since = range?.since ?? dates[0];
|
|
3174
|
+
const until = range?.until ?? dates[dates.length - 1];
|
|
3175
|
+
const startDate = alignToSunday(new Date(`${since}T00:00:00Z`));
|
|
3176
|
+
const endDate = new Date(`${until}T00:00:00Z`);
|
|
3177
|
+
const usageMap = buildUsageMap(daily);
|
|
3178
|
+
const usageValues = Array.from(usageMap.values());
|
|
3179
|
+
const quantiles = computeQuantiles2(usageValues);
|
|
3180
|
+
const maxTokens = usageValues.reduce((max, value) => value > max ? value : max, 0);
|
|
3181
|
+
const weeks = [];
|
|
3182
|
+
const monthMarkers = [];
|
|
3183
|
+
let weekIndex = 0;
|
|
3184
|
+
let lastMonth = -1;
|
|
3185
|
+
for (let cursor = new Date(startDate);cursor <= endDate; weekIndex += 1) {
|
|
3186
|
+
const weekDays = [];
|
|
3187
|
+
const month = cursor.getUTCMonth();
|
|
3188
|
+
const year = cursor.getUTCFullYear();
|
|
3189
|
+
if (month !== lastMonth) {
|
|
3190
|
+
monthMarkers.push({
|
|
3191
|
+
label: MONTH_LABELS[month] ?? "",
|
|
3192
|
+
month,
|
|
3193
|
+
year,
|
|
3194
|
+
weekIndex
|
|
3195
|
+
});
|
|
3196
|
+
lastMonth = month;
|
|
3197
|
+
}
|
|
3198
|
+
const dayCursor = new Date(cursor);
|
|
3199
|
+
for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
|
|
3200
|
+
const dateString = formatDate(dayCursor);
|
|
3201
|
+
const tokens = usageMap.get(dateString) ?? 0;
|
|
3202
|
+
weekDays.push({
|
|
3203
|
+
date: dateString,
|
|
3204
|
+
tokens,
|
|
3205
|
+
level: getLevel2(tokens, quantiles),
|
|
3206
|
+
dayIndex,
|
|
3207
|
+
weekIndex
|
|
3208
|
+
});
|
|
3209
|
+
dayCursor.setUTCDate(dayCursor.getUTCDate() + 1);
|
|
3210
|
+
}
|
|
3211
|
+
weeks.push({
|
|
3212
|
+
index: weekIndex,
|
|
3213
|
+
days: weekDays
|
|
3214
|
+
});
|
|
3215
|
+
cursor.setUTCDate(cursor.getUTCDate() + 7);
|
|
3216
|
+
}
|
|
3217
|
+
return {
|
|
3218
|
+
weeks,
|
|
3219
|
+
monthMarkers,
|
|
3220
|
+
maxTokens,
|
|
3221
|
+
since,
|
|
3222
|
+
until
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// packages/renderers/dist/terminal/heatmap.js
|
|
3227
|
+
var DAY_LABELS2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
3228
|
+
var DAY_LABEL_WIDTH2 = 4;
|
|
3229
|
+
var WEEK_COLUMN_WIDTH = 2;
|
|
3230
|
+
var LEGEND_TEXT = "Less";
|
|
3231
|
+
var LEGEND_TEXT_MORE = "More";
|
|
3232
|
+
function buildMonthHeader(model, visibleStartWeek, displayWeekCount) {
|
|
3233
|
+
const header = Array.from({ length: displayWeekCount * WEEK_COLUMN_WIDTH }, () => " ");
|
|
3234
|
+
let nextFreeIndex = 0;
|
|
3235
|
+
let placedLabels = 0;
|
|
3236
|
+
const visibleMarkers = model.monthMarkers.filter((marker) => marker.weekIndex >= visibleStartWeek);
|
|
3237
|
+
for (const marker of visibleMarkers) {
|
|
3238
|
+
const startIndex = Math.max((marker.weekIndex - visibleStartWeek) * WEEK_COLUMN_WIDTH, nextFreeIndex);
|
|
3239
|
+
const remaining = header.length - startIndex;
|
|
3240
|
+
if (remaining < 3) {
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
for (let offset = 0;offset < marker.label.length; offset += 1) {
|
|
3244
|
+
header[startIndex + offset] = marker.label[offset] ?? " ";
|
|
3245
|
+
}
|
|
3246
|
+
nextFreeIndex = startIndex + marker.label.length + 1;
|
|
3247
|
+
placedLabels += 1;
|
|
3248
|
+
}
|
|
3249
|
+
const line = header.some((cell) => cell !== " ") ? `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}` : null;
|
|
3250
|
+
const uniqueVisibleMonths = visibleMarkers.map((marker) => `${marker.label} ${String(marker.year)}`).filter((value, index, values) => values.indexOf(value) === index);
|
|
3251
|
+
const caption = placedLabels === 0 && uniqueVisibleMonths.length === 1 ? ` ${uniqueVisibleMonths[0]}` : null;
|
|
3252
|
+
return { caption, line };
|
|
3253
|
+
}
|
|
3254
|
+
function renderTerminalHeatmap(daily, options) {
|
|
3255
|
+
const model = buildHeatmapModel(daily);
|
|
3256
|
+
if (!model) {
|
|
3257
|
+
return " No usage data available in the selected range.";
|
|
3258
|
+
}
|
|
3259
|
+
const availableColumns = Math.max(WEEK_COLUMN_WIDTH, options.width - DAY_LABEL_WIDTH2);
|
|
3260
|
+
const maxWeeks = Math.max(1, Math.floor(availableColumns / WEEK_COLUMN_WIDTH));
|
|
3261
|
+
const displayWeeks = model.weeks.slice(Math.max(0, model.weeks.length - maxWeeks));
|
|
3262
|
+
const visibleStartWeek = model.weeks.length - displayWeeks.length;
|
|
3263
|
+
const { caption, line } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length);
|
|
3264
|
+
const lines = [];
|
|
3265
|
+
if (caption) {
|
|
3266
|
+
lines.push(caption);
|
|
3267
|
+
}
|
|
3268
|
+
if (line) {
|
|
3269
|
+
lines.push(line);
|
|
2484
3270
|
}
|
|
2485
|
-
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3271
|
+
for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
|
|
3272
|
+
const label = DAY_LABELS2[dayIndex] ?? " ";
|
|
3273
|
+
let row = `${label} `.slice(0, DAY_LABEL_WIDTH2);
|
|
3274
|
+
for (const week of displayWeeks) {
|
|
3275
|
+
const cell = week.days[dayIndex] ?? { level: 0, tokens: 0 };
|
|
3276
|
+
const block = [
|
|
3277
|
+
HEATMAP_BLOCKS.EMPTY,
|
|
3278
|
+
HEATMAP_BLOCKS.LIGHT,
|
|
3279
|
+
HEATMAP_BLOCKS.MEDIUM,
|
|
3280
|
+
HEATMAP_BLOCKS.DARK,
|
|
3281
|
+
HEATMAP_BLOCKS.FULL
|
|
3282
|
+
][cell.level] ?? HEATMAP_BLOCKS.EMPTY;
|
|
3283
|
+
const color = intensityColor(cell.tokens, model.maxTokens);
|
|
3284
|
+
row += `${colorize(block, color, options.noColor)} `;
|
|
2491
3285
|
}
|
|
3286
|
+
lines.push(row.trimEnd());
|
|
3287
|
+
}
|
|
3288
|
+
lines.push(`${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${[
|
|
3289
|
+
HEATMAP_BLOCKS.EMPTY,
|
|
3290
|
+
HEATMAP_BLOCKS.LIGHT,
|
|
3291
|
+
HEATMAP_BLOCKS.MEDIUM,
|
|
3292
|
+
HEATMAP_BLOCKS.DARK,
|
|
3293
|
+
HEATMAP_BLOCKS.FULL
|
|
3294
|
+
].join("")} ${LEGEND_TEXT_MORE}`);
|
|
3295
|
+
return lines.join(`
|
|
3296
|
+
`);
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// packages/renderers/dist/terminal/dashboard.js
|
|
3300
|
+
var BOX_H2 = "\u2500";
|
|
3301
|
+
var BOX_V2 = "\u2502";
|
|
3302
|
+
var BOX_TL2 = "\u250C";
|
|
3303
|
+
var BOX_TR2 = "\u2510";
|
|
3304
|
+
var BOX_BL2 = "\u2514";
|
|
3305
|
+
var BOX_BR2 = "\u2518";
|
|
3306
|
+
var BAR_CHAR = "\u2588";
|
|
3307
|
+
var TRACK_CHAR = "\u2591";
|
|
3308
|
+
function divider(width) {
|
|
3309
|
+
return BOX_H2.repeat(width);
|
|
3310
|
+
}
|
|
3311
|
+
function boxedHeader2(title, width, noColor2) {
|
|
3312
|
+
const inner = Math.max(1, width - 2);
|
|
3313
|
+
const padded = ` ${title} `;
|
|
3314
|
+
const remaining = Math.max(0, inner - padded.length);
|
|
3315
|
+
const left = Math.floor(remaining / 2);
|
|
3316
|
+
const right = remaining - left;
|
|
3317
|
+
const titleLine = `${BOX_V2}${" ".repeat(left)}${colorize(padded, "bold", noColor2)}${" ".repeat(right)}${BOX_V2}`;
|
|
3318
|
+
return [
|
|
3319
|
+
`${BOX_TL2}${BOX_H2.repeat(inner)}${BOX_TR2}`,
|
|
3320
|
+
titleLine,
|
|
3321
|
+
`${BOX_BL2}${BOX_H2.repeat(inner)}${BOX_BR2}`
|
|
3322
|
+
].join(`
|
|
3323
|
+
`);
|
|
3324
|
+
}
|
|
3325
|
+
function renderSectionTitle(title, noColor2) {
|
|
3326
|
+
return colorize(` ${title}`, "bold", noColor2);
|
|
3327
|
+
}
|
|
3328
|
+
function renderSummary(parts, width, noColor2) {
|
|
3329
|
+
if (parts.length === 0)
|
|
3330
|
+
return [];
|
|
3331
|
+
const colored = parts.map((part, index) => colorize(part, index % 2 === 0 ? "cyan" : "green", noColor2));
|
|
3332
|
+
const line = colored.join(colorize(" | ", "dim", noColor2));
|
|
3333
|
+
return [truncateVisible(` ${line}`, width)];
|
|
3334
|
+
}
|
|
3335
|
+
function renderTrend(trend, width, noColor2) {
|
|
3336
|
+
if (!trend)
|
|
3337
|
+
return [];
|
|
3338
|
+
return [
|
|
3339
|
+
truncateVisible(` ${colorize("Recent Trend", "bold", noColor2)} ${colorize(trend, "green", noColor2)}`, width)
|
|
3340
|
+
];
|
|
3341
|
+
}
|
|
3342
|
+
function renderMetricLine(entry, width, noColor2) {
|
|
3343
|
+
const label = entry.label;
|
|
3344
|
+
const value = colorize(entry.value, "cyan", noColor2);
|
|
3345
|
+
const gutter = 2;
|
|
3346
|
+
const safeWidth = Math.max(12, width);
|
|
3347
|
+
const valueWidth = Math.min(Math.max(6, visibleLength(entry.value)), Math.max(6, Math.floor(safeWidth * 0.45)));
|
|
3348
|
+
const labelWidth = Math.max(4, safeWidth - valueWidth - gutter);
|
|
3349
|
+
return `${truncateVisible(label, labelWidth)}${" ".repeat(gutter)}${padVisible(value, valueWidth)}`;
|
|
3350
|
+
}
|
|
3351
|
+
function renderMetrics(entries, width, noColor2) {
|
|
3352
|
+
if (entries.length === 0)
|
|
3353
|
+
return [];
|
|
3354
|
+
const innerWidth = Math.max(16, width - 2);
|
|
3355
|
+
if (width >= 86 && entries.length > 3) {
|
|
3356
|
+
const midpoint = Math.ceil(entries.length / 2);
|
|
3357
|
+
const left = entries.slice(0, midpoint).map((entry) => ` ${renderMetricLine(entry, Math.floor((innerWidth - 3) / 2), noColor2)}`);
|
|
3358
|
+
const right = entries.slice(midpoint).map((entry) => ` ${renderMetricLine(entry, Math.floor((innerWidth - 3) / 2), noColor2)}`);
|
|
3359
|
+
return renderColumns(left, right, innerWidth, 0.5, 3);
|
|
3360
|
+
}
|
|
3361
|
+
return entries.map((entry) => ` ${renderMetricLine(entry, innerWidth, noColor2)}`);
|
|
3362
|
+
}
|
|
3363
|
+
function renderPatternList(title, entries, width, noColor2) {
|
|
3364
|
+
if (entries.length === 0)
|
|
3365
|
+
return [];
|
|
3366
|
+
const lines = [renderSectionTitle(title, noColor2)];
|
|
3367
|
+
const innerWidth = Math.max(18, width - 2);
|
|
3368
|
+
const nameWidth = Math.min(22, Math.max(8, Math.floor(innerWidth * 0.35)));
|
|
3369
|
+
const valueWidth = 6;
|
|
3370
|
+
const barWidth = Math.max(6, innerWidth - nameWidth - valueWidth - 6);
|
|
3371
|
+
for (const entry of entries) {
|
|
3372
|
+
const fillLength = Math.max(1, Math.round(entry.share * barWidth));
|
|
3373
|
+
const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
|
|
3374
|
+
const track = TRACK_CHAR.repeat(Math.max(0, barWidth - fillLength));
|
|
3375
|
+
const line = ` ${colorize(truncateVisible(entry.label, nameWidth).padEnd(nameWidth), "yellow", noColor2)} ${fill}${track} ${entry.value.padStart(valueWidth)}`;
|
|
3376
|
+
lines.push(truncateVisible(line, width));
|
|
3377
|
+
}
|
|
3378
|
+
return lines;
|
|
3379
|
+
}
|
|
3380
|
+
function renderPatternColumns(provider, width, noColor2) {
|
|
3381
|
+
const dayLines = renderPatternList("Day of Week", provider.dayOfWeek, Math.floor((width - 3) / 2), noColor2);
|
|
3382
|
+
const modelLines = renderPatternList("Top Models", provider.topModels, Math.floor((width - 3) / 2), noColor2);
|
|
3383
|
+
if (dayLines.length > 0 && modelLines.length > 0 && width >= 96) {
|
|
3384
|
+
return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((line) => ` ${line}`);
|
|
3385
|
+
}
|
|
3386
|
+
return [...dayLines, ...dayLines.length > 0 && modelLines.length > 0 ? [""] : [], ...modelLines];
|
|
3387
|
+
}
|
|
3388
|
+
function renderInsights(insights, width, noColor2) {
|
|
3389
|
+
if (insights.length === 0)
|
|
3390
|
+
return [];
|
|
3391
|
+
return [
|
|
3392
|
+
renderSectionTitle("Insights", noColor2),
|
|
3393
|
+
...insights.map((insight) => truncateVisible(` ${colorize("*", "green", noColor2)} ${insight}`, width))
|
|
3394
|
+
];
|
|
3395
|
+
}
|
|
3396
|
+
function renderProviderSection(provider, width, noColor2, showInsights) {
|
|
3397
|
+
const sections = [
|
|
3398
|
+
boxedHeader2(provider.provider.displayName, width, noColor2),
|
|
3399
|
+
...renderSummary(provider.summary, width, noColor2),
|
|
3400
|
+
...renderTrend(provider.trend, width, noColor2),
|
|
3401
|
+
"",
|
|
3402
|
+
renderSectionTitle("Heatmap", noColor2),
|
|
3403
|
+
renderTerminalHeatmap(provider.provider.daily, { width: width - 2, noColor: noColor2 }),
|
|
3404
|
+
"",
|
|
3405
|
+
renderSectionTitle("Stats", noColor2),
|
|
3406
|
+
...renderMetrics(provider.metrics, width, noColor2)
|
|
3407
|
+
];
|
|
3408
|
+
const patternLines = renderPatternColumns(provider, width, noColor2);
|
|
3409
|
+
if (patternLines.length > 0) {
|
|
3410
|
+
sections.push("", ...patternLines);
|
|
3411
|
+
}
|
|
3412
|
+
const insightLines = showInsights ? renderInsights(provider.insights, width, noColor2) : [];
|
|
3413
|
+
if (insightLines.length > 0) {
|
|
3414
|
+
sections.push("", ...insightLines);
|
|
3415
|
+
}
|
|
3416
|
+
return sections.join(`
|
|
3417
|
+
`);
|
|
3418
|
+
}
|
|
3419
|
+
function renderOverview(model, width, noColor2) {
|
|
3420
|
+
const sections = [
|
|
3421
|
+
boxedHeader2("Overview", width, noColor2),
|
|
3422
|
+
...renderSummary(model.overview.summary, width, noColor2),
|
|
3423
|
+
...renderTrend(model.overview.trend, width, noColor2),
|
|
3424
|
+
"",
|
|
3425
|
+
...renderMetrics(model.overview.metrics, width, noColor2)
|
|
3426
|
+
];
|
|
3427
|
+
if (model.overview.providerLeaders.length > 1) {
|
|
3428
|
+
sections.push("", ...renderPatternList("Provider Mix", model.overview.providerLeaders, width, noColor2));
|
|
2492
3429
|
}
|
|
2493
3430
|
return sections.join(`
|
|
2494
3431
|
`);
|
|
2495
3432
|
}
|
|
2496
|
-
function
|
|
3433
|
+
function renderDashboardModel(model, options) {
|
|
2497
3434
|
const width = options.width;
|
|
2498
3435
|
const noColor2 = options.noColor;
|
|
2499
|
-
const sections = [
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
3436
|
+
const sections = [
|
|
3437
|
+
boxedHeader2("Tokenleak", width, noColor2),
|
|
3438
|
+
...renderSummary([model.rangeLabel, ...model.overview.summary], width, noColor2)
|
|
3439
|
+
];
|
|
3440
|
+
if (model.activeProviders.length === 0) {
|
|
3441
|
+
sections.push("");
|
|
3442
|
+
sections.push(" No provider activity in the selected range.");
|
|
3443
|
+
if (model.inactiveProviders.length > 0) {
|
|
3444
|
+
sections.push(truncateVisible(` Checked: ${model.inactiveProviders.join(", ")}`, width));
|
|
3445
|
+
}
|
|
2504
3446
|
return sections.join(`
|
|
2505
3447
|
`);
|
|
2506
3448
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const
|
|
2510
|
-
sections.push(
|
|
2511
|
-
if (i < output.providers.length - 1) {
|
|
2512
|
-
sections.push("");
|
|
2513
|
-
sections.push(divider(width));
|
|
2514
|
-
sections.push("");
|
|
2515
|
-
}
|
|
3449
|
+
sections.push("", renderOverview(model, width, noColor2));
|
|
3450
|
+
for (const provider of model.activeProviders) {
|
|
3451
|
+
const providerSection = renderProviderSection(provider, width, noColor2, options.showInsights);
|
|
3452
|
+
sections.push("", divider(width), "", providerSection);
|
|
2516
3453
|
}
|
|
2517
|
-
if (
|
|
3454
|
+
if (model.inactiveProviders.length > 0) {
|
|
2518
3455
|
sections.push("");
|
|
2519
|
-
sections.push(
|
|
2520
|
-
sections.push("");
|
|
2521
|
-
sections.push(boxedHeader("Overall", width, noColor2));
|
|
2522
|
-
sections.push("");
|
|
2523
|
-
sections.push(renderStats(output.aggregated, width, noColor2));
|
|
3456
|
+
sections.push(truncateVisible(` No activity in range: ${model.inactiveProviders.join(", ")}`, width));
|
|
2524
3457
|
}
|
|
2525
3458
|
return sections.join(`
|
|
2526
3459
|
`);
|
|
2527
3460
|
}
|
|
2528
3461
|
|
|
2529
3462
|
// packages/renderers/dist/terminal/oneliner.js
|
|
2530
|
-
function
|
|
3463
|
+
function countActiveProviders(providers) {
|
|
3464
|
+
return providers.filter((provider) => provider.daily.some((entry) => entry.totalTokens > 0)).length;
|
|
3465
|
+
}
|
|
3466
|
+
function renderOneliner(output, options) {
|
|
2531
3467
|
const streak = output.aggregated.currentStreak;
|
|
2532
3468
|
const tokens = formatTokens(output.aggregated.totalTokens);
|
|
2533
3469
|
const cost = formatCost2(output.aggregated.totalCost);
|
|
2534
|
-
const providerCount = output.providers
|
|
2535
|
-
|
|
3470
|
+
const providerCount = countActiveProviders(output.providers);
|
|
3471
|
+
if (providerCount === 0) {
|
|
3472
|
+
return `no activity | ${tokens} tokens | ${cost}`;
|
|
3473
|
+
}
|
|
3474
|
+
return `${streak}d streak | ${tokens} tokens | ${cost} | ${providerCount} active`;
|
|
2536
3475
|
}
|
|
2537
3476
|
|
|
2538
3477
|
// packages/renderers/dist/terminal/terminal-renderer.js
|
|
2539
|
-
var
|
|
3478
|
+
var MIN_COMPACT_WIDTH = 32;
|
|
2540
3479
|
|
|
2541
3480
|
class TerminalRenderer {
|
|
2542
3481
|
format = "terminal";
|
|
@@ -2545,79 +3484,494 @@ class TerminalRenderer {
|
|
|
2545
3484
|
...options,
|
|
2546
3485
|
noColor: options.noColor
|
|
2547
3486
|
};
|
|
2548
|
-
if (effectiveOptions.width <
|
|
3487
|
+
if (effectiveOptions.width < MIN_COMPACT_WIDTH) {
|
|
2549
3488
|
return renderOneliner(output, effectiveOptions);
|
|
2550
3489
|
}
|
|
2551
|
-
|
|
3490
|
+
const model = buildDashboardModel(output, effectiveOptions);
|
|
3491
|
+
if (model.mode === "compact") {
|
|
3492
|
+
return renderCompactDashboard(model, effectiveOptions);
|
|
3493
|
+
}
|
|
3494
|
+
return renderDashboardModel(model, effectiveOptions);
|
|
2552
3495
|
}
|
|
2553
3496
|
}
|
|
2554
|
-
// packages/renderers/dist/
|
|
2555
|
-
var
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3497
|
+
// packages/renderers/dist/terminal/colors.js
|
|
3498
|
+
var ESC2 = "\x1B[";
|
|
3499
|
+
var RESET = `${ESC2}0m`;
|
|
3500
|
+
function colorize256(text2, code, noColor2) {
|
|
3501
|
+
if (noColor2)
|
|
3502
|
+
return text2;
|
|
3503
|
+
return `${ESC2}38;5;${code}m${text2}${RESET}`;
|
|
3504
|
+
}
|
|
3505
|
+
function bold256(text2, code, noColor2) {
|
|
3506
|
+
if (noColor2)
|
|
3507
|
+
return text2;
|
|
3508
|
+
return `${ESC2}1;38;5;${code}m${text2}${RESET}`;
|
|
3509
|
+
}
|
|
3510
|
+
function inverse256(text2, code, noColor2) {
|
|
3511
|
+
if (noColor2)
|
|
3512
|
+
return text2;
|
|
3513
|
+
return `${ESC2}7;38;5;${code}m${text2}${RESET}`;
|
|
3514
|
+
}
|
|
3515
|
+
function dim(text2, noColor2) {
|
|
3516
|
+
if (noColor2)
|
|
3517
|
+
return text2;
|
|
3518
|
+
return `${ESC2}2m${text2}${RESET}`;
|
|
3519
|
+
}
|
|
3520
|
+
function bold2(text2, noColor2) {
|
|
3521
|
+
if (noColor2)
|
|
3522
|
+
return text2;
|
|
3523
|
+
return `${ESC2}1m${text2}${RESET}`;
|
|
3524
|
+
}
|
|
3525
|
+
var DOW_COLORS = {
|
|
3526
|
+
Sun: 213,
|
|
3527
|
+
Mon: 33,
|
|
3528
|
+
Tue: 40,
|
|
3529
|
+
Wed: 208,
|
|
3530
|
+
Thu: 141,
|
|
3531
|
+
Fri: 220,
|
|
3532
|
+
Sat: 209
|
|
3533
|
+
};
|
|
3534
|
+
var TOD_COLORS = {
|
|
3535
|
+
"After midnight": 213,
|
|
3536
|
+
Morning: 208,
|
|
3537
|
+
Afternoon: 40,
|
|
3538
|
+
Evening: 33,
|
|
3539
|
+
Night: 141
|
|
3540
|
+
};
|
|
3541
|
+
var MODEL_COLORS = [
|
|
3542
|
+
33,
|
|
3543
|
+
40,
|
|
3544
|
+
208,
|
|
3545
|
+
141,
|
|
3546
|
+
220,
|
|
3547
|
+
209,
|
|
3548
|
+
213,
|
|
3549
|
+
51,
|
|
3550
|
+
196,
|
|
3551
|
+
118
|
|
2568
3552
|
];
|
|
2569
|
-
var
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
"NOV",
|
|
2581
|
-
"DEC"
|
|
3553
|
+
var PROJECT_COLORS = [
|
|
3554
|
+
40,
|
|
3555
|
+
33,
|
|
3556
|
+
208,
|
|
3557
|
+
213,
|
|
3558
|
+
220,
|
|
3559
|
+
141,
|
|
3560
|
+
209,
|
|
3561
|
+
51,
|
|
3562
|
+
196,
|
|
3563
|
+
118
|
|
2582
3564
|
];
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
3565
|
+
// packages/renderers/dist/terminal/tab-views/tab-bar.js
|
|
3566
|
+
var TIME_RANGES = ["7d", "30d", "90d", "365d"];
|
|
3567
|
+
var METRIC_TABS = ["overview", "sess", "tok", "model", "cwd", "dow", "tod"];
|
|
3568
|
+
var TAB_LABELS = {
|
|
3569
|
+
overview: "overview",
|
|
3570
|
+
sess: "sess",
|
|
3571
|
+
tok: "tok",
|
|
3572
|
+
model: "model",
|
|
3573
|
+
cwd: "cwd",
|
|
3574
|
+
dow: "dow",
|
|
3575
|
+
tod: "tod"
|
|
3576
|
+
};
|
|
3577
|
+
var ACTIVE_COLOR = 33;
|
|
3578
|
+
var HINT_COLOR = 220;
|
|
3579
|
+
function renderTabBar(activeRange, activeTab, width, noColor2) {
|
|
3580
|
+
const rangeParts = TIME_RANGES.map((r) => {
|
|
3581
|
+
if (r === activeRange) {
|
|
3582
|
+
return inverse256(` ${r} `, ACTIVE_COLOR, noColor2);
|
|
3583
|
+
}
|
|
3584
|
+
return dim(` ${r} `, noColor2);
|
|
3585
|
+
});
|
|
3586
|
+
const tabParts = METRIC_TABS.map((t) => {
|
|
3587
|
+
if (t === activeTab) {
|
|
3588
|
+
return inverse256(` ${TAB_LABELS[t]} `, ACTIVE_COLOR, noColor2);
|
|
3589
|
+
}
|
|
3590
|
+
return dim(` ${TAB_LABELS[t]} `, noColor2);
|
|
3591
|
+
});
|
|
3592
|
+
const separator = dim(" \u2502 ", noColor2);
|
|
3593
|
+
const rangeLine = ` ${rangeParts.join(" ")}${separator}${tabParts.join(" ")}`;
|
|
3594
|
+
const hints = [
|
|
3595
|
+
`${bold256("\u2190/\u2192", HINT_COLOR, noColor2)} range`,
|
|
3596
|
+
`${bold256("tab", HINT_COLOR, noColor2)} metric`,
|
|
3597
|
+
`${bold256("1-7", HINT_COLOR, noColor2)} jump`,
|
|
3598
|
+
`${bold256("\u2191/\u2193", HINT_COLOR, noColor2)} scroll`,
|
|
3599
|
+
`${bold256("q", HINT_COLOR, noColor2)} close`
|
|
3600
|
+
];
|
|
3601
|
+
const hintLine = ` ${hints.join(dim(" \xB7 ", noColor2))}`;
|
|
3602
|
+
return [
|
|
3603
|
+
truncateVisible(rangeLine, width),
|
|
3604
|
+
truncateVisible(hintLine, width)
|
|
3605
|
+
].join(`
|
|
3606
|
+
`);
|
|
2594
3607
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
3608
|
+
// packages/renderers/dist/terminal/tab-views/overview-view.js
|
|
3609
|
+
function renderOverviewView(output, options) {
|
|
3610
|
+
return renderDashboardModel(buildDashboardModel(output, options), options);
|
|
2597
3611
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
3612
|
+
// packages/renderers/dist/terminal/tab-views/dow-view.js
|
|
3613
|
+
var BAR_CHAR2 = "\u2588";
|
|
3614
|
+
var TRACK_CHAR2 = "\u2591";
|
|
3615
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
3616
|
+
function formatTokens2(count) {
|
|
3617
|
+
if (count >= 1e6)
|
|
3618
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
3619
|
+
if (count >= 1000)
|
|
3620
|
+
return `${(count / 1000).toFixed(0)}K`;
|
|
3621
|
+
return String(count);
|
|
2600
3622
|
}
|
|
2601
|
-
function
|
|
2602
|
-
|
|
2603
|
-
if (nonZero.length === 0)
|
|
2604
|
-
return [0, 0, 0];
|
|
2605
|
-
const q = (p) => {
|
|
2606
|
-
const idx = Math.floor(p * (nonZero.length - 1));
|
|
2607
|
-
return nonZero[idx] ?? 0;
|
|
2608
|
-
};
|
|
2609
|
-
return [q(0.25), q(0.5), q(0.75)];
|
|
3623
|
+
function formatCost3(cost) {
|
|
3624
|
+
return `$${cost.toFixed(2)}`;
|
|
2610
3625
|
}
|
|
2611
|
-
function
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
if (
|
|
2619
|
-
return
|
|
2620
|
-
|
|
3626
|
+
function renderDowView(output, width, noColor2) {
|
|
3627
|
+
const dow = output.aggregated.dayOfWeek;
|
|
3628
|
+
if (dow.length === 0) {
|
|
3629
|
+
return ` ${dim("No day-of-week data available.", noColor2)}`;
|
|
3630
|
+
}
|
|
3631
|
+
const maxTokens = Math.max(...dow.map((d) => d.tokens), 0);
|
|
3632
|
+
const totalTokens = dow.reduce((sum, d) => sum + d.tokens, 0);
|
|
3633
|
+
if (maxTokens <= 0) {
|
|
3634
|
+
return ` ${dim("No activity in the selected range.", noColor2)}`;
|
|
3635
|
+
}
|
|
3636
|
+
const lines = [bold2(" Day of Week", noColor2), ""];
|
|
3637
|
+
const nameWidth = 5;
|
|
3638
|
+
const valueWidth = 8;
|
|
3639
|
+
const costWidth = 10;
|
|
3640
|
+
const shareWidth = 6;
|
|
3641
|
+
const barWidth = Math.max(8, width - nameWidth - valueWidth - costWidth - shareWidth - 10);
|
|
3642
|
+
for (const entry of dow) {
|
|
3643
|
+
const label = DAY_NAMES[entry.day] ?? "???";
|
|
3644
|
+
const colorCode = DOW_COLORS[label] ?? 33;
|
|
3645
|
+
const share = totalTokens > 0 ? entry.tokens / totalTokens : 0;
|
|
3646
|
+
const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
|
|
3647
|
+
const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
|
|
3648
|
+
const bar = colorize256(BAR_CHAR2.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR2.repeat(Math.max(0, barWidth - fillLen)), noColor2);
|
|
3649
|
+
const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
|
|
3650
|
+
const tokStr = formatTokens2(entry.tokens).padStart(valueWidth);
|
|
3651
|
+
const costStr = formatCost3(entry.cost).padStart(costWidth);
|
|
3652
|
+
lines.push(truncateVisible(` ${colorize256(label.padEnd(nameWidth), colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
|
|
3653
|
+
}
|
|
3654
|
+
return lines.join(`
|
|
3655
|
+
`);
|
|
3656
|
+
}
|
|
3657
|
+
// packages/renderers/dist/terminal/tab-views/tod-view.js
|
|
3658
|
+
var BAR_CHAR3 = "\u2588";
|
|
3659
|
+
var TRACK_CHAR3 = "\u2591";
|
|
3660
|
+
var BUCKET_RANGES = [
|
|
3661
|
+
{ label: "After midnight", hours: "0-5", start: 0, end: 5 },
|
|
3662
|
+
{ label: "Morning", hours: "6-11", start: 6, end: 11 },
|
|
3663
|
+
{ label: "Afternoon", hours: "12-16", start: 12, end: 16 },
|
|
3664
|
+
{ label: "Evening", hours: "17-21", start: 17, end: 21 },
|
|
3665
|
+
{ label: "Night", hours: "22-23", start: 22, end: 23 }
|
|
3666
|
+
];
|
|
3667
|
+
function groupIntoBuckets(hourOfDay) {
|
|
3668
|
+
return BUCKET_RANGES.map(({ label, hours, start, end }) => {
|
|
3669
|
+
let tokens = 0;
|
|
3670
|
+
let cost = 0;
|
|
3671
|
+
let count = 0;
|
|
3672
|
+
for (let h = start;h <= end; h++) {
|
|
3673
|
+
const entry = hourOfDay[h];
|
|
3674
|
+
if (entry) {
|
|
3675
|
+
tokens += entry.tokens;
|
|
3676
|
+
cost += entry.cost;
|
|
3677
|
+
count += entry.count;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
return { label, hours, tokens, cost, count };
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
function formatTokens3(n) {
|
|
3684
|
+
if (n >= 1e6)
|
|
3685
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3686
|
+
if (n >= 1000)
|
|
3687
|
+
return `${(n / 1000).toFixed(0)}K`;
|
|
3688
|
+
return String(n);
|
|
3689
|
+
}
|
|
3690
|
+
function formatCost4(cost) {
|
|
3691
|
+
return `$${cost.toFixed(2)}`;
|
|
3692
|
+
}
|
|
3693
|
+
function renderTodView(output, width, noColor2) {
|
|
3694
|
+
const hourOfDay = output.more?.hourOfDay;
|
|
3695
|
+
if (!hourOfDay || hourOfDay.length === 0) {
|
|
3696
|
+
return ` ${dim("No event-level data available for time-of-day analysis.", noColor2)}`;
|
|
3697
|
+
}
|
|
3698
|
+
const buckets = groupIntoBuckets(hourOfDay);
|
|
3699
|
+
const maxTokens = Math.max(...buckets.map((b) => b.tokens), 0);
|
|
3700
|
+
const totalTokens = buckets.reduce((sum, b) => sum + b.tokens, 0);
|
|
3701
|
+
if (maxTokens <= 0) {
|
|
3702
|
+
return ` ${dim("No activity in the selected range.", noColor2)}`;
|
|
3703
|
+
}
|
|
3704
|
+
const lines = [bold2(" Time of Day (UTC)", noColor2), ""];
|
|
3705
|
+
const labelWidth = 16;
|
|
3706
|
+
const valueWidth = 8;
|
|
3707
|
+
const costWidth = 10;
|
|
3708
|
+
const shareWidth = 6;
|
|
3709
|
+
const barWidth = Math.max(8, width - labelWidth - valueWidth - costWidth - shareWidth - 10);
|
|
3710
|
+
for (const bucket of buckets) {
|
|
3711
|
+
const colorCode = TOD_COLORS[bucket.label] ?? 33;
|
|
3712
|
+
const share = totalTokens > 0 ? bucket.tokens / totalTokens : 0;
|
|
3713
|
+
const ratio = maxTokens > 0 ? bucket.tokens / maxTokens : 0;
|
|
3714
|
+
const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
|
|
3715
|
+
const bar = colorize256(BAR_CHAR3.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR3.repeat(Math.max(0, barWidth - fillLen)), noColor2);
|
|
3716
|
+
const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
|
|
3717
|
+
const tokStr = formatTokens3(bucket.tokens).padStart(valueWidth);
|
|
3718
|
+
const costStr = formatCost4(bucket.cost).padStart(costWidth);
|
|
3719
|
+
lines.push(truncateVisible(` ${colorize256(bucket.label.padEnd(labelWidth), colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
|
|
3720
|
+
}
|
|
3721
|
+
lines.push("");
|
|
3722
|
+
lines.push(` ${dim(`Hours shown in UTC. ${buckets.reduce((s, b) => s + b.count, 0)} events total.`, noColor2)}`);
|
|
3723
|
+
return lines.join(`
|
|
3724
|
+
`);
|
|
3725
|
+
}
|
|
3726
|
+
// packages/renderers/dist/terminal/tab-views/session-view.js
|
|
3727
|
+
function formatTokens4(n) {
|
|
3728
|
+
if (n >= 1e6)
|
|
3729
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3730
|
+
if (n >= 1000)
|
|
3731
|
+
return `${(n / 1000).toFixed(0)}K`;
|
|
3732
|
+
return String(n);
|
|
3733
|
+
}
|
|
3734
|
+
function formatCost5(cost) {
|
|
3735
|
+
return `$${cost.toFixed(2)}`;
|
|
3736
|
+
}
|
|
3737
|
+
function formatDuration2(ms) {
|
|
3738
|
+
if (ms < 60000)
|
|
3739
|
+
return `${(ms / 1000).toFixed(0)}s`;
|
|
3740
|
+
if (ms < 3600000)
|
|
3741
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
3742
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
3743
|
+
}
|
|
3744
|
+
function renderSessionView(output, width, noColor2) {
|
|
3745
|
+
const metrics = output.more?.sessionMetrics;
|
|
3746
|
+
if (!metrics || metrics.totalSessions === 0) {
|
|
3747
|
+
return ` ${dim("No event-level data available for session analysis.", noColor2)}`;
|
|
3748
|
+
}
|
|
3749
|
+
const lines = [bold2(" Sessions", noColor2), ""];
|
|
3750
|
+
const parts = [
|
|
3751
|
+
bold256(`${metrics.totalSessions}`, 33, noColor2) + " sessions",
|
|
3752
|
+
bold256(formatCost5(metrics.averageCost), 40, noColor2) + " avg/session",
|
|
3753
|
+
bold256(formatTokens4(metrics.averageTokens), 208, noColor2) + " avg tokens/session"
|
|
3754
|
+
];
|
|
3755
|
+
lines.push(truncateVisible(` ${parts.join(dim(" \xB7 ", noColor2))}`, width));
|
|
3756
|
+
lines.push("");
|
|
3757
|
+
const labelWidth = 24;
|
|
3758
|
+
const addMetric = (label, value) => {
|
|
3759
|
+
lines.push(truncateVisible(` ${dim(label.padEnd(labelWidth), noColor2)} ${bold2(value, noColor2)}`, width));
|
|
3760
|
+
};
|
|
3761
|
+
addMetric("Total sessions", String(metrics.totalSessions));
|
|
3762
|
+
addMetric("Avg tokens/session", formatTokens4(metrics.averageTokens));
|
|
3763
|
+
addMetric("Avg cost/session", formatCost5(metrics.averageCost));
|
|
3764
|
+
addMetric("Avg messages/session", metrics.averageMessages.toFixed(1));
|
|
3765
|
+
if (metrics.averageDurationMs !== null) {
|
|
3766
|
+
addMetric("Avg duration", formatDuration2(metrics.averageDurationMs));
|
|
3767
|
+
}
|
|
3768
|
+
addMetric("Projects", String(metrics.projectCount));
|
|
3769
|
+
if (metrics.longestSession) {
|
|
3770
|
+
lines.push("");
|
|
3771
|
+
lines.push(` ${bold2("Longest Session", noColor2)}`);
|
|
3772
|
+
addMetric(" Label", metrics.longestSession.label);
|
|
3773
|
+
addMetric(" Tokens", formatTokens4(metrics.longestSession.tokens));
|
|
3774
|
+
addMetric(" Cost", formatCost5(metrics.longestSession.cost));
|
|
3775
|
+
addMetric(" Messages", String(metrics.longestSession.count));
|
|
3776
|
+
if (metrics.longestSession.durationMs !== null) {
|
|
3777
|
+
addMetric(" Duration", formatDuration2(metrics.longestSession.durationMs));
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
if (metrics.topProject) {
|
|
3781
|
+
lines.push("");
|
|
3782
|
+
lines.push(truncateVisible(` ${dim("Top project:", noColor2)} ${bold256(metrics.topProject.name, 40, noColor2)} (${formatTokens4(metrics.topProject.tokens)})`, width));
|
|
3783
|
+
}
|
|
3784
|
+
return lines.join(`
|
|
3785
|
+
`);
|
|
3786
|
+
}
|
|
3787
|
+
// packages/renderers/dist/terminal/tab-views/model-view.js
|
|
3788
|
+
var BAR_CHAR4 = "\u2588";
|
|
3789
|
+
var TRACK_CHAR4 = "\u2591";
|
|
3790
|
+
function formatTokens5(n) {
|
|
3791
|
+
if (n >= 1e6)
|
|
3792
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3793
|
+
if (n >= 1000)
|
|
3794
|
+
return `${(n / 1000).toFixed(0)}K`;
|
|
3795
|
+
return String(n);
|
|
3796
|
+
}
|
|
3797
|
+
function formatCost6(cost) {
|
|
3798
|
+
return `$${cost.toFixed(2)}`;
|
|
3799
|
+
}
|
|
3800
|
+
function renderModelView(output, width, noColor2) {
|
|
3801
|
+
const models = output.aggregated.topModels;
|
|
3802
|
+
if (models.length === 0) {
|
|
3803
|
+
return ` ${dim("No model data available.", noColor2)}`;
|
|
3804
|
+
}
|
|
3805
|
+
const lines = [bold2(" Models", noColor2), ""];
|
|
3806
|
+
const nameWidth = Math.min(28, Math.max(12, Math.floor(width * 0.25)));
|
|
3807
|
+
const valueWidth = 8;
|
|
3808
|
+
const costWidth = 10;
|
|
3809
|
+
const shareWidth = 6;
|
|
3810
|
+
const barWidth = Math.max(8, width - nameWidth - valueWidth - costWidth - shareWidth - 10);
|
|
3811
|
+
const maxTokens = Math.max(...models.map((m) => m.tokens), 0);
|
|
3812
|
+
for (let i = 0;i < models.length; i++) {
|
|
3813
|
+
const model = models[i];
|
|
3814
|
+
const colorCode = MODEL_COLORS[i % MODEL_COLORS.length];
|
|
3815
|
+
const ratio = maxTokens > 0 ? model.tokens / maxTokens : 0;
|
|
3816
|
+
const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
|
|
3817
|
+
const bar = colorize256(BAR_CHAR4.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR4.repeat(Math.max(0, barWidth - fillLen)), noColor2);
|
|
3818
|
+
const shareStr = `${model.percentage.toFixed(0)}%`.padStart(shareWidth);
|
|
3819
|
+
const tokStr = formatTokens5(model.tokens).padStart(valueWidth);
|
|
3820
|
+
const costStr = formatCost6(model.cost).padStart(costWidth);
|
|
3821
|
+
const name = model.model.length > nameWidth ? model.model.slice(0, nameWidth - 1) + "\u2026" : model.model.padEnd(nameWidth);
|
|
3822
|
+
lines.push(truncateVisible(` ${colorize256(name, colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
|
|
3823
|
+
}
|
|
3824
|
+
const io = output.more?.inputOutput;
|
|
3825
|
+
if (io) {
|
|
3826
|
+
lines.push("");
|
|
3827
|
+
lines.push(` ${bold2("Input / Output Ratio", noColor2)}`);
|
|
3828
|
+
const inputShare = 1 - io.outputShare;
|
|
3829
|
+
const ioBarWidth = Math.max(10, width - 20);
|
|
3830
|
+
const inputLen = Math.round(inputShare * ioBarWidth);
|
|
3831
|
+
const outputLen = ioBarWidth - inputLen;
|
|
3832
|
+
const ioBar = colorize256(BAR_CHAR4.repeat(inputLen), 33, noColor2) + colorize256(BAR_CHAR4.repeat(outputLen), 40, noColor2);
|
|
3833
|
+
lines.push(truncateVisible(` ${ioBar} ${dim(`input ${(inputShare * 100).toFixed(0)}%`, noColor2)} ${dim(`output ${(io.outputShare * 100).toFixed(0)}%`, noColor2)}`, width));
|
|
3834
|
+
}
|
|
3835
|
+
return lines.join(`
|
|
3836
|
+
`);
|
|
3837
|
+
}
|
|
3838
|
+
// packages/renderers/dist/terminal/tab-views/token-view.js
|
|
3839
|
+
var BAR_CHAR5 = "\u2588";
|
|
3840
|
+
function formatTokens6(n) {
|
|
3841
|
+
if (n >= 1e6)
|
|
3842
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3843
|
+
if (n >= 1000)
|
|
3844
|
+
return `${(n / 1000).toFixed(0)}K`;
|
|
3845
|
+
return String(n);
|
|
3846
|
+
}
|
|
3847
|
+
function formatCost7(cost) {
|
|
3848
|
+
return `$${cost.toFixed(2)}`;
|
|
3849
|
+
}
|
|
3850
|
+
function renderTokenView(output, width, noColor2) {
|
|
3851
|
+
const stats = output.aggregated;
|
|
3852
|
+
const lines = [bold2(" Tokens", noColor2), ""];
|
|
3853
|
+
const parts = [
|
|
3854
|
+
bold256(formatTokens6(stats.totalTokens), 33, noColor2) + " total",
|
|
3855
|
+
bold256(formatCost7(stats.totalCost), 40, noColor2) + " cost",
|
|
3856
|
+
bold256(`${stats.activeDays}`, 208, noColor2) + " active days"
|
|
3857
|
+
];
|
|
3858
|
+
lines.push(truncateVisible(` ${parts.join(dim(" \xB7 ", noColor2))}`, width));
|
|
3859
|
+
lines.push("");
|
|
3860
|
+
lines.push(` ${bold2("Heatmap", noColor2)}`);
|
|
3861
|
+
const merged = output.providers.flatMap((p) => p.daily);
|
|
3862
|
+
lines.push(renderTerminalHeatmap(merged, { width: width - 2, noColor: noColor2 }));
|
|
3863
|
+
lines.push("");
|
|
3864
|
+
const io = output.more?.inputOutput;
|
|
3865
|
+
if (io) {
|
|
3866
|
+
lines.push(` ${bold2("Input / Output", noColor2)}`);
|
|
3867
|
+
const inputShare = 1 - io.outputShare;
|
|
3868
|
+
const barWidth = Math.max(10, width - 20);
|
|
3869
|
+
const inputLen = Math.round(inputShare * barWidth);
|
|
3870
|
+
const outputLen = barWidth - inputLen;
|
|
3871
|
+
const bar = colorize256(BAR_CHAR5.repeat(inputLen), 33, noColor2) + colorize256(BAR_CHAR5.repeat(outputLen), 40, noColor2);
|
|
3872
|
+
lines.push(truncateVisible(` ${bar} ${dim(`in ${(inputShare * 100).toFixed(0)}%`, noColor2)} ${dim(`out ${(io.outputShare * 100).toFixed(0)}%`, noColor2)}`, width));
|
|
3873
|
+
lines.push("");
|
|
3874
|
+
}
|
|
3875
|
+
const cache = output.more?.cacheEconomics;
|
|
3876
|
+
if (cache) {
|
|
3877
|
+
lines.push(` ${bold2("Cache Economics", noColor2)}`);
|
|
3878
|
+
const labelWidth = 20;
|
|
3879
|
+
const addLine = (label, value) => {
|
|
3880
|
+
lines.push(truncateVisible(` ${dim(label.padEnd(labelWidth), noColor2)} ${bold2(value, noColor2)}`, width));
|
|
3881
|
+
};
|
|
3882
|
+
addLine("Read tokens", formatTokens6(cache.readTokens));
|
|
3883
|
+
addLine("Write tokens", formatTokens6(cache.writeTokens));
|
|
3884
|
+
addLine("Read coverage", `${(cache.readCoverage * 100).toFixed(1)}%`);
|
|
3885
|
+
if (cache.reuseRatio !== null) {
|
|
3886
|
+
addLine("Reuse ratio", `${cache.reuseRatio.toFixed(1)}x`);
|
|
3887
|
+
}
|
|
3888
|
+
lines.push("");
|
|
3889
|
+
}
|
|
3890
|
+
const burn = output.more?.monthlyBurn;
|
|
3891
|
+
if (burn) {
|
|
3892
|
+
lines.push(` ${bold2("Monthly Burn Projection", noColor2)}`);
|
|
3893
|
+
const labelWidth = 20;
|
|
3894
|
+
lines.push(truncateVisible(` ${dim("Projected tokens".padEnd(labelWidth), noColor2)} ${bold2(formatTokens6(burn.projectedTokens), noColor2)}`, width));
|
|
3895
|
+
lines.push(truncateVisible(` ${dim("Projected cost".padEnd(labelWidth), noColor2)} ${bold2(formatCost7(burn.projectedCost), noColor2)}`, width));
|
|
3896
|
+
lines.push(truncateVisible(` ${dim("Observed days".padEnd(labelWidth), noColor2)} ${bold2(`${burn.observedDays} / ${burn.calendarDays}`, noColor2)}`, width));
|
|
3897
|
+
}
|
|
3898
|
+
return lines.join(`
|
|
3899
|
+
`);
|
|
3900
|
+
}
|
|
3901
|
+
// packages/renderers/dist/terminal/tab-views/cwd-view.js
|
|
3902
|
+
var BAR_CHAR6 = "\u2588";
|
|
3903
|
+
var TRACK_CHAR5 = "\u2591";
|
|
3904
|
+
function formatTokens7(n) {
|
|
3905
|
+
if (n >= 1e6)
|
|
3906
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
3907
|
+
if (n >= 1000)
|
|
3908
|
+
return `${(n / 1000).toFixed(0)}K`;
|
|
3909
|
+
return String(n);
|
|
3910
|
+
}
|
|
3911
|
+
function renderCwdView(output, width, noColor2) {
|
|
3912
|
+
const breakdown = output.more?.sessionMetrics?.projectBreakdown;
|
|
3913
|
+
if (!breakdown || breakdown.length === 0) {
|
|
3914
|
+
return ` ${dim("No event-level data available for project breakdown.", noColor2)}`;
|
|
3915
|
+
}
|
|
3916
|
+
const lines = [bold2(" Projects", noColor2), ""];
|
|
3917
|
+
const maxTokens = Math.max(...breakdown.map((p) => p.tokens), 0);
|
|
3918
|
+
const totalTokens = breakdown.reduce((sum, p) => sum + p.tokens, 0);
|
|
3919
|
+
if (maxTokens <= 0) {
|
|
3920
|
+
return ` ${dim("No project activity in the selected range.", noColor2)}`;
|
|
3921
|
+
}
|
|
3922
|
+
const nameWidth = Math.min(30, Math.max(12, Math.floor(width * 0.3)));
|
|
3923
|
+
const valueWidth = 8;
|
|
3924
|
+
const shareWidth = 6;
|
|
3925
|
+
const barWidth = Math.max(8, width - nameWidth - valueWidth - shareWidth - 8);
|
|
3926
|
+
for (let i = 0;i < breakdown.length; i++) {
|
|
3927
|
+
const project = breakdown[i];
|
|
3928
|
+
const colorCode = PROJECT_COLORS[i % PROJECT_COLORS.length];
|
|
3929
|
+
const ratio = maxTokens > 0 ? project.tokens / maxTokens : 0;
|
|
3930
|
+
const share = totalTokens > 0 ? project.tokens / totalTokens : 0;
|
|
3931
|
+
const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
|
|
3932
|
+
const bar = colorize256(BAR_CHAR6.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR5.repeat(Math.max(0, barWidth - fillLen)), noColor2);
|
|
3933
|
+
const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
|
|
3934
|
+
const tokStr = formatTokens7(project.tokens).padStart(valueWidth);
|
|
3935
|
+
const name = project.name.length > nameWidth ? project.name.slice(0, nameWidth - 1) + "\u2026" : project.name.padEnd(nameWidth);
|
|
3936
|
+
lines.push(truncateVisible(` ${colorize256(name, colorCode, noColor2)} ${bar} ${shareStr} ${tokStr}`, width));
|
|
3937
|
+
}
|
|
3938
|
+
lines.push("");
|
|
3939
|
+
lines.push(` ${dim(`${breakdown.length} project${breakdown.length === 1 ? "" : "s"} shown (top 10 by tokens)`, noColor2)}`);
|
|
3940
|
+
return lines.join(`
|
|
3941
|
+
`);
|
|
3942
|
+
}
|
|
3943
|
+
// packages/renderers/dist/live/template.js
|
|
3944
|
+
var MONTH_NAMES_FULL2 = [
|
|
3945
|
+
"JAN",
|
|
3946
|
+
"FEB",
|
|
3947
|
+
"MAR",
|
|
3948
|
+
"APR",
|
|
3949
|
+
"MAY",
|
|
3950
|
+
"JUN",
|
|
3951
|
+
"JUL",
|
|
3952
|
+
"AUG",
|
|
3953
|
+
"SEP",
|
|
3954
|
+
"OCT",
|
|
3955
|
+
"NOV",
|
|
3956
|
+
"DEC"
|
|
3957
|
+
];
|
|
3958
|
+
function formatDateRange2(since, until) {
|
|
3959
|
+
const s = new Date(since + "T00:00:00Z");
|
|
3960
|
+
const u = new Date(until + "T00:00:00Z");
|
|
3961
|
+
const diffMs = u.getTime() - s.getTime();
|
|
3962
|
+
const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
3963
|
+
const sMonth = MONTH_NAMES_FULL2[s.getUTCMonth()] ?? "";
|
|
3964
|
+
const uMonth = MONTH_NAMES_FULL2[u.getUTCMonth()] ?? "";
|
|
3965
|
+
return `${sMonth} ${s.getUTCFullYear()} — ${uMonth} ${u.getUTCFullYear()} · ${days} DAYS`;
|
|
3966
|
+
}
|
|
3967
|
+
function formatPercentage2(rate) {
|
|
3968
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
3969
|
+
}
|
|
3970
|
+
function formatStreak2(n) {
|
|
3971
|
+
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
3972
|
+
}
|
|
3973
|
+
function esc(s) {
|
|
3974
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2621
3975
|
}
|
|
2622
3976
|
function hexToRgb2(hex) {
|
|
2623
3977
|
const h = hex.replace("#", "");
|
|
@@ -2646,44 +4000,32 @@ function buildHeatmapScale2(colors, isDark) {
|
|
|
2646
4000
|
})
|
|
2647
4001
|
];
|
|
2648
4002
|
}
|
|
2649
|
-
function buildHeatmapCells(
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
4003
|
+
function buildHeatmapCells(provider, since, until) {
|
|
4004
|
+
const model = buildHeatmapModel(provider.daily, { since, until });
|
|
4005
|
+
if (!model) {
|
|
4006
|
+
return { cells: [], months: [], totalCols: 0 };
|
|
2653
4007
|
}
|
|
2654
|
-
const dates = daily.map((d) => d.date).sort();
|
|
2655
|
-
const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
2656
|
-
const startStr = since ?? dates[0] ?? endStr;
|
|
2657
|
-
const end = new Date(endStr + "T00:00:00Z");
|
|
2658
|
-
const start = new Date(startStr + "T00:00:00Z");
|
|
2659
|
-
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
2660
|
-
const allTokens = Array.from(tokenMap.values());
|
|
2661
|
-
const quantiles = computeQuantiles3(allTokens);
|
|
2662
4008
|
const cells = [];
|
|
2663
4009
|
const months = [];
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
months.push({ label: MONTH_NAMES2[month] ?? "", col });
|
|
4010
|
+
for (const marker of model.monthMarkers) {
|
|
4011
|
+
months.push({ label: marker.label, col: marker.weekIndex });
|
|
4012
|
+
}
|
|
4013
|
+
for (const week of model.weeks) {
|
|
4014
|
+
for (const day of week.days) {
|
|
4015
|
+
cells.push({
|
|
4016
|
+
date: day.date,
|
|
4017
|
+
tokens: day.tokens,
|
|
4018
|
+
level: day.level,
|
|
4019
|
+
row: day.dayIndex,
|
|
4020
|
+
col: week.index
|
|
4021
|
+
});
|
|
2677
4022
|
}
|
|
2678
|
-
if (row === 6)
|
|
2679
|
-
col++;
|
|
2680
|
-
current.setUTCDate(current.getUTCDate() + 1);
|
|
2681
4023
|
}
|
|
2682
|
-
return { cells, months, totalCols:
|
|
4024
|
+
return { cells, months, totalCols: model.weeks.length };
|
|
2683
4025
|
}
|
|
2684
4026
|
function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
|
|
2685
4027
|
const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
|
|
2686
|
-
const { cells, months, totalCols } = buildHeatmapCells(provider
|
|
4028
|
+
const { cells, months, totalCols } = buildHeatmapCells(provider, since, until);
|
|
2687
4029
|
const heatmapWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
|
|
2688
4030
|
const heatmapHeight = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
|
|
2689
4031
|
const cellsHtml = cells.map((c) => {
|
|
@@ -3001,54 +4343,1353 @@ function loadConfig() {
|
|
|
3001
4343
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
3002
4344
|
return parsed;
|
|
3003
4345
|
}
|
|
3004
|
-
return {};
|
|
3005
|
-
} catch {
|
|
3006
|
-
return {};
|
|
3007
|
-
}
|
|
4346
|
+
return {};
|
|
4347
|
+
} catch {
|
|
4348
|
+
return {};
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
// packages/cli/src/errors.ts
|
|
4353
|
+
class TokenleakError extends Error {
|
|
4354
|
+
constructor(message) {
|
|
4355
|
+
super(message);
|
|
4356
|
+
this.name = "TokenleakError";
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
function handleError(error) {
|
|
4360
|
+
if (error instanceof TokenleakError) {
|
|
4361
|
+
process.stderr.write(`Error: ${error.message}
|
|
4362
|
+
`);
|
|
4363
|
+
} else if (error instanceof Error) {
|
|
4364
|
+
process.stderr.write(`Error: ${error.message}
|
|
4365
|
+
`);
|
|
4366
|
+
} else {
|
|
4367
|
+
process.stderr.write(`Error: ${String(error)}
|
|
4368
|
+
`);
|
|
4369
|
+
}
|
|
4370
|
+
process.exit(1);
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
// packages/cli/src/date-range.ts
|
|
4374
|
+
var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
|
4375
|
+
function isValidDate(dateStr) {
|
|
4376
|
+
if (!DATE_FORMAT.test(dateStr))
|
|
4377
|
+
return false;
|
|
4378
|
+
const d = new Date(dateStr + "T00:00:00Z");
|
|
4379
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
|
|
4380
|
+
}
|
|
4381
|
+
function computeDateRange(args) {
|
|
4382
|
+
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
4383
|
+
if (args.until && !isValidDate(args.until)) {
|
|
4384
|
+
throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
|
|
4385
|
+
}
|
|
4386
|
+
if (args.since && !isValidDate(args.since)) {
|
|
4387
|
+
throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
|
|
4388
|
+
}
|
|
4389
|
+
let since;
|
|
4390
|
+
if (args.since) {
|
|
4391
|
+
since = args.since;
|
|
4392
|
+
} else {
|
|
4393
|
+
const daysBack = args.days ?? DEFAULT_DAYS;
|
|
4394
|
+
const d = new Date(until);
|
|
4395
|
+
d.setDate(d.getDate() - daysBack);
|
|
4396
|
+
since = d.toISOString().slice(0, 10);
|
|
4397
|
+
}
|
|
4398
|
+
if (since > until) {
|
|
4399
|
+
throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
|
|
4400
|
+
}
|
|
4401
|
+
return { since, until };
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
// packages/cli/src/env.ts
|
|
4405
|
+
var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
|
|
4406
|
+
var VALID_THEMES = new Set(["dark", "light"]);
|
|
4407
|
+
function loadEnvOverrides() {
|
|
4408
|
+
const overrides = {};
|
|
4409
|
+
const format = process.env["TOKENLEAK_FORMAT"];
|
|
4410
|
+
if (format && VALID_FORMATS.has(format)) {
|
|
4411
|
+
overrides.format = format;
|
|
4412
|
+
}
|
|
4413
|
+
const theme = process.env["TOKENLEAK_THEME"];
|
|
4414
|
+
if (theme && VALID_THEMES.has(theme)) {
|
|
4415
|
+
overrides.theme = theme;
|
|
4416
|
+
}
|
|
4417
|
+
const days = process.env["TOKENLEAK_DAYS"];
|
|
4418
|
+
if (days !== undefined && days !== "") {
|
|
4419
|
+
const parsed = Number(days);
|
|
4420
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
4421
|
+
overrides.days = parsed;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
return overrides;
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
// packages/cli/src/flags.ts
|
|
4428
|
+
var CLI_FLAG_ORDER = [
|
|
4429
|
+
"format",
|
|
4430
|
+
"theme",
|
|
4431
|
+
"since",
|
|
4432
|
+
"until",
|
|
4433
|
+
"days",
|
|
4434
|
+
"output",
|
|
4435
|
+
"width",
|
|
4436
|
+
"provider",
|
|
4437
|
+
"compare",
|
|
4438
|
+
"upload",
|
|
4439
|
+
"claude",
|
|
4440
|
+
"codex",
|
|
4441
|
+
"openCode",
|
|
4442
|
+
"allProviders",
|
|
4443
|
+
"listProviders",
|
|
4444
|
+
"more",
|
|
4445
|
+
"clipboard",
|
|
4446
|
+
"open",
|
|
4447
|
+
"liveServer",
|
|
4448
|
+
"noColor",
|
|
4449
|
+
"noInsights"
|
|
4450
|
+
];
|
|
4451
|
+
var CLI_FLAG_NAMES = {
|
|
4452
|
+
format: "--format",
|
|
4453
|
+
theme: "--theme",
|
|
4454
|
+
since: "--since",
|
|
4455
|
+
until: "--until",
|
|
4456
|
+
days: "--days",
|
|
4457
|
+
output: "--output",
|
|
4458
|
+
width: "--width",
|
|
4459
|
+
provider: "--provider",
|
|
4460
|
+
compare: "--compare",
|
|
4461
|
+
upload: "--upload",
|
|
4462
|
+
claude: "--claude",
|
|
4463
|
+
codex: "--codex",
|
|
4464
|
+
openCode: "--open-code",
|
|
4465
|
+
allProviders: "--all-providers",
|
|
4466
|
+
listProviders: "--list-providers",
|
|
4467
|
+
more: "--more",
|
|
4468
|
+
clipboard: "--clipboard",
|
|
4469
|
+
open: "--open",
|
|
4470
|
+
liveServer: "--live-server",
|
|
4471
|
+
noColor: "--no-color",
|
|
4472
|
+
noInsights: "--no-insights"
|
|
4473
|
+
};
|
|
4474
|
+
function buildCliArgTokens(cliArgs) {
|
|
4475
|
+
const tokens = [];
|
|
4476
|
+
for (const key of CLI_FLAG_ORDER) {
|
|
4477
|
+
const value = cliArgs[key];
|
|
4478
|
+
if (value === undefined || value === false || value === null) {
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
const flag = CLI_FLAG_NAMES[key];
|
|
4482
|
+
if (!flag)
|
|
4483
|
+
continue;
|
|
4484
|
+
tokens.push(flag);
|
|
4485
|
+
if (value !== true) {
|
|
4486
|
+
tokens.push(String(value));
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
return tokens;
|
|
4490
|
+
}
|
|
4491
|
+
function buildCliPreview(cliArgs) {
|
|
4492
|
+
const tokens = buildCliArgTokens(cliArgs);
|
|
4493
|
+
return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
// packages/cli/src/interactive.ts
|
|
4497
|
+
import { emitKeypressEvents } from "readline";
|
|
4498
|
+
import { createInterface } from "readline/promises";
|
|
4499
|
+
var INTERACTIVE_FLAG_LINES = [
|
|
4500
|
+
"-f, --format <format> terminal | png | svg | json",
|
|
4501
|
+
"-t, --theme <theme> dark | light",
|
|
4502
|
+
"-s, --since <date> YYYY-MM-DD start date",
|
|
4503
|
+
"-u, --until <date> YYYY-MM-DD end date",
|
|
4504
|
+
"-d, --days <number> trailing days window",
|
|
4505
|
+
"-o, --output <path> write output to a file",
|
|
4506
|
+
"-w, --width <number> terminal render width",
|
|
4507
|
+
"-p, --provider <list> comma-separated providers",
|
|
4508
|
+
" --claude shortcut for Claude Code",
|
|
4509
|
+
" --codex shortcut for Codex",
|
|
4510
|
+
" --open-code shortcut for Open Code",
|
|
4511
|
+
" --all-providers ignore provider filters",
|
|
4512
|
+
" --list-providers show provider registry",
|
|
4513
|
+
" --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
|
|
4514
|
+
" --more richer PNG/SVG stats",
|
|
4515
|
+
" --clipboard copy rendered output",
|
|
4516
|
+
" --open open generated file",
|
|
4517
|
+
" --upload <target> gist",
|
|
4518
|
+
"-L, --live-server local interactive dashboard",
|
|
4519
|
+
" --no-color disable ANSI colors",
|
|
4520
|
+
" --no-insights hide terminal insights",
|
|
4521
|
+
" --help print help",
|
|
4522
|
+
" --version print version"
|
|
4523
|
+
];
|
|
4524
|
+
var TAB_RANGE_DAY_COUNTS = {
|
|
4525
|
+
"7d": 7,
|
|
4526
|
+
"30d": 30,
|
|
4527
|
+
"90d": 90,
|
|
4528
|
+
"365d": 365
|
|
4529
|
+
};
|
|
4530
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
4531
|
+
var ESC3 = "\x1B[";
|
|
4532
|
+
var RESET2 = `${ESC3}0m`;
|
|
4533
|
+
var BOLD = `${ESC3}1m`;
|
|
4534
|
+
var DIM = `${ESC3}2m`;
|
|
4535
|
+
var CYAN = `${ESC3}36m`;
|
|
4536
|
+
var GREEN = `${ESC3}32m`;
|
|
4537
|
+
var YELLOW = `${ESC3}33m`;
|
|
4538
|
+
var RED = `${ESC3}31m`;
|
|
4539
|
+
var WHITE = `${ESC3}97m`;
|
|
4540
|
+
var HOME_CLEAR = "\x1B[H\x1B[J";
|
|
4541
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
4542
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
4543
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
4544
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
4545
|
+
var ALT_SCROLL_ON = "\x1B[?1007h";
|
|
4546
|
+
var ALT_SCROLL_OFF = "\x1B[?1007l";
|
|
4547
|
+
var LOADING_TICK_MS = 120;
|
|
4548
|
+
function color(text2, code) {
|
|
4549
|
+
return `${code}${text2}${RESET2}`;
|
|
4550
|
+
}
|
|
4551
|
+
function stripAnsi2(text2) {
|
|
4552
|
+
return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
4553
|
+
}
|
|
4554
|
+
function visibleLength2(text2) {
|
|
4555
|
+
return stripAnsi2(text2).length;
|
|
4556
|
+
}
|
|
4557
|
+
function padVisible2(text2, width) {
|
|
4558
|
+
const padding = Math.max(0, width - visibleLength2(text2));
|
|
4559
|
+
return text2 + " ".repeat(padding);
|
|
4560
|
+
}
|
|
4561
|
+
function truncateVisible2(text2, width) {
|
|
4562
|
+
if (width <= 0)
|
|
4563
|
+
return "";
|
|
4564
|
+
const plain = stripAnsi2(text2);
|
|
4565
|
+
if (plain.length <= width)
|
|
4566
|
+
return text2;
|
|
4567
|
+
const limit = width <= 3 ? width : width - 3;
|
|
4568
|
+
let visibleCount = 0;
|
|
4569
|
+
let index = 0;
|
|
4570
|
+
let result = "";
|
|
4571
|
+
let sawAnsi = false;
|
|
4572
|
+
while (index < text2.length && visibleCount < limit) {
|
|
4573
|
+
if (text2[index] === "\x1B") {
|
|
4574
|
+
const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
|
|
4575
|
+
if (match) {
|
|
4576
|
+
result += match[0];
|
|
4577
|
+
index += match[0].length;
|
|
4578
|
+
sawAnsi = true;
|
|
4579
|
+
continue;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
result += text2[index];
|
|
4583
|
+
index += 1;
|
|
4584
|
+
visibleCount += 1;
|
|
4585
|
+
}
|
|
4586
|
+
if (width <= 3) {
|
|
4587
|
+
return sawAnsi ? `${result}${RESET2}` : result;
|
|
4588
|
+
}
|
|
4589
|
+
return sawAnsi ? `${result}...${RESET2}` : `${result}...`;
|
|
4590
|
+
}
|
|
4591
|
+
function joinColumns(left, right, totalWidth) {
|
|
4592
|
+
const gutter = 3;
|
|
4593
|
+
const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
|
|
4594
|
+
const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
|
|
4595
|
+
const rows = Math.max(left.length, right.length);
|
|
4596
|
+
const lines = [];
|
|
4597
|
+
for (let index = 0;index < rows; index++) {
|
|
4598
|
+
const leftLine = truncateVisible2(left[index] ?? "", leftWidth);
|
|
4599
|
+
const rightLine = truncateVisible2(right[index] ?? "", rightWidth);
|
|
4600
|
+
lines.push(`${padVisible2(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
|
|
4601
|
+
}
|
|
4602
|
+
return lines;
|
|
4603
|
+
}
|
|
4604
|
+
function renderRule(width) {
|
|
4605
|
+
return color("-".repeat(width), DIM);
|
|
4606
|
+
}
|
|
4607
|
+
function describeRequest(args) {
|
|
4608
|
+
const output = typeof args["output"] === "string" ? args["output"] : null;
|
|
4609
|
+
if (args["liveServer"]) {
|
|
4610
|
+
return {
|
|
4611
|
+
title: "Live Dashboard",
|
|
4612
|
+
loadingTitle: "Starting live dashboard",
|
|
4613
|
+
loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
|
|
4614
|
+
executionMode: "inherit"
|
|
4615
|
+
};
|
|
4616
|
+
}
|
|
4617
|
+
if (args["listProviders"]) {
|
|
4618
|
+
return {
|
|
4619
|
+
title: "Provider Registry",
|
|
4620
|
+
loadingTitle: "Loading provider registry",
|
|
4621
|
+
loadingDetail: "Checking registered providers and current availability.",
|
|
4622
|
+
executionMode: "capture"
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
if (args["compare"]) {
|
|
4626
|
+
return {
|
|
4627
|
+
title: "Compare Report",
|
|
4628
|
+
loadingTitle: "Building compare report",
|
|
4629
|
+
loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
|
|
4630
|
+
executionMode: "capture"
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
switch (args["format"]) {
|
|
4634
|
+
case "json":
|
|
4635
|
+
return {
|
|
4636
|
+
title: "JSON Export",
|
|
4637
|
+
loadingTitle: "Generating JSON report",
|
|
4638
|
+
loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
|
|
4639
|
+
executionMode: "capture"
|
|
4640
|
+
};
|
|
4641
|
+
case "svg":
|
|
4642
|
+
return {
|
|
4643
|
+
title: "SVG Export",
|
|
4644
|
+
loadingTitle: "Rendering SVG",
|
|
4645
|
+
loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
|
|
4646
|
+
executionMode: "capture"
|
|
4647
|
+
};
|
|
4648
|
+
case "png":
|
|
4649
|
+
return {
|
|
4650
|
+
title: "PNG Export",
|
|
4651
|
+
loadingTitle: "Rendering PNG",
|
|
4652
|
+
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.",
|
|
4653
|
+
executionMode: "capture"
|
|
4654
|
+
};
|
|
4655
|
+
default:
|
|
4656
|
+
return {
|
|
4657
|
+
title: "Terminal Dashboard",
|
|
4658
|
+
loadingTitle: "Generating terminal dashboard",
|
|
4659
|
+
loadingDetail: "Reading provider logs and aggregating token usage.",
|
|
4660
|
+
executionMode: "capture"
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
function finalizeCliArgs(args) {
|
|
4665
|
+
const finalized = { ...args };
|
|
4666
|
+
const format = finalized["format"];
|
|
4667
|
+
if (finalized["compare"] && (format === "png" || format === "svg")) {
|
|
4668
|
+
finalized["more"] = true;
|
|
4669
|
+
}
|
|
4670
|
+
if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
|
|
4671
|
+
if (format === "png" || format === "svg" || format === "json") {
|
|
4672
|
+
finalized["output"] = `tokenleak.${format}`;
|
|
4673
|
+
} else {
|
|
4674
|
+
delete finalized["open"];
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
if (format === "png") {
|
|
4678
|
+
delete finalized["clipboard"];
|
|
4679
|
+
delete finalized["upload"];
|
|
4680
|
+
}
|
|
4681
|
+
return finalized;
|
|
4682
|
+
}
|
|
4683
|
+
function createRunCommand(args) {
|
|
4684
|
+
const finalizedArgs = finalizeCliArgs(args);
|
|
4685
|
+
return {
|
|
4686
|
+
type: "run",
|
|
4687
|
+
request: {
|
|
4688
|
+
args: finalizedArgs,
|
|
4689
|
+
preview: buildCliPreview(finalizedArgs),
|
|
4690
|
+
...describeRequest(finalizedArgs)
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
function renderMenu(options, selectedIndex) {
|
|
4695
|
+
return options.map((option, index) => {
|
|
4696
|
+
const isSelected = index === selectedIndex;
|
|
4697
|
+
const prefix = isSelected ? color(">", GREEN) : " ";
|
|
4698
|
+
const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
|
|
4699
|
+
const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
|
|
4700
|
+
const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
|
|
4701
|
+
return `${prefix} [${number}] ${title} ${description}`;
|
|
4702
|
+
});
|
|
4703
|
+
}
|
|
4704
|
+
function renderFlagPanel() {
|
|
4705
|
+
return [
|
|
4706
|
+
color("All Flags", WHITE + BOLD),
|
|
4707
|
+
color("Every flag remains available while using the launcher.", DIM),
|
|
4708
|
+
"",
|
|
4709
|
+
...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
|
|
4710
|
+
];
|
|
4711
|
+
}
|
|
4712
|
+
function renderMenuPanel(context, options, selectedIndex) {
|
|
4713
|
+
const selected = options[selectedIndex];
|
|
4714
|
+
return [
|
|
4715
|
+
color("Tokenleak Interactive Launcher", WHITE + BOLD),
|
|
4716
|
+
`${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
|
|
4717
|
+
"",
|
|
4718
|
+
color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
|
|
4719
|
+
color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
|
|
4720
|
+
"",
|
|
4721
|
+
...renderMenu(options, selectedIndex),
|
|
4722
|
+
"",
|
|
4723
|
+
color("Preview", WHITE + BOLD),
|
|
4724
|
+
color(selected.preview, GREEN),
|
|
4725
|
+
"",
|
|
4726
|
+
color("Keys", WHITE + BOLD),
|
|
4727
|
+
`${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
|
|
4728
|
+
"",
|
|
4729
|
+
renderRule(44)
|
|
4730
|
+
];
|
|
4731
|
+
}
|
|
4732
|
+
function renderHelpOverlay(helpText, width) {
|
|
4733
|
+
const lines = helpText.trimEnd().split(`
|
|
4734
|
+
`);
|
|
4735
|
+
const header = [
|
|
4736
|
+
color("Tokenleak Help", WHITE + BOLD),
|
|
4737
|
+
color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
|
|
4738
|
+
""
|
|
4739
|
+
];
|
|
4740
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible2(line, width))].join(`
|
|
4741
|
+
`)}`;
|
|
4742
|
+
}
|
|
4743
|
+
function renderLauncher(context, options, selectedIndex) {
|
|
4744
|
+
const width = process.stdout.columns ?? 120;
|
|
4745
|
+
const menuPanel = renderMenuPanel(context, options, selectedIndex);
|
|
4746
|
+
const flagPanel = renderFlagPanel();
|
|
4747
|
+
const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
|
|
4748
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
4749
|
+
`)}`;
|
|
4750
|
+
}
|
|
4751
|
+
function renderProgressBar(frame, width) {
|
|
4752
|
+
const innerWidth = Math.max(12, width - 2);
|
|
4753
|
+
const headSize = Math.max(4, Math.floor(innerWidth / 5));
|
|
4754
|
+
const travel = Math.max(1, innerWidth - headSize);
|
|
4755
|
+
const cycle = travel * 2;
|
|
4756
|
+
const offset = frame % cycle;
|
|
4757
|
+
const start = offset <= travel ? offset : cycle - offset;
|
|
4758
|
+
const cells = Array.from({ length: innerWidth }, (_, index) => {
|
|
4759
|
+
if (index >= start && index < start + headSize) {
|
|
4760
|
+
return "=";
|
|
4761
|
+
}
|
|
4762
|
+
return "-";
|
|
4763
|
+
}).join("");
|
|
4764
|
+
return color(`[${cells}]`, CYAN);
|
|
4765
|
+
}
|
|
4766
|
+
function renderLoading(request, frame = 0, startedAt = Date.now()) {
|
|
4767
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
|
|
4768
|
+
const progressBar = renderProgressBar(frame, 34);
|
|
4769
|
+
const lines = [
|
|
4770
|
+
color(request.loadingTitle, WHITE + BOLD),
|
|
4771
|
+
color(request.preview, CYAN),
|
|
4772
|
+
"",
|
|
4773
|
+
color(request.loadingDetail, DIM),
|
|
4774
|
+
"",
|
|
4775
|
+
color("Status", WHITE + BOLD),
|
|
4776
|
+
color("Working... stay in tokenleak while this finishes.", YELLOW),
|
|
4777
|
+
"",
|
|
4778
|
+
color("Progress", WHITE + BOLD),
|
|
4779
|
+
progressBar,
|
|
4780
|
+
color(`Elapsed ${elapsedSeconds}s`, DIM),
|
|
4781
|
+
"",
|
|
4782
|
+
renderRule(44)
|
|
4783
|
+
];
|
|
4784
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4785
|
+
`)}`;
|
|
4786
|
+
}
|
|
4787
|
+
function clampScrollOffset(offset, totalLines, viewportHeight) {
|
|
4788
|
+
const maxOffset = Math.max(0, totalLines - Math.max(1, viewportHeight));
|
|
4789
|
+
return Math.min(Math.max(0, offset), maxOffset);
|
|
4790
|
+
}
|
|
4791
|
+
function buildOutputSectionLines(title, content, width) {
|
|
4792
|
+
const normalized = content.trimEnd();
|
|
4793
|
+
if (!normalized)
|
|
4794
|
+
return [];
|
|
4795
|
+
const lines = normalized.split(`
|
|
4796
|
+
`).map((line) => truncateVisible2(line, width));
|
|
4797
|
+
return [
|
|
4798
|
+
color(title, WHITE + BOLD),
|
|
4799
|
+
...lines,
|
|
4800
|
+
""
|
|
4801
|
+
];
|
|
4802
|
+
}
|
|
4803
|
+
function renderResult(request, result, scrollOffset = 0) {
|
|
4804
|
+
const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
|
4805
|
+
const rows = process.stdout.rows ?? 40;
|
|
4806
|
+
const statusColor = result.ok ? GREEN : RED;
|
|
4807
|
+
const statusLabel = result.ok ? "Completed" : "Failed";
|
|
4808
|
+
const header = [
|
|
4809
|
+
color(request.title, WHITE + BOLD),
|
|
4810
|
+
color(request.preview, CYAN),
|
|
4811
|
+
"",
|
|
4812
|
+
`${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
|
|
4813
|
+
color(result.summary, DIM),
|
|
4814
|
+
""
|
|
4815
|
+
];
|
|
4816
|
+
const contentLines = [
|
|
4817
|
+
...buildOutputSectionLines("Output", result.stdout, width),
|
|
4818
|
+
...buildOutputSectionLines("Messages", result.stderr, width)
|
|
4819
|
+
];
|
|
4820
|
+
const body = contentLines.length > 0 ? contentLines : [color("No captured output for this command.", DIM), ""];
|
|
4821
|
+
const footer = [
|
|
4822
|
+
renderRule(44),
|
|
4823
|
+
`${color("Up/Down", YELLOW)} scroll ${color("PgUp/PgDn", YELLOW)} page ${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
|
|
4824
|
+
];
|
|
4825
|
+
const viewportHeight = Math.max(4, rows - header.length - footer.length - 1);
|
|
4826
|
+
const effectiveOffset = clampScrollOffset(scrollOffset, body.length, viewportHeight);
|
|
4827
|
+
const visibleBody = body.slice(effectiveOffset, effectiveOffset + viewportHeight);
|
|
4828
|
+
const padding = Array.from({ length: Math.max(0, viewportHeight - visibleBody.length) }, () => "");
|
|
4829
|
+
const scrollStatus = body.length > viewportHeight ? color(`Lines ${effectiveOffset + 1}-${Math.min(body.length, effectiveOffset + viewportHeight)} of ${body.length}`, DIM) : color("All command output is visible.", DIM);
|
|
4830
|
+
const lines = [
|
|
4831
|
+
...header,
|
|
4832
|
+
...visibleBody,
|
|
4833
|
+
...padding,
|
|
4834
|
+
scrollStatus,
|
|
4835
|
+
...footer
|
|
4836
|
+
];
|
|
4837
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4838
|
+
`)}`;
|
|
4839
|
+
}
|
|
4840
|
+
function enterAltScreen() {
|
|
4841
|
+
process.stdout.write(`${ALT_SCREEN_ON}${ALT_SCROLL_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
|
|
4842
|
+
}
|
|
4843
|
+
function leaveAltScreen() {
|
|
4844
|
+
process.stdout.write(`${SHOW_CURSOR}${ALT_SCROLL_OFF}${ALT_SCREEN_OFF}`);
|
|
4845
|
+
}
|
|
4846
|
+
function paint(content) {
|
|
4847
|
+
process.stdout.write(content);
|
|
4848
|
+
}
|
|
4849
|
+
function suspendRawMode() {
|
|
4850
|
+
if (process.stdin.isTTY) {
|
|
4851
|
+
process.stdin.setRawMode(false);
|
|
4852
|
+
}
|
|
4853
|
+
process.stdin.pause();
|
|
4854
|
+
process.stdout.write(SHOW_CURSOR);
|
|
4855
|
+
}
|
|
4856
|
+
function resumeRawMode() {
|
|
4857
|
+
if (process.stdin.isTTY) {
|
|
4858
|
+
process.stdin.setRawMode(true);
|
|
4859
|
+
}
|
|
4860
|
+
process.stdin.resume();
|
|
4861
|
+
process.stdout.write(HIDE_CURSOR);
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
class InteractiveExitError extends Error {
|
|
4865
|
+
constructor() {
|
|
4866
|
+
super("Interactive session cancelled");
|
|
4867
|
+
this.name = "InteractiveExitError";
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
var PROVIDER_CHOICES = [
|
|
4871
|
+
{ value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
|
|
4872
|
+
{ value: "codex", label: "Codex", description: "OpenAI session logs" },
|
|
4873
|
+
{ value: "open-code", label: "Open Code", description: "Open Code storage and database" }
|
|
4874
|
+
];
|
|
4875
|
+
function isInteractiveExitError(error) {
|
|
4876
|
+
return error instanceof InteractiveExitError;
|
|
4877
|
+
}
|
|
4878
|
+
function parsePositiveInteger(value) {
|
|
4879
|
+
const trimmed = value.trim();
|
|
4880
|
+
if (trimmed === "")
|
|
4881
|
+
return null;
|
|
4882
|
+
const parsed = Number(trimmed);
|
|
4883
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4884
|
+
throw new Error(`Expected a positive whole number, received "${value}".`);
|
|
4885
|
+
}
|
|
4886
|
+
return parsed;
|
|
4887
|
+
}
|
|
4888
|
+
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`) {
|
|
4889
|
+
const optionLines = options.map((option, index) => {
|
|
4890
|
+
const isSelected = index === selectedIndex;
|
|
4891
|
+
const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
|
|
4892
|
+
const pointer = isSelected ? color(">", GREEN) : " ";
|
|
4893
|
+
const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
|
|
4894
|
+
const titleColor = isSelected ? WHITE + BOLD : WHITE;
|
|
4895
|
+
const descriptionColor = isSelected ? CYAN : DIM;
|
|
4896
|
+
return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
|
|
4897
|
+
});
|
|
4898
|
+
const lines = [
|
|
4899
|
+
color(title, WHITE + BOLD),
|
|
4900
|
+
color(description, DIM),
|
|
4901
|
+
"",
|
|
4902
|
+
...optionLines,
|
|
4903
|
+
"",
|
|
4904
|
+
renderRule(44),
|
|
4905
|
+
footer
|
|
4906
|
+
];
|
|
4907
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4908
|
+
`)}`;
|
|
4909
|
+
}
|
|
4910
|
+
async function ask(prompt, initialValue = "") {
|
|
4911
|
+
const readline = createInterface({
|
|
4912
|
+
input: process.stdin,
|
|
4913
|
+
output: process.stdout
|
|
4914
|
+
});
|
|
4915
|
+
let settled = false;
|
|
4916
|
+
return new Promise((resolve, reject) => {
|
|
4917
|
+
const finish = (fn) => {
|
|
4918
|
+
if (settled)
|
|
4919
|
+
return;
|
|
4920
|
+
settled = true;
|
|
4921
|
+
readline.off("SIGINT", onSigint);
|
|
4922
|
+
readline.close();
|
|
4923
|
+
fn();
|
|
4924
|
+
};
|
|
4925
|
+
const onSigint = () => {
|
|
4926
|
+
finish(() => reject(new InteractiveExitError));
|
|
4927
|
+
};
|
|
4928
|
+
readline.on("SIGINT", onSigint);
|
|
4929
|
+
const suffix = initialValue ? ` (${initialValue})` : "";
|
|
4930
|
+
readline.question(`${prompt}${suffix}: `).then((value) => {
|
|
4931
|
+
finish(() => resolve(value.trim() || initialValue));
|
|
4932
|
+
}).catch((error) => {
|
|
4933
|
+
finish(() => reject(error));
|
|
4934
|
+
});
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
async function askYesNo(prompt, defaultValue = false) {
|
|
4938
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
4939
|
+
const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
|
|
4940
|
+
if (value === "")
|
|
4941
|
+
return defaultValue;
|
|
4942
|
+
return value === "y" || value === "yes";
|
|
4943
|
+
}
|
|
4944
|
+
async function promptSingleChoice(title, description, options, initialIndex = 0) {
|
|
4945
|
+
return new Promise((resolve, reject) => {
|
|
4946
|
+
let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
|
|
4947
|
+
const onKeypress = (_input, key) => {
|
|
4948
|
+
if (key.ctrl && key.name === "c") {
|
|
4949
|
+
cleanup();
|
|
4950
|
+
reject(new InteractiveExitError);
|
|
4951
|
+
return;
|
|
4952
|
+
}
|
|
4953
|
+
if (key.name === "up") {
|
|
4954
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
4955
|
+
render();
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
if (key.name === "down") {
|
|
4959
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
4960
|
+
render();
|
|
4961
|
+
return;
|
|
4962
|
+
}
|
|
4963
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
4964
|
+
if (digit) {
|
|
4965
|
+
const index = Number(digit) - 1;
|
|
4966
|
+
if (index < options.length) {
|
|
4967
|
+
selectedIndex = index;
|
|
4968
|
+
cleanup();
|
|
4969
|
+
resolve(options[selectedIndex].value);
|
|
4970
|
+
}
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4974
|
+
cleanup();
|
|
4975
|
+
resolve(options[selectedIndex].value);
|
|
4976
|
+
}
|
|
4977
|
+
};
|
|
4978
|
+
function render() {
|
|
4979
|
+
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`));
|
|
4980
|
+
}
|
|
4981
|
+
function cleanup() {
|
|
4982
|
+
process.stdin.off("keypress", onKeypress);
|
|
4983
|
+
suspendRawMode();
|
|
4984
|
+
}
|
|
4985
|
+
render();
|
|
4986
|
+
resumeRawMode();
|
|
4987
|
+
process.stdin.on("keypress", onKeypress);
|
|
4988
|
+
});
|
|
4989
|
+
}
|
|
4990
|
+
async function promptMultiChoice(title, description, options, initialValues = []) {
|
|
4991
|
+
return new Promise((resolve, reject) => {
|
|
4992
|
+
let selectedIndex = 0;
|
|
4993
|
+
const selectedValues = new Set(initialValues);
|
|
4994
|
+
const onKeypress = (_input, key) => {
|
|
4995
|
+
if (key.ctrl && key.name === "c") {
|
|
4996
|
+
cleanup();
|
|
4997
|
+
reject(new InteractiveExitError);
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
if (key.name === "up") {
|
|
5001
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
5002
|
+
render();
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
if (key.name === "down") {
|
|
5006
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
5007
|
+
render();
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
5010
|
+
if (key.name === "space") {
|
|
5011
|
+
toggleSelected(selectedIndex);
|
|
5012
|
+
render();
|
|
5013
|
+
return;
|
|
5014
|
+
}
|
|
5015
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
5016
|
+
if (digit) {
|
|
5017
|
+
const index = Number(digit) - 1;
|
|
5018
|
+
if (index < options.length) {
|
|
5019
|
+
selectedIndex = index;
|
|
5020
|
+
toggleSelected(selectedIndex);
|
|
5021
|
+
render();
|
|
5022
|
+
}
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
if (key.name === "return" || key.name === "enter") {
|
|
5026
|
+
cleanup();
|
|
5027
|
+
resolve(Array.from(selectedValues));
|
|
5028
|
+
}
|
|
5029
|
+
};
|
|
5030
|
+
function toggleSelected(index) {
|
|
5031
|
+
const value = options[index].value;
|
|
5032
|
+
if (selectedValues.has(value)) {
|
|
5033
|
+
selectedValues.delete(value);
|
|
5034
|
+
} else {
|
|
5035
|
+
selectedValues.add(value);
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
function render() {
|
|
5039
|
+
paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
|
|
5040
|
+
}
|
|
5041
|
+
function cleanup() {
|
|
5042
|
+
process.stdin.off("keypress", onKeypress);
|
|
5043
|
+
suspendRawMode();
|
|
5044
|
+
}
|
|
5045
|
+
render();
|
|
5046
|
+
resumeRawMode();
|
|
5047
|
+
process.stdin.on("keypress", onKeypress);
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
5050
|
+
function applySelectedProviders(args, providers) {
|
|
5051
|
+
if (providers.length === 0)
|
|
5052
|
+
return;
|
|
5053
|
+
args["provider"] = providers.join(",");
|
|
5054
|
+
}
|
|
5055
|
+
async function promptTheme(defaultTheme = "dark") {
|
|
5056
|
+
const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
|
|
5057
|
+
{ value: "dark", label: "Dark", description: "High-contrast dark canvas" },
|
|
5058
|
+
{ value: "light", label: "Light", description: "Bright export with light background" }
|
|
5059
|
+
], defaultTheme === "light" ? 1 : 0);
|
|
5060
|
+
return theme;
|
|
5061
|
+
}
|
|
5062
|
+
async function promptDateWindow() {
|
|
5063
|
+
const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
|
|
5064
|
+
{ value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
|
|
5065
|
+
{ value: "30", label: "Last 30 days", description: "Short-term trend window" },
|
|
5066
|
+
{ value: "90", label: "Last 90 days", description: "Default overview" },
|
|
5067
|
+
{ value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
|
|
5068
|
+
{ value: "custom", label: "Custom range", description: "Enter exact dates manually" }
|
|
5069
|
+
], 2);
|
|
5070
|
+
if (choice !== "custom") {
|
|
5071
|
+
return { days: Number(choice) };
|
|
5072
|
+
}
|
|
5073
|
+
const since = await ask("Since date YYYY-MM-DD");
|
|
5074
|
+
const until = await ask("Until date YYYY-MM-DD (blank for today)");
|
|
5075
|
+
const args = { since };
|
|
5076
|
+
if (until)
|
|
5077
|
+
args["until"] = until;
|
|
5078
|
+
return args;
|
|
5079
|
+
}
|
|
5080
|
+
async function promptProviderSelection(title = "Provider Filter") {
|
|
5081
|
+
return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
|
|
5082
|
+
}
|
|
5083
|
+
async function promptOutputPath(defaultPath) {
|
|
5084
|
+
return ask("Output file", defaultPath);
|
|
5085
|
+
}
|
|
5086
|
+
async function promptWidth() {
|
|
5087
|
+
const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
|
|
5088
|
+
{ value: "80", label: "80 columns", description: "Standard terminal width" },
|
|
5089
|
+
{ value: "100", label: "100 columns", description: "Balanced dashboard layout" },
|
|
5090
|
+
{ value: "120", label: "120 columns", description: "Wide dashboard layout" },
|
|
5091
|
+
{ value: "custom", label: "Custom width", description: "Enter an exact width" }
|
|
5092
|
+
], 1);
|
|
5093
|
+
if (choice !== "custom") {
|
|
5094
|
+
return Number(choice);
|
|
5095
|
+
}
|
|
5096
|
+
while (true) {
|
|
5097
|
+
try {
|
|
5098
|
+
const parsed = parsePositiveInteger(await ask("Custom width"));
|
|
5099
|
+
if (parsed !== null) {
|
|
5100
|
+
return parsed;
|
|
5101
|
+
}
|
|
5102
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
|
|
5103
|
+
${color("Enter a positive whole number to continue.", DIM)}
|
|
5104
|
+
|
|
5105
|
+
Press Enter to try again.`);
|
|
5106
|
+
await ask("");
|
|
5107
|
+
} catch (error) {
|
|
5108
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
|
|
5109
|
+
${color(error instanceof Error ? error.message : String(error), DIM)}
|
|
5110
|
+
|
|
5111
|
+
Press Enter to try again.`);
|
|
5112
|
+
await ask("");
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
async function promptCompareSetting() {
|
|
5117
|
+
const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
|
|
5118
|
+
{ value: "off", label: "No compare", description: "Render a standard single-period report" },
|
|
5119
|
+
{ value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
|
|
5120
|
+
{ value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
|
|
5121
|
+
]);
|
|
5122
|
+
if (choice === "off")
|
|
5123
|
+
return null;
|
|
5124
|
+
if (choice === "auto")
|
|
5125
|
+
return "auto";
|
|
5126
|
+
return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
|
|
5127
|
+
}
|
|
5128
|
+
function inferDashboardTimeRange(rangeArgs) {
|
|
5129
|
+
const days = typeof rangeArgs["days"] === "number" ? rangeArgs["days"] : null;
|
|
5130
|
+
if (days !== null) {
|
|
5131
|
+
if (days <= 7)
|
|
5132
|
+
return "7d";
|
|
5133
|
+
if (days <= 30)
|
|
5134
|
+
return "30d";
|
|
5135
|
+
if (days <= 90)
|
|
5136
|
+
return "90d";
|
|
5137
|
+
return "365d";
|
|
5138
|
+
}
|
|
5139
|
+
const since = typeof rangeArgs["since"] === "string" ? rangeArgs["since"].trim() : "";
|
|
5140
|
+
if (!since)
|
|
5141
|
+
return "30d";
|
|
5142
|
+
const rawUntil = typeof rangeArgs["until"] === "string" ? rangeArgs["until"].trim() : "";
|
|
5143
|
+
const until = rawUntil || new Date().toISOString().slice(0, 10);
|
|
5144
|
+
const sinceMs = Date.parse(`${since}T00:00:00.000Z`);
|
|
5145
|
+
const untilMs = Date.parse(`${until}T00:00:00.000Z`);
|
|
5146
|
+
if (!Number.isFinite(sinceMs) || !Number.isFinite(untilMs) || sinceMs > untilMs) {
|
|
5147
|
+
return "30d";
|
|
5148
|
+
}
|
|
5149
|
+
const spanDays = Math.max(1, Math.ceil((untilMs - sinceMs) / DAY_MS));
|
|
5150
|
+
if (spanDays <= 7)
|
|
5151
|
+
return "7d";
|
|
5152
|
+
if (spanDays <= 30)
|
|
5153
|
+
return "30d";
|
|
5154
|
+
if (spanDays <= 90)
|
|
5155
|
+
return "90d";
|
|
5156
|
+
return "365d";
|
|
5157
|
+
}
|
|
5158
|
+
function buildTabbedDashboardOptions(rangeArgs, providers, width, noInsights, noColor2) {
|
|
5159
|
+
const options = {
|
|
5160
|
+
initialTimeRange: inferDashboardTimeRange(rangeArgs),
|
|
5161
|
+
noColor: noColor2,
|
|
5162
|
+
noInsights
|
|
5163
|
+
};
|
|
5164
|
+
const rawSince = typeof rangeArgs["since"] === "string" ? rangeArgs["since"].trim() : "";
|
|
5165
|
+
const rawUntil = typeof rangeArgs["until"] === "string" ? rangeArgs["until"].trim() : "";
|
|
5166
|
+
const since = rawSince || undefined;
|
|
5167
|
+
const until = rawUntil || undefined;
|
|
5168
|
+
if (since) {
|
|
5169
|
+
options.initialRange = computeDateRange({ since, until });
|
|
5170
|
+
options.until = options.initialRange.until;
|
|
5171
|
+
} else if (until) {
|
|
5172
|
+
options.until = computeDateRange({ until }).until;
|
|
5173
|
+
}
|
|
5174
|
+
if (providers.length > 0) {
|
|
5175
|
+
options.providerNames = [...providers];
|
|
5176
|
+
}
|
|
5177
|
+
if (width !== null) {
|
|
5178
|
+
options.width = width;
|
|
5179
|
+
}
|
|
5180
|
+
return options;
|
|
5181
|
+
}
|
|
5182
|
+
function createTabbedDashboardRequest(options) {
|
|
5183
|
+
const args = {};
|
|
5184
|
+
const initialDays = options.initialTimeRange ? TAB_RANGE_DAY_COUNTS[options.initialTimeRange] : undefined;
|
|
5185
|
+
if (options.initialRange) {
|
|
5186
|
+
args["since"] = options.initialRange.since;
|
|
5187
|
+
args["until"] = options.initialRange.until;
|
|
5188
|
+
} else {
|
|
5189
|
+
if (initialDays !== undefined)
|
|
5190
|
+
args["days"] = initialDays;
|
|
5191
|
+
if (options.until)
|
|
5192
|
+
args["until"] = options.until;
|
|
5193
|
+
}
|
|
5194
|
+
if (options.providerNames && options.providerNames.length > 0) {
|
|
5195
|
+
args["provider"] = options.providerNames.join(",");
|
|
5196
|
+
}
|
|
5197
|
+
if (options.width !== undefined)
|
|
5198
|
+
args["width"] = options.width;
|
|
5199
|
+
if (options.noInsights)
|
|
5200
|
+
args["noInsights"] = true;
|
|
5201
|
+
if (options.noColor)
|
|
5202
|
+
args["noColor"] = true;
|
|
5203
|
+
return {
|
|
5204
|
+
args,
|
|
5205
|
+
preview: buildCliPreview(args),
|
|
5206
|
+
title: "Launch Dashboard",
|
|
5207
|
+
loadingTitle: "Starting dashboard",
|
|
5208
|
+
loadingDetail: "Launching the interactive terminal dashboard",
|
|
5209
|
+
executionMode: "inherit"
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
async function buildDashboardPreset() {
|
|
5213
|
+
const rangeArgs = await promptDateWindow();
|
|
5214
|
+
const providers = await promptProviderSelection();
|
|
5215
|
+
const width = await promptWidth();
|
|
5216
|
+
const noInsights = await askYesNo("Hide insights panel", false);
|
|
5217
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
5218
|
+
return {
|
|
5219
|
+
type: "tabbed-dashboard",
|
|
5220
|
+
options: buildTabbedDashboardOptions(rangeArgs, providers, width, noInsights, noColor2)
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
async function buildJsonPreset() {
|
|
5224
|
+
const rangeArgs = await promptDateWindow();
|
|
5225
|
+
const providers = await promptProviderSelection();
|
|
5226
|
+
const compare = await promptCompareSetting();
|
|
5227
|
+
const saveToFile = await askYesNo("Write JSON to a file", false);
|
|
5228
|
+
const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
|
|
5229
|
+
const args = {
|
|
5230
|
+
format: "json",
|
|
5231
|
+
...rangeArgs
|
|
5232
|
+
};
|
|
5233
|
+
applySelectedProviders(args, providers);
|
|
5234
|
+
if (compare)
|
|
5235
|
+
args["compare"] = compare;
|
|
5236
|
+
if (saveToFile) {
|
|
5237
|
+
args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
|
|
5238
|
+
}
|
|
5239
|
+
if (clipboard)
|
|
5240
|
+
args["clipboard"] = true;
|
|
5241
|
+
return createRunCommand(args);
|
|
5242
|
+
}
|
|
5243
|
+
async function buildImagePreset(format) {
|
|
5244
|
+
const theme = await promptTheme();
|
|
5245
|
+
const rangeArgs = await promptDateWindow();
|
|
5246
|
+
const providers = await promptProviderSelection("Provider Filter");
|
|
5247
|
+
const compare = await promptCompareSetting();
|
|
5248
|
+
const output = await promptOutputPath(`tokenleak.${format}`);
|
|
5249
|
+
const shouldOpen = await askYesNo("Open the file when done", true);
|
|
5250
|
+
const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
|
|
5251
|
+
const args = {
|
|
5252
|
+
format,
|
|
5253
|
+
theme,
|
|
5254
|
+
output,
|
|
5255
|
+
open: shouldOpen,
|
|
5256
|
+
more,
|
|
5257
|
+
...rangeArgs
|
|
5258
|
+
};
|
|
5259
|
+
applySelectedProviders(args, providers);
|
|
5260
|
+
if (compare)
|
|
5261
|
+
args["compare"] = compare;
|
|
5262
|
+
return createRunCommand(args);
|
|
5263
|
+
}
|
|
5264
|
+
async function buildComparePreset() {
|
|
5265
|
+
const rangeArgs = await promptDateWindow();
|
|
5266
|
+
const providers = await promptProviderSelection();
|
|
5267
|
+
const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
|
|
5268
|
+
{ value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
|
|
5269
|
+
{ value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
|
|
5270
|
+
]);
|
|
5271
|
+
const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
|
|
5272
|
+
const saveToFile = await askYesNo("Write compare output to a file", false);
|
|
5273
|
+
const args = {
|
|
5274
|
+
format: "json",
|
|
5275
|
+
compare,
|
|
5276
|
+
...rangeArgs
|
|
5277
|
+
};
|
|
5278
|
+
applySelectedProviders(args, providers);
|
|
5279
|
+
if (saveToFile) {
|
|
5280
|
+
args["output"] = await promptOutputPath("tokenleak-compare.json");
|
|
5281
|
+
}
|
|
5282
|
+
return createRunCommand(args);
|
|
5283
|
+
}
|
|
5284
|
+
async function buildLivePreset() {
|
|
5285
|
+
const theme = await promptTheme();
|
|
5286
|
+
const rangeArgs = await promptDateWindow();
|
|
5287
|
+
const providers = await promptProviderSelection();
|
|
5288
|
+
const more = await askYesNo("Enable --more stats", true);
|
|
5289
|
+
const args = {
|
|
5290
|
+
liveServer: true,
|
|
5291
|
+
theme,
|
|
5292
|
+
more,
|
|
5293
|
+
...rangeArgs
|
|
5294
|
+
};
|
|
5295
|
+
applySelectedProviders(args, providers);
|
|
5296
|
+
return createRunCommand(args);
|
|
5297
|
+
}
|
|
5298
|
+
async function askFormatChoice() {
|
|
5299
|
+
return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
|
|
5300
|
+
{ value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
|
|
5301
|
+
{ value: "json", label: "JSON", description: "Structured machine-readable output" },
|
|
5302
|
+
{ value: "svg", label: "SVG", description: "Shareable vector export" },
|
|
5303
|
+
{ value: "png", label: "PNG", description: "Raster export for social and docs" }
|
|
5304
|
+
]);
|
|
5305
|
+
}
|
|
5306
|
+
async function buildCustomCommand() {
|
|
5307
|
+
const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
|
|
5308
|
+
{ value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
|
|
5309
|
+
{ value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
|
|
5310
|
+
{ value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
|
|
5311
|
+
]);
|
|
5312
|
+
if (mode === "live-server") {
|
|
5313
|
+
return buildLivePreset();
|
|
5314
|
+
}
|
|
5315
|
+
if (mode === "list-providers") {
|
|
5316
|
+
return createRunCommand({ listProviders: true });
|
|
5317
|
+
}
|
|
5318
|
+
const format = await askFormatChoice();
|
|
5319
|
+
const theme = format === "terminal" ? null : await promptTheme();
|
|
5320
|
+
const rangeArgs = await promptDateWindow();
|
|
5321
|
+
const providers = await promptProviderSelection();
|
|
5322
|
+
const compare = await promptCompareSetting();
|
|
5323
|
+
const width = format === "terminal" ? await promptWidth() : null;
|
|
5324
|
+
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}`);
|
|
5325
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
5326
|
+
const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
|
|
5327
|
+
const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
|
|
5328
|
+
const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
|
|
5329
|
+
const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
|
|
5330
|
+
const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
|
|
5331
|
+
const args = {
|
|
5332
|
+
format,
|
|
5333
|
+
...rangeArgs
|
|
5334
|
+
};
|
|
5335
|
+
if (theme)
|
|
5336
|
+
args["theme"] = theme;
|
|
5337
|
+
if (compare)
|
|
5338
|
+
args["compare"] = compare;
|
|
5339
|
+
if (width)
|
|
5340
|
+
args["width"] = width;
|
|
5341
|
+
if (output)
|
|
5342
|
+
args["output"] = output;
|
|
5343
|
+
if (noColor2)
|
|
5344
|
+
args["noColor"] = true;
|
|
5345
|
+
if (noInsights)
|
|
5346
|
+
args["noInsights"] = true;
|
|
5347
|
+
if (more)
|
|
5348
|
+
args["more"] = true;
|
|
5349
|
+
if (clipboard)
|
|
5350
|
+
args["clipboard"] = true;
|
|
5351
|
+
if (open)
|
|
5352
|
+
args["open"] = true;
|
|
5353
|
+
if (upload)
|
|
5354
|
+
args["upload"] = upload;
|
|
5355
|
+
applySelectedProviders(args, providers);
|
|
5356
|
+
return createRunCommand(args);
|
|
5357
|
+
}
|
|
5358
|
+
function createMenuOptions() {
|
|
5359
|
+
return [
|
|
5360
|
+
{
|
|
5361
|
+
digit: "1",
|
|
5362
|
+
title: "Launch Dashboard",
|
|
5363
|
+
description: "guided terminal view",
|
|
5364
|
+
preview: "tokenleak --days 90",
|
|
5365
|
+
select: buildDashboardPreset
|
|
5366
|
+
},
|
|
5367
|
+
{
|
|
5368
|
+
digit: "2",
|
|
5369
|
+
title: "Export JSON",
|
|
5370
|
+
description: "structured output for scripts",
|
|
5371
|
+
preview: "tokenleak --format json",
|
|
5372
|
+
select: buildJsonPreset
|
|
5373
|
+
},
|
|
5374
|
+
{
|
|
5375
|
+
digit: "3",
|
|
5376
|
+
title: "Export SVG",
|
|
5377
|
+
description: "shareable vector card",
|
|
5378
|
+
preview: "tokenleak --format svg --output tokenleak.svg",
|
|
5379
|
+
select: async () => buildImagePreset("svg")
|
|
5380
|
+
},
|
|
5381
|
+
{
|
|
5382
|
+
digit: "4",
|
|
5383
|
+
title: "Export PNG",
|
|
5384
|
+
description: "social-ready raster image",
|
|
5385
|
+
preview: "tokenleak --format png --output tokenleak.png --more",
|
|
5386
|
+
select: async () => buildImagePreset("png")
|
|
5387
|
+
},
|
|
5388
|
+
{
|
|
5389
|
+
digit: "5",
|
|
5390
|
+
title: "Compare Periods",
|
|
5391
|
+
description: "diff current vs previous usage",
|
|
5392
|
+
preview: "tokenleak --compare auto --format json",
|
|
5393
|
+
select: buildComparePreset
|
|
5394
|
+
},
|
|
5395
|
+
{
|
|
5396
|
+
digit: "6",
|
|
5397
|
+
title: "Start Live Server",
|
|
5398
|
+
description: "browser dashboard on localhost",
|
|
5399
|
+
preview: "tokenleak --live-server --theme dark",
|
|
5400
|
+
select: buildLivePreset
|
|
5401
|
+
},
|
|
5402
|
+
{
|
|
5403
|
+
digit: "7",
|
|
5404
|
+
title: "Build Custom Command",
|
|
5405
|
+
description: "configure flags interactively",
|
|
5406
|
+
preview: "tokenleak --format terminal --days 90",
|
|
5407
|
+
select: buildCustomCommand
|
|
5408
|
+
},
|
|
5409
|
+
{
|
|
5410
|
+
digit: "8",
|
|
5411
|
+
title: "Full Help",
|
|
5412
|
+
description: "examples and complete usage",
|
|
5413
|
+
preview: "tokenleak --help",
|
|
5414
|
+
select: async () => ({ type: "show-help" })
|
|
5415
|
+
},
|
|
5416
|
+
{
|
|
5417
|
+
digit: "9",
|
|
5418
|
+
title: "List Providers",
|
|
5419
|
+
description: "detect available registries",
|
|
5420
|
+
preview: "tokenleak --list-providers",
|
|
5421
|
+
select: async () => createRunCommand({ listProviders: true })
|
|
5422
|
+
},
|
|
5423
|
+
{
|
|
5424
|
+
digit: "0",
|
|
5425
|
+
title: "Exit",
|
|
5426
|
+
description: "close the launcher",
|
|
5427
|
+
preview: "exit",
|
|
5428
|
+
select: async () => ({ type: "exit" })
|
|
5429
|
+
}
|
|
5430
|
+
];
|
|
3008
5431
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
5432
|
+
async function promptForMenuCommand(context, options, state) {
|
|
5433
|
+
let showHelp = false;
|
|
5434
|
+
let resolving = false;
|
|
5435
|
+
return new Promise((resolve, reject) => {
|
|
5436
|
+
const onKeypress = async (_input, key) => {
|
|
5437
|
+
if (resolving) {
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
5440
|
+
if (key.ctrl && key.name === "c") {
|
|
5441
|
+
cleanup();
|
|
5442
|
+
resolve({ type: "exit" });
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
if (showHelp) {
|
|
5446
|
+
if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
|
|
5447
|
+
showHelp = false;
|
|
5448
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
5449
|
+
}
|
|
5450
|
+
return;
|
|
5451
|
+
}
|
|
5452
|
+
if (key.name === "up") {
|
|
5453
|
+
state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
|
|
5454
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5457
|
+
if (key.name === "down") {
|
|
5458
|
+
state.selectedIndex = (state.selectedIndex + 1) % options.length;
|
|
5459
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
if (key.name === "h") {
|
|
5463
|
+
showHelp = true;
|
|
5464
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5467
|
+
if (key.name === "q" || key.name === "escape") {
|
|
5468
|
+
cleanup();
|
|
5469
|
+
resolve({ type: "exit" });
|
|
5470
|
+
return;
|
|
5471
|
+
}
|
|
5472
|
+
const digit = key.sequence?.match(/^[0-9]$/)?.[0];
|
|
5473
|
+
if (digit) {
|
|
5474
|
+
const nextIndex = options.findIndex((option) => option.digit === digit);
|
|
5475
|
+
if (nextIndex >= 0) {
|
|
5476
|
+
state.selectedIndex = nextIndex;
|
|
5477
|
+
if (digit === "8") {
|
|
5478
|
+
showHelp = true;
|
|
5479
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
resolving = true;
|
|
5483
|
+
cleanup();
|
|
5484
|
+
try {
|
|
5485
|
+
const command = await options[nextIndex].select();
|
|
5486
|
+
resolve(command);
|
|
5487
|
+
} catch (error) {
|
|
5488
|
+
if (isInteractiveExitError(error)) {
|
|
5489
|
+
resolve({ type: "exit" });
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
reject(error);
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
return;
|
|
5496
|
+
}
|
|
5497
|
+
if (key.name === "return" || key.name === "enter") {
|
|
5498
|
+
if (options[state.selectedIndex].digit === "8") {
|
|
5499
|
+
showHelp = true;
|
|
5500
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
resolving = true;
|
|
5504
|
+
cleanup();
|
|
5505
|
+
try {
|
|
5506
|
+
const command = await options[state.selectedIndex].select();
|
|
5507
|
+
resolve(command);
|
|
5508
|
+
} catch (error) {
|
|
5509
|
+
if (isInteractiveExitError(error)) {
|
|
5510
|
+
resolve({ type: "exit" });
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
reject(error);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
function cleanup() {
|
|
5518
|
+
process.stdin.off("keypress", onKeypress);
|
|
5519
|
+
suspendRawMode();
|
|
3028
5520
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
5521
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
5522
|
+
resumeRawMode();
|
|
5523
|
+
process.stdin.on("keypress", onKeypress);
|
|
5524
|
+
});
|
|
3031
5525
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
5526
|
+
async function showExecutionResult(request, result) {
|
|
5527
|
+
const body = [
|
|
5528
|
+
...buildOutputSectionLines("Output", result.stdout, Math.max(60, (process.stdout.columns ?? 120) - 1)),
|
|
5529
|
+
...buildOutputSectionLines("Messages", result.stderr, Math.max(60, (process.stdout.columns ?? 120) - 1))
|
|
5530
|
+
];
|
|
5531
|
+
let scrollOffset = 0;
|
|
5532
|
+
return new Promise((resolve) => {
|
|
5533
|
+
const viewportHeight = () => {
|
|
5534
|
+
const rows = process.stdout.rows ?? 40;
|
|
5535
|
+
const headerLines = 6;
|
|
5536
|
+
const footerLines = 3;
|
|
5537
|
+
return Math.max(4, rows - headerLines - footerLines);
|
|
5538
|
+
};
|
|
5539
|
+
const render = () => {
|
|
5540
|
+
paint(renderResult(request, result, scrollOffset));
|
|
5541
|
+
};
|
|
5542
|
+
const onKeypress = (_input, key) => {
|
|
5543
|
+
if (key.ctrl && key.name === "c") {
|
|
5544
|
+
cleanup();
|
|
5545
|
+
resolve("exit");
|
|
5546
|
+
return;
|
|
5547
|
+
}
|
|
5548
|
+
if (key.name === "q" || key.name === "escape") {
|
|
5549
|
+
cleanup();
|
|
5550
|
+
resolve("exit");
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
if (key.name === "return" || key.name === "enter") {
|
|
5554
|
+
cleanup();
|
|
5555
|
+
resolve("menu");
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
const page = viewportHeight();
|
|
5559
|
+
if (key.name === "up") {
|
|
5560
|
+
scrollOffset = clampScrollOffset(scrollOffset - 1, body.length, page);
|
|
5561
|
+
render();
|
|
5562
|
+
return;
|
|
5563
|
+
}
|
|
5564
|
+
if (key.name === "down") {
|
|
5565
|
+
scrollOffset = clampScrollOffset(scrollOffset + 1, body.length, page);
|
|
5566
|
+
render();
|
|
5567
|
+
return;
|
|
5568
|
+
}
|
|
5569
|
+
if (key.name === "pageup") {
|
|
5570
|
+
scrollOffset = clampScrollOffset(scrollOffset - page, body.length, page);
|
|
5571
|
+
render();
|
|
5572
|
+
return;
|
|
5573
|
+
}
|
|
5574
|
+
if (key.name === "pagedown") {
|
|
5575
|
+
scrollOffset = clampScrollOffset(scrollOffset + page, body.length, page);
|
|
5576
|
+
render();
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
if (key.name === "home") {
|
|
5580
|
+
scrollOffset = 0;
|
|
5581
|
+
render();
|
|
5582
|
+
return;
|
|
5583
|
+
}
|
|
5584
|
+
if (key.name === "end") {
|
|
5585
|
+
scrollOffset = clampScrollOffset(Number.MAX_SAFE_INTEGER, body.length, page);
|
|
5586
|
+
render();
|
|
5587
|
+
}
|
|
5588
|
+
};
|
|
5589
|
+
const cleanup = () => {
|
|
5590
|
+
process.stdin.off("keypress", onKeypress);
|
|
5591
|
+
suspendRawMode();
|
|
5592
|
+
};
|
|
5593
|
+
render();
|
|
5594
|
+
resumeRawMode();
|
|
5595
|
+
process.stdin.on("keypress", onKeypress);
|
|
5596
|
+
});
|
|
3039
5597
|
}
|
|
3040
|
-
function
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
5598
|
+
function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
|
|
5599
|
+
return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
|
|
5600
|
+
}
|
|
5601
|
+
async function startInteractiveCli(context, execute, launchTabbedDashboard) {
|
|
5602
|
+
const options = createMenuOptions();
|
|
5603
|
+
const state = { selectedIndex: 0 };
|
|
5604
|
+
let interrupted = false;
|
|
5605
|
+
let ignoreSigint = false;
|
|
5606
|
+
const onSigint = () => {
|
|
5607
|
+
if (!ignoreSigint) {
|
|
5608
|
+
interrupted = true;
|
|
5609
|
+
}
|
|
5610
|
+
};
|
|
5611
|
+
emitKeypressEvents(process.stdin);
|
|
5612
|
+
enterAltScreen();
|
|
5613
|
+
process.on("SIGINT", onSigint);
|
|
5614
|
+
try {
|
|
5615
|
+
while (true) {
|
|
5616
|
+
if (interrupted) {
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5619
|
+
const command = await promptForMenuCommand(context, options, state);
|
|
5620
|
+
if (command.type === "exit") {
|
|
5621
|
+
return;
|
|
5622
|
+
}
|
|
5623
|
+
if (command.type === "show-help") {
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
if (command.type === "tabbed-dashboard") {
|
|
5627
|
+
if (launchTabbedDashboard) {
|
|
5628
|
+
const request = createTabbedDashboardRequest(command.options);
|
|
5629
|
+
let dashboardError = null;
|
|
5630
|
+
leaveAltScreen();
|
|
5631
|
+
try {
|
|
5632
|
+
ignoreSigint = true;
|
|
5633
|
+
await launchTabbedDashboard(command.options);
|
|
5634
|
+
} catch (error) {
|
|
5635
|
+
dashboardError = error;
|
|
5636
|
+
} finally {
|
|
5637
|
+
ignoreSigint = false;
|
|
5638
|
+
enterAltScreen();
|
|
5639
|
+
}
|
|
5640
|
+
if (dashboardError) {
|
|
5641
|
+
const message = dashboardError instanceof Error ? dashboardError.message : String(dashboardError);
|
|
5642
|
+
const next2 = await showExecutionResult(request, {
|
|
5643
|
+
ok: false,
|
|
5644
|
+
summary: message,
|
|
5645
|
+
stdout: "",
|
|
5646
|
+
stderr: message
|
|
5647
|
+
});
|
|
5648
|
+
if (next2 === "exit") {
|
|
5649
|
+
return;
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
continue;
|
|
5654
|
+
}
|
|
5655
|
+
const startedAt = Date.now();
|
|
5656
|
+
let loadingFrame = 0;
|
|
5657
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
5658
|
+
const loadingTicker = setInterval(() => {
|
|
5659
|
+
loadingFrame += 1;
|
|
5660
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
5661
|
+
}, LOADING_TICK_MS);
|
|
5662
|
+
let result;
|
|
5663
|
+
try {
|
|
5664
|
+
if (command.request.executionMode === "inherit") {
|
|
5665
|
+
clearInterval(loadingTicker);
|
|
5666
|
+
leaveAltScreen();
|
|
5667
|
+
try {
|
|
5668
|
+
ignoreSigint = true;
|
|
5669
|
+
result = await execute(command.request);
|
|
5670
|
+
} finally {
|
|
5671
|
+
ignoreSigint = false;
|
|
5672
|
+
enterAltScreen();
|
|
5673
|
+
}
|
|
5674
|
+
} else {
|
|
5675
|
+
result = await execute(command.request);
|
|
5676
|
+
}
|
|
5677
|
+
} finally {
|
|
5678
|
+
clearInterval(loadingTicker);
|
|
5679
|
+
}
|
|
5680
|
+
if (interrupted) {
|
|
5681
|
+
return;
|
|
5682
|
+
}
|
|
5683
|
+
const next = await showExecutionResult(command.request, result);
|
|
5684
|
+
if (next === "exit") {
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
} finally {
|
|
5689
|
+
process.off("SIGINT", onSigint);
|
|
5690
|
+
suspendRawMode();
|
|
5691
|
+
leaveAltScreen();
|
|
3050
5692
|
}
|
|
3051
|
-
process.exit(1);
|
|
3052
5693
|
}
|
|
3053
5694
|
|
|
3054
5695
|
// packages/cli/src/sharing/clipboard.ts
|
|
@@ -3149,6 +5790,341 @@ async function uploadToGist(content, filename, description) {
|
|
|
3149
5790
|
} catch {}
|
|
3150
5791
|
}
|
|
3151
5792
|
}
|
|
5793
|
+
// packages/cli/src/data-loader.ts
|
|
5794
|
+
async function loadTokenleakData(providers, range) {
|
|
5795
|
+
const results = await Promise.all(providers.map(async (p) => {
|
|
5796
|
+
try {
|
|
5797
|
+
return await p.load(range);
|
|
5798
|
+
} catch {
|
|
5799
|
+
return null;
|
|
5800
|
+
}
|
|
5801
|
+
}));
|
|
5802
|
+
const providerDataList = results.filter((r) => r !== null);
|
|
5803
|
+
if (providerDataList.length === 0) {
|
|
5804
|
+
throw new TokenleakError("No provider data found");
|
|
5805
|
+
}
|
|
5806
|
+
const mergedDaily = mergeProviderData(providerDataList);
|
|
5807
|
+
const stats = aggregate(mergedDaily, range.until);
|
|
5808
|
+
return {
|
|
5809
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5810
|
+
generated: new Date().toISOString(),
|
|
5811
|
+
dateRange: range,
|
|
5812
|
+
providers: providerDataList,
|
|
5813
|
+
aggregated: stats,
|
|
5814
|
+
more: buildMoreStats(providerDataList, range)
|
|
5815
|
+
};
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5818
|
+
// packages/cli/src/tabbed-dashboard.ts
|
|
5819
|
+
var HOME_CLEAR2 = "\x1B[H\x1B[J";
|
|
5820
|
+
var HIDE_CURSOR2 = "\x1B[?25l";
|
|
5821
|
+
var SHOW_CURSOR2 = "\x1B[?25h";
|
|
5822
|
+
var ALT_SCREEN_ON2 = "\x1B[?1049h";
|
|
5823
|
+
var ALT_SCREEN_OFF2 = "\x1B[?1049l";
|
|
5824
|
+
var ALT_SCROLL_ON2 = "\x1B[?1007h";
|
|
5825
|
+
var ALT_SCROLL_OFF2 = "\x1B[?1007l";
|
|
5826
|
+
var DIM2 = "\x1B[2m";
|
|
5827
|
+
var RESET3 = "\x1B[0m";
|
|
5828
|
+
var BOLD2 = "\x1B[1m";
|
|
5829
|
+
var YELLOW2 = "\x1B[33m";
|
|
5830
|
+
function timeRangeToDays(range) {
|
|
5831
|
+
switch (range) {
|
|
5832
|
+
case "7d":
|
|
5833
|
+
return 7;
|
|
5834
|
+
case "30d":
|
|
5835
|
+
return 30;
|
|
5836
|
+
case "90d":
|
|
5837
|
+
return 90;
|
|
5838
|
+
case "365d":
|
|
5839
|
+
return 365;
|
|
5840
|
+
default:
|
|
5841
|
+
return 30;
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
function computeRange(range, baseUntil) {
|
|
5845
|
+
const until = baseUntil;
|
|
5846
|
+
const d = new Date(until);
|
|
5847
|
+
d.setDate(d.getDate() - timeRangeToDays(range));
|
|
5848
|
+
const since = d.toISOString().slice(0, 10);
|
|
5849
|
+
return { since, until };
|
|
5850
|
+
}
|
|
5851
|
+
function resolveRange(state, timeRange) {
|
|
5852
|
+
if (state.initialRange && timeRange === state.initialTimeRange) {
|
|
5853
|
+
return state.initialRange;
|
|
5854
|
+
}
|
|
5855
|
+
return computeRange(timeRange, state.baseUntil);
|
|
5856
|
+
}
|
|
5857
|
+
async function loadForRange(state, providers, timeRange) {
|
|
5858
|
+
const cached = state.dataCache.get(timeRange);
|
|
5859
|
+
if (cached)
|
|
5860
|
+
return cached;
|
|
5861
|
+
const inflight = state.inflightLoads.get(timeRange);
|
|
5862
|
+
if (inflight)
|
|
5863
|
+
return inflight;
|
|
5864
|
+
const range = resolveRange(state, timeRange);
|
|
5865
|
+
const loadPromise = loadTokenleakData(providers, range).then((output) => {
|
|
5866
|
+
state.dataCache.set(timeRange, output);
|
|
5867
|
+
return output;
|
|
5868
|
+
}).finally(() => {
|
|
5869
|
+
state.inflightLoads.delete(timeRange);
|
|
5870
|
+
});
|
|
5871
|
+
state.inflightLoads.set(timeRange, loadPromise);
|
|
5872
|
+
return loadPromise;
|
|
5873
|
+
}
|
|
5874
|
+
function getRenderWidth(state) {
|
|
5875
|
+
const terminalWidth = Math.max(40, (process.stdout.columns ?? 80) - 1);
|
|
5876
|
+
if (state.width === null) {
|
|
5877
|
+
return terminalWidth;
|
|
5878
|
+
}
|
|
5879
|
+
return Math.max(40, Math.min(terminalWidth, state.width));
|
|
5880
|
+
}
|
|
5881
|
+
function getViewportHeight(state, width, rows) {
|
|
5882
|
+
const headerLines = renderTabBar(state.timeRange, state.metricTab, width, state.noColor).split(`
|
|
5883
|
+
`).length + 2;
|
|
5884
|
+
const footerLines = 1;
|
|
5885
|
+
return Math.max(4, rows - headerLines - footerLines - 1);
|
|
5886
|
+
}
|
|
5887
|
+
function renderActiveView(output, tab, width, noColor2, noInsights) {
|
|
5888
|
+
const options = {
|
|
5889
|
+
format: "terminal",
|
|
5890
|
+
theme: "dark",
|
|
5891
|
+
width,
|
|
5892
|
+
showInsights: !noInsights,
|
|
5893
|
+
noColor: noColor2,
|
|
5894
|
+
output: null,
|
|
5895
|
+
more: true
|
|
5896
|
+
};
|
|
5897
|
+
switch (tab) {
|
|
5898
|
+
case "overview":
|
|
5899
|
+
return renderOverviewView(output, options);
|
|
5900
|
+
case "sess":
|
|
5901
|
+
return renderSessionView(output, width, noColor2);
|
|
5902
|
+
case "tok":
|
|
5903
|
+
return renderTokenView(output, width, noColor2);
|
|
5904
|
+
case "model":
|
|
5905
|
+
return renderModelView(output, width, noColor2);
|
|
5906
|
+
case "cwd":
|
|
5907
|
+
return renderCwdView(output, width, noColor2);
|
|
5908
|
+
case "dow":
|
|
5909
|
+
return renderDowView(output, width, noColor2);
|
|
5910
|
+
case "tod":
|
|
5911
|
+
return renderTodView(output, width, noColor2);
|
|
5912
|
+
default:
|
|
5913
|
+
return renderOverviewView(output, options);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
function renderScreen(output, state) {
|
|
5917
|
+
const width = getRenderWidth(state);
|
|
5918
|
+
const rows = process.stdout.rows ?? 40;
|
|
5919
|
+
const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
|
|
5920
|
+
const tabBarLines = tabBar.split(`
|
|
5921
|
+
`);
|
|
5922
|
+
const rangeLabel = state.noColor ? ` ${output.dateRange.since} \u2192 ${output.dateRange.until}` : ` ${DIM2}${output.dateRange.since} \u2192 ${output.dateRange.until}${RESET3}`;
|
|
5923
|
+
const headerLines = [...tabBarLines, rangeLabel, ""];
|
|
5924
|
+
const footerLines = [""];
|
|
5925
|
+
const viewportHeight = getViewportHeight(state, width, rows);
|
|
5926
|
+
const viewContent = renderActiveView(output, state.metricTab, width, state.noColor, state.noInsights);
|
|
5927
|
+
const contentLines = viewContent.split(`
|
|
5928
|
+
`);
|
|
5929
|
+
const effectiveOffset = clampScrollOffset(state.scrollOffset, contentLines.length, viewportHeight);
|
|
5930
|
+
state.scrollOffset = effectiveOffset;
|
|
5931
|
+
const visibleContent = contentLines.slice(effectiveOffset, effectiveOffset + viewportHeight);
|
|
5932
|
+
const padding = Array.from({ length: Math.max(0, viewportHeight - visibleContent.length) }, () => "");
|
|
5933
|
+
const scrollInfo = contentLines.length > viewportHeight ? state.noColor ? ` Lines ${effectiveOffset + 1}-${Math.min(contentLines.length, effectiveOffset + viewportHeight)} of ${contentLines.length}` : ` ${DIM2}Lines ${effectiveOffset + 1}-${Math.min(contentLines.length, effectiveOffset + viewportHeight)} of ${contentLines.length}${RESET3}` : "";
|
|
5934
|
+
return `${HOME_CLEAR2}${HIDE_CURSOR2}${[
|
|
5935
|
+
...headerLines,
|
|
5936
|
+
...visibleContent,
|
|
5937
|
+
...padding,
|
|
5938
|
+
scrollInfo,
|
|
5939
|
+
...footerLines
|
|
5940
|
+
].join(`
|
|
5941
|
+
`)}`;
|
|
5942
|
+
}
|
|
5943
|
+
function renderLoading2(state) {
|
|
5944
|
+
const width = getRenderWidth(state);
|
|
5945
|
+
const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
|
|
5946
|
+
const loading = state.noColor ? " Loading data..." : ` ${YELLOW2}${BOLD2}Loading data...${RESET3}`;
|
|
5947
|
+
return `${HOME_CLEAR2}${HIDE_CURSOR2}${tabBar}
|
|
5948
|
+
|
|
5949
|
+
${loading}`;
|
|
5950
|
+
}
|
|
5951
|
+
function enterAltScreen2() {
|
|
5952
|
+
process.stdout.write(`${ALT_SCREEN_ON2}${ALT_SCROLL_ON2}${HOME_CLEAR2}${HIDE_CURSOR2}`);
|
|
5953
|
+
}
|
|
5954
|
+
function leaveAltScreen2() {
|
|
5955
|
+
process.stdout.write(`${SHOW_CURSOR2}${ALT_SCROLL_OFF2}${ALT_SCREEN_OFF2}`);
|
|
5956
|
+
}
|
|
5957
|
+
function paint2(content) {
|
|
5958
|
+
process.stdout.write(content);
|
|
5959
|
+
}
|
|
5960
|
+
function suspendRawMode2() {
|
|
5961
|
+
if (process.stdin.isTTY) {
|
|
5962
|
+
process.stdin.setRawMode(false);
|
|
5963
|
+
}
|
|
5964
|
+
process.stdin.pause();
|
|
5965
|
+
process.stdout.write(SHOW_CURSOR2);
|
|
5966
|
+
}
|
|
5967
|
+
function resumeRawMode2() {
|
|
5968
|
+
if (process.stdin.isTTY) {
|
|
5969
|
+
process.stdin.setRawMode(true);
|
|
5970
|
+
}
|
|
5971
|
+
process.stdin.resume();
|
|
5972
|
+
process.stdout.write(HIDE_CURSOR2);
|
|
5973
|
+
}
|
|
5974
|
+
async function startTabbedDashboard(providers, options) {
|
|
5975
|
+
const state = {
|
|
5976
|
+
timeRange: options.initialTimeRange ?? "30d",
|
|
5977
|
+
initialTimeRange: options.initialTimeRange ?? "30d",
|
|
5978
|
+
metricTab: "overview",
|
|
5979
|
+
scrollOffset: 0,
|
|
5980
|
+
dataCache: new Map,
|
|
5981
|
+
inflightLoads: new Map,
|
|
5982
|
+
noColor: options.noColor,
|
|
5983
|
+
noInsights: options.noInsights ?? false,
|
|
5984
|
+
baseUntil: options.until ?? new Date().toISOString().slice(0, 10),
|
|
5985
|
+
initialRange: options.initialRange ?? null,
|
|
5986
|
+
width: options.width ?? null
|
|
5987
|
+
};
|
|
5988
|
+
enterAltScreen2();
|
|
5989
|
+
let currentOutput = null;
|
|
5990
|
+
let activeLoadId = 0;
|
|
5991
|
+
let shouldClose = false;
|
|
5992
|
+
const loadAndRender = async (timeRange) => {
|
|
5993
|
+
const loadId = ++activeLoadId;
|
|
5994
|
+
paint2(renderLoading2(state));
|
|
5995
|
+
let output;
|
|
5996
|
+
try {
|
|
5997
|
+
output = await loadForRange(state, providers, timeRange);
|
|
5998
|
+
} catch (error) {
|
|
5999
|
+
if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
|
|
6000
|
+
return;
|
|
6001
|
+
}
|
|
6002
|
+
throw error;
|
|
6003
|
+
}
|
|
6004
|
+
if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
|
|
6005
|
+
return;
|
|
6006
|
+
}
|
|
6007
|
+
currentOutput = output;
|
|
6008
|
+
paint2(renderScreen(currentOutput, state));
|
|
6009
|
+
};
|
|
6010
|
+
const rerender = () => {
|
|
6011
|
+
if (currentOutput) {
|
|
6012
|
+
paint2(renderScreen(currentOutput, state));
|
|
6013
|
+
}
|
|
6014
|
+
};
|
|
6015
|
+
const onResize = () => {
|
|
6016
|
+
rerender();
|
|
6017
|
+
};
|
|
6018
|
+
process.stdout.on("resize", onResize);
|
|
6019
|
+
try {
|
|
6020
|
+
await loadAndRender(state.timeRange);
|
|
6021
|
+
let fatalError = null;
|
|
6022
|
+
await new Promise((resolve) => {
|
|
6023
|
+
const settleFailure = (error) => {
|
|
6024
|
+
fatalError = error;
|
|
6025
|
+
cleanup();
|
|
6026
|
+
resolve();
|
|
6027
|
+
};
|
|
6028
|
+
const runAsyncAction = (action) => {
|
|
6029
|
+
action().catch((error) => {
|
|
6030
|
+
settleFailure(error);
|
|
6031
|
+
});
|
|
6032
|
+
};
|
|
6033
|
+
const onKeypress = (_input, key) => {
|
|
6034
|
+
if (key.ctrl && key.name === "c") {
|
|
6035
|
+
cleanup();
|
|
6036
|
+
resolve();
|
|
6037
|
+
return;
|
|
6038
|
+
}
|
|
6039
|
+
if (key.name === "q" || key.name === "escape") {
|
|
6040
|
+
cleanup();
|
|
6041
|
+
resolve();
|
|
6042
|
+
return;
|
|
6043
|
+
}
|
|
6044
|
+
if (key.name === "left") {
|
|
6045
|
+
const idx = TIME_RANGES.indexOf(state.timeRange);
|
|
6046
|
+
const newIdx = (idx - 1 + TIME_RANGES.length) % TIME_RANGES.length;
|
|
6047
|
+
state.timeRange = TIME_RANGES[newIdx];
|
|
6048
|
+
state.scrollOffset = 0;
|
|
6049
|
+
runAsyncAction(() => loadAndRender(state.timeRange));
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
if (key.name === "right") {
|
|
6053
|
+
const idx = TIME_RANGES.indexOf(state.timeRange);
|
|
6054
|
+
const newIdx = (idx + 1) % TIME_RANGES.length;
|
|
6055
|
+
state.timeRange = TIME_RANGES[newIdx];
|
|
6056
|
+
state.scrollOffset = 0;
|
|
6057
|
+
runAsyncAction(() => loadAndRender(state.timeRange));
|
|
6058
|
+
return;
|
|
6059
|
+
}
|
|
6060
|
+
if (key.name === "tab") {
|
|
6061
|
+
const idx = METRIC_TABS.indexOf(state.metricTab);
|
|
6062
|
+
const newIdx = key.shift ? (idx - 1 + METRIC_TABS.length) % METRIC_TABS.length : (idx + 1) % METRIC_TABS.length;
|
|
6063
|
+
state.metricTab = METRIC_TABS[newIdx];
|
|
6064
|
+
state.scrollOffset = 0;
|
|
6065
|
+
rerender();
|
|
6066
|
+
return;
|
|
6067
|
+
}
|
|
6068
|
+
const digit = key.sequence?.match(/^[1-7]$/)?.[0];
|
|
6069
|
+
if (digit) {
|
|
6070
|
+
const tabIdx = Number(digit) - 1;
|
|
6071
|
+
if (tabIdx < METRIC_TABS.length) {
|
|
6072
|
+
state.metricTab = METRIC_TABS[tabIdx];
|
|
6073
|
+
state.scrollOffset = 0;
|
|
6074
|
+
rerender();
|
|
6075
|
+
}
|
|
6076
|
+
return;
|
|
6077
|
+
}
|
|
6078
|
+
const rows = process.stdout.rows ?? 40;
|
|
6079
|
+
const viewportHeight = getViewportHeight(state, getRenderWidth(state), rows);
|
|
6080
|
+
if (key.name === "up") {
|
|
6081
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - 1);
|
|
6082
|
+
rerender();
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6085
|
+
if (key.name === "down") {
|
|
6086
|
+
state.scrollOffset += 1;
|
|
6087
|
+
rerender();
|
|
6088
|
+
return;
|
|
6089
|
+
}
|
|
6090
|
+
if (key.name === "pageup") {
|
|
6091
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - viewportHeight);
|
|
6092
|
+
rerender();
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
6095
|
+
if (key.name === "pagedown") {
|
|
6096
|
+
state.scrollOffset += viewportHeight;
|
|
6097
|
+
rerender();
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
6100
|
+
if (key.name === "home") {
|
|
6101
|
+
state.scrollOffset = 0;
|
|
6102
|
+
rerender();
|
|
6103
|
+
return;
|
|
6104
|
+
}
|
|
6105
|
+
if (key.name === "end") {
|
|
6106
|
+
state.scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
6107
|
+
rerender();
|
|
6108
|
+
return;
|
|
6109
|
+
}
|
|
6110
|
+
};
|
|
6111
|
+
function cleanup() {
|
|
6112
|
+
shouldClose = true;
|
|
6113
|
+
process.stdin.off("keypress", onKeypress);
|
|
6114
|
+
suspendRawMode2();
|
|
6115
|
+
}
|
|
6116
|
+
resumeRawMode2();
|
|
6117
|
+
process.stdin.on("keypress", onKeypress);
|
|
6118
|
+
});
|
|
6119
|
+
if (fatalError) {
|
|
6120
|
+
throw fatalError;
|
|
6121
|
+
}
|
|
6122
|
+
} finally {
|
|
6123
|
+
process.stdout.off("resize", onResize);
|
|
6124
|
+
leaveAltScreen2();
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
|
|
3152
6128
|
// packages/cli/src/cli.ts
|
|
3153
6129
|
var FORMAT_VALUES = ["json", "svg", "png", "terminal"];
|
|
3154
6130
|
var THEME_VALUES = ["dark", "light"];
|
|
@@ -3208,6 +6184,7 @@ function buildHelpText() {
|
|
|
3208
6184
|
return [
|
|
3209
6185
|
`tokenleak ${VERSION}`,
|
|
3210
6186
|
"Visualize AI coding assistant token usage across providers.",
|
|
6187
|
+
"Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
|
|
3211
6188
|
"",
|
|
3212
6189
|
"Usage:",
|
|
3213
6190
|
" tokenleak [flags]",
|
|
@@ -3229,6 +6206,7 @@ function buildHelpText() {
|
|
|
3229
6206
|
" -w, --width <number> Terminal render width",
|
|
3230
6207
|
" -p, --provider <list> Provider filter list, comma-separated",
|
|
3231
6208
|
" --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
|
|
6209
|
+
" --more Add expanded PNG/SVG stats and unlock compare cards",
|
|
3232
6210
|
" --clipboard Copy rendered output to the clipboard",
|
|
3233
6211
|
" --open Open the generated output file",
|
|
3234
6212
|
" --upload <target> Upload rendered output, currently: gist",
|
|
@@ -3272,6 +6250,81 @@ function normalizeCliArg(arg) {
|
|
|
3272
6250
|
};
|
|
3273
6251
|
return flagMap[arg] ?? arg;
|
|
3274
6252
|
}
|
|
6253
|
+
function buildInteractiveSummary(cliArgs, ok, exitCode) {
|
|
6254
|
+
if (!ok) {
|
|
6255
|
+
return `Command exited with code ${exitCode}.`;
|
|
6256
|
+
}
|
|
6257
|
+
if (typeof cliArgs["output"] === "string") {
|
|
6258
|
+
const outputPath = cliArgs["output"];
|
|
6259
|
+
const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
|
|
6260
|
+
return `${format2} written to ${outputPath}.`;
|
|
6261
|
+
}
|
|
6262
|
+
if (cliArgs["listProviders"]) {
|
|
6263
|
+
return "Provider registry loaded.";
|
|
6264
|
+
}
|
|
6265
|
+
if (cliArgs["liveServer"]) {
|
|
6266
|
+
return "Live dashboard stopped.";
|
|
6267
|
+
}
|
|
6268
|
+
if (cliArgs["compare"]) {
|
|
6269
|
+
return "Compare report generated.";
|
|
6270
|
+
}
|
|
6271
|
+
const format = String(cliArgs["format"] ?? "terminal");
|
|
6272
|
+
if (format === "terminal") {
|
|
6273
|
+
return "Terminal dashboard generated.";
|
|
6274
|
+
}
|
|
6275
|
+
return `${format.toUpperCase()} command finished successfully.`;
|
|
6276
|
+
}
|
|
6277
|
+
async function executeInteractiveCommand(request) {
|
|
6278
|
+
try {
|
|
6279
|
+
const cliPath = process.argv[1];
|
|
6280
|
+
if (!cliPath) {
|
|
6281
|
+
return {
|
|
6282
|
+
ok: false,
|
|
6283
|
+
summary: "Could not resolve the current tokenleak entrypoint.",
|
|
6284
|
+
stdout: "",
|
|
6285
|
+
stderr: "Error: process.argv[1] is missing."
|
|
6286
|
+
};
|
|
6287
|
+
}
|
|
6288
|
+
const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
|
|
6289
|
+
if (request.executionMode === "inherit") {
|
|
6290
|
+
const proc2 = Bun.spawn(command, {
|
|
6291
|
+
stdin: "inherit",
|
|
6292
|
+
stdout: "inherit",
|
|
6293
|
+
stderr: "inherit"
|
|
6294
|
+
});
|
|
6295
|
+
const exitCode2 = await proc2.exited;
|
|
6296
|
+
return {
|
|
6297
|
+
ok: exitCode2 === 0,
|
|
6298
|
+
summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
|
|
6299
|
+
stdout: "",
|
|
6300
|
+
stderr: ""
|
|
6301
|
+
};
|
|
6302
|
+
}
|
|
6303
|
+
const proc = Bun.spawn(command, {
|
|
6304
|
+
stdin: "ignore",
|
|
6305
|
+
stdout: "pipe",
|
|
6306
|
+
stderr: "pipe"
|
|
6307
|
+
});
|
|
6308
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
6309
|
+
proc.exited,
|
|
6310
|
+
new Response(proc.stdout).text(),
|
|
6311
|
+
new Response(proc.stderr).text()
|
|
6312
|
+
]);
|
|
6313
|
+
return {
|
|
6314
|
+
ok: exitCode === 0,
|
|
6315
|
+
summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
|
|
6316
|
+
stdout,
|
|
6317
|
+
stderr
|
|
6318
|
+
};
|
|
6319
|
+
} catch (error) {
|
|
6320
|
+
return {
|
|
6321
|
+
ok: false,
|
|
6322
|
+
summary: "Interactive command failed before it could finish.",
|
|
6323
|
+
stdout: "",
|
|
6324
|
+
stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
|
|
6325
|
+
};
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
3275
6328
|
function normalizeCliArgv(argv) {
|
|
3276
6329
|
const normalized = argv.map(normalizeCliArg);
|
|
3277
6330
|
const result = [];
|
|
@@ -3330,35 +6383,6 @@ function inferFormatFromPath(filePath) {
|
|
|
3330
6383
|
return null;
|
|
3331
6384
|
}
|
|
3332
6385
|
}
|
|
3333
|
-
var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
|
3334
|
-
function isValidDate(dateStr) {
|
|
3335
|
-
if (!DATE_FORMAT.test(dateStr))
|
|
3336
|
-
return false;
|
|
3337
|
-
const d = new Date(dateStr + "T00:00:00Z");
|
|
3338
|
-
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
|
|
3339
|
-
}
|
|
3340
|
-
function computeDateRange(args) {
|
|
3341
|
-
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
3342
|
-
if (args.until && !isValidDate(args.until)) {
|
|
3343
|
-
throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
|
|
3344
|
-
}
|
|
3345
|
-
if (args.since && !isValidDate(args.since)) {
|
|
3346
|
-
throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
|
|
3347
|
-
}
|
|
3348
|
-
let since;
|
|
3349
|
-
if (args.since) {
|
|
3350
|
-
since = args.since;
|
|
3351
|
-
} else {
|
|
3352
|
-
const daysBack = args.days ?? DEFAULT_DAYS;
|
|
3353
|
-
const d = new Date(until);
|
|
3354
|
-
d.setDate(d.getDate() - daysBack);
|
|
3355
|
-
since = d.toISOString().slice(0, 10);
|
|
3356
|
-
}
|
|
3357
|
-
if (since > until) {
|
|
3358
|
-
throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
|
|
3359
|
-
}
|
|
3360
|
-
return { since, until };
|
|
3361
|
-
}
|
|
3362
6386
|
function resolveConfig(cliArgs) {
|
|
3363
6387
|
const fileConfig = loadConfig();
|
|
3364
6388
|
const envConfig = loadEnvOverrides();
|
|
@@ -3370,6 +6394,7 @@ function resolveConfig(cliArgs) {
|
|
|
3370
6394
|
width: 80,
|
|
3371
6395
|
noColor: false,
|
|
3372
6396
|
noInsights: false,
|
|
6397
|
+
more: false,
|
|
3373
6398
|
claude: false,
|
|
3374
6399
|
codex: false,
|
|
3375
6400
|
openCode: false,
|
|
@@ -3393,6 +6418,8 @@ function resolveConfig(cliArgs) {
|
|
|
3393
6418
|
merged.noColor = fileConfig.noColor;
|
|
3394
6419
|
if (fileConfig.noInsights !== undefined)
|
|
3395
6420
|
merged.noInsights = fileConfig.noInsights;
|
|
6421
|
+
if (fileConfig.more !== undefined)
|
|
6422
|
+
merged.more = fileConfig.more;
|
|
3396
6423
|
if (envConfig.format)
|
|
3397
6424
|
merged.format = envConfig.format;
|
|
3398
6425
|
if (envConfig.theme)
|
|
@@ -3434,6 +6461,9 @@ function resolveConfig(cliArgs) {
|
|
|
3434
6461
|
if (cliArgs["noInsights"] !== undefined) {
|
|
3435
6462
|
result.noInsights = cliArgs["noInsights"];
|
|
3436
6463
|
}
|
|
6464
|
+
if (cliArgs["more"] !== undefined) {
|
|
6465
|
+
result.more = cliArgs["more"];
|
|
6466
|
+
}
|
|
3437
6467
|
if (cliArgs["compare"] !== undefined) {
|
|
3438
6468
|
result.compare = cliArgs["compare"];
|
|
3439
6469
|
}
|
|
@@ -3511,7 +6541,11 @@ async function runCompare(compareStr, currentRange, _registry, available) {
|
|
|
3511
6541
|
loadAndAggregate(currentRange, available),
|
|
3512
6542
|
loadAndAggregate(previousRange, available)
|
|
3513
6543
|
]);
|
|
3514
|
-
return
|
|
6544
|
+
return {
|
|
6545
|
+
compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
|
|
6546
|
+
currentData: currentResult.data,
|
|
6547
|
+
previousData: previousResult.data
|
|
6548
|
+
};
|
|
3515
6549
|
}
|
|
3516
6550
|
async function run(cliArgs) {
|
|
3517
6551
|
const config = resolveConfig(cliArgs);
|
|
@@ -3544,12 +6578,45 @@ async function run(cliArgs) {
|
|
|
3544
6578
|
throw new TokenleakError("No provider data found");
|
|
3545
6579
|
}
|
|
3546
6580
|
if (config.compare) {
|
|
6581
|
+
const compareResult = await runCompare(config.compare, dateRange, registry, available);
|
|
6582
|
+
if (config.more && (config.format === "png" || config.format === "svg")) {
|
|
6583
|
+
const compareOutput = {
|
|
6584
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6585
|
+
generated: new Date().toISOString(),
|
|
6586
|
+
dateRange,
|
|
6587
|
+
providers: compareResult.currentData,
|
|
6588
|
+
aggregated: compareResult.compareOutput.periodA.stats,
|
|
6589
|
+
more: buildMoreStats(compareResult.currentData, dateRange, {
|
|
6590
|
+
previousRange: compareResult.compareOutput.periodB.range,
|
|
6591
|
+
previousProviders: compareResult.previousData
|
|
6592
|
+
})
|
|
6593
|
+
};
|
|
6594
|
+
const renderer2 = getRenderer(config.format);
|
|
6595
|
+
const renderOptions2 = {
|
|
6596
|
+
format: config.format,
|
|
6597
|
+
theme: config.theme,
|
|
6598
|
+
width: config.width,
|
|
6599
|
+
showInsights: !config.noInsights,
|
|
6600
|
+
noColor: config.noColor,
|
|
6601
|
+
output: config.output,
|
|
6602
|
+
more: true
|
|
6603
|
+
};
|
|
6604
|
+
const rendered3 = await renderer2.render(compareOutput, renderOptions2);
|
|
6605
|
+
if (config.output) {
|
|
6606
|
+
const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
|
|
6607
|
+
writeFileSync(config.output, data);
|
|
6608
|
+
} else {
|
|
6609
|
+
const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
|
|
6610
|
+
process.stdout.write(text2 + `
|
|
6611
|
+
`);
|
|
6612
|
+
}
|
|
6613
|
+
return;
|
|
6614
|
+
}
|
|
3547
6615
|
if (config.format !== "json" && config.format !== "terminal") {
|
|
3548
6616
|
process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
|
|
3549
6617
|
`);
|
|
3550
6618
|
}
|
|
3551
|
-
const
|
|
3552
|
-
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
6619
|
+
const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
|
|
3553
6620
|
if (config.output) {
|
|
3554
6621
|
writeFileSync(config.output, rendered2);
|
|
3555
6622
|
} else {
|
|
@@ -3576,7 +6643,8 @@ async function run(cliArgs) {
|
|
|
3576
6643
|
generated: new Date().toISOString(),
|
|
3577
6644
|
dateRange,
|
|
3578
6645
|
providers: providerDataList,
|
|
3579
|
-
aggregated: stats
|
|
6646
|
+
aggregated: stats,
|
|
6647
|
+
more: config.more ? buildMoreStats(providerDataList, dateRange) : null
|
|
3580
6648
|
};
|
|
3581
6649
|
if (config.liveServer) {
|
|
3582
6650
|
const ignoredFlags = [];
|
|
@@ -3598,7 +6666,8 @@ async function run(cliArgs) {
|
|
|
3598
6666
|
width: config.width,
|
|
3599
6667
|
showInsights: !config.noInsights,
|
|
3600
6668
|
noColor: config.noColor,
|
|
3601
|
-
output: config.output
|
|
6669
|
+
output: config.output,
|
|
6670
|
+
more: config.more
|
|
3602
6671
|
};
|
|
3603
6672
|
const { port } = await startLiveServer(output, renderOptions2);
|
|
3604
6673
|
await new Promise((resolve) => {
|
|
@@ -3621,7 +6690,8 @@ Shutting down server...
|
|
|
3621
6690
|
width: config.width,
|
|
3622
6691
|
showInsights: !config.noInsights,
|
|
3623
6692
|
noColor: config.noColor,
|
|
3624
|
-
output: config.output
|
|
6693
|
+
output: config.output,
|
|
6694
|
+
more: config.more
|
|
3625
6695
|
};
|
|
3626
6696
|
const rendered = await renderer.render(output, renderOptions);
|
|
3627
6697
|
if (config.output) {
|
|
@@ -3710,6 +6780,11 @@ var main = defineCommand({
|
|
|
3710
6780
|
description: "Hide insights panel",
|
|
3711
6781
|
default: false
|
|
3712
6782
|
},
|
|
6783
|
+
more: {
|
|
6784
|
+
type: "boolean",
|
|
6785
|
+
description: "Add expanded PNG/SVG stats and compare cards",
|
|
6786
|
+
default: false
|
|
6787
|
+
},
|
|
3713
6788
|
compare: {
|
|
3714
6789
|
type: "string",
|
|
3715
6790
|
description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
|
|
@@ -3786,6 +6861,8 @@ var main = defineCommand({
|
|
|
3786
6861
|
cliArgs["noColor"] = true;
|
|
3787
6862
|
if (args.noInsights)
|
|
3788
6863
|
cliArgs["noInsights"] = true;
|
|
6864
|
+
if (args.more)
|
|
6865
|
+
cliArgs["more"] = true;
|
|
3789
6866
|
if (args.compare !== undefined)
|
|
3790
6867
|
cliArgs["compare"] = args.compare;
|
|
3791
6868
|
if (args.provider !== undefined)
|
|
@@ -3827,12 +6904,31 @@ if (isDirectExecution) {
|
|
|
3827
6904
|
process.stdout.write(buildVersionText());
|
|
3828
6905
|
process.exit(0);
|
|
3829
6906
|
}
|
|
3830
|
-
|
|
6907
|
+
if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
6908
|
+
const registry = new ProviderRegistry;
|
|
6909
|
+
registerBuiltInProviders(registry);
|
|
6910
|
+
const available = await registry.getAvailable();
|
|
6911
|
+
const launchTabbed = async (opts) => {
|
|
6912
|
+
const requested = new Set(opts.providerNames ?? []);
|
|
6913
|
+
const scopedProviders = requested.size > 0 ? available.filter((provider) => providerMatchesFilter(provider, requested)) : available;
|
|
6914
|
+
if (scopedProviders.length === 0) {
|
|
6915
|
+
throw new TokenleakError("No provider data found");
|
|
6916
|
+
}
|
|
6917
|
+
await startTabbedDashboard(scopedProviders, opts);
|
|
6918
|
+
};
|
|
6919
|
+
await startInteractiveCli({
|
|
6920
|
+
version: VERSION,
|
|
6921
|
+
helpText: buildHelpText()
|
|
6922
|
+
}, executeInteractiveCommand, launchTabbed);
|
|
6923
|
+
} else {
|
|
6924
|
+
await runMain(main);
|
|
6925
|
+
}
|
|
3831
6926
|
}
|
|
3832
6927
|
export {
|
|
3833
6928
|
run,
|
|
3834
6929
|
resolveConfig,
|
|
3835
6930
|
normalizeCliArgv,
|
|
3836
6931
|
inferFormatFromPath,
|
|
3837
|
-
computeDateRange
|
|
6932
|
+
computeDateRange,
|
|
6933
|
+
buildInteractiveSummary
|
|
3838
6934
|
};
|