tokenleak 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -14
- package/package.json +1 -1
- package/tokenleak +1884 -45
package/tokenleak
CHANGED
|
@@ -755,6 +755,234 @@ function computePreviousPeriod(current) {
|
|
|
755
755
|
until: formatDateStringUtc(prevUntil)
|
|
756
756
|
};
|
|
757
757
|
}
|
|
758
|
+
// packages/core/dist/aggregation/more.js
|
|
759
|
+
function daysInMonth(dateString) {
|
|
760
|
+
const [year, month] = dateString.split("-").map(Number);
|
|
761
|
+
if (!year || !month) {
|
|
762
|
+
return 30;
|
|
763
|
+
}
|
|
764
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
765
|
+
}
|
|
766
|
+
function buildInputOutput(providers) {
|
|
767
|
+
let inputTokens = 0;
|
|
768
|
+
let outputTokens = 0;
|
|
769
|
+
for (const provider of providers) {
|
|
770
|
+
for (const day of provider.daily) {
|
|
771
|
+
inputTokens += day.inputTokens;
|
|
772
|
+
outputTokens += day.outputTokens;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const nonCacheTokens = inputTokens + outputTokens;
|
|
776
|
+
return {
|
|
777
|
+
inputPerOutput: outputTokens > 0 ? inputTokens / outputTokens : null,
|
|
778
|
+
outputPerInput: inputTokens > 0 ? outputTokens / inputTokens : null,
|
|
779
|
+
outputShare: nonCacheTokens > 0 ? outputTokens / nonCacheTokens : 0
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function buildMonthlyBurn(providers, range) {
|
|
783
|
+
const monthPrefix = range.until.slice(0, 7);
|
|
784
|
+
const monthStart = `${monthPrefix}-01`;
|
|
785
|
+
const observedSince = range.since > monthStart ? range.since : monthStart;
|
|
786
|
+
const observedDays = Math.max(1, Math.round((Date.parse(`${range.until}T00:00:00Z`) - Date.parse(`${observedSince}T00:00:00Z`)) / 86400000) + 1);
|
|
787
|
+
let observedTokens = 0;
|
|
788
|
+
let observedCost = 0;
|
|
789
|
+
for (const provider of providers) {
|
|
790
|
+
for (const day of provider.daily) {
|
|
791
|
+
if (day.date >= observedSince && day.date <= range.until) {
|
|
792
|
+
observedTokens += day.totalTokens;
|
|
793
|
+
observedCost += day.cost;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const calendarDays = daysInMonth(range.until);
|
|
798
|
+
const tokensPerDay = observedTokens / observedDays;
|
|
799
|
+
const costPerDay = observedCost / observedDays;
|
|
800
|
+
return {
|
|
801
|
+
projectedTokens: tokensPerDay * calendarDays,
|
|
802
|
+
projectedCost: costPerDay * calendarDays,
|
|
803
|
+
observedDays,
|
|
804
|
+
calendarDays
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function buildCacheEconomics(providers) {
|
|
808
|
+
let readTokens = 0;
|
|
809
|
+
let writeTokens = 0;
|
|
810
|
+
let inputTokens = 0;
|
|
811
|
+
for (const provider of providers) {
|
|
812
|
+
for (const day of provider.daily) {
|
|
813
|
+
readTokens += day.cacheReadTokens;
|
|
814
|
+
writeTokens += day.cacheWriteTokens;
|
|
815
|
+
inputTokens += day.inputTokens;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const readCoverage = readTokens + inputTokens > 0 ? readTokens / (readTokens + inputTokens) : 0;
|
|
819
|
+
return {
|
|
820
|
+
readTokens,
|
|
821
|
+
writeTokens,
|
|
822
|
+
readCoverage,
|
|
823
|
+
reuseRatio: writeTokens > 0 ? readTokens / writeTokens : null
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function collectEvents(providers) {
|
|
827
|
+
return providers.flatMap((provider) => provider.events ?? []);
|
|
828
|
+
}
|
|
829
|
+
function buildHourOfDay(events) {
|
|
830
|
+
const buckets = Array.from({ length: 24 }, (_, hour) => ({
|
|
831
|
+
hour,
|
|
832
|
+
tokens: 0,
|
|
833
|
+
cost: 0,
|
|
834
|
+
count: 0
|
|
835
|
+
}));
|
|
836
|
+
for (const event of events) {
|
|
837
|
+
const date = new Date(event.timestamp);
|
|
838
|
+
if (Number.isNaN(date.getTime())) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const bucket = buckets[date.getUTCHours()];
|
|
842
|
+
if (!bucket) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
bucket.tokens += event.totalTokens;
|
|
846
|
+
bucket.cost += event.cost;
|
|
847
|
+
bucket.count += 1;
|
|
848
|
+
}
|
|
849
|
+
return buckets;
|
|
850
|
+
}
|
|
851
|
+
function buildSessionMetrics(events) {
|
|
852
|
+
const sessions = new Map;
|
|
853
|
+
const projects = new Map;
|
|
854
|
+
for (const event of events) {
|
|
855
|
+
const key = event.sessionId?.trim() || `${event.provider}:${event.timestamp}`;
|
|
856
|
+
const timestamp = Date.parse(event.timestamp);
|
|
857
|
+
const safeTime = Number.isFinite(timestamp) ? timestamp : 0;
|
|
858
|
+
let session = sessions.get(key);
|
|
859
|
+
if (!session) {
|
|
860
|
+
session = {
|
|
861
|
+
label: event.projectId?.trim() || event.sessionId?.trim() || key,
|
|
862
|
+
tokens: 0,
|
|
863
|
+
cost: 0,
|
|
864
|
+
count: 0,
|
|
865
|
+
projectId: event.projectId?.trim() || undefined,
|
|
866
|
+
firstTimestamp: safeTime,
|
|
867
|
+
lastTimestamp: safeTime,
|
|
868
|
+
explicitDurationMs: 0,
|
|
869
|
+
hasExplicitDuration: false
|
|
870
|
+
};
|
|
871
|
+
sessions.set(key, session);
|
|
872
|
+
}
|
|
873
|
+
session.tokens += event.totalTokens;
|
|
874
|
+
session.cost += event.cost;
|
|
875
|
+
session.count += 1;
|
|
876
|
+
session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
|
|
877
|
+
session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
|
|
878
|
+
if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
|
|
879
|
+
session.explicitDurationMs += Math.max(0, event.durationMs);
|
|
880
|
+
session.hasExplicitDuration = true;
|
|
881
|
+
}
|
|
882
|
+
if (event.projectId?.trim()) {
|
|
883
|
+
projects.set(event.projectId, (projects.get(event.projectId) ?? 0) + event.totalTokens);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const sessionEntries = [...sessions.values()];
|
|
887
|
+
const totalSessions = sessionEntries.length;
|
|
888
|
+
let totalTokens = 0;
|
|
889
|
+
let totalCost = 0;
|
|
890
|
+
let totalMessages = 0;
|
|
891
|
+
let durationTotal = 0;
|
|
892
|
+
let durationCount = 0;
|
|
893
|
+
let longestSession = null;
|
|
894
|
+
let longestSessionDuration = -1;
|
|
895
|
+
for (const session of sessionEntries) {
|
|
896
|
+
totalTokens += session.tokens;
|
|
897
|
+
totalCost += session.cost;
|
|
898
|
+
totalMessages += session.count;
|
|
899
|
+
const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
|
|
900
|
+
if (derivedDurationMs > 0) {
|
|
901
|
+
durationTotal += derivedDurationMs;
|
|
902
|
+
durationCount += 1;
|
|
903
|
+
}
|
|
904
|
+
if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
|
|
905
|
+
longestSessionDuration = derivedDurationMs;
|
|
906
|
+
longestSession = {
|
|
907
|
+
label: session.label,
|
|
908
|
+
tokens: session.tokens,
|
|
909
|
+
cost: session.cost,
|
|
910
|
+
count: session.count,
|
|
911
|
+
durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
let topProject = null;
|
|
916
|
+
for (const [name, tokens] of projects) {
|
|
917
|
+
if (!topProject || tokens > topProject.tokens) {
|
|
918
|
+
topProject = { name, tokens };
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
totalSessions,
|
|
923
|
+
averageTokens: totalSessions > 0 ? totalTokens / totalSessions : 0,
|
|
924
|
+
averageCost: totalSessions > 0 ? totalCost / totalSessions : 0,
|
|
925
|
+
averageMessages: totalSessions > 0 ? totalMessages / totalSessions : 0,
|
|
926
|
+
averageDurationMs: durationCount > 0 ? durationTotal / durationCount : null,
|
|
927
|
+
longestSession,
|
|
928
|
+
projectCount: projects.size,
|
|
929
|
+
topProject
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
|
|
933
|
+
const currentModelTokens = new Map;
|
|
934
|
+
const previousModelTokens = new Map;
|
|
935
|
+
let currentTotal = 0;
|
|
936
|
+
let previousTotal = 0;
|
|
937
|
+
for (const provider of currentProviders) {
|
|
938
|
+
for (const day of provider.daily) {
|
|
939
|
+
for (const model of day.models) {
|
|
940
|
+
currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
941
|
+
currentTotal += model.totalTokens;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const provider of previousProviders) {
|
|
946
|
+
for (const day of provider.daily) {
|
|
947
|
+
for (const model of day.models) {
|
|
948
|
+
previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
|
|
949
|
+
previousTotal += model.totalTokens;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const models = new Set([
|
|
954
|
+
...currentModelTokens.keys(),
|
|
955
|
+
...previousModelTokens.keys()
|
|
956
|
+
]);
|
|
957
|
+
return [...models].map((model) => {
|
|
958
|
+
const currentTokens = currentModelTokens.get(model) ?? 0;
|
|
959
|
+
const previousTokens = previousModelTokens.get(model) ?? 0;
|
|
960
|
+
const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
|
|
961
|
+
const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
|
|
962
|
+
return {
|
|
963
|
+
model,
|
|
964
|
+
currentShare,
|
|
965
|
+
previousShare,
|
|
966
|
+
deltaShare: currentShare - previousShare,
|
|
967
|
+
currentTokens,
|
|
968
|
+
previousTokens
|
|
969
|
+
};
|
|
970
|
+
}).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
|
|
971
|
+
}
|
|
972
|
+
function buildMoreStats(providers, range, compare = null) {
|
|
973
|
+
const events = collectEvents(providers);
|
|
974
|
+
return {
|
|
975
|
+
inputOutput: buildInputOutput(providers),
|
|
976
|
+
monthlyBurn: buildMonthlyBurn(providers, range),
|
|
977
|
+
cacheEconomics: buildCacheEconomics(providers),
|
|
978
|
+
hourOfDay: buildHourOfDay(events),
|
|
979
|
+
sessionMetrics: buildSessionMetrics(events),
|
|
980
|
+
compare: compare ? {
|
|
981
|
+
previousRange: compare.previousRange,
|
|
982
|
+
modelMixShift: computeModelMixShift(providers, compare.previousProviders)
|
|
983
|
+
} : null
|
|
984
|
+
};
|
|
985
|
+
}
|
|
758
986
|
// packages/core/dist/index.js
|
|
759
987
|
var VERSION = "1.0.2";
|
|
760
988
|
|
|
@@ -1022,7 +1250,7 @@ async function* splitJsonlRecords(filePath) {
|
|
|
1022
1250
|
}
|
|
1023
1251
|
// packages/registry/dist/providers/claude-code.js
|
|
1024
1252
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1025
|
-
import { join } from "path";
|
|
1253
|
+
import { dirname, join, relative, sep } from "path";
|
|
1026
1254
|
import { homedir } from "os";
|
|
1027
1255
|
|
|
1028
1256
|
// packages/registry/dist/utils.js
|
|
@@ -1101,6 +1329,7 @@ function extractUsage(record) {
|
|
|
1101
1329
|
}
|
|
1102
1330
|
return {
|
|
1103
1331
|
date,
|
|
1332
|
+
timestamp,
|
|
1104
1333
|
model,
|
|
1105
1334
|
inputTokens,
|
|
1106
1335
|
outputTokens,
|
|
@@ -1181,13 +1410,18 @@ class ClaudeCodeProvider {
|
|
|
1181
1410
|
async load(range) {
|
|
1182
1411
|
const files = collectJsonlFiles(this.baseDir);
|
|
1183
1412
|
const allRecords = [];
|
|
1413
|
+
const allEvents = [];
|
|
1184
1414
|
for (const file of files) {
|
|
1185
1415
|
const latestRecordsByMessageId = new Map;
|
|
1186
1416
|
const anonymousRecords = [];
|
|
1417
|
+
const relativeFile = relative(this.baseDir, file).split(sep).join("/");
|
|
1418
|
+
const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
|
|
1187
1419
|
try {
|
|
1188
1420
|
for await (const record of splitJsonlRecords(file)) {
|
|
1189
1421
|
const usage = extractUsage(record);
|
|
1190
1422
|
if (usage !== null && isInRange(usage.date, range)) {
|
|
1423
|
+
usage.sessionId = relativeFile;
|
|
1424
|
+
usage.projectId = projectId;
|
|
1191
1425
|
if (usage.messageId) {
|
|
1192
1426
|
latestRecordsByMessageId.set(usage.messageId, usage);
|
|
1193
1427
|
} else {
|
|
@@ -1201,6 +1435,24 @@ class ClaudeCodeProvider {
|
|
|
1201
1435
|
allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
|
|
1202
1436
|
}
|
|
1203
1437
|
const daily = buildDailyUsage(allRecords);
|
|
1438
|
+
for (const record of allRecords) {
|
|
1439
|
+
const normalizedModel = normalizeModelName(record.model);
|
|
1440
|
+
const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1441
|
+
allEvents.push({
|
|
1442
|
+
provider: this.name,
|
|
1443
|
+
timestamp: record.timestamp,
|
|
1444
|
+
date: record.date,
|
|
1445
|
+
model: normalizedModel,
|
|
1446
|
+
inputTokens: record.inputTokens,
|
|
1447
|
+
outputTokens: record.outputTokens,
|
|
1448
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1449
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1450
|
+
totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
|
|
1451
|
+
cost,
|
|
1452
|
+
sessionId: record.sessionId,
|
|
1453
|
+
projectId: record.projectId
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1204
1456
|
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
1205
1457
|
const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
|
|
1206
1458
|
return {
|
|
@@ -1209,13 +1461,14 @@ class ClaudeCodeProvider {
|
|
|
1209
1461
|
daily,
|
|
1210
1462
|
totalTokens,
|
|
1211
1463
|
totalCost,
|
|
1212
|
-
colors: this.colors
|
|
1464
|
+
colors: this.colors,
|
|
1465
|
+
events: allEvents
|
|
1213
1466
|
};
|
|
1214
1467
|
}
|
|
1215
1468
|
}
|
|
1216
1469
|
// packages/registry/dist/providers/codex.js
|
|
1217
1470
|
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1218
|
-
import { join as join2 } from "path";
|
|
1471
|
+
import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
|
|
1219
1472
|
import { homedir as homedir2 } from "os";
|
|
1220
1473
|
var CODEX_COLORS = {
|
|
1221
1474
|
primary: "#10a37f",
|
|
@@ -1373,6 +1626,7 @@ function parseTokenCountUsage(record, context) {
|
|
|
1373
1626
|
const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
|
|
1374
1627
|
return {
|
|
1375
1628
|
date,
|
|
1629
|
+
timestamp,
|
|
1376
1630
|
model: context.model,
|
|
1377
1631
|
inputTokens,
|
|
1378
1632
|
outputTokens: usage.outputTokens,
|
|
@@ -1403,6 +1657,7 @@ function parseUsageRecord(record, context) {
|
|
|
1403
1657
|
}
|
|
1404
1658
|
return {
|
|
1405
1659
|
date,
|
|
1660
|
+
timestamp: legacyEvent.timestamp,
|
|
1406
1661
|
model: compactModelDateSuffix(legacyEvent.model),
|
|
1407
1662
|
inputTokens: legacyEvent.usage.input_tokens,
|
|
1408
1663
|
outputTokens: legacyEvent.usage.output_tokens,
|
|
@@ -1429,11 +1684,14 @@ class CodexProvider {
|
|
|
1429
1684
|
async load(range) {
|
|
1430
1685
|
const dailyMap = new Map;
|
|
1431
1686
|
const files = collectJsonlFiles2(this.sessionsDir);
|
|
1687
|
+
const events = [];
|
|
1432
1688
|
for (const file of files) {
|
|
1433
1689
|
const context = {
|
|
1434
1690
|
model: "gpt-5",
|
|
1435
1691
|
previousTotals: null
|
|
1436
1692
|
};
|
|
1693
|
+
const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
|
|
1694
|
+
const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
|
|
1437
1695
|
try {
|
|
1438
1696
|
for await (const record of splitJsonlRecords(file)) {
|
|
1439
1697
|
const usage = parseUsageRecord(record, context);
|
|
@@ -1443,12 +1701,28 @@ class CodexProvider {
|
|
|
1443
1701
|
if (!isInRange(usage.date, range)) {
|
|
1444
1702
|
continue;
|
|
1445
1703
|
}
|
|
1704
|
+
usage.sessionId = relativeFile;
|
|
1705
|
+
usage.projectId = projectDir === "." ? undefined : projectDir;
|
|
1446
1706
|
const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
|
|
1447
1707
|
const inputTokens = usage.inputTokens;
|
|
1448
1708
|
const outputTokens = usage.outputTokens;
|
|
1449
1709
|
const cacheReadTokens = usage.cacheReadTokens;
|
|
1450
1710
|
const cacheWriteTokens = usage.cacheWriteTokens;
|
|
1451
1711
|
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1712
|
+
events.push({
|
|
1713
|
+
provider: this.name,
|
|
1714
|
+
timestamp: usage.timestamp,
|
|
1715
|
+
date: usage.date,
|
|
1716
|
+
model: normalizedModel,
|
|
1717
|
+
inputTokens,
|
|
1718
|
+
outputTokens,
|
|
1719
|
+
cacheReadTokens,
|
|
1720
|
+
cacheWriteTokens,
|
|
1721
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
1722
|
+
cost,
|
|
1723
|
+
sessionId: usage.sessionId,
|
|
1724
|
+
projectId: usage.projectId
|
|
1725
|
+
});
|
|
1452
1726
|
if (!dailyMap.has(usage.date)) {
|
|
1453
1727
|
dailyMap.set(usage.date, new Map);
|
|
1454
1728
|
}
|
|
@@ -1503,7 +1777,8 @@ class CodexProvider {
|
|
|
1503
1777
|
daily,
|
|
1504
1778
|
totalTokens,
|
|
1505
1779
|
totalCost,
|
|
1506
|
-
colors: this.colors
|
|
1780
|
+
colors: this.colors,
|
|
1781
|
+
events
|
|
1507
1782
|
};
|
|
1508
1783
|
}
|
|
1509
1784
|
}
|
|
@@ -1513,7 +1788,7 @@ import { join as join3 } from "path";
|
|
|
1513
1788
|
import { homedir as homedir3 } from "os";
|
|
1514
1789
|
import { Database } from "bun:sqlite";
|
|
1515
1790
|
var PROVIDER_NAME = "open-code";
|
|
1516
|
-
var DISPLAY_NAME = "
|
|
1791
|
+
var DISPLAY_NAME = "OpenCode";
|
|
1517
1792
|
var COLORS = {
|
|
1518
1793
|
primary: "#6366f1",
|
|
1519
1794
|
secondary: "#a78bfa",
|
|
@@ -1549,12 +1824,46 @@ function extractDate2(createdAt) {
|
|
|
1549
1824
|
}
|
|
1550
1825
|
return date.toISOString().slice(0, 10);
|
|
1551
1826
|
}
|
|
1827
|
+
function toTimestampMillis(createdAt) {
|
|
1828
|
+
const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
|
|
1829
|
+
if (!Number.isFinite(timestamp)) {
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
|
|
1833
|
+
return Number.isFinite(millis) ? millis : null;
|
|
1834
|
+
}
|
|
1835
|
+
function toIsoTimestamp(createdAt) {
|
|
1836
|
+
const millis = toTimestampMillis(createdAt);
|
|
1837
|
+
if (millis === null) {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
const date = new Date(millis);
|
|
1841
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1842
|
+
}
|
|
1552
1843
|
function getRecordCost(record) {
|
|
1553
1844
|
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
1554
1845
|
return record.explicitCost;
|
|
1555
1846
|
}
|
|
1556
1847
|
return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1557
1848
|
}
|
|
1849
|
+
function toUsageEvent(record) {
|
|
1850
|
+
const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
1851
|
+
return {
|
|
1852
|
+
provider: PROVIDER_NAME,
|
|
1853
|
+
timestamp: record.timestamp,
|
|
1854
|
+
date: record.date,
|
|
1855
|
+
model: normalizeModelName(record.model),
|
|
1856
|
+
inputTokens: record.inputTokens,
|
|
1857
|
+
outputTokens: record.outputTokens,
|
|
1858
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1859
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1860
|
+
totalTokens,
|
|
1861
|
+
cost: getRecordCost(record),
|
|
1862
|
+
sessionId: record.sessionId,
|
|
1863
|
+
projectId: record.projectId,
|
|
1864
|
+
durationMs: record.durationMs
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1558
1867
|
function buildProviderData(records) {
|
|
1559
1868
|
const byDate = new Map;
|
|
1560
1869
|
for (const record of records) {
|
|
@@ -1614,7 +1923,8 @@ function buildProviderData(records) {
|
|
|
1614
1923
|
daily,
|
|
1615
1924
|
totalTokens,
|
|
1616
1925
|
totalCost,
|
|
1617
|
-
colors: COLORS
|
|
1926
|
+
colors: COLORS,
|
|
1927
|
+
events: records.map(toUsageEvent)
|
|
1618
1928
|
};
|
|
1619
1929
|
}
|
|
1620
1930
|
function loadFromSqlite(dbPath, range) {
|
|
@@ -1629,18 +1939,21 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1629
1939
|
if (tables.length === 0) {
|
|
1630
1940
|
return [];
|
|
1631
1941
|
}
|
|
1632
|
-
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1942
|
+
const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1633
1943
|
const records = [];
|
|
1634
1944
|
for (const row of rows) {
|
|
1635
1945
|
const date = extractDate2(row.created_at);
|
|
1636
|
-
|
|
1946
|
+
const timestamp = toIsoTimestamp(row.created_at);
|
|
1947
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1637
1948
|
records.push({
|
|
1638
1949
|
date,
|
|
1950
|
+
timestamp,
|
|
1639
1951
|
model: row.model,
|
|
1640
1952
|
inputTokens: row.input_tokens,
|
|
1641
1953
|
outputTokens: row.output_tokens,
|
|
1642
1954
|
cacheReadTokens: 0,
|
|
1643
|
-
cacheWriteTokens: 0
|
|
1955
|
+
cacheWriteTokens: 0,
|
|
1956
|
+
sessionId: row.session_id
|
|
1644
1957
|
});
|
|
1645
1958
|
}
|
|
1646
1959
|
}
|
|
@@ -1666,14 +1979,17 @@ function loadFromLegacyJson(sessionsDir, range) {
|
|
|
1666
1979
|
continue;
|
|
1667
1980
|
}
|
|
1668
1981
|
const date = extractDate2(msg.created_at);
|
|
1669
|
-
|
|
1982
|
+
const timestamp = toIsoTimestamp(msg.created_at);
|
|
1983
|
+
if (date && timestamp && isInRange(date, range)) {
|
|
1670
1984
|
records.push({
|
|
1671
1985
|
date,
|
|
1986
|
+
timestamp,
|
|
1672
1987
|
model: msg.model,
|
|
1673
1988
|
inputTokens: msg.usage.input_tokens,
|
|
1674
1989
|
outputTokens: msg.usage.output_tokens,
|
|
1675
1990
|
cacheReadTokens: 0,
|
|
1676
|
-
cacheWriteTokens: 0
|
|
1991
|
+
cacheWriteTokens: 0,
|
|
1992
|
+
sessionId: file
|
|
1677
1993
|
});
|
|
1678
1994
|
}
|
|
1679
1995
|
}
|
|
@@ -1711,7 +2027,8 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1711
2027
|
continue;
|
|
1712
2028
|
}
|
|
1713
2029
|
const date = extractDate2(createdAt);
|
|
1714
|
-
|
|
2030
|
+
const timestamp = toIsoTimestamp(createdAt);
|
|
2031
|
+
if (!date || !timestamp || !isInRange(date, range)) {
|
|
1715
2032
|
continue;
|
|
1716
2033
|
}
|
|
1717
2034
|
const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
|
|
@@ -1720,13 +2037,21 @@ function loadFromCurrentStorage(baseDir, range) {
|
|
|
1720
2037
|
const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
|
|
1721
2038
|
const record = {
|
|
1722
2039
|
date,
|
|
2040
|
+
timestamp,
|
|
1723
2041
|
model,
|
|
1724
2042
|
inputTokens,
|
|
1725
2043
|
outputTokens,
|
|
1726
2044
|
cacheReadTokens,
|
|
1727
2045
|
cacheWriteTokens,
|
|
1728
|
-
explicitCost: typeof message.cost === "number" ? message.cost : undefined
|
|
2046
|
+
explicitCost: typeof message.cost === "number" ? message.cost : undefined,
|
|
2047
|
+
sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
|
|
1729
2048
|
};
|
|
2049
|
+
const completedAt = message.time?.completed;
|
|
2050
|
+
const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
|
|
2051
|
+
const createdMs = Date.parse(record.timestamp);
|
|
2052
|
+
if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
|
|
2053
|
+
record.durationMs = completedMs - createdMs;
|
|
2054
|
+
}
|
|
1730
2055
|
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1731
2056
|
if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
|
|
1732
2057
|
continue;
|
|
@@ -1831,7 +2156,7 @@ var DOT_GAP = 8;
|
|
|
1831
2156
|
var CELL_SIZE = 16;
|
|
1832
2157
|
var CELL_GAP = 4;
|
|
1833
2158
|
var STAT_GRID_COLS = 3;
|
|
1834
|
-
var MODEL_BAR_HEIGHT =
|
|
2159
|
+
var MODEL_BAR_HEIGHT = 11;
|
|
1835
2160
|
var DAY_LABEL_WIDTH = 44;
|
|
1836
2161
|
var MONTH_LABEL_HEIGHT = 24;
|
|
1837
2162
|
var PROVIDER_SECTION_GAP = 36;
|
|
@@ -1841,24 +2166,27 @@ var MODEL_BAR_GAP = 36;
|
|
|
1841
2166
|
var MODEL_PERCENT_WIDTH = 40;
|
|
1842
2167
|
|
|
1843
2168
|
// packages/renderers/dist/png/terminal-card.js
|
|
1844
|
-
var FONT_FAMILY = "'
|
|
2169
|
+
var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
2170
|
+
var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
|
|
1845
2171
|
function getCardTheme(mode) {
|
|
1846
2172
|
if (mode === "dark") {
|
|
1847
2173
|
return {
|
|
1848
2174
|
bg: "#09090b",
|
|
1849
|
-
fg: "#
|
|
1850
|
-
muted: "#
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2175
|
+
fg: "#f0f0f0",
|
|
2176
|
+
muted: "#6b7280",
|
|
2177
|
+
labelFg: "#b0b8c4",
|
|
2178
|
+
border: "rgba(255,255,255,0.08)",
|
|
2179
|
+
accent: "#34d399",
|
|
2180
|
+
heatmapEmpty: "#1a1a22",
|
|
2181
|
+
barTrack: "#151520",
|
|
2182
|
+
titlebarBorder: "rgba(255,255,255,0.08)"
|
|
1856
2183
|
};
|
|
1857
2184
|
}
|
|
1858
2185
|
return {
|
|
1859
2186
|
bg: "#fafafa",
|
|
1860
2187
|
fg: "#18181b",
|
|
1861
2188
|
muted: "#a1a1aa",
|
|
2189
|
+
labelFg: "#71717a",
|
|
1862
2190
|
border: "rgba(0,0,0,0.08)",
|
|
1863
2191
|
accent: "#059669",
|
|
1864
2192
|
heatmapEmpty: "#e4e4e7",
|
|
@@ -1870,7 +2198,7 @@ function buildHeatmapScale(colors, isDark) {
|
|
|
1870
2198
|
const [startHex, endHex] = colors.gradient;
|
|
1871
2199
|
const s = hexToRgb(startHex);
|
|
1872
2200
|
const e = hexToRgb(endHex);
|
|
1873
|
-
const opacities = isDark ? [0.
|
|
2201
|
+
const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
|
|
1874
2202
|
return [
|
|
1875
2203
|
"transparent",
|
|
1876
2204
|
...opacities.map((t) => {
|
|
@@ -1957,6 +2285,41 @@ function formatPercentage(rate) {
|
|
|
1957
2285
|
function formatStreak(n) {
|
|
1958
2286
|
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
1959
2287
|
}
|
|
2288
|
+
function formatRatio(value, suffix = "x") {
|
|
2289
|
+
if (value === null || !Number.isFinite(value)) {
|
|
2290
|
+
return "n/a";
|
|
2291
|
+
}
|
|
2292
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
|
|
2293
|
+
}
|
|
2294
|
+
function formatPercentPoints(value) {
|
|
2295
|
+
const prefix = value >= 0 ? "+" : "";
|
|
2296
|
+
return `${prefix}${(value * 100).toFixed(1)}pp`;
|
|
2297
|
+
}
|
|
2298
|
+
function formatHour(hour) {
|
|
2299
|
+
return `${hour.toString().padStart(2, "0")}:00`;
|
|
2300
|
+
}
|
|
2301
|
+
function formatDuration(durationMs) {
|
|
2302
|
+
if (durationMs === null || durationMs === undefined || durationMs <= 0) {
|
|
2303
|
+
return "n/a";
|
|
2304
|
+
}
|
|
2305
|
+
const totalMinutes = Math.round(durationMs / 60000);
|
|
2306
|
+
if (totalMinutes < 60) {
|
|
2307
|
+
return `${totalMinutes}m`;
|
|
2308
|
+
}
|
|
2309
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
2310
|
+
const minutes = totalMinutes % 60;
|
|
2311
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
2312
|
+
}
|
|
2313
|
+
function truncateText(value, maxLength) {
|
|
2314
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
2315
|
+
}
|
|
2316
|
+
function formatSessionSummary(summary) {
|
|
2317
|
+
const duration = formatDuration(summary.durationMs);
|
|
2318
|
+
if (duration === "n/a") {
|
|
2319
|
+
return truncateText(summary.label, 20);
|
|
2320
|
+
}
|
|
2321
|
+
return truncateText(`${summary.label} \xB7 ${duration}`, 24);
|
|
2322
|
+
}
|
|
1960
2323
|
function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
1961
2324
|
const tokenMap = new Map;
|
|
1962
2325
|
for (const d of daily) {
|
|
@@ -2013,6 +2376,149 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
|
2013
2376
|
`);
|
|
2014
2377
|
return { svg, gridWidth, height };
|
|
2015
2378
|
}
|
|
2379
|
+
function renderSectionHeader(x, y, title, theme, cardAccent) {
|
|
2380
|
+
const parts = [];
|
|
2381
|
+
parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
|
|
2382
|
+
parts.push(`<text x="${x + 12}" y="${y}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.8">${escapeXml(title)}</text>`);
|
|
2383
|
+
return parts.join(`
|
|
2384
|
+
`);
|
|
2385
|
+
}
|
|
2386
|
+
function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
|
|
2387
|
+
const parts = [];
|
|
2388
|
+
const height = 38 + lines.length * 22;
|
|
2389
|
+
parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2390
|
+
parts.push(`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">${escapeXml(title)}</text>`);
|
|
2391
|
+
lines.forEach((line, index) => {
|
|
2392
|
+
const lineY = y + 48 + index * 22;
|
|
2393
|
+
parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(line.label)}</text>`);
|
|
2394
|
+
parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(line.accent ? cardAccent : theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(line.value)}</text>`);
|
|
2395
|
+
});
|
|
2396
|
+
return parts.join(`
|
|
2397
|
+
`);
|
|
2398
|
+
}
|
|
2399
|
+
function buildProviderHourBuckets(providers) {
|
|
2400
|
+
return providers.map((p) => {
|
|
2401
|
+
const hours = new Array(24).fill(0);
|
|
2402
|
+
for (const event of p.events ?? []) {
|
|
2403
|
+
const date = new Date(event.timestamp);
|
|
2404
|
+
if (!Number.isNaN(date.getTime())) {
|
|
2405
|
+
const h = date.getUTCHours();
|
|
2406
|
+
hours[h] += event.totalTokens;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return { provider: p.provider, color: p.colors.primary, hours };
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
|
|
2413
|
+
const chartHeight = 140;
|
|
2414
|
+
const innerHeight = 72;
|
|
2415
|
+
const baselineY = y + 92;
|
|
2416
|
+
const barAreaX = x + 18;
|
|
2417
|
+
const barAreaWidth = width - 36;
|
|
2418
|
+
const barGap = 4;
|
|
2419
|
+
const barWidth = (barAreaWidth - barGap * 23) / 24;
|
|
2420
|
+
const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
|
|
2421
|
+
const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
|
|
2422
|
+
const isMulti = providers.length > 1;
|
|
2423
|
+
const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
|
|
2424
|
+
let legendSvg = "";
|
|
2425
|
+
if (isMulti && providerBuckets.length > 0) {
|
|
2426
|
+
const titleWidth = 105;
|
|
2427
|
+
let legendX = x + 18 + titleWidth + 12;
|
|
2428
|
+
const legendY = y + 22;
|
|
2429
|
+
for (const bucket of providerBuckets) {
|
|
2430
|
+
const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
|
|
2431
|
+
legendSvg += `<rect x="${legendX}" y="${legendY - 8}" width="8" height="8" rx="2" fill="${escapeXml(bucket.color)}" opacity="0.85"/>` + `<text x="${legendX + 12}" y="${legendY - 1}" fill="${escapeXml(theme.fg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(displayName)}</text>`;
|
|
2432
|
+
legendX += 12 + displayName.length * 5.5 + 16;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
const bars = [
|
|
2436
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2437
|
+
`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">HOUR OF DAY</text>`,
|
|
2438
|
+
legendSvg,
|
|
2439
|
+
`<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(busiest ? `${formatHour(busiest.hour)} peak` : "No session events")}</text>`,
|
|
2440
|
+
`<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
|
|
2441
|
+
];
|
|
2442
|
+
bars.push(`<defs><filter id="peakGlow" x="-50%" y="-50%" width="200%" height="200%">` + `<feGaussianBlur stdDeviation="4" result="blur"/>` + `<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>` + `</filter></defs>`);
|
|
2443
|
+
if (isMulti) {
|
|
2444
|
+
const provBaseOpacity = isDark ? "0.45" : "0.3";
|
|
2445
|
+
const provMidOpacity = isDark ? "0.85" : "0.75";
|
|
2446
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2447
|
+
const gradId = `hod-prov-${bi}`;
|
|
2448
|
+
bars.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provBaseOpacity}"/>` + `<stop offset="60%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2449
|
+
}
|
|
2450
|
+
hourOfDay.forEach((entry, index) => {
|
|
2451
|
+
if (entry.tokens <= 0)
|
|
2452
|
+
return;
|
|
2453
|
+
const totalRatio = entry.tokens / maxTokens;
|
|
2454
|
+
const totalBarHeight = Math.max(4, totalRatio * innerHeight);
|
|
2455
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2456
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour;
|
|
2457
|
+
if (isPeak) {
|
|
2458
|
+
const topY2 = baselineY - totalBarHeight;
|
|
2459
|
+
bars.push(`<rect x="${colX - 2}" y="${topY2 - 2}" width="${barWidth + 4}" height="${totalBarHeight + 4}" rx="5" fill="${escapeXml(providerBuckets[0]?.color ?? cardAccent)}" opacity="0.12" filter="url(#peakGlow)"/>`);
|
|
2460
|
+
}
|
|
2461
|
+
const clipId = `hod-clip-${index}`;
|
|
2462
|
+
const topY = baselineY - totalBarHeight;
|
|
2463
|
+
bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
|
|
2464
|
+
let offsetY = 0;
|
|
2465
|
+
for (let bi = 0;bi < providerBuckets.length; bi++) {
|
|
2466
|
+
const tokens = providerBuckets[bi].hours[index] ?? 0;
|
|
2467
|
+
if (tokens <= 0)
|
|
2468
|
+
continue;
|
|
2469
|
+
const segHeight = tokens / entry.tokens * totalBarHeight;
|
|
2470
|
+
const segY = baselineY - offsetY - segHeight;
|
|
2471
|
+
bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
|
|
2472
|
+
offsetY += segHeight;
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
} else {
|
|
2476
|
+
const hodGradId = "hod-bar-grad";
|
|
2477
|
+
const hodBaseOpacity = isDark ? "0.25" : "0.1";
|
|
2478
|
+
const hodMidOpacity = isDark ? "0.75" : "0.6";
|
|
2479
|
+
bars.push(`<defs><linearGradient id="${escapeXml(hodGradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodBaseOpacity}"/>` + `<stop offset="40%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(cardAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2480
|
+
hourOfDay.forEach((entry, index) => {
|
|
2481
|
+
const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
|
|
2482
|
+
const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
|
|
2483
|
+
const colX = barAreaX + index * (barWidth + barGap);
|
|
2484
|
+
const colY = baselineY - barHeight;
|
|
2485
|
+
const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
|
|
2486
|
+
if (isPeak) {
|
|
2487
|
+
bars.push(`<rect x="${colX - 2}" y="${colY - 2}" width="${barWidth + 4}" height="${barHeight + 4}" rx="5" fill="${escapeXml(cardAccent)}" opacity="0.15" filter="url(#peakGlow)"/>`);
|
|
2488
|
+
}
|
|
2489
|
+
bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
[0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
|
|
2493
|
+
const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
|
|
2494
|
+
bars.push(`<text x="${labelX}" y="${y + 116}" fill="${escapeXml(theme.labelFg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="middle">${escapeXml(hour.toString().padStart(2, "0"))}</text>`);
|
|
2495
|
+
});
|
|
2496
|
+
return {
|
|
2497
|
+
svg: bars.join(`
|
|
2498
|
+
`),
|
|
2499
|
+
height: chartHeight
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
function renderModelMixShift(x, y, width, more, theme, cardAccent) {
|
|
2503
|
+
if (!more.compare || more.compare.modelMixShift.length === 0) {
|
|
2504
|
+
return { svg: "", height: 0 };
|
|
2505
|
+
}
|
|
2506
|
+
const rows = more.compare.modelMixShift.slice(0, 4);
|
|
2507
|
+
const height = 38 + rows.length * 24;
|
|
2508
|
+
const parts = [
|
|
2509
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
|
|
2510
|
+
`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">MODEL MIX SHIFT</text>`,
|
|
2511
|
+
`<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${more.compare.previousRange.since} \u2192 ${more.compare.previousRange.until}`)}</text>`
|
|
2512
|
+
];
|
|
2513
|
+
rows.forEach((row, index) => {
|
|
2514
|
+
const lineY = y + 48 + index * 24;
|
|
2515
|
+
parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(truncateText(row.model, 28))}</text>`);
|
|
2516
|
+
parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(row.deltaShare >= 0 ? cardAccent : "#f97316")}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(formatPercentPoints(row.deltaShare))}</text>`);
|
|
2517
|
+
parts.push(`<text x="${x + width - 110}" y="${lineY}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${(row.previousShare * 100).toFixed(1)}% \u2192 ${(row.currentShare * 100).toFixed(1)}%`)}</text>`);
|
|
2518
|
+
});
|
|
2519
|
+
return { svg: parts.join(`
|
|
2520
|
+
`), height };
|
|
2521
|
+
}
|
|
2016
2522
|
function renderTerminalCardSvg(output, options) {
|
|
2017
2523
|
const theme = getCardTheme(options.theme);
|
|
2018
2524
|
const isDark = options.theme === "dark";
|
|
@@ -2021,6 +2527,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2021
2527
|
const { since, until } = output.dateRange;
|
|
2022
2528
|
const providers = output.providers;
|
|
2023
2529
|
const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
|
|
2530
|
+
const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
|
|
2024
2531
|
const providerHeatmaps = providers.map((p) => {
|
|
2025
2532
|
const heatmapColors = buildHeatmapScale(p.colors, isDark);
|
|
2026
2533
|
return {
|
|
@@ -2050,20 +2557,20 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2050
2557
|
}
|
|
2051
2558
|
sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
|
|
2052
2559
|
y = TITLEBAR_HEIGHT + pad * 0.6;
|
|
2053
|
-
sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(
|
|
2560
|
+
sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(MONO_FONT_FAMILY)}" font-weight="500">` + `<tspan fill="${escapeXml(cardAccent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(cardAccent)}">_</tspan>` + `</text>`);
|
|
2054
2561
|
y += 40;
|
|
2055
2562
|
const dateRangeText = formatDateRange(since, until);
|
|
2056
2563
|
sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
|
|
2057
2564
|
y += 40;
|
|
2058
2565
|
for (let pi = 0;pi < providerHeatmaps.length; pi++) {
|
|
2059
2566
|
const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
|
|
2060
|
-
const provDotRadius =
|
|
2567
|
+
const provDotRadius = 7;
|
|
2061
2568
|
const provColor = provider.colors.primary;
|
|
2062
|
-
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y +
|
|
2063
|
-
sections.push(`<text x="${pad + provDotRadius * 2 +
|
|
2569
|
+
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
|
|
2570
|
+
sections.push(`<text x="${pad + provDotRadius * 2 + 12}" y="${y + 15}" fill="${escapeXml(theme.fg)}" font-size="17" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(provider.displayName)}</text>`);
|
|
2064
2571
|
const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
|
|
2065
|
-
sections.push(`<text x="${cardWidth - pad}" y="${y +
|
|
2066
|
-
y +=
|
|
2572
|
+
sections.push(`<text x="${cardWidth - pad}" y="${y + 15}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
|
|
2573
|
+
y += 32;
|
|
2067
2574
|
const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
|
|
2068
2575
|
sections.push(`<g transform="translate(${pad}, ${y})">`);
|
|
2069
2576
|
sections.push(heatmapSvg);
|
|
@@ -2103,7 +2610,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2103
2610
|
const stat = row[i];
|
|
2104
2611
|
const x = pad + i * statColWidth;
|
|
2105
2612
|
sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
|
|
2106
|
-
const valueColor = stat.accent ? cardAccent : theme.fg;
|
|
2613
|
+
const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
|
|
2107
2614
|
sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(stat.value)}</text>`);
|
|
2108
2615
|
}
|
|
2109
2616
|
}
|
|
@@ -2114,24 +2621,127 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2114
2621
|
y += 8;
|
|
2115
2622
|
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2116
2623
|
y += 28;
|
|
2117
|
-
sections.push(
|
|
2624
|
+
sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
|
|
2118
2625
|
y += 24;
|
|
2119
2626
|
const topModels2 = stats.topModels.slice(0, 3);
|
|
2627
|
+
const rankWidth = 28;
|
|
2120
2628
|
const modelNameWidth = MODEL_NAME_WIDTH;
|
|
2121
2629
|
const barGap = MODEL_BAR_GAP;
|
|
2122
2630
|
const percentX = cardWidth - pad;
|
|
2123
|
-
const barX = pad + modelNameWidth;
|
|
2631
|
+
const barX = pad + rankWidth + modelNameWidth;
|
|
2124
2632
|
const barMaxWidth = Math.max(48, percentX - barX - barGap);
|
|
2125
2633
|
for (const [index, model] of topModels2.entries()) {
|
|
2126
2634
|
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(`<
|
|
2635
|
+
sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(cardAccent)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" opacity="0.7">${escapeXml(String(index + 1))}</text>`);
|
|
2636
|
+
sections.push(`<text x="${pad + rankWidth}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml(model.model)}</text>`);
|
|
2637
|
+
const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
|
|
2638
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
|
|
2129
2639
|
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.
|
|
2640
|
+
sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(barAccent)}" stop-opacity="${isDark ? "0.45" : "0.27"}"/>` + `<stop offset="100%" stop-color="${escapeXml(barAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
|
|
2641
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
|
|
2642
|
+
sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
|
|
2133
2643
|
y += 32;
|
|
2134
2644
|
}
|
|
2645
|
+
if (options.more && output.more) {
|
|
2646
|
+
const more = output.more;
|
|
2647
|
+
const cardGap = 16;
|
|
2648
|
+
const detailCardWidth = (contentWidth - cardGap) / 2;
|
|
2649
|
+
y += 8;
|
|
2650
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2651
|
+
y += 28;
|
|
2652
|
+
sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
|
|
2653
|
+
y += 24;
|
|
2654
|
+
const efficiencyLines = [
|
|
2655
|
+
{
|
|
2656
|
+
label: "Input / Output",
|
|
2657
|
+
value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
|
|
2658
|
+
accent: true
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
label: "Output / Input",
|
|
2662
|
+
value: formatRatio(more.inputOutput.outputPerInput)
|
|
2663
|
+
},
|
|
2664
|
+
{
|
|
2665
|
+
label: "Output Share",
|
|
2666
|
+
value: formatPercentage(more.inputOutput.outputShare)
|
|
2667
|
+
}
|
|
2668
|
+
];
|
|
2669
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
|
|
2670
|
+
const burnLines = [
|
|
2671
|
+
{
|
|
2672
|
+
label: "Projected Cost",
|
|
2673
|
+
value: formatCost(more.monthlyBurn.projectedCost),
|
|
2674
|
+
accent: true
|
|
2675
|
+
},
|
|
2676
|
+
{
|
|
2677
|
+
label: "Projected Tokens",
|
|
2678
|
+
value: formatNumber(more.monthlyBurn.projectedTokens)
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
label: "Based On",
|
|
2682
|
+
value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
|
|
2683
|
+
}
|
|
2684
|
+
];
|
|
2685
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
|
|
2686
|
+
y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
|
|
2687
|
+
const cacheLines = [
|
|
2688
|
+
{
|
|
2689
|
+
label: "Cache Reads",
|
|
2690
|
+
value: formatNumber(more.cacheEconomics.readTokens),
|
|
2691
|
+
accent: true
|
|
2692
|
+
},
|
|
2693
|
+
{
|
|
2694
|
+
label: "Cache Writes",
|
|
2695
|
+
value: formatNumber(more.cacheEconomics.writeTokens)
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
label: "Read Coverage",
|
|
2699
|
+
value: formatPercentage(more.cacheEconomics.readCoverage)
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
label: "Reuse Ratio",
|
|
2703
|
+
value: formatRatio(more.cacheEconomics.reuseRatio)
|
|
2704
|
+
}
|
|
2705
|
+
];
|
|
2706
|
+
sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
|
|
2707
|
+
const sessionLines = [
|
|
2708
|
+
{
|
|
2709
|
+
label: "Sessions",
|
|
2710
|
+
value: String(more.sessionMetrics.totalSessions),
|
|
2711
|
+
accent: true
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
label: "Avg Tokens",
|
|
2715
|
+
value: formatNumber(more.sessionMetrics.averageTokens)
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
label: "Avg Messages",
|
|
2719
|
+
value: more.sessionMetrics.averageMessages.toFixed(1)
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
label: "Avg Duration",
|
|
2723
|
+
value: formatDuration(more.sessionMetrics.averageDurationMs)
|
|
2724
|
+
},
|
|
2725
|
+
{
|
|
2726
|
+
label: "Longest Session",
|
|
2727
|
+
value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
|
|
2728
|
+
},
|
|
2729
|
+
{
|
|
2730
|
+
label: "Top Project",
|
|
2731
|
+
value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
|
|
2732
|
+
}
|
|
2733
|
+
];
|
|
2734
|
+
sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
|
|
2735
|
+
y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
|
|
2736
|
+
const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
|
|
2737
|
+
sections.push(hourChart.svg);
|
|
2738
|
+
y += hourChart.height + 16;
|
|
2739
|
+
const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
|
|
2740
|
+
if (mixShift.height > 0) {
|
|
2741
|
+
sections.push(mixShift.svg);
|
|
2742
|
+
y += mixShift.height + 12;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2135
2745
|
y += pad * 0.5;
|
|
2136
2746
|
const cardHeight = y;
|
|
2137
2747
|
const svg = sections.join(`
|
|
@@ -3051,6 +3661,1097 @@ function handleError(error) {
|
|
|
3051
3661
|
process.exit(1);
|
|
3052
3662
|
}
|
|
3053
3663
|
|
|
3664
|
+
// packages/cli/src/flags.ts
|
|
3665
|
+
var CLI_FLAG_ORDER = [
|
|
3666
|
+
"format",
|
|
3667
|
+
"theme",
|
|
3668
|
+
"since",
|
|
3669
|
+
"until",
|
|
3670
|
+
"days",
|
|
3671
|
+
"output",
|
|
3672
|
+
"width",
|
|
3673
|
+
"provider",
|
|
3674
|
+
"compare",
|
|
3675
|
+
"upload",
|
|
3676
|
+
"claude",
|
|
3677
|
+
"codex",
|
|
3678
|
+
"openCode",
|
|
3679
|
+
"allProviders",
|
|
3680
|
+
"listProviders",
|
|
3681
|
+
"more",
|
|
3682
|
+
"clipboard",
|
|
3683
|
+
"open",
|
|
3684
|
+
"liveServer",
|
|
3685
|
+
"noColor",
|
|
3686
|
+
"noInsights"
|
|
3687
|
+
];
|
|
3688
|
+
var CLI_FLAG_NAMES = {
|
|
3689
|
+
format: "--format",
|
|
3690
|
+
theme: "--theme",
|
|
3691
|
+
since: "--since",
|
|
3692
|
+
until: "--until",
|
|
3693
|
+
days: "--days",
|
|
3694
|
+
output: "--output",
|
|
3695
|
+
width: "--width",
|
|
3696
|
+
provider: "--provider",
|
|
3697
|
+
compare: "--compare",
|
|
3698
|
+
upload: "--upload",
|
|
3699
|
+
claude: "--claude",
|
|
3700
|
+
codex: "--codex",
|
|
3701
|
+
openCode: "--open-code",
|
|
3702
|
+
allProviders: "--all-providers",
|
|
3703
|
+
listProviders: "--list-providers",
|
|
3704
|
+
more: "--more",
|
|
3705
|
+
clipboard: "--clipboard",
|
|
3706
|
+
open: "--open",
|
|
3707
|
+
liveServer: "--live-server",
|
|
3708
|
+
noColor: "--no-color",
|
|
3709
|
+
noInsights: "--no-insights"
|
|
3710
|
+
};
|
|
3711
|
+
function buildCliArgTokens(cliArgs) {
|
|
3712
|
+
const tokens = [];
|
|
3713
|
+
for (const key of CLI_FLAG_ORDER) {
|
|
3714
|
+
const value = cliArgs[key];
|
|
3715
|
+
if (value === undefined || value === false || value === null) {
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
const flag = CLI_FLAG_NAMES[key];
|
|
3719
|
+
if (!flag)
|
|
3720
|
+
continue;
|
|
3721
|
+
tokens.push(flag);
|
|
3722
|
+
if (value !== true) {
|
|
3723
|
+
tokens.push(String(value));
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return tokens;
|
|
3727
|
+
}
|
|
3728
|
+
function buildCliPreview(cliArgs) {
|
|
3729
|
+
const tokens = buildCliArgTokens(cliArgs);
|
|
3730
|
+
return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
// packages/cli/src/interactive.ts
|
|
3734
|
+
import { emitKeypressEvents } from "readline";
|
|
3735
|
+
import { createInterface } from "readline/promises";
|
|
3736
|
+
var INTERACTIVE_FLAG_LINES = [
|
|
3737
|
+
"-f, --format <format> terminal | png | svg | json",
|
|
3738
|
+
"-t, --theme <theme> dark | light",
|
|
3739
|
+
"-s, --since <date> YYYY-MM-DD start date",
|
|
3740
|
+
"-u, --until <date> YYYY-MM-DD end date",
|
|
3741
|
+
"-d, --days <number> trailing days window",
|
|
3742
|
+
"-o, --output <path> write output to a file",
|
|
3743
|
+
"-w, --width <number> terminal render width",
|
|
3744
|
+
"-p, --provider <list> comma-separated providers",
|
|
3745
|
+
" --claude shortcut for Claude Code",
|
|
3746
|
+
" --codex shortcut for Codex",
|
|
3747
|
+
" --open-code shortcut for Open Code",
|
|
3748
|
+
" --all-providers ignore provider filters",
|
|
3749
|
+
" --list-providers show provider registry",
|
|
3750
|
+
" --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
|
|
3751
|
+
" --more richer PNG/SVG stats",
|
|
3752
|
+
" --clipboard copy rendered output",
|
|
3753
|
+
" --open open generated file",
|
|
3754
|
+
" --upload <target> gist",
|
|
3755
|
+
"-L, --live-server local interactive dashboard",
|
|
3756
|
+
" --no-color disable ANSI colors",
|
|
3757
|
+
" --no-insights hide terminal insights",
|
|
3758
|
+
" --help print help",
|
|
3759
|
+
" --version print version"
|
|
3760
|
+
];
|
|
3761
|
+
var ESC2 = "\x1B[";
|
|
3762
|
+
var RESET = `${ESC2}0m`;
|
|
3763
|
+
var BOLD = `${ESC2}1m`;
|
|
3764
|
+
var DIM = `${ESC2}2m`;
|
|
3765
|
+
var CYAN = `${ESC2}36m`;
|
|
3766
|
+
var GREEN = `${ESC2}32m`;
|
|
3767
|
+
var YELLOW = `${ESC2}33m`;
|
|
3768
|
+
var RED = `${ESC2}31m`;
|
|
3769
|
+
var WHITE = `${ESC2}97m`;
|
|
3770
|
+
var HOME_CLEAR = "\x1B[H\x1B[J";
|
|
3771
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
3772
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
3773
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
3774
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
3775
|
+
var LOADING_TICK_MS = 120;
|
|
3776
|
+
function color(text2, code) {
|
|
3777
|
+
return `${code}${text2}${RESET}`;
|
|
3778
|
+
}
|
|
3779
|
+
function stripAnsi(text2) {
|
|
3780
|
+
return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
|
|
3781
|
+
}
|
|
3782
|
+
function visibleLength(text2) {
|
|
3783
|
+
return stripAnsi(text2).length;
|
|
3784
|
+
}
|
|
3785
|
+
function padVisible(text2, width) {
|
|
3786
|
+
const padding = Math.max(0, width - visibleLength(text2));
|
|
3787
|
+
return text2 + " ".repeat(padding);
|
|
3788
|
+
}
|
|
3789
|
+
function truncateVisible(text2, width) {
|
|
3790
|
+
if (width <= 0)
|
|
3791
|
+
return "";
|
|
3792
|
+
const plain = stripAnsi(text2);
|
|
3793
|
+
if (plain.length <= width)
|
|
3794
|
+
return text2;
|
|
3795
|
+
const limit = width <= 3 ? width : width - 3;
|
|
3796
|
+
let visibleCount = 0;
|
|
3797
|
+
let index = 0;
|
|
3798
|
+
let result = "";
|
|
3799
|
+
let sawAnsi = false;
|
|
3800
|
+
while (index < text2.length && visibleCount < limit) {
|
|
3801
|
+
if (text2[index] === "\x1B") {
|
|
3802
|
+
const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
|
|
3803
|
+
if (match) {
|
|
3804
|
+
result += match[0];
|
|
3805
|
+
index += match[0].length;
|
|
3806
|
+
sawAnsi = true;
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
result += text2[index];
|
|
3811
|
+
index += 1;
|
|
3812
|
+
visibleCount += 1;
|
|
3813
|
+
}
|
|
3814
|
+
if (width <= 3) {
|
|
3815
|
+
return sawAnsi ? `${result}${RESET}` : result;
|
|
3816
|
+
}
|
|
3817
|
+
return sawAnsi ? `${result}...${RESET}` : `${result}...`;
|
|
3818
|
+
}
|
|
3819
|
+
function joinColumns(left, right, totalWidth) {
|
|
3820
|
+
const gutter = 3;
|
|
3821
|
+
const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
|
|
3822
|
+
const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
|
|
3823
|
+
const rows = Math.max(left.length, right.length);
|
|
3824
|
+
const lines = [];
|
|
3825
|
+
for (let index = 0;index < rows; index++) {
|
|
3826
|
+
const leftLine = truncateVisible(left[index] ?? "", leftWidth);
|
|
3827
|
+
const rightLine = truncateVisible(right[index] ?? "", rightWidth);
|
|
3828
|
+
lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
|
|
3829
|
+
}
|
|
3830
|
+
return lines;
|
|
3831
|
+
}
|
|
3832
|
+
function renderRule(width) {
|
|
3833
|
+
return color("-".repeat(width), DIM);
|
|
3834
|
+
}
|
|
3835
|
+
function describeRequest(args) {
|
|
3836
|
+
const output = typeof args["output"] === "string" ? args["output"] : null;
|
|
3837
|
+
if (args["liveServer"]) {
|
|
3838
|
+
return {
|
|
3839
|
+
title: "Live Dashboard",
|
|
3840
|
+
loadingTitle: "Starting live dashboard",
|
|
3841
|
+
loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
|
|
3842
|
+
executionMode: "inherit"
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
if (args["listProviders"]) {
|
|
3846
|
+
return {
|
|
3847
|
+
title: "Provider Registry",
|
|
3848
|
+
loadingTitle: "Loading provider registry",
|
|
3849
|
+
loadingDetail: "Checking registered providers and current availability.",
|
|
3850
|
+
executionMode: "capture"
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
if (args["compare"]) {
|
|
3854
|
+
return {
|
|
3855
|
+
title: "Compare Report",
|
|
3856
|
+
loadingTitle: "Building compare report",
|
|
3857
|
+
loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
|
|
3858
|
+
executionMode: "capture"
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
switch (args["format"]) {
|
|
3862
|
+
case "json":
|
|
3863
|
+
return {
|
|
3864
|
+
title: "JSON Export",
|
|
3865
|
+
loadingTitle: "Generating JSON report",
|
|
3866
|
+
loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
|
|
3867
|
+
executionMode: "capture"
|
|
3868
|
+
};
|
|
3869
|
+
case "svg":
|
|
3870
|
+
return {
|
|
3871
|
+
title: "SVG Export",
|
|
3872
|
+
loadingTitle: "Rendering SVG",
|
|
3873
|
+
loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
|
|
3874
|
+
executionMode: "capture"
|
|
3875
|
+
};
|
|
3876
|
+
case "png":
|
|
3877
|
+
return {
|
|
3878
|
+
title: "PNG Export",
|
|
3879
|
+
loadingTitle: "Rendering PNG",
|
|
3880
|
+
loadingDetail: output ? `Rendering the PNG card and writing it to ${output}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
|
|
3881
|
+
executionMode: "capture"
|
|
3882
|
+
};
|
|
3883
|
+
default:
|
|
3884
|
+
return {
|
|
3885
|
+
title: "Terminal Dashboard",
|
|
3886
|
+
loadingTitle: "Generating terminal dashboard",
|
|
3887
|
+
loadingDetail: "Reading provider logs and aggregating token usage.",
|
|
3888
|
+
executionMode: "capture"
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function finalizeCliArgs(args) {
|
|
3893
|
+
const finalized = { ...args };
|
|
3894
|
+
const format = finalized["format"];
|
|
3895
|
+
if (finalized["compare"] && (format === "png" || format === "svg")) {
|
|
3896
|
+
finalized["more"] = true;
|
|
3897
|
+
}
|
|
3898
|
+
if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
|
|
3899
|
+
if (format === "png" || format === "svg" || format === "json") {
|
|
3900
|
+
finalized["output"] = `tokenleak.${format}`;
|
|
3901
|
+
} else {
|
|
3902
|
+
delete finalized["open"];
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (format === "png") {
|
|
3906
|
+
delete finalized["clipboard"];
|
|
3907
|
+
delete finalized["upload"];
|
|
3908
|
+
}
|
|
3909
|
+
return finalized;
|
|
3910
|
+
}
|
|
3911
|
+
function createRunCommand(args) {
|
|
3912
|
+
const finalizedArgs = finalizeCliArgs(args);
|
|
3913
|
+
return {
|
|
3914
|
+
type: "run",
|
|
3915
|
+
request: {
|
|
3916
|
+
args: finalizedArgs,
|
|
3917
|
+
preview: buildCliPreview(finalizedArgs),
|
|
3918
|
+
...describeRequest(finalizedArgs)
|
|
3919
|
+
}
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function renderMenu(options, selectedIndex) {
|
|
3923
|
+
return options.map((option, index) => {
|
|
3924
|
+
const isSelected = index === selectedIndex;
|
|
3925
|
+
const prefix = isSelected ? color(">", GREEN) : " ";
|
|
3926
|
+
const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
|
|
3927
|
+
const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
|
|
3928
|
+
const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
|
|
3929
|
+
return `${prefix} [${number}] ${title} ${description}`;
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
function renderFlagPanel() {
|
|
3933
|
+
return [
|
|
3934
|
+
color("All Flags", WHITE + BOLD),
|
|
3935
|
+
color("Every flag remains available while using the launcher.", DIM),
|
|
3936
|
+
"",
|
|
3937
|
+
...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
|
|
3938
|
+
];
|
|
3939
|
+
}
|
|
3940
|
+
function renderMenuPanel(context, options, selectedIndex) {
|
|
3941
|
+
const selected = options[selectedIndex];
|
|
3942
|
+
return [
|
|
3943
|
+
color("Tokenleak Interactive Launcher", WHITE + BOLD),
|
|
3944
|
+
`${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
|
|
3945
|
+
"",
|
|
3946
|
+
color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
|
|
3947
|
+
color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
|
|
3948
|
+
"",
|
|
3949
|
+
...renderMenu(options, selectedIndex),
|
|
3950
|
+
"",
|
|
3951
|
+
color("Preview", WHITE + BOLD),
|
|
3952
|
+
color(selected.preview, GREEN),
|
|
3953
|
+
"",
|
|
3954
|
+
color("Keys", WHITE + BOLD),
|
|
3955
|
+
`${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
|
|
3956
|
+
"",
|
|
3957
|
+
renderRule(44)
|
|
3958
|
+
];
|
|
3959
|
+
}
|
|
3960
|
+
function renderHelpOverlay(helpText, width) {
|
|
3961
|
+
const lines = helpText.trimEnd().split(`
|
|
3962
|
+
`);
|
|
3963
|
+
const header = [
|
|
3964
|
+
color("Tokenleak Help", WHITE + BOLD),
|
|
3965
|
+
color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
|
|
3966
|
+
""
|
|
3967
|
+
];
|
|
3968
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible(line, width))].join(`
|
|
3969
|
+
`)}`;
|
|
3970
|
+
}
|
|
3971
|
+
function renderLauncher(context, options, selectedIndex) {
|
|
3972
|
+
const width = process.stdout.columns ?? 120;
|
|
3973
|
+
const menuPanel = renderMenuPanel(context, options, selectedIndex);
|
|
3974
|
+
const flagPanel = renderFlagPanel();
|
|
3975
|
+
const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
|
|
3976
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
3977
|
+
`)}`;
|
|
3978
|
+
}
|
|
3979
|
+
function renderProgressBar(frame, width) {
|
|
3980
|
+
const innerWidth = Math.max(12, width - 2);
|
|
3981
|
+
const headSize = Math.max(4, Math.floor(innerWidth / 5));
|
|
3982
|
+
const travel = Math.max(1, innerWidth - headSize);
|
|
3983
|
+
const cycle = travel * 2;
|
|
3984
|
+
const offset = frame % cycle;
|
|
3985
|
+
const start = offset <= travel ? offset : cycle - offset;
|
|
3986
|
+
const cells = Array.from({ length: innerWidth }, (_, index) => {
|
|
3987
|
+
if (index >= start && index < start + headSize) {
|
|
3988
|
+
return "=";
|
|
3989
|
+
}
|
|
3990
|
+
return "-";
|
|
3991
|
+
}).join("");
|
|
3992
|
+
return color(`[${cells}]`, CYAN);
|
|
3993
|
+
}
|
|
3994
|
+
function renderLoading(request, frame = 0, startedAt = Date.now()) {
|
|
3995
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
|
|
3996
|
+
const progressBar = renderProgressBar(frame, 34);
|
|
3997
|
+
const lines = [
|
|
3998
|
+
color(request.loadingTitle, WHITE + BOLD),
|
|
3999
|
+
color(request.preview, CYAN),
|
|
4000
|
+
"",
|
|
4001
|
+
color(request.loadingDetail, DIM),
|
|
4002
|
+
"",
|
|
4003
|
+
color("Status", WHITE + BOLD),
|
|
4004
|
+
color("Working... stay in tokenleak while this finishes.", YELLOW),
|
|
4005
|
+
"",
|
|
4006
|
+
color("Progress", WHITE + BOLD),
|
|
4007
|
+
progressBar,
|
|
4008
|
+
color(`Elapsed ${elapsedSeconds}s`, DIM),
|
|
4009
|
+
"",
|
|
4010
|
+
renderRule(44)
|
|
4011
|
+
];
|
|
4012
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4013
|
+
`)}`;
|
|
4014
|
+
}
|
|
4015
|
+
function clipOutputLines(lines, limit) {
|
|
4016
|
+
if (limit <= 0)
|
|
4017
|
+
return [];
|
|
4018
|
+
if (lines.length <= limit)
|
|
4019
|
+
return lines;
|
|
4020
|
+
const visible = lines.slice(0, Math.max(0, limit - 1));
|
|
4021
|
+
visible.push(color(`... ${lines.length - visible.length} more lines hidden`, DIM));
|
|
4022
|
+
return visible;
|
|
4023
|
+
}
|
|
4024
|
+
function renderOutputSection(title, content, width, maxLines) {
|
|
4025
|
+
const normalized = content.trimEnd();
|
|
4026
|
+
if (!normalized)
|
|
4027
|
+
return [];
|
|
4028
|
+
const lines = normalized.split(`
|
|
4029
|
+
`).map((line) => truncateVisible(line, width));
|
|
4030
|
+
return [
|
|
4031
|
+
color(title, WHITE + BOLD),
|
|
4032
|
+
...clipOutputLines(lines, maxLines),
|
|
4033
|
+
""
|
|
4034
|
+
];
|
|
4035
|
+
}
|
|
4036
|
+
function renderResult(request, result) {
|
|
4037
|
+
const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
|
4038
|
+
const rows = process.stdout.rows ?? 40;
|
|
4039
|
+
const statusColor = result.ok ? GREEN : RED;
|
|
4040
|
+
const statusLabel = result.ok ? "Completed" : "Failed";
|
|
4041
|
+
const fixedLines = 10;
|
|
4042
|
+
const outputBudget = Math.max(8, rows - fixedLines);
|
|
4043
|
+
const firstSectionLines = result.stdout.trim() && result.stderr.trim() ? Math.max(4, Math.floor(outputBudget * 0.6)) : outputBudget;
|
|
4044
|
+
const secondSectionLines = Math.max(4, outputBudget - firstSectionLines);
|
|
4045
|
+
const body = [
|
|
4046
|
+
color(request.title, WHITE + BOLD),
|
|
4047
|
+
color(request.preview, CYAN),
|
|
4048
|
+
"",
|
|
4049
|
+
`${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
|
|
4050
|
+
color(result.summary, DIM),
|
|
4051
|
+
"",
|
|
4052
|
+
...renderOutputSection("Output", result.stdout, width, firstSectionLines),
|
|
4053
|
+
...renderOutputSection("Messages", result.stderr, width, secondSectionLines),
|
|
4054
|
+
renderRule(44),
|
|
4055
|
+
`${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
|
|
4056
|
+
];
|
|
4057
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
|
|
4058
|
+
`)}`;
|
|
4059
|
+
}
|
|
4060
|
+
function enterAltScreen() {
|
|
4061
|
+
process.stdout.write(`${ALT_SCREEN_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
|
|
4062
|
+
}
|
|
4063
|
+
function leaveAltScreen() {
|
|
4064
|
+
process.stdout.write(`${SHOW_CURSOR}${ALT_SCREEN_OFF}`);
|
|
4065
|
+
}
|
|
4066
|
+
function paint(content) {
|
|
4067
|
+
process.stdout.write(content);
|
|
4068
|
+
}
|
|
4069
|
+
function suspendRawMode() {
|
|
4070
|
+
if (process.stdin.isTTY) {
|
|
4071
|
+
process.stdin.setRawMode(false);
|
|
4072
|
+
}
|
|
4073
|
+
process.stdin.pause();
|
|
4074
|
+
process.stdout.write(SHOW_CURSOR);
|
|
4075
|
+
}
|
|
4076
|
+
function resumeRawMode() {
|
|
4077
|
+
if (process.stdin.isTTY) {
|
|
4078
|
+
process.stdin.setRawMode(true);
|
|
4079
|
+
}
|
|
4080
|
+
process.stdin.resume();
|
|
4081
|
+
process.stdout.write(HIDE_CURSOR);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
class InteractiveExitError extends Error {
|
|
4085
|
+
constructor() {
|
|
4086
|
+
super("Interactive session cancelled");
|
|
4087
|
+
this.name = "InteractiveExitError";
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
var PROVIDER_CHOICES = [
|
|
4091
|
+
{ value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
|
|
4092
|
+
{ value: "codex", label: "Codex", description: "OpenAI session logs" },
|
|
4093
|
+
{ value: "open-code", label: "Open Code", description: "Open Code storage and database" }
|
|
4094
|
+
];
|
|
4095
|
+
function isInteractiveExitError(error) {
|
|
4096
|
+
return error instanceof InteractiveExitError;
|
|
4097
|
+
}
|
|
4098
|
+
function parsePositiveInteger(value) {
|
|
4099
|
+
const trimmed = value.trim();
|
|
4100
|
+
if (trimmed === "")
|
|
4101
|
+
return null;
|
|
4102
|
+
const parsed = Number(trimmed);
|
|
4103
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4104
|
+
throw new Error(`Expected a positive whole number, received "${value}".`);
|
|
4105
|
+
}
|
|
4106
|
+
return parsed;
|
|
4107
|
+
}
|
|
4108
|
+
function renderChoiceScreen(title, description, options, selectedIndex, selectedValues, footer = `${color("Up/Down", YELLOW)} move ${color("Space", YELLOW)} toggle ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`) {
|
|
4109
|
+
const optionLines = options.map((option, index) => {
|
|
4110
|
+
const isSelected = index === selectedIndex;
|
|
4111
|
+
const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
|
|
4112
|
+
const pointer = isSelected ? color(">", GREEN) : " ";
|
|
4113
|
+
const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
|
|
4114
|
+
const titleColor = isSelected ? WHITE + BOLD : WHITE;
|
|
4115
|
+
const descriptionColor = isSelected ? CYAN : DIM;
|
|
4116
|
+
return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
|
|
4117
|
+
});
|
|
4118
|
+
const lines = [
|
|
4119
|
+
color(title, WHITE + BOLD),
|
|
4120
|
+
color(description, DIM),
|
|
4121
|
+
"",
|
|
4122
|
+
...optionLines,
|
|
4123
|
+
"",
|
|
4124
|
+
renderRule(44),
|
|
4125
|
+
footer
|
|
4126
|
+
];
|
|
4127
|
+
return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
|
|
4128
|
+
`)}`;
|
|
4129
|
+
}
|
|
4130
|
+
async function ask(prompt, initialValue = "") {
|
|
4131
|
+
const readline = createInterface({
|
|
4132
|
+
input: process.stdin,
|
|
4133
|
+
output: process.stdout
|
|
4134
|
+
});
|
|
4135
|
+
let settled = false;
|
|
4136
|
+
return new Promise((resolve, reject) => {
|
|
4137
|
+
const finish = (fn) => {
|
|
4138
|
+
if (settled)
|
|
4139
|
+
return;
|
|
4140
|
+
settled = true;
|
|
4141
|
+
readline.off("SIGINT", onSigint);
|
|
4142
|
+
readline.close();
|
|
4143
|
+
fn();
|
|
4144
|
+
};
|
|
4145
|
+
const onSigint = () => {
|
|
4146
|
+
finish(() => reject(new InteractiveExitError));
|
|
4147
|
+
};
|
|
4148
|
+
readline.on("SIGINT", onSigint);
|
|
4149
|
+
const suffix = initialValue ? ` (${initialValue})` : "";
|
|
4150
|
+
readline.question(`${prompt}${suffix}: `).then((value) => {
|
|
4151
|
+
finish(() => resolve(value.trim() || initialValue));
|
|
4152
|
+
}).catch((error) => {
|
|
4153
|
+
finish(() => reject(error));
|
|
4154
|
+
});
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
async function askYesNo(prompt, defaultValue = false) {
|
|
4158
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
4159
|
+
const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
|
|
4160
|
+
if (value === "")
|
|
4161
|
+
return defaultValue;
|
|
4162
|
+
return value === "y" || value === "yes";
|
|
4163
|
+
}
|
|
4164
|
+
async function promptSingleChoice(title, description, options, initialIndex = 0) {
|
|
4165
|
+
return new Promise((resolve, reject) => {
|
|
4166
|
+
let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
|
|
4167
|
+
const onKeypress = (_input, key) => {
|
|
4168
|
+
if (key.ctrl && key.name === "c") {
|
|
4169
|
+
cleanup();
|
|
4170
|
+
reject(new InteractiveExitError);
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
if (key.name === "up") {
|
|
4174
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
4175
|
+
render();
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
if (key.name === "down") {
|
|
4179
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
4180
|
+
render();
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
4184
|
+
if (digit) {
|
|
4185
|
+
const index = Number(digit) - 1;
|
|
4186
|
+
if (index < options.length) {
|
|
4187
|
+
selectedIndex = index;
|
|
4188
|
+
cleanup();
|
|
4189
|
+
resolve(options[selectedIndex].value);
|
|
4190
|
+
}
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4194
|
+
cleanup();
|
|
4195
|
+
resolve(options[selectedIndex].value);
|
|
4196
|
+
}
|
|
4197
|
+
};
|
|
4198
|
+
function render() {
|
|
4199
|
+
paint(renderChoiceScreen(title, description, options, selectedIndex, undefined, `${color("Up/Down", YELLOW)} move ${color("1-9", YELLOW)} pick ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`));
|
|
4200
|
+
}
|
|
4201
|
+
function cleanup() {
|
|
4202
|
+
process.stdin.off("keypress", onKeypress);
|
|
4203
|
+
suspendRawMode();
|
|
4204
|
+
}
|
|
4205
|
+
render();
|
|
4206
|
+
resumeRawMode();
|
|
4207
|
+
process.stdin.on("keypress", onKeypress);
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
async function promptMultiChoice(title, description, options, initialValues = []) {
|
|
4211
|
+
return new Promise((resolve, reject) => {
|
|
4212
|
+
let selectedIndex = 0;
|
|
4213
|
+
const selectedValues = new Set(initialValues);
|
|
4214
|
+
const onKeypress = (_input, key) => {
|
|
4215
|
+
if (key.ctrl && key.name === "c") {
|
|
4216
|
+
cleanup();
|
|
4217
|
+
reject(new InteractiveExitError);
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (key.name === "up") {
|
|
4221
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
4222
|
+
render();
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
if (key.name === "down") {
|
|
4226
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
4227
|
+
render();
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
if (key.name === "space") {
|
|
4231
|
+
toggleSelected(selectedIndex);
|
|
4232
|
+
render();
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
const digit = key.sequence?.match(/^[1-9]$/)?.[0];
|
|
4236
|
+
if (digit) {
|
|
4237
|
+
const index = Number(digit) - 1;
|
|
4238
|
+
if (index < options.length) {
|
|
4239
|
+
selectedIndex = index;
|
|
4240
|
+
toggleSelected(selectedIndex);
|
|
4241
|
+
render();
|
|
4242
|
+
}
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4246
|
+
cleanup();
|
|
4247
|
+
resolve(Array.from(selectedValues));
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
4250
|
+
function toggleSelected(index) {
|
|
4251
|
+
const value = options[index].value;
|
|
4252
|
+
if (selectedValues.has(value)) {
|
|
4253
|
+
selectedValues.delete(value);
|
|
4254
|
+
} else {
|
|
4255
|
+
selectedValues.add(value);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
function render() {
|
|
4259
|
+
paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
|
|
4260
|
+
}
|
|
4261
|
+
function cleanup() {
|
|
4262
|
+
process.stdin.off("keypress", onKeypress);
|
|
4263
|
+
suspendRawMode();
|
|
4264
|
+
}
|
|
4265
|
+
render();
|
|
4266
|
+
resumeRawMode();
|
|
4267
|
+
process.stdin.on("keypress", onKeypress);
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
function applySelectedProviders(args, providers) {
|
|
4271
|
+
if (providers.length === 0)
|
|
4272
|
+
return;
|
|
4273
|
+
args["provider"] = providers.join(",");
|
|
4274
|
+
}
|
|
4275
|
+
async function promptTheme(defaultTheme = "dark") {
|
|
4276
|
+
const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
|
|
4277
|
+
{ value: "dark", label: "Dark", description: "High-contrast dark canvas" },
|
|
4278
|
+
{ value: "light", label: "Light", description: "Bright export with light background" }
|
|
4279
|
+
], defaultTheme === "light" ? 1 : 0);
|
|
4280
|
+
return theme;
|
|
4281
|
+
}
|
|
4282
|
+
async function promptDateWindow() {
|
|
4283
|
+
const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
|
|
4284
|
+
{ value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
|
|
4285
|
+
{ value: "30", label: "Last 30 days", description: "Short-term trend window" },
|
|
4286
|
+
{ value: "90", label: "Last 90 days", description: "Default overview" },
|
|
4287
|
+
{ value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
|
|
4288
|
+
{ value: "custom", label: "Custom range", description: "Enter exact dates manually" }
|
|
4289
|
+
], 2);
|
|
4290
|
+
if (choice !== "custom") {
|
|
4291
|
+
return { days: Number(choice) };
|
|
4292
|
+
}
|
|
4293
|
+
const since = await ask("Since date YYYY-MM-DD");
|
|
4294
|
+
const until = await ask("Until date YYYY-MM-DD (blank for today)");
|
|
4295
|
+
const args = { since };
|
|
4296
|
+
if (until)
|
|
4297
|
+
args["until"] = until;
|
|
4298
|
+
return args;
|
|
4299
|
+
}
|
|
4300
|
+
async function promptProviderSelection(title = "Provider Filter") {
|
|
4301
|
+
return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
|
|
4302
|
+
}
|
|
4303
|
+
async function promptOutputPath(defaultPath) {
|
|
4304
|
+
return ask("Output file", defaultPath);
|
|
4305
|
+
}
|
|
4306
|
+
async function promptWidth() {
|
|
4307
|
+
const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
|
|
4308
|
+
{ value: "80", label: "80 columns", description: "Standard terminal width" },
|
|
4309
|
+
{ value: "100", label: "100 columns", description: "Balanced dashboard layout" },
|
|
4310
|
+
{ value: "120", label: "120 columns", description: "Wide dashboard layout" },
|
|
4311
|
+
{ value: "custom", label: "Custom width", description: "Enter an exact width" }
|
|
4312
|
+
], 1);
|
|
4313
|
+
if (choice !== "custom") {
|
|
4314
|
+
return Number(choice);
|
|
4315
|
+
}
|
|
4316
|
+
while (true) {
|
|
4317
|
+
try {
|
|
4318
|
+
const parsed = parsePositiveInteger(await ask("Custom width"));
|
|
4319
|
+
if (parsed !== null) {
|
|
4320
|
+
return parsed;
|
|
4321
|
+
}
|
|
4322
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
|
|
4323
|
+
${color("Enter a positive whole number to continue.", DIM)}
|
|
4324
|
+
|
|
4325
|
+
Press Enter to try again.`);
|
|
4326
|
+
await ask("");
|
|
4327
|
+
} catch (error) {
|
|
4328
|
+
paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
|
|
4329
|
+
${color(error instanceof Error ? error.message : String(error), DIM)}
|
|
4330
|
+
|
|
4331
|
+
Press Enter to try again.`);
|
|
4332
|
+
await ask("");
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
async function promptCompareSetting() {
|
|
4337
|
+
const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
|
|
4338
|
+
{ value: "off", label: "No compare", description: "Render a standard single-period report" },
|
|
4339
|
+
{ value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
|
|
4340
|
+
{ value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
|
|
4341
|
+
]);
|
|
4342
|
+
if (choice === "off")
|
|
4343
|
+
return null;
|
|
4344
|
+
if (choice === "auto")
|
|
4345
|
+
return "auto";
|
|
4346
|
+
return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
|
|
4347
|
+
}
|
|
4348
|
+
async function buildDashboardPreset() {
|
|
4349
|
+
const rangeArgs = await promptDateWindow();
|
|
4350
|
+
const providers = await promptProviderSelection();
|
|
4351
|
+
const width = await promptWidth();
|
|
4352
|
+
const noInsights = await askYesNo("Hide insights panel", false);
|
|
4353
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
4354
|
+
const args = { ...rangeArgs };
|
|
4355
|
+
applySelectedProviders(args, providers);
|
|
4356
|
+
if (width)
|
|
4357
|
+
args["width"] = width;
|
|
4358
|
+
if (noInsights)
|
|
4359
|
+
args["noInsights"] = true;
|
|
4360
|
+
if (noColor2)
|
|
4361
|
+
args["noColor"] = true;
|
|
4362
|
+
return createRunCommand(args);
|
|
4363
|
+
}
|
|
4364
|
+
async function buildJsonPreset() {
|
|
4365
|
+
const rangeArgs = await promptDateWindow();
|
|
4366
|
+
const providers = await promptProviderSelection();
|
|
4367
|
+
const compare = await promptCompareSetting();
|
|
4368
|
+
const saveToFile = await askYesNo("Write JSON to a file", false);
|
|
4369
|
+
const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
|
|
4370
|
+
const args = {
|
|
4371
|
+
format: "json",
|
|
4372
|
+
...rangeArgs
|
|
4373
|
+
};
|
|
4374
|
+
applySelectedProviders(args, providers);
|
|
4375
|
+
if (compare)
|
|
4376
|
+
args["compare"] = compare;
|
|
4377
|
+
if (saveToFile) {
|
|
4378
|
+
args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
|
|
4379
|
+
}
|
|
4380
|
+
if (clipboard)
|
|
4381
|
+
args["clipboard"] = true;
|
|
4382
|
+
return createRunCommand(args);
|
|
4383
|
+
}
|
|
4384
|
+
async function buildImagePreset(format) {
|
|
4385
|
+
const theme = await promptTheme();
|
|
4386
|
+
const rangeArgs = await promptDateWindow();
|
|
4387
|
+
const providers = await promptProviderSelection("Provider Filter");
|
|
4388
|
+
const compare = await promptCompareSetting();
|
|
4389
|
+
const output = await promptOutputPath(`tokenleak.${format}`);
|
|
4390
|
+
const shouldOpen = await askYesNo("Open the file when done", true);
|
|
4391
|
+
const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
|
|
4392
|
+
const args = {
|
|
4393
|
+
format,
|
|
4394
|
+
theme,
|
|
4395
|
+
output,
|
|
4396
|
+
open: shouldOpen,
|
|
4397
|
+
more,
|
|
4398
|
+
...rangeArgs
|
|
4399
|
+
};
|
|
4400
|
+
applySelectedProviders(args, providers);
|
|
4401
|
+
if (compare)
|
|
4402
|
+
args["compare"] = compare;
|
|
4403
|
+
return createRunCommand(args);
|
|
4404
|
+
}
|
|
4405
|
+
async function buildComparePreset() {
|
|
4406
|
+
const rangeArgs = await promptDateWindow();
|
|
4407
|
+
const providers = await promptProviderSelection();
|
|
4408
|
+
const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
|
|
4409
|
+
{ value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
|
|
4410
|
+
{ value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
|
|
4411
|
+
]);
|
|
4412
|
+
const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
|
|
4413
|
+
const saveToFile = await askYesNo("Write compare output to a file", false);
|
|
4414
|
+
const args = {
|
|
4415
|
+
format: "json",
|
|
4416
|
+
compare,
|
|
4417
|
+
...rangeArgs
|
|
4418
|
+
};
|
|
4419
|
+
applySelectedProviders(args, providers);
|
|
4420
|
+
if (saveToFile) {
|
|
4421
|
+
args["output"] = await promptOutputPath("tokenleak-compare.json");
|
|
4422
|
+
}
|
|
4423
|
+
return createRunCommand(args);
|
|
4424
|
+
}
|
|
4425
|
+
async function buildLivePreset() {
|
|
4426
|
+
const theme = await promptTheme();
|
|
4427
|
+
const rangeArgs = await promptDateWindow();
|
|
4428
|
+
const providers = await promptProviderSelection();
|
|
4429
|
+
const more = await askYesNo("Enable --more stats", true);
|
|
4430
|
+
const args = {
|
|
4431
|
+
liveServer: true,
|
|
4432
|
+
theme,
|
|
4433
|
+
more,
|
|
4434
|
+
...rangeArgs
|
|
4435
|
+
};
|
|
4436
|
+
applySelectedProviders(args, providers);
|
|
4437
|
+
return createRunCommand(args);
|
|
4438
|
+
}
|
|
4439
|
+
async function askFormatChoice() {
|
|
4440
|
+
return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
|
|
4441
|
+
{ value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
|
|
4442
|
+
{ value: "json", label: "JSON", description: "Structured machine-readable output" },
|
|
4443
|
+
{ value: "svg", label: "SVG", description: "Shareable vector export" },
|
|
4444
|
+
{ value: "png", label: "PNG", description: "Raster export for social and docs" }
|
|
4445
|
+
]);
|
|
4446
|
+
}
|
|
4447
|
+
async function buildCustomCommand() {
|
|
4448
|
+
const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
|
|
4449
|
+
{ value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
|
|
4450
|
+
{ value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
|
|
4451
|
+
{ value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
|
|
4452
|
+
]);
|
|
4453
|
+
if (mode === "live-server") {
|
|
4454
|
+
return buildLivePreset();
|
|
4455
|
+
}
|
|
4456
|
+
if (mode === "list-providers") {
|
|
4457
|
+
return createRunCommand({ listProviders: true });
|
|
4458
|
+
}
|
|
4459
|
+
const format = await askFormatChoice();
|
|
4460
|
+
const theme = format === "terminal" ? null : await promptTheme();
|
|
4461
|
+
const rangeArgs = await promptDateWindow();
|
|
4462
|
+
const providers = await promptProviderSelection();
|
|
4463
|
+
const compare = await promptCompareSetting();
|
|
4464
|
+
const width = format === "terminal" ? await promptWidth() : null;
|
|
4465
|
+
const output = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
|
|
4466
|
+
const noColor2 = await askYesNo("Disable ANSI colors", false);
|
|
4467
|
+
const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
|
|
4468
|
+
const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
|
|
4469
|
+
const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
|
|
4470
|
+
const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
|
|
4471
|
+
const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
|
|
4472
|
+
const args = {
|
|
4473
|
+
format,
|
|
4474
|
+
...rangeArgs
|
|
4475
|
+
};
|
|
4476
|
+
if (theme)
|
|
4477
|
+
args["theme"] = theme;
|
|
4478
|
+
if (compare)
|
|
4479
|
+
args["compare"] = compare;
|
|
4480
|
+
if (width)
|
|
4481
|
+
args["width"] = width;
|
|
4482
|
+
if (output)
|
|
4483
|
+
args["output"] = output;
|
|
4484
|
+
if (noColor2)
|
|
4485
|
+
args["noColor"] = true;
|
|
4486
|
+
if (noInsights)
|
|
4487
|
+
args["noInsights"] = true;
|
|
4488
|
+
if (more)
|
|
4489
|
+
args["more"] = true;
|
|
4490
|
+
if (clipboard)
|
|
4491
|
+
args["clipboard"] = true;
|
|
4492
|
+
if (open)
|
|
4493
|
+
args["open"] = true;
|
|
4494
|
+
if (upload)
|
|
4495
|
+
args["upload"] = upload;
|
|
4496
|
+
applySelectedProviders(args, providers);
|
|
4497
|
+
return createRunCommand(args);
|
|
4498
|
+
}
|
|
4499
|
+
function createMenuOptions() {
|
|
4500
|
+
return [
|
|
4501
|
+
{
|
|
4502
|
+
digit: "1",
|
|
4503
|
+
title: "Launch Dashboard",
|
|
4504
|
+
description: "guided terminal view",
|
|
4505
|
+
preview: "tokenleak --days 90",
|
|
4506
|
+
select: buildDashboardPreset
|
|
4507
|
+
},
|
|
4508
|
+
{
|
|
4509
|
+
digit: "2",
|
|
4510
|
+
title: "Export JSON",
|
|
4511
|
+
description: "structured output for scripts",
|
|
4512
|
+
preview: "tokenleak --format json",
|
|
4513
|
+
select: buildJsonPreset
|
|
4514
|
+
},
|
|
4515
|
+
{
|
|
4516
|
+
digit: "3",
|
|
4517
|
+
title: "Export SVG",
|
|
4518
|
+
description: "shareable vector card",
|
|
4519
|
+
preview: "tokenleak --format svg --output tokenleak.svg",
|
|
4520
|
+
select: async () => buildImagePreset("svg")
|
|
4521
|
+
},
|
|
4522
|
+
{
|
|
4523
|
+
digit: "4",
|
|
4524
|
+
title: "Export PNG",
|
|
4525
|
+
description: "social-ready raster image",
|
|
4526
|
+
preview: "tokenleak --format png --output tokenleak.png --more",
|
|
4527
|
+
select: async () => buildImagePreset("png")
|
|
4528
|
+
},
|
|
4529
|
+
{
|
|
4530
|
+
digit: "5",
|
|
4531
|
+
title: "Compare Periods",
|
|
4532
|
+
description: "diff current vs previous usage",
|
|
4533
|
+
preview: "tokenleak --compare auto --format json",
|
|
4534
|
+
select: buildComparePreset
|
|
4535
|
+
},
|
|
4536
|
+
{
|
|
4537
|
+
digit: "6",
|
|
4538
|
+
title: "Start Live Server",
|
|
4539
|
+
description: "browser dashboard on localhost",
|
|
4540
|
+
preview: "tokenleak --live-server --theme dark",
|
|
4541
|
+
select: buildLivePreset
|
|
4542
|
+
},
|
|
4543
|
+
{
|
|
4544
|
+
digit: "7",
|
|
4545
|
+
title: "Build Custom Command",
|
|
4546
|
+
description: "configure flags interactively",
|
|
4547
|
+
preview: "tokenleak --format terminal --days 90",
|
|
4548
|
+
select: buildCustomCommand
|
|
4549
|
+
},
|
|
4550
|
+
{
|
|
4551
|
+
digit: "8",
|
|
4552
|
+
title: "Full Help",
|
|
4553
|
+
description: "examples and complete usage",
|
|
4554
|
+
preview: "tokenleak --help",
|
|
4555
|
+
select: async () => ({ type: "show-help" })
|
|
4556
|
+
},
|
|
4557
|
+
{
|
|
4558
|
+
digit: "9",
|
|
4559
|
+
title: "List Providers",
|
|
4560
|
+
description: "detect available registries",
|
|
4561
|
+
preview: "tokenleak --list-providers",
|
|
4562
|
+
select: async () => createRunCommand({ listProviders: true })
|
|
4563
|
+
},
|
|
4564
|
+
{
|
|
4565
|
+
digit: "0",
|
|
4566
|
+
title: "Exit",
|
|
4567
|
+
description: "close the launcher",
|
|
4568
|
+
preview: "exit",
|
|
4569
|
+
select: async () => ({ type: "exit" })
|
|
4570
|
+
}
|
|
4571
|
+
];
|
|
4572
|
+
}
|
|
4573
|
+
async function waitForSingleKey() {
|
|
4574
|
+
return new Promise((resolve) => {
|
|
4575
|
+
const onKeypress = (_input, key) => {
|
|
4576
|
+
process.stdin.off("keypress", onKeypress);
|
|
4577
|
+
suspendRawMode();
|
|
4578
|
+
resolve(key);
|
|
4579
|
+
};
|
|
4580
|
+
resumeRawMode();
|
|
4581
|
+
process.stdin.on("keypress", onKeypress);
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
async function promptForMenuCommand(context, options, state) {
|
|
4585
|
+
let showHelp = false;
|
|
4586
|
+
let resolving = false;
|
|
4587
|
+
return new Promise((resolve, reject) => {
|
|
4588
|
+
const onKeypress = async (_input, key) => {
|
|
4589
|
+
if (resolving) {
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
if (key.ctrl && key.name === "c") {
|
|
4593
|
+
cleanup();
|
|
4594
|
+
resolve({ type: "exit" });
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
if (showHelp) {
|
|
4598
|
+
if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
|
|
4599
|
+
showHelp = false;
|
|
4600
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4601
|
+
}
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (key.name === "up") {
|
|
4605
|
+
state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
|
|
4606
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
if (key.name === "down") {
|
|
4610
|
+
state.selectedIndex = (state.selectedIndex + 1) % options.length;
|
|
4611
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
if (key.name === "h") {
|
|
4615
|
+
showHelp = true;
|
|
4616
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
if (key.name === "q" || key.name === "escape") {
|
|
4620
|
+
cleanup();
|
|
4621
|
+
resolve({ type: "exit" });
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
const digit = key.sequence?.match(/^[0-9]$/)?.[0];
|
|
4625
|
+
if (digit) {
|
|
4626
|
+
const nextIndex = options.findIndex((option) => option.digit === digit);
|
|
4627
|
+
if (nextIndex >= 0) {
|
|
4628
|
+
state.selectedIndex = nextIndex;
|
|
4629
|
+
if (digit === "8") {
|
|
4630
|
+
showHelp = true;
|
|
4631
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
4634
|
+
resolving = true;
|
|
4635
|
+
cleanup();
|
|
4636
|
+
try {
|
|
4637
|
+
const command = await options[nextIndex].select();
|
|
4638
|
+
resolve(command);
|
|
4639
|
+
} catch (error) {
|
|
4640
|
+
if (isInteractiveExitError(error)) {
|
|
4641
|
+
resolve({ type: "exit" });
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
reject(error);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
if (key.name === "return" || key.name === "enter") {
|
|
4650
|
+
if (options[state.selectedIndex].digit === "8") {
|
|
4651
|
+
showHelp = true;
|
|
4652
|
+
paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
|
|
4653
|
+
return;
|
|
4654
|
+
}
|
|
4655
|
+
resolving = true;
|
|
4656
|
+
cleanup();
|
|
4657
|
+
try {
|
|
4658
|
+
const command = await options[state.selectedIndex].select();
|
|
4659
|
+
resolve(command);
|
|
4660
|
+
} catch (error) {
|
|
4661
|
+
if (isInteractiveExitError(error)) {
|
|
4662
|
+
resolve({ type: "exit" });
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
reject(error);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
function cleanup() {
|
|
4670
|
+
process.stdin.off("keypress", onKeypress);
|
|
4671
|
+
suspendRawMode();
|
|
4672
|
+
}
|
|
4673
|
+
paint(renderLauncher(context, options, state.selectedIndex));
|
|
4674
|
+
resumeRawMode();
|
|
4675
|
+
process.stdin.on("keypress", onKeypress);
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
async function showExecutionResult(request, result) {
|
|
4679
|
+
paint(renderResult(request, result));
|
|
4680
|
+
const key = await waitForSingleKey();
|
|
4681
|
+
if (key.ctrl && key.name === "c")
|
|
4682
|
+
return "exit";
|
|
4683
|
+
if (key.name === "q" || key.name === "escape")
|
|
4684
|
+
return "exit";
|
|
4685
|
+
return "menu";
|
|
4686
|
+
}
|
|
4687
|
+
function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
|
|
4688
|
+
return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
|
|
4689
|
+
}
|
|
4690
|
+
async function startInteractiveCli(context, execute) {
|
|
4691
|
+
const options = createMenuOptions();
|
|
4692
|
+
const state = { selectedIndex: 0 };
|
|
4693
|
+
let interrupted = false;
|
|
4694
|
+
let ignoreSigint = false;
|
|
4695
|
+
const onSigint = () => {
|
|
4696
|
+
if (!ignoreSigint) {
|
|
4697
|
+
interrupted = true;
|
|
4698
|
+
}
|
|
4699
|
+
};
|
|
4700
|
+
emitKeypressEvents(process.stdin);
|
|
4701
|
+
enterAltScreen();
|
|
4702
|
+
process.on("SIGINT", onSigint);
|
|
4703
|
+
try {
|
|
4704
|
+
while (true) {
|
|
4705
|
+
if (interrupted) {
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const command = await promptForMenuCommand(context, options, state);
|
|
4709
|
+
if (command.type === "exit") {
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
if (command.type === "show-help") {
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
const startedAt = Date.now();
|
|
4716
|
+
let loadingFrame = 0;
|
|
4717
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
4718
|
+
const loadingTicker = setInterval(() => {
|
|
4719
|
+
loadingFrame += 1;
|
|
4720
|
+
paint(renderLoading(command.request, loadingFrame, startedAt));
|
|
4721
|
+
}, LOADING_TICK_MS);
|
|
4722
|
+
let result;
|
|
4723
|
+
try {
|
|
4724
|
+
if (command.request.executionMode === "inherit") {
|
|
4725
|
+
clearInterval(loadingTicker);
|
|
4726
|
+
leaveAltScreen();
|
|
4727
|
+
try {
|
|
4728
|
+
ignoreSigint = true;
|
|
4729
|
+
result = await execute(command.request);
|
|
4730
|
+
} finally {
|
|
4731
|
+
ignoreSigint = false;
|
|
4732
|
+
enterAltScreen();
|
|
4733
|
+
}
|
|
4734
|
+
} else {
|
|
4735
|
+
result = await execute(command.request);
|
|
4736
|
+
}
|
|
4737
|
+
} finally {
|
|
4738
|
+
clearInterval(loadingTicker);
|
|
4739
|
+
}
|
|
4740
|
+
if (interrupted) {
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
const next = await showExecutionResult(command.request, result);
|
|
4744
|
+
if (next === "exit") {
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
} finally {
|
|
4749
|
+
process.off("SIGINT", onSigint);
|
|
4750
|
+
suspendRawMode();
|
|
4751
|
+
leaveAltScreen();
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
3054
4755
|
// packages/cli/src/sharing/clipboard.ts
|
|
3055
4756
|
var PLATFORM_COMMANDS = {
|
|
3056
4757
|
darwin: ["pbcopy"],
|
|
@@ -3208,6 +4909,7 @@ function buildHelpText() {
|
|
|
3208
4909
|
return [
|
|
3209
4910
|
`tokenleak ${VERSION}`,
|
|
3210
4911
|
"Visualize AI coding assistant token usage across providers.",
|
|
4912
|
+
"Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
|
|
3211
4913
|
"",
|
|
3212
4914
|
"Usage:",
|
|
3213
4915
|
" tokenleak [flags]",
|
|
@@ -3229,6 +4931,7 @@ function buildHelpText() {
|
|
|
3229
4931
|
" -w, --width <number> Terminal render width",
|
|
3230
4932
|
" -p, --provider <list> Provider filter list, comma-separated",
|
|
3231
4933
|
" --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
|
|
4934
|
+
" --more Add expanded PNG/SVG stats and unlock compare cards",
|
|
3232
4935
|
" --clipboard Copy rendered output to the clipboard",
|
|
3233
4936
|
" --open Open the generated output file",
|
|
3234
4937
|
" --upload <target> Upload rendered output, currently: gist",
|
|
@@ -3272,6 +4975,81 @@ function normalizeCliArg(arg) {
|
|
|
3272
4975
|
};
|
|
3273
4976
|
return flagMap[arg] ?? arg;
|
|
3274
4977
|
}
|
|
4978
|
+
function buildInteractiveSummary(cliArgs, ok, exitCode) {
|
|
4979
|
+
if (!ok) {
|
|
4980
|
+
return `Command exited with code ${exitCode}.`;
|
|
4981
|
+
}
|
|
4982
|
+
if (typeof cliArgs["output"] === "string") {
|
|
4983
|
+
const outputPath = cliArgs["output"];
|
|
4984
|
+
const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
|
|
4985
|
+
return `${format2} written to ${outputPath}.`;
|
|
4986
|
+
}
|
|
4987
|
+
if (cliArgs["listProviders"]) {
|
|
4988
|
+
return "Provider registry loaded.";
|
|
4989
|
+
}
|
|
4990
|
+
if (cliArgs["liveServer"]) {
|
|
4991
|
+
return "Live dashboard stopped.";
|
|
4992
|
+
}
|
|
4993
|
+
if (cliArgs["compare"]) {
|
|
4994
|
+
return "Compare report generated.";
|
|
4995
|
+
}
|
|
4996
|
+
const format = String(cliArgs["format"] ?? "terminal");
|
|
4997
|
+
if (format === "terminal") {
|
|
4998
|
+
return "Terminal dashboard generated.";
|
|
4999
|
+
}
|
|
5000
|
+
return `${format.toUpperCase()} command finished successfully.`;
|
|
5001
|
+
}
|
|
5002
|
+
async function executeInteractiveCommand(request) {
|
|
5003
|
+
try {
|
|
5004
|
+
const cliPath = process.argv[1];
|
|
5005
|
+
if (!cliPath) {
|
|
5006
|
+
return {
|
|
5007
|
+
ok: false,
|
|
5008
|
+
summary: "Could not resolve the current tokenleak entrypoint.",
|
|
5009
|
+
stdout: "",
|
|
5010
|
+
stderr: "Error: process.argv[1] is missing."
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
|
|
5014
|
+
if (request.executionMode === "inherit") {
|
|
5015
|
+
const proc2 = Bun.spawn(command, {
|
|
5016
|
+
stdin: "inherit",
|
|
5017
|
+
stdout: "inherit",
|
|
5018
|
+
stderr: "inherit"
|
|
5019
|
+
});
|
|
5020
|
+
const exitCode2 = await proc2.exited;
|
|
5021
|
+
return {
|
|
5022
|
+
ok: exitCode2 === 0,
|
|
5023
|
+
summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
|
|
5024
|
+
stdout: "",
|
|
5025
|
+
stderr: ""
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
const proc = Bun.spawn(command, {
|
|
5029
|
+
stdin: "ignore",
|
|
5030
|
+
stdout: "pipe",
|
|
5031
|
+
stderr: "pipe"
|
|
5032
|
+
});
|
|
5033
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
5034
|
+
proc.exited,
|
|
5035
|
+
new Response(proc.stdout).text(),
|
|
5036
|
+
new Response(proc.stderr).text()
|
|
5037
|
+
]);
|
|
5038
|
+
return {
|
|
5039
|
+
ok: exitCode === 0,
|
|
5040
|
+
summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
|
|
5041
|
+
stdout,
|
|
5042
|
+
stderr
|
|
5043
|
+
};
|
|
5044
|
+
} catch (error) {
|
|
5045
|
+
return {
|
|
5046
|
+
ok: false,
|
|
5047
|
+
summary: "Interactive command failed before it could finish.",
|
|
5048
|
+
stdout: "",
|
|
5049
|
+
stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
3275
5053
|
function normalizeCliArgv(argv) {
|
|
3276
5054
|
const normalized = argv.map(normalizeCliArg);
|
|
3277
5055
|
const result = [];
|
|
@@ -3370,6 +5148,7 @@ function resolveConfig(cliArgs) {
|
|
|
3370
5148
|
width: 80,
|
|
3371
5149
|
noColor: false,
|
|
3372
5150
|
noInsights: false,
|
|
5151
|
+
more: false,
|
|
3373
5152
|
claude: false,
|
|
3374
5153
|
codex: false,
|
|
3375
5154
|
openCode: false,
|
|
@@ -3393,6 +5172,8 @@ function resolveConfig(cliArgs) {
|
|
|
3393
5172
|
merged.noColor = fileConfig.noColor;
|
|
3394
5173
|
if (fileConfig.noInsights !== undefined)
|
|
3395
5174
|
merged.noInsights = fileConfig.noInsights;
|
|
5175
|
+
if (fileConfig.more !== undefined)
|
|
5176
|
+
merged.more = fileConfig.more;
|
|
3396
5177
|
if (envConfig.format)
|
|
3397
5178
|
merged.format = envConfig.format;
|
|
3398
5179
|
if (envConfig.theme)
|
|
@@ -3434,6 +5215,9 @@ function resolveConfig(cliArgs) {
|
|
|
3434
5215
|
if (cliArgs["noInsights"] !== undefined) {
|
|
3435
5216
|
result.noInsights = cliArgs["noInsights"];
|
|
3436
5217
|
}
|
|
5218
|
+
if (cliArgs["more"] !== undefined) {
|
|
5219
|
+
result.more = cliArgs["more"];
|
|
5220
|
+
}
|
|
3437
5221
|
if (cliArgs["compare"] !== undefined) {
|
|
3438
5222
|
result.compare = cliArgs["compare"];
|
|
3439
5223
|
}
|
|
@@ -3511,7 +5295,11 @@ async function runCompare(compareStr, currentRange, _registry, available) {
|
|
|
3511
5295
|
loadAndAggregate(currentRange, available),
|
|
3512
5296
|
loadAndAggregate(previousRange, available)
|
|
3513
5297
|
]);
|
|
3514
|
-
return
|
|
5298
|
+
return {
|
|
5299
|
+
compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
|
|
5300
|
+
currentData: currentResult.data,
|
|
5301
|
+
previousData: previousResult.data
|
|
5302
|
+
};
|
|
3515
5303
|
}
|
|
3516
5304
|
async function run(cliArgs) {
|
|
3517
5305
|
const config = resolveConfig(cliArgs);
|
|
@@ -3544,12 +5332,45 @@ async function run(cliArgs) {
|
|
|
3544
5332
|
throw new TokenleakError("No provider data found");
|
|
3545
5333
|
}
|
|
3546
5334
|
if (config.compare) {
|
|
5335
|
+
const compareResult = await runCompare(config.compare, dateRange, registry, available);
|
|
5336
|
+
if (config.more && (config.format === "png" || config.format === "svg")) {
|
|
5337
|
+
const compareOutput = {
|
|
5338
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5339
|
+
generated: new Date().toISOString(),
|
|
5340
|
+
dateRange,
|
|
5341
|
+
providers: compareResult.currentData,
|
|
5342
|
+
aggregated: compareResult.compareOutput.periodA.stats,
|
|
5343
|
+
more: buildMoreStats(compareResult.currentData, dateRange, {
|
|
5344
|
+
previousRange: compareResult.compareOutput.periodB.range,
|
|
5345
|
+
previousProviders: compareResult.previousData
|
|
5346
|
+
})
|
|
5347
|
+
};
|
|
5348
|
+
const renderer2 = getRenderer(config.format);
|
|
5349
|
+
const renderOptions2 = {
|
|
5350
|
+
format: config.format,
|
|
5351
|
+
theme: config.theme,
|
|
5352
|
+
width: config.width,
|
|
5353
|
+
showInsights: !config.noInsights,
|
|
5354
|
+
noColor: config.noColor,
|
|
5355
|
+
output: config.output,
|
|
5356
|
+
more: true
|
|
5357
|
+
};
|
|
5358
|
+
const rendered3 = await renderer2.render(compareOutput, renderOptions2);
|
|
5359
|
+
if (config.output) {
|
|
5360
|
+
const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
|
|
5361
|
+
writeFileSync(config.output, data);
|
|
5362
|
+
} else {
|
|
5363
|
+
const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
|
|
5364
|
+
process.stdout.write(text2 + `
|
|
5365
|
+
`);
|
|
5366
|
+
}
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
3547
5369
|
if (config.format !== "json" && config.format !== "terminal") {
|
|
3548
5370
|
process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
|
|
3549
5371
|
`);
|
|
3550
5372
|
}
|
|
3551
|
-
const
|
|
3552
|
-
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
5373
|
+
const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
|
|
3553
5374
|
if (config.output) {
|
|
3554
5375
|
writeFileSync(config.output, rendered2);
|
|
3555
5376
|
} else {
|
|
@@ -3576,7 +5397,8 @@ async function run(cliArgs) {
|
|
|
3576
5397
|
generated: new Date().toISOString(),
|
|
3577
5398
|
dateRange,
|
|
3578
5399
|
providers: providerDataList,
|
|
3579
|
-
aggregated: stats
|
|
5400
|
+
aggregated: stats,
|
|
5401
|
+
more: config.more ? buildMoreStats(providerDataList, dateRange) : null
|
|
3580
5402
|
};
|
|
3581
5403
|
if (config.liveServer) {
|
|
3582
5404
|
const ignoredFlags = [];
|
|
@@ -3598,7 +5420,8 @@ async function run(cliArgs) {
|
|
|
3598
5420
|
width: config.width,
|
|
3599
5421
|
showInsights: !config.noInsights,
|
|
3600
5422
|
noColor: config.noColor,
|
|
3601
|
-
output: config.output
|
|
5423
|
+
output: config.output,
|
|
5424
|
+
more: config.more
|
|
3602
5425
|
};
|
|
3603
5426
|
const { port } = await startLiveServer(output, renderOptions2);
|
|
3604
5427
|
await new Promise((resolve) => {
|
|
@@ -3621,7 +5444,8 @@ Shutting down server...
|
|
|
3621
5444
|
width: config.width,
|
|
3622
5445
|
showInsights: !config.noInsights,
|
|
3623
5446
|
noColor: config.noColor,
|
|
3624
|
-
output: config.output
|
|
5447
|
+
output: config.output,
|
|
5448
|
+
more: config.more
|
|
3625
5449
|
};
|
|
3626
5450
|
const rendered = await renderer.render(output, renderOptions);
|
|
3627
5451
|
if (config.output) {
|
|
@@ -3710,6 +5534,11 @@ var main = defineCommand({
|
|
|
3710
5534
|
description: "Hide insights panel",
|
|
3711
5535
|
default: false
|
|
3712
5536
|
},
|
|
5537
|
+
more: {
|
|
5538
|
+
type: "boolean",
|
|
5539
|
+
description: "Add expanded PNG/SVG stats and compare cards",
|
|
5540
|
+
default: false
|
|
5541
|
+
},
|
|
3713
5542
|
compare: {
|
|
3714
5543
|
type: "string",
|
|
3715
5544
|
description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
|
|
@@ -3786,6 +5615,8 @@ var main = defineCommand({
|
|
|
3786
5615
|
cliArgs["noColor"] = true;
|
|
3787
5616
|
if (args.noInsights)
|
|
3788
5617
|
cliArgs["noInsights"] = true;
|
|
5618
|
+
if (args.more)
|
|
5619
|
+
cliArgs["more"] = true;
|
|
3789
5620
|
if (args.compare !== undefined)
|
|
3790
5621
|
cliArgs["compare"] = args.compare;
|
|
3791
5622
|
if (args.provider !== undefined)
|
|
@@ -3827,12 +5658,20 @@ if (isDirectExecution) {
|
|
|
3827
5658
|
process.stdout.write(buildVersionText());
|
|
3828
5659
|
process.exit(0);
|
|
3829
5660
|
}
|
|
3830
|
-
|
|
5661
|
+
if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
5662
|
+
await startInteractiveCli({
|
|
5663
|
+
version: VERSION,
|
|
5664
|
+
helpText: buildHelpText()
|
|
5665
|
+
}, executeInteractiveCommand);
|
|
5666
|
+
} else {
|
|
5667
|
+
await runMain(main);
|
|
5668
|
+
}
|
|
3831
5669
|
}
|
|
3832
5670
|
export {
|
|
3833
5671
|
run,
|
|
3834
5672
|
resolveConfig,
|
|
3835
5673
|
normalizeCliArgv,
|
|
3836
5674
|
inferFormatFromPath,
|
|
3837
|
-
computeDateRange
|
|
5675
|
+
computeDateRange,
|
|
5676
|
+
buildInteractiveSummary
|
|
3838
5677
|
};
|