vibestats 1.3.13 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/index.js +1010 -199
- package/dist/web/404.html +1 -0
- package/dist/web/_next/static/chunks/231.de8a9ee791ed9eef.js +1 -0
- package/dist/web/_next/static/chunks/498-74a3a367cb8442f0.js +1 -0
- package/dist/web/_next/static/chunks/562.a5731acd5543100e.js +1 -0
- package/dist/web/_next/static/chunks/816-8960921093d57453.js +1 -0
- package/dist/web/_next/static/chunks/832-93b990b7ceeaa2c5.js +1 -0
- package/dist/web/_next/static/chunks/971-0ab6a23b2af361a6.js +1 -0
- package/dist/web/_next/static/chunks/app/_not-found/page-91f35a114767b957.js +1 -0
- package/dist/web/_next/static/chunks/app/activity/[slug]/page-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/activity/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/changelog/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/compare/[slug]/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/compare/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/dashboard/activity/page-e84049538e03061e.js +1 -0
- package/dist/web/_next/static/chunks/app/dashboard/layout-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/dashboard/page-4834f263664e8663.js +1 -0
- package/dist/web/_next/static/chunks/app/dashboard/usage/page-b42b457b8fd3865c.js +1 -0
- package/dist/web/_next/static/chunks/app/dashboard/wrapped/page-268dced4ee2726f7.js +1 -0
- package/dist/web/_next/static/chunks/app/docs/commands/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/docs/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/features/[slug]/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/features/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/guides/[slug]/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/guides/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/images/[...slug]/route-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-6ebd9b17af55ec02.js +1 -0
- package/dist/web/_next/static/chunks/app/page-dfb7bbdb5999dc75.js +1 -0
- package/dist/web/_next/static/chunks/app/robots.txt/route-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/s/[slug]/route-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/sitemap.xml/route-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/usage/[slug]/page-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/use-cases/[slug]/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/use-cases/page-5d0587c1d5910e68.js +1 -0
- package/dist/web/_next/static/chunks/app/wrapped/[slug]/page-bd4706aecd453508.js +1 -0
- package/dist/web/_next/static/chunks/app/wrapped/page-66b00de7736097db.js +1 -0
- package/dist/web/_next/static/chunks/c476d598-9099ed8b975ae1d6.js +1 -0
- package/dist/web/_next/static/chunks/framework-1c6a486f6592f084.js +1 -0
- package/dist/web/_next/static/chunks/main-app-6619364ab1f13fb7.js +1 -0
- package/dist/web/_next/static/chunks/main-f6fa273a9100cc16.js +1 -0
- package/dist/web/_next/static/chunks/pages/_app-55552e79b4ca5b96.js +1 -0
- package/dist/web/_next/static/chunks/pages/_error-da3c1b00689f457b.js +1 -0
- package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/_next/static/chunks/webpack-175b2f5685d7557b.js +1 -0
- package/dist/web/_next/static/css/335de1248158d380.css +3 -0
- package/dist/web/_next/static/gPirvBMhpRSdtR0tsMj2H/_buildManifest.js +1 -0
- package/dist/web/_next/static/gPirvBMhpRSdtR0tsMj2H/_ssgManifest.js +1 -0
- package/dist/web/activity.html +1 -0
- package/dist/web/activity.txt +19 -0
- package/dist/web/changelog.html +1 -0
- package/dist/web/changelog.txt +60 -0
- package/dist/web/compare/claude-code-vs-codex-cli-tracking.html +1 -0
- package/dist/web/compare/claude-code-vs-codex-cli-tracking.txt +25 -0
- package/dist/web/compare/daily-vs-monthly-ai-usage-reports.html +1 -0
- package/dist/web/compare/daily-vs-monthly-ai-usage-reports.txt +24 -0
- package/dist/web/compare/local-vs-cloud-ai-analytics.html +1 -0
- package/dist/web/compare/local-vs-cloud-ai-analytics.txt +25 -0
- package/dist/web/compare/manual-tracking-vs-vibestats.html +1 -0
- package/dist/web/compare/manual-tracking-vs-vibestats.txt +24 -0
- package/dist/web/compare/querystring-shares-vs-stored-share-pages.html +1 -0
- package/dist/web/compare/querystring-shares-vs-stored-share-pages.txt +25 -0
- package/dist/web/compare/session-breakdown-vs-model-breakdown.html +1 -0
- package/dist/web/compare/session-breakdown-vs-model-breakdown.txt +25 -0
- package/dist/web/compare/single-source-vs-combined-tracking.html +1 -0
- package/dist/web/compare/single-source-vs-combined-tracking.txt +25 -0
- package/dist/web/compare/terminal-table-vs-json-output.html +1 -0
- package/dist/web/compare/terminal-table-vs-json-output.txt +24 -0
- package/dist/web/compare/token-volume-vs-cost-estimation.html +1 -0
- package/dist/web/compare/token-volume-vs-cost-estimation.txt +24 -0
- package/dist/web/compare/wrapped-vs-activity-heatmap.html +1 -0
- package/dist/web/compare/wrapped-vs-activity-heatmap.txt +25 -0
- package/dist/web/compare.html +1 -0
- package/dist/web/compare.txt +32 -0
- package/dist/web/dashboard/activity.html +1 -0
- package/dist/web/dashboard/activity.txt +20 -0
- package/dist/web/dashboard/usage.html +1 -0
- package/dist/web/dashboard/usage.txt +20 -0
- package/dist/web/dashboard/wrapped.html +1 -0
- package/dist/web/dashboard/wrapped.txt +20 -0
- package/dist/web/dashboard.html +1 -0
- package/dist/web/dashboard.txt +20 -0
- package/dist/web/docs/commands.html +1 -0
- package/dist/web/docs/commands.txt +33 -0
- package/dist/web/docs.html +1 -0
- package/dist/web/docs.txt +25 -0
- package/dist/web/features/ai-coding-activity-heatmap.html +1 -0
- package/dist/web/features/ai-coding-activity-heatmap.txt +25 -0
- package/dist/web/features/ai-coding-wrapped.html +1 -0
- package/dist/web/features/ai-coding-wrapped.txt +24 -0
- package/dist/web/features/claude-code-stats.html +1 -0
- package/dist/web/features/claude-code-stats.txt +24 -0
- package/dist/web/features/claude-diagnostics.html +1 -0
- package/dist/web/features/claude-diagnostics.txt +24 -0
- package/dist/web/features/codex-cli-stats.html +1 -0
- package/dist/web/features/codex-cli-stats.txt +24 -0
- package/dist/web/features/combined-ai-coding-stats.html +1 -0
- package/dist/web/features/combined-ai-coding-stats.txt +25 -0
- package/dist/web/features/json-and-automation-exports.html +1 -0
- package/dist/web/features/json-and-automation-exports.txt +25 -0
- package/dist/web/features/model-breakdown.html +1 -0
- package/dist/web/features/model-breakdown.txt +24 -0
- package/dist/web/features/privacy-first-analytics.html +1 -0
- package/dist/web/features/privacy-first-analytics.txt +25 -0
- package/dist/web/features/session-breakdown.html +1 -0
- package/dist/web/features/session-breakdown.txt +24 -0
- package/dist/web/features/share-pages.html +1 -0
- package/dist/web/features/share-pages.txt +24 -0
- package/dist/web/features/token-and-cost-tracking.html +1 -0
- package/dist/web/features/token-and-cost-tracking.txt +24 -0
- package/dist/web/features.html +1 -0
- package/dist/web/features.txt +33 -0
- package/dist/web/guides/how-to-build-ai-activity-heatmap.html +1 -0
- package/dist/web/guides/how-to-build-ai-activity-heatmap.txt +24 -0
- package/dist/web/guides/how-to-compare-model-usage.html +1 -0
- package/dist/web/guides/how-to-compare-model-usage.txt +24 -0
- package/dist/web/guides/how-to-create-ai-coding-wrapped.html +1 -0
- package/dist/web/guides/how-to-create-ai-coding-wrapped.txt +24 -0
- package/dist/web/guides/how-to-measure-ai-token-costs.html +1 -0
- package/dist/web/guides/how-to-measure-ai-token-costs.txt +24 -0
- package/dist/web/guides/how-to-read-claude-diagnostics.html +1 -0
- package/dist/web/guides/how-to-read-claude-diagnostics.txt +24 -0
- package/dist/web/guides/how-to-share-ai-coding-stats.html +1 -0
- package/dist/web/guides/how-to-share-ai-coding-stats.txt +24 -0
- package/dist/web/guides/how-to-track-claude-code-usage.html +1 -0
- package/dist/web/guides/how-to-track-claude-code-usage.txt +24 -0
- package/dist/web/guides/how-to-track-codex-cli-usage.html +1 -0
- package/dist/web/guides/how-to-track-codex-cli-usage.txt +24 -0
- package/dist/web/guides.html +1 -0
- package/dist/web/guides.txt +27 -0
- package/dist/web/index.html +1 -0
- package/dist/web/index.txt +23 -0
- package/dist/web/robots.txt +7 -0
- package/dist/web/sitemap.xml +279 -0
- package/dist/web/use-cases/ai-agencies.html +1 -0
- package/dist/web/use-cases/ai-agencies.txt +23 -0
- package/dist/web/use-cases/consultants.html +1 -0
- package/dist/web/use-cases/consultants.txt +23 -0
- package/dist/web/use-cases/engineering-managers.html +1 -0
- package/dist/web/use-cases/engineering-managers.txt +24 -0
- package/dist/web/use-cases/freelancers.html +1 -0
- package/dist/web/use-cases/freelancers.txt +23 -0
- package/dist/web/use-cases/indie-hackers.html +1 -0
- package/dist/web/use-cases/indie-hackers.txt +23 -0
- package/dist/web/use-cases/monthly-reporting.html +1 -0
- package/dist/web/use-cases/monthly-reporting.txt +23 -0
- package/dist/web/use-cases/open-source-maintainers.html +1 -0
- package/dist/web/use-cases/open-source-maintainers.txt +24 -0
- package/dist/web/use-cases/personal-retrospectives.html +1 -0
- package/dist/web/use-cases/personal-retrospectives.txt +24 -0
- package/dist/web/use-cases/researchers.html +1 -0
- package/dist/web/use-cases/researchers.txt +23 -0
- package/dist/web/use-cases/weekly-reviews.html +1 -0
- package/dist/web/use-cases/weekly-reviews.txt +23 -0
- package/dist/web/use-cases.html +1 -0
- package/dist/web/use-cases.txt +31 -0
- package/dist/web/wrapped.html +1 -0
- package/dist/web/wrapped.txt +20 -0
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -902,9 +902,9 @@ function buildActivityHeatmapLines(graph, options) {
|
|
|
902
902
|
|
|
903
903
|
// src/anthropic-sources.ts
|
|
904
904
|
import { execFile } from "child_process";
|
|
905
|
-
import { promises as
|
|
906
|
-
import { homedir } from "os";
|
|
907
|
-
import { basename, join } from "path";
|
|
905
|
+
import { promises as fs2 } from "fs";
|
|
906
|
+
import { homedir as homedir2 } from "os";
|
|
907
|
+
import { basename, join as join2 } from "path";
|
|
908
908
|
import { promisify } from "util";
|
|
909
909
|
|
|
910
910
|
// src/shared/async.ts
|
|
@@ -934,12 +934,168 @@ async function mapWithConcurrency(items, concurrency, worker) {
|
|
|
934
934
|
return results;
|
|
935
935
|
}
|
|
936
936
|
|
|
937
|
+
// src/usage/cache.ts
|
|
938
|
+
import { promises as fs } from "fs";
|
|
939
|
+
import { homedir } from "os";
|
|
940
|
+
import { dirname, join } from "path";
|
|
941
|
+
var USAGE_CACHE_SCHEMA_VERSION = 1;
|
|
942
|
+
var USAGE_CACHE_FILENAME = "usage-entries-v1.json";
|
|
943
|
+
var CACHE_LOCK_TIMEOUT_MS = 2e3;
|
|
944
|
+
var CACHE_LOCK_RETRY_MS = 25;
|
|
945
|
+
var CACHE_LOCK_STALE_MS = 3e4;
|
|
946
|
+
var cacheWriteQueue = Promise.resolve();
|
|
947
|
+
function getUsageCacheDir() {
|
|
948
|
+
return process.env.VIBESTATS_CACHE_DIR || join(homedir(), ".vibestats", "cache");
|
|
949
|
+
}
|
|
950
|
+
function getUsageCachePath() {
|
|
951
|
+
return join(getUsageCacheDir(), USAGE_CACHE_FILENAME);
|
|
952
|
+
}
|
|
953
|
+
function shouldUsePersistentUsageCache(options = {}) {
|
|
954
|
+
return options.useCache !== false;
|
|
955
|
+
}
|
|
956
|
+
function createMissingFingerprint(path) {
|
|
957
|
+
return {
|
|
958
|
+
path,
|
|
959
|
+
size: -1,
|
|
960
|
+
mtimeMs: -1
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async function getSourceFingerprint(path) {
|
|
964
|
+
try {
|
|
965
|
+
const stats = await fs.stat(path);
|
|
966
|
+
if (!stats.isFile()) return null;
|
|
967
|
+
return {
|
|
968
|
+
path,
|
|
969
|
+
size: stats.size,
|
|
970
|
+
mtimeMs: Math.trunc(stats.mtimeMs)
|
|
971
|
+
};
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function fingerprintMatches(left, right) {
|
|
977
|
+
return Boolean(left && left.path === right.path && left.size === right.size && left.mtimeMs === right.mtimeMs);
|
|
978
|
+
}
|
|
979
|
+
function dependencyFingerprintsMatch(cached, current) {
|
|
980
|
+
if ((cached?.length || 0) !== current.length) return false;
|
|
981
|
+
for (let index = 0; index < current.length; index++) {
|
|
982
|
+
if (!fingerprintMatches(cached?.[index], current[index])) return false;
|
|
983
|
+
}
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
function getCachedUsageValue(cache, key, fingerprint, dependencyFingerprints = []) {
|
|
987
|
+
const record = cache.entries[key];
|
|
988
|
+
if (!record) return null;
|
|
989
|
+
if (!fingerprintMatches(record.fingerprint, fingerprint)) return null;
|
|
990
|
+
if (!dependencyFingerprintsMatch(record.dependencyFingerprints, dependencyFingerprints)) return null;
|
|
991
|
+
return record.value;
|
|
992
|
+
}
|
|
993
|
+
function setCachedUsageValue(cache, key, kind, fingerprint, value, dependencyFingerprints = []) {
|
|
994
|
+
cache.entries[key] = {
|
|
995
|
+
kind,
|
|
996
|
+
fingerprint,
|
|
997
|
+
dependencyFingerprints,
|
|
998
|
+
value,
|
|
999
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function createEmptyUsageCache() {
|
|
1003
|
+
return {
|
|
1004
|
+
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
1005
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1006
|
+
entries: {}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function isUsageCache(value) {
|
|
1010
|
+
const candidate = value;
|
|
1011
|
+
return candidate?.schemaVersion === USAGE_CACHE_SCHEMA_VERSION && candidate.entries !== null && typeof candidate.entries === "object";
|
|
1012
|
+
}
|
|
1013
|
+
async function readUsageCacheFile() {
|
|
1014
|
+
try {
|
|
1015
|
+
const content = await fs.readFile(getUsageCachePath(), "utf-8");
|
|
1016
|
+
const parsed = JSON.parse(content);
|
|
1017
|
+
if (!isUsageCache(parsed)) return createEmptyUsageCache();
|
|
1018
|
+
return parsed;
|
|
1019
|
+
} catch {
|
|
1020
|
+
return createEmptyUsageCache();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async function readUsageCacheForLookup(options = {}) {
|
|
1024
|
+
if (!shouldUsePersistentUsageCache(options) || options.refreshCache) {
|
|
1025
|
+
return createEmptyUsageCache();
|
|
1026
|
+
}
|
|
1027
|
+
return readUsageCacheFile();
|
|
1028
|
+
}
|
|
1029
|
+
async function writeUsageCacheFile(cache) {
|
|
1030
|
+
const cachePath = getUsageCachePath();
|
|
1031
|
+
await fs.mkdir(dirname(cachePath), { recursive: true });
|
|
1032
|
+
const tmpPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1033
|
+
cache.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1034
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(cache, null, 2)}
|
|
1035
|
+
`, "utf-8");
|
|
1036
|
+
await fs.rename(tmpPath, cachePath);
|
|
1037
|
+
}
|
|
1038
|
+
function sleep(ms) {
|
|
1039
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1040
|
+
}
|
|
1041
|
+
async function withUsageCacheLock(task) {
|
|
1042
|
+
const lockPath = `${getUsageCachePath()}.lock`;
|
|
1043
|
+
await fs.mkdir(dirname(lockPath), { recursive: true });
|
|
1044
|
+
const start = Date.now();
|
|
1045
|
+
let lockHandle = null;
|
|
1046
|
+
while (!lockHandle) {
|
|
1047
|
+
try {
|
|
1048
|
+
lockHandle = await fs.open(lockPath, "wx");
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
const code = error.code;
|
|
1051
|
+
if (code !== "EEXIST" || Date.now() - start >= CACHE_LOCK_TIMEOUT_MS) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
const lockStats = await fs.stat(lockPath);
|
|
1056
|
+
if (Date.now() - lockStats.mtimeMs > CACHE_LOCK_STALE_MS) {
|
|
1057
|
+
await fs.unlink(lockPath);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
} catch {
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
await sleep(CACHE_LOCK_RETRY_MS);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
await task();
|
|
1068
|
+
} finally {
|
|
1069
|
+
try {
|
|
1070
|
+
await lockHandle.close();
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
await fs.unlink(lockPath);
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function updateUsageCache(options, mutator) {
|
|
1080
|
+
if (!shouldUsePersistentUsageCache(options)) return;
|
|
1081
|
+
cacheWriteQueue = cacheWriteQueue.then(() => withUsageCacheLock(async () => {
|
|
1082
|
+
const cache = await readUsageCacheFile();
|
|
1083
|
+
mutator(cache);
|
|
1084
|
+
await writeUsageCacheFile(cache);
|
|
1085
|
+
})).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
await cacheWriteQueue;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
937
1090
|
// src/anthropic-sources.ts
|
|
938
1091
|
var execFileAsync = promisify(execFile);
|
|
939
1092
|
var SQLITE_SEPARATOR = "";
|
|
940
1093
|
var SQLITE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
941
1094
|
var FILE_PARSE_CONCURRENCY = getRecommendedConcurrency();
|
|
942
1095
|
var anthropicEntryCache = /* @__PURE__ */ new Map();
|
|
1096
|
+
var CLAUDE_JSONL_CACHE_KIND = "claude-jsonl";
|
|
1097
|
+
var FACTORY_SETTINGS_CACHE_KIND = "factory-settings";
|
|
1098
|
+
var OPENCODE_DB_CACHE_KIND = "opencode-db";
|
|
943
1099
|
function toLocalDateString(isoTimestamp) {
|
|
944
1100
|
const date = new Date(isoTimestamp);
|
|
945
1101
|
const year = date.getFullYear();
|
|
@@ -949,7 +1105,7 @@ function toLocalDateString(isoTimestamp) {
|
|
|
949
1105
|
}
|
|
950
1106
|
async function pathExists(path) {
|
|
951
1107
|
try {
|
|
952
|
-
await
|
|
1108
|
+
await fs2.access(path);
|
|
953
1109
|
return true;
|
|
954
1110
|
} catch {
|
|
955
1111
|
return false;
|
|
@@ -957,7 +1113,7 @@ async function pathExists(path) {
|
|
|
957
1113
|
}
|
|
958
1114
|
async function safeRealpath(path) {
|
|
959
1115
|
try {
|
|
960
|
-
return await
|
|
1116
|
+
return await fs2.realpath(path);
|
|
961
1117
|
} catch {
|
|
962
1118
|
return path;
|
|
963
1119
|
}
|
|
@@ -967,17 +1123,17 @@ async function findFiles(dir, matcher, visited = /* @__PURE__ */ new Set(), resu
|
|
|
967
1123
|
const realPath = await safeRealpath(dir);
|
|
968
1124
|
if (visited.has(realPath)) return result;
|
|
969
1125
|
visited.add(realPath);
|
|
970
|
-
let
|
|
1126
|
+
let entries;
|
|
971
1127
|
try {
|
|
972
|
-
|
|
1128
|
+
entries = await fs2.readdir(dir);
|
|
973
1129
|
} catch {
|
|
974
1130
|
return result;
|
|
975
1131
|
}
|
|
976
|
-
for (const entry of
|
|
977
|
-
const fullPath =
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
const fullPath = join2(dir, entry);
|
|
978
1134
|
let stat;
|
|
979
1135
|
try {
|
|
980
|
-
stat = await
|
|
1136
|
+
stat = await fs2.stat(fullPath);
|
|
981
1137
|
} catch {
|
|
982
1138
|
continue;
|
|
983
1139
|
}
|
|
@@ -1019,36 +1175,99 @@ function calculateKnownCost(modelName, inputTokens, outputTokens, cacheWriteToke
|
|
|
1019
1175
|
function entryTotalTokens(entry) {
|
|
1020
1176
|
return entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
|
|
1021
1177
|
}
|
|
1178
|
+
function getPersistentCacheKey(kind, filePath) {
|
|
1179
|
+
return `${kind}:${filePath}`;
|
|
1180
|
+
}
|
|
1181
|
+
function isPathWithin(path, root) {
|
|
1182
|
+
const normalizedRoot = root.endsWith("/") ? root : `${root}/`;
|
|
1183
|
+
return path === root || path.startsWith(normalizedRoot);
|
|
1184
|
+
}
|
|
1185
|
+
function toCachedAnthropicEntry(entry) {
|
|
1186
|
+
return {
|
|
1187
|
+
timestamp: entry.timestamp,
|
|
1188
|
+
rawModel: entry.rawModel,
|
|
1189
|
+
inputTokens: entry.inputTokens,
|
|
1190
|
+
outputTokens: entry.outputTokens,
|
|
1191
|
+
cacheWriteTokens: entry.cacheWriteTokens,
|
|
1192
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
1193
|
+
explicitCost: entry.explicitCost,
|
|
1194
|
+
messageCount: entry.messageCount,
|
|
1195
|
+
sourceKey: entry.sourceKey,
|
|
1196
|
+
sessionId: entry.sessionId,
|
|
1197
|
+
sessionKind: entry.sessionKind,
|
|
1198
|
+
subagentId: entry.subagentId
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function rehydrateAnthropicEntry(entry) {
|
|
1202
|
+
return {
|
|
1203
|
+
date: toLocalDateString(entry.timestamp),
|
|
1204
|
+
timestamp: entry.timestamp,
|
|
1205
|
+
rawModel: entry.rawModel,
|
|
1206
|
+
model: getModelDisplayName(entry.rawModel),
|
|
1207
|
+
inputTokens: entry.inputTokens,
|
|
1208
|
+
outputTokens: entry.outputTokens,
|
|
1209
|
+
cacheWriteTokens: entry.cacheWriteTokens,
|
|
1210
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
1211
|
+
cost: calculateKnownCost(
|
|
1212
|
+
entry.rawModel,
|
|
1213
|
+
entry.inputTokens,
|
|
1214
|
+
entry.outputTokens,
|
|
1215
|
+
entry.cacheWriteTokens,
|
|
1216
|
+
entry.cacheReadTokens,
|
|
1217
|
+
entry.explicitCost
|
|
1218
|
+
),
|
|
1219
|
+
messageCount: entry.messageCount,
|
|
1220
|
+
source: "claude",
|
|
1221
|
+
sourceKey: entry.sourceKey,
|
|
1222
|
+
sessionId: entry.sessionId,
|
|
1223
|
+
sessionKind: entry.sessionKind,
|
|
1224
|
+
subagentId: entry.subagentId
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function rehydrateAnthropicEntries(entries) {
|
|
1228
|
+
return entries.map(rehydrateAnthropicEntry);
|
|
1229
|
+
}
|
|
1230
|
+
function cacheAnthropicEntries(entries) {
|
|
1231
|
+
return entries.map((entry) => toCachedAnthropicEntry(entry));
|
|
1232
|
+
}
|
|
1233
|
+
async function getOpenCodeDependencyFingerprints(dbPath) {
|
|
1234
|
+
const walPath = `${dbPath}-wal`;
|
|
1235
|
+
const shmPath = `${dbPath}-shm`;
|
|
1236
|
+
return [
|
|
1237
|
+
await getSourceFingerprint(walPath) || createMissingFingerprint(walPath),
|
|
1238
|
+
await getSourceFingerprint(shmPath) || createMissingFingerprint(shmPath)
|
|
1239
|
+
];
|
|
1240
|
+
}
|
|
1022
1241
|
async function resolveProjectDir(projectsDir, cwd) {
|
|
1023
1242
|
let current = cwd;
|
|
1024
1243
|
while (current && current !== "/") {
|
|
1025
1244
|
const encoded = current.replace(/\//g, "-");
|
|
1026
|
-
const candidate =
|
|
1245
|
+
const candidate = join2(projectsDir, encoded);
|
|
1027
1246
|
if (await pathExists(candidate)) return candidate;
|
|
1028
|
-
const parent =
|
|
1247
|
+
const parent = join2(current, "..");
|
|
1029
1248
|
if (parent === current) break;
|
|
1030
1249
|
current = parent;
|
|
1031
1250
|
}
|
|
1032
1251
|
return null;
|
|
1033
1252
|
}
|
|
1034
1253
|
function getClaudeDir() {
|
|
1035
|
-
return process.env.CLAUDE_HOME ||
|
|
1254
|
+
return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
|
|
1036
1255
|
}
|
|
1037
1256
|
function getOpenCodeDbPath() {
|
|
1038
1257
|
if (process.env.OPENCODE_DB_PATH) {
|
|
1039
1258
|
return process.env.OPENCODE_DB_PATH;
|
|
1040
1259
|
}
|
|
1041
|
-
return
|
|
1260
|
+
return join2(homedir2(), ".local", "share", "opencode", "opencode.db");
|
|
1042
1261
|
}
|
|
1043
1262
|
function getFactorySessionsDir() {
|
|
1044
1263
|
if (process.env.FACTORY_SESSIONS_DIR) {
|
|
1045
1264
|
return process.env.FACTORY_SESSIONS_DIR;
|
|
1046
1265
|
}
|
|
1047
|
-
return
|
|
1266
|
+
return join2(homedir2(), ".factory", "sessions");
|
|
1048
1267
|
}
|
|
1049
1268
|
async function claudeCompatibleDataExists() {
|
|
1050
1269
|
const [claudeProjects, opencodeDb, factorySessions] = await Promise.all([
|
|
1051
|
-
pathExists(
|
|
1270
|
+
pathExists(join2(getClaudeDir(), "projects")),
|
|
1052
1271
|
pathExists(getOpenCodeDbPath()),
|
|
1053
1272
|
pathExists(getFactorySessionsDir())
|
|
1054
1273
|
]);
|
|
@@ -1060,12 +1279,14 @@ function getAnthropicEntryCacheKey(options) {
|
|
|
1060
1279
|
opencodeDbPath: getOpenCodeDbPath(),
|
|
1061
1280
|
factorySessionsDir: getFactorySessionsDir(),
|
|
1062
1281
|
projectFilter: options.projectFilter || "",
|
|
1063
|
-
families: options.families || []
|
|
1282
|
+
families: options.families || [],
|
|
1283
|
+
useCache: options.useCache !== false,
|
|
1284
|
+
refreshCache: options.refreshCache === true
|
|
1064
1285
|
});
|
|
1065
1286
|
}
|
|
1066
1287
|
async function parseClaudeProjectFile(filePath) {
|
|
1067
1288
|
try {
|
|
1068
|
-
const content = await
|
|
1289
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1069
1290
|
const lines = content.split("\n");
|
|
1070
1291
|
const fallbackSessionId = basename(filePath, ".jsonl");
|
|
1071
1292
|
const isSubagentFile = filePath.includes("/subagents/");
|
|
@@ -1116,27 +1337,70 @@ async function parseClaudeProjectFile(filePath) {
|
|
|
1116
1337
|
return [];
|
|
1117
1338
|
}
|
|
1118
1339
|
}
|
|
1119
|
-
async function parseClaudeProjectEntries(projectFilter) {
|
|
1340
|
+
async function parseClaudeProjectEntries(projectFilter, cacheOptions = {}) {
|
|
1120
1341
|
const claudeDir = getClaudeDir();
|
|
1121
|
-
const projectsDir =
|
|
1342
|
+
const projectsDir = join2(claudeDir, "projects");
|
|
1122
1343
|
if (!await pathExists(projectsDir)) return [];
|
|
1123
1344
|
let searchDir = projectsDir;
|
|
1124
1345
|
if (projectFilter) {
|
|
1125
1346
|
const resolved = await resolveProjectDir(projectsDir, projectFilter);
|
|
1126
|
-
if (!resolved) return
|
|
1347
|
+
if (!resolved) return [];
|
|
1127
1348
|
searchDir = resolved;
|
|
1128
1349
|
}
|
|
1129
1350
|
const jsonlFiles = await findFiles(searchDir, (entry) => entry.endsWith(".jsonl"));
|
|
1351
|
+
const persistentCache = await readUsageCacheForLookup(cacheOptions);
|
|
1352
|
+
const retainedKeys = /* @__PURE__ */ new Set();
|
|
1353
|
+
const updates = [];
|
|
1130
1354
|
const parsedFiles = await mapWithConcurrency(
|
|
1131
1355
|
jsonlFiles,
|
|
1132
1356
|
FILE_PARSE_CONCURRENCY,
|
|
1133
|
-
(filePath) =>
|
|
1357
|
+
async (filePath) => {
|
|
1358
|
+
const fingerprint = await getSourceFingerprint(filePath);
|
|
1359
|
+
if (!fingerprint) return [];
|
|
1360
|
+
const key = getPersistentCacheKey(CLAUDE_JSONL_CACHE_KIND, filePath);
|
|
1361
|
+
retainedKeys.add(key);
|
|
1362
|
+
const cached = getCachedUsageValue(persistentCache, key, fingerprint);
|
|
1363
|
+
if (cached) {
|
|
1364
|
+
return rehydrateAnthropicEntries(cached);
|
|
1365
|
+
}
|
|
1366
|
+
const entries = await parseClaudeProjectFile(filePath);
|
|
1367
|
+
updates.push({ key, fingerprint, value: cacheAnthropicEntries(entries) });
|
|
1368
|
+
return entries;
|
|
1369
|
+
}
|
|
1134
1370
|
);
|
|
1371
|
+
await updateUsageCache(cacheOptions, (cache) => {
|
|
1372
|
+
for (const [key, record] of Object.entries(cache.entries)) {
|
|
1373
|
+
if (record.kind === CLAUDE_JSONL_CACHE_KIND && isPathWithin(record.fingerprint.path, searchDir) && !retainedKeys.has(key)) {
|
|
1374
|
+
delete cache.entries[key];
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
for (const update of updates) {
|
|
1378
|
+
setCachedUsageValue(cache, update.key, CLAUDE_JSONL_CACHE_KIND, update.fingerprint, update.value);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1135
1381
|
return parsedFiles.flat();
|
|
1136
1382
|
}
|
|
1137
|
-
async function parseOpenCodeEntries() {
|
|
1383
|
+
async function parseOpenCodeEntries(cacheOptions = {}) {
|
|
1138
1384
|
const dbPath = getOpenCodeDbPath();
|
|
1139
|
-
|
|
1385
|
+
const key = getPersistentCacheKey(OPENCODE_DB_CACHE_KIND, dbPath);
|
|
1386
|
+
const fingerprint = await getSourceFingerprint(dbPath);
|
|
1387
|
+
if (!fingerprint) {
|
|
1388
|
+
await updateUsageCache(cacheOptions, (cache) => {
|
|
1389
|
+
delete cache.entries[key];
|
|
1390
|
+
});
|
|
1391
|
+
return [];
|
|
1392
|
+
}
|
|
1393
|
+
const dependencyFingerprints = await getOpenCodeDependencyFingerprints(dbPath);
|
|
1394
|
+
const persistentCache = await readUsageCacheForLookup(cacheOptions);
|
|
1395
|
+
const cached = getCachedUsageValue(
|
|
1396
|
+
persistentCache,
|
|
1397
|
+
key,
|
|
1398
|
+
fingerprint,
|
|
1399
|
+
dependencyFingerprints
|
|
1400
|
+
);
|
|
1401
|
+
if (cached) {
|
|
1402
|
+
return rehydrateAnthropicEntries(cached);
|
|
1403
|
+
}
|
|
1140
1404
|
const sql = `
|
|
1141
1405
|
SELECT
|
|
1142
1406
|
COALESCE(m.id, ''),
|
|
@@ -1165,7 +1429,7 @@ async function parseOpenCodeEntries() {
|
|
|
1165
1429
|
} catch {
|
|
1166
1430
|
return [];
|
|
1167
1431
|
}
|
|
1168
|
-
const
|
|
1432
|
+
const entries = [];
|
|
1169
1433
|
for (const line of stdout.split("\n")) {
|
|
1170
1434
|
if (!line.trim()) continue;
|
|
1171
1435
|
const [
|
|
@@ -1190,7 +1454,7 @@ async function parseOpenCodeEntries() {
|
|
|
1190
1454
|
const cacheReadTokens = Number(cacheReadText) || 0;
|
|
1191
1455
|
const explicitCost = Number(costText) || 0;
|
|
1192
1456
|
const sessionKind = parentId ? "subagent" : "main";
|
|
1193
|
-
|
|
1457
|
+
entries.push({
|
|
1194
1458
|
date: toLocalDateString(timestamp),
|
|
1195
1459
|
timestamp,
|
|
1196
1460
|
rawModel,
|
|
@@ -1200,6 +1464,7 @@ async function parseOpenCodeEntries() {
|
|
|
1200
1464
|
cacheWriteTokens,
|
|
1201
1465
|
cacheReadTokens,
|
|
1202
1466
|
cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost),
|
|
1467
|
+
explicitCost,
|
|
1203
1468
|
messageCount: 1,
|
|
1204
1469
|
source: "claude",
|
|
1205
1470
|
sourceKey: "opencode",
|
|
@@ -1208,7 +1473,17 @@ async function parseOpenCodeEntries() {
|
|
|
1208
1473
|
subagentId: sessionKind === "subagent" ? messageId || sessionId : void 0
|
|
1209
1474
|
});
|
|
1210
1475
|
}
|
|
1211
|
-
|
|
1476
|
+
await updateUsageCache(cacheOptions, (cache) => {
|
|
1477
|
+
setCachedUsageValue(
|
|
1478
|
+
cache,
|
|
1479
|
+
key,
|
|
1480
|
+
OPENCODE_DB_CACHE_KIND,
|
|
1481
|
+
fingerprint,
|
|
1482
|
+
cacheAnthropicEntries(entries),
|
|
1483
|
+
dependencyFingerprints
|
|
1484
|
+
);
|
|
1485
|
+
});
|
|
1486
|
+
return entries;
|
|
1212
1487
|
}
|
|
1213
1488
|
async function readFactorySessionMeta(sessionFilePath) {
|
|
1214
1489
|
let messageCount = 0;
|
|
@@ -1216,7 +1491,7 @@ async function readFactorySessionMeta(sessionFilePath) {
|
|
|
1216
1491
|
let sessionKind = "main";
|
|
1217
1492
|
let subagentId;
|
|
1218
1493
|
try {
|
|
1219
|
-
const content = await
|
|
1494
|
+
const content = await fs2.readFile(sessionFilePath, "utf-8");
|
|
1220
1495
|
for (const line of content.split("\n")) {
|
|
1221
1496
|
if (!line.trim()) continue;
|
|
1222
1497
|
try {
|
|
@@ -1242,13 +1517,33 @@ async function readFactorySessionMeta(sessionFilePath) {
|
|
|
1242
1517
|
}
|
|
1243
1518
|
return { messageCount: Math.max(messageCount, 1), timestamp, sessionKind, subagentId };
|
|
1244
1519
|
}
|
|
1245
|
-
async function parseFactoryEntries() {
|
|
1520
|
+
async function parseFactoryEntries(cacheOptions = {}) {
|
|
1246
1521
|
const sessionsDir = getFactorySessionsDir();
|
|
1247
1522
|
if (!await pathExists(sessionsDir)) return [];
|
|
1248
1523
|
const settingsFiles = await findFiles(sessionsDir, (entry) => entry.endsWith(".settings.json"));
|
|
1524
|
+
const persistentCache = await readUsageCacheForLookup(cacheOptions);
|
|
1525
|
+
const retainedKeys = /* @__PURE__ */ new Set();
|
|
1526
|
+
const updates = [];
|
|
1249
1527
|
const parsedFiles = await mapWithConcurrency(settingsFiles, FILE_PARSE_CONCURRENCY, async (settingsPath) => {
|
|
1528
|
+
const fingerprint = await getSourceFingerprint(settingsPath);
|
|
1529
|
+
if (!fingerprint) return null;
|
|
1530
|
+
const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
|
|
1531
|
+
const dependencyFingerprints = [
|
|
1532
|
+
await getSourceFingerprint(sessionFilePath) || createMissingFingerprint(sessionFilePath)
|
|
1533
|
+
];
|
|
1534
|
+
const key = getPersistentCacheKey(FACTORY_SETTINGS_CACHE_KIND, settingsPath);
|
|
1535
|
+
retainedKeys.add(key);
|
|
1536
|
+
const cached = getCachedUsageValue(
|
|
1537
|
+
persistentCache,
|
|
1538
|
+
key,
|
|
1539
|
+
fingerprint,
|
|
1540
|
+
dependencyFingerprints
|
|
1541
|
+
);
|
|
1542
|
+
if (cached) {
|
|
1543
|
+
return rehydrateAnthropicEntries(cached);
|
|
1544
|
+
}
|
|
1250
1545
|
try {
|
|
1251
|
-
const settings = JSON.parse(await
|
|
1546
|
+
const settings = JSON.parse(await fs2.readFile(settingsPath, "utf-8"));
|
|
1252
1547
|
const usage = settings.tokenUsage;
|
|
1253
1548
|
const configuredModel = settings.model;
|
|
1254
1549
|
if (!usage || !configuredModel) return null;
|
|
@@ -1259,12 +1554,11 @@ async function parseFactoryEntries() {
|
|
|
1259
1554
|
const outputTokens = (usage.outputTokens || 0) + (usage.thinkingTokens || 0);
|
|
1260
1555
|
const totalTokens = inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens;
|
|
1261
1556
|
if (totalTokens === 0) return null;
|
|
1262
|
-
const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
|
|
1263
1557
|
const sessionId = basename(settingsPath, ".settings.json");
|
|
1264
1558
|
const meta = await readFactorySessionMeta(sessionFilePath);
|
|
1265
1559
|
const timestamp = settings.providerLockTimestamp || meta.timestamp;
|
|
1266
1560
|
if (!timestamp) return null;
|
|
1267
|
-
|
|
1561
|
+
const entry = {
|
|
1268
1562
|
date: toLocalDateString(timestamp),
|
|
1269
1563
|
timestamp,
|
|
1270
1564
|
rawModel,
|
|
@@ -1281,11 +1575,35 @@ async function parseFactoryEntries() {
|
|
|
1281
1575
|
sessionKind: meta.sessionKind,
|
|
1282
1576
|
subagentId: meta.subagentId
|
|
1283
1577
|
};
|
|
1578
|
+
updates.push({
|
|
1579
|
+
key,
|
|
1580
|
+
fingerprint,
|
|
1581
|
+
dependencyFingerprints,
|
|
1582
|
+
value: cacheAnthropicEntries([entry])
|
|
1583
|
+
});
|
|
1584
|
+
return entry;
|
|
1284
1585
|
} catch {
|
|
1285
1586
|
return null;
|
|
1286
1587
|
}
|
|
1287
1588
|
});
|
|
1288
|
-
|
|
1589
|
+
await updateUsageCache(cacheOptions, (cache) => {
|
|
1590
|
+
for (const [key, record] of Object.entries(cache.entries)) {
|
|
1591
|
+
if (record.kind === FACTORY_SETTINGS_CACHE_KIND && isPathWithin(record.fingerprint.path, sessionsDir) && !retainedKeys.has(key)) {
|
|
1592
|
+
delete cache.entries[key];
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
for (const update of updates) {
|
|
1596
|
+
setCachedUsageValue(
|
|
1597
|
+
cache,
|
|
1598
|
+
update.key,
|
|
1599
|
+
FACTORY_SETTINGS_CACHE_KIND,
|
|
1600
|
+
update.fingerprint,
|
|
1601
|
+
update.value,
|
|
1602
|
+
update.dependencyFingerprints
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
return parsedFiles.flat().filter((entry) => entry !== null);
|
|
1289
1607
|
}
|
|
1290
1608
|
async function collectAnthropicUsageEntries(options = {}) {
|
|
1291
1609
|
const cacheKey = getAnthropicEntryCacheKey(options);
|
|
@@ -1295,22 +1613,22 @@ async function collectAnthropicUsageEntries(options = {}) {
|
|
|
1295
1613
|
}
|
|
1296
1614
|
const loadPromise = (async () => {
|
|
1297
1615
|
const [claudeEntries, opencodeEntries, factoryEntries] = await Promise.all([
|
|
1298
|
-
parseClaudeProjectEntries(options.projectFilter),
|
|
1299
|
-
parseOpenCodeEntries(),
|
|
1300
|
-
parseFactoryEntries()
|
|
1616
|
+
parseClaudeProjectEntries(options.projectFilter, options),
|
|
1617
|
+
parseOpenCodeEntries(options),
|
|
1618
|
+
parseFactoryEntries(options)
|
|
1301
1619
|
]);
|
|
1302
|
-
const
|
|
1620
|
+
const entries = [...claudeEntries, ...opencodeEntries, ...factoryEntries];
|
|
1303
1621
|
if (!options.families || options.families.length === 0) {
|
|
1304
|
-
return
|
|
1622
|
+
return entries;
|
|
1305
1623
|
}
|
|
1306
|
-
return
|
|
1624
|
+
return entries.filter((entry) => matchesModelFamilies(entry.rawModel, options.families));
|
|
1307
1625
|
})();
|
|
1308
1626
|
anthropicEntryCache.set(cacheKey, loadPromise);
|
|
1309
1627
|
return loadPromise;
|
|
1310
1628
|
}
|
|
1311
1629
|
async function loadClaudeStatsFromJsonl(options = {}) {
|
|
1312
|
-
const
|
|
1313
|
-
if (
|
|
1630
|
+
const entries = await collectAnthropicUsageEntries(options);
|
|
1631
|
+
if (entries.length === 0) {
|
|
1314
1632
|
return null;
|
|
1315
1633
|
}
|
|
1316
1634
|
const modelUsage = /* @__PURE__ */ new Map();
|
|
@@ -1321,7 +1639,7 @@ async function loadClaudeStatsFromJsonl(options = {}) {
|
|
|
1321
1639
|
const hourCounts = {};
|
|
1322
1640
|
let firstTimestamp = null;
|
|
1323
1641
|
let totalMessages = 0;
|
|
1324
|
-
for (const entry of
|
|
1642
|
+
for (const entry of entries) {
|
|
1325
1643
|
const existing = modelUsage.get(entry.rawModel) ?? {
|
|
1326
1644
|
inputTokens: 0,
|
|
1327
1645
|
outputTokens: 0,
|
|
@@ -1386,13 +1704,14 @@ async function loadClaudeStatsFromJsonl(options = {}) {
|
|
|
1386
1704
|
}
|
|
1387
1705
|
|
|
1388
1706
|
// src/codex-loader.ts
|
|
1389
|
-
import { promises as
|
|
1390
|
-
import { homedir as
|
|
1391
|
-
import { join as
|
|
1707
|
+
import { promises as fs3 } from "fs";
|
|
1708
|
+
import { homedir as homedir3 } from "os";
|
|
1709
|
+
import { join as join3 } from "path";
|
|
1392
1710
|
var FILE_PARSE_CONCURRENCY2 = getRecommendedConcurrency();
|
|
1393
1711
|
var codexParseCache = /* @__PURE__ */ new Map();
|
|
1712
|
+
var CODEX_JSONL_CACHE_KIND = "codex-jsonl";
|
|
1394
1713
|
function getCodexDir() {
|
|
1395
|
-
return process.env.CODEX_HOME ||
|
|
1714
|
+
return process.env.CODEX_HOME || join3(homedir3(), ".codex");
|
|
1396
1715
|
}
|
|
1397
1716
|
function toLocalDateString2(isoTimestamp) {
|
|
1398
1717
|
const date = new Date(isoTimestamp);
|
|
@@ -1403,7 +1722,7 @@ function toLocalDateString2(isoTimestamp) {
|
|
|
1403
1722
|
}
|
|
1404
1723
|
async function pathExists2(path) {
|
|
1405
1724
|
try {
|
|
1406
|
-
await
|
|
1725
|
+
await fs3.access(path);
|
|
1407
1726
|
return true;
|
|
1408
1727
|
} catch {
|
|
1409
1728
|
return false;
|
|
@@ -1412,24 +1731,24 @@ async function pathExists2(path) {
|
|
|
1412
1731
|
async function codexDataExists() {
|
|
1413
1732
|
const codexDir = getCodexDir();
|
|
1414
1733
|
if (!await pathExists2(codexDir)) return false;
|
|
1415
|
-
const sessionsDir =
|
|
1416
|
-
const archivedDir =
|
|
1734
|
+
const sessionsDir = join3(codexDir, "sessions");
|
|
1735
|
+
const archivedDir = join3(codexDir, "archived_sessions");
|
|
1417
1736
|
return await pathExists2(sessionsDir) || await pathExists2(archivedDir);
|
|
1418
1737
|
}
|
|
1419
1738
|
async function findJsonlFiles(dir) {
|
|
1420
1739
|
const files = [];
|
|
1421
1740
|
if (!await pathExists2(dir)) return files;
|
|
1422
|
-
let
|
|
1741
|
+
let entries;
|
|
1423
1742
|
try {
|
|
1424
|
-
|
|
1743
|
+
entries = await fs3.readdir(dir);
|
|
1425
1744
|
} catch {
|
|
1426
1745
|
return files;
|
|
1427
1746
|
}
|
|
1428
|
-
for (const entry of
|
|
1429
|
-
const fullPath =
|
|
1747
|
+
for (const entry of entries) {
|
|
1748
|
+
const fullPath = join3(dir, entry);
|
|
1430
1749
|
let stat;
|
|
1431
1750
|
try {
|
|
1432
|
-
stat = await
|
|
1751
|
+
stat = await fs3.stat(fullPath);
|
|
1433
1752
|
} catch {
|
|
1434
1753
|
continue;
|
|
1435
1754
|
}
|
|
@@ -1441,15 +1760,110 @@ async function findJsonlFiles(dir) {
|
|
|
1441
1760
|
}
|
|
1442
1761
|
return files;
|
|
1443
1762
|
}
|
|
1444
|
-
function
|
|
1763
|
+
function getPersistentCacheKey2(kind, filePath) {
|
|
1764
|
+
return `${kind}:${filePath}`;
|
|
1765
|
+
}
|
|
1766
|
+
function isPathWithin2(path, root) {
|
|
1767
|
+
const normalizedRoot = root.endsWith("/") ? root : `${root}/`;
|
|
1768
|
+
return path === root || path.startsWith(normalizedRoot);
|
|
1769
|
+
}
|
|
1770
|
+
function toCachedCodexUsageEntry(entry) {
|
|
1771
|
+
return {
|
|
1772
|
+
timestamp: entry.timestamp,
|
|
1773
|
+
rawModel: entry.rawModel,
|
|
1774
|
+
inputTokens: entry.inputTokens,
|
|
1775
|
+
outputTokens: entry.outputTokens,
|
|
1776
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
1777
|
+
reasoningTokens: entry.reasoningTokens,
|
|
1778
|
+
messageCount: entry.messageCount,
|
|
1779
|
+
sessionId: entry.sessionId,
|
|
1780
|
+
sessionKind: entry.sessionKind,
|
|
1781
|
+
subagentId: entry.subagentId
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function rehydrateCodexUsageEntry(entry) {
|
|
1785
|
+
const pricing = getCodexModelPricing(entry.rawModel);
|
|
1786
|
+
return {
|
|
1787
|
+
date: toLocalDateString2(entry.timestamp),
|
|
1788
|
+
timestamp: entry.timestamp,
|
|
1789
|
+
model: getCodexModelDisplayName(entry.rawModel),
|
|
1790
|
+
rawModel: entry.rawModel,
|
|
1791
|
+
inputTokens: entry.inputTokens,
|
|
1792
|
+
outputTokens: entry.outputTokens,
|
|
1793
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
1794
|
+
reasoningTokens: entry.reasoningTokens,
|
|
1795
|
+
cost: entry.inputTokens * pricing.input / 1e6 + entry.outputTokens * pricing.output / 1e6 + entry.cacheReadTokens * pricing.cachedInput / 1e6,
|
|
1796
|
+
messageCount: entry.messageCount,
|
|
1797
|
+
sessionId: entry.sessionId,
|
|
1798
|
+
sessionKind: entry.sessionKind,
|
|
1799
|
+
subagentId: entry.subagentId,
|
|
1800
|
+
source: "codex",
|
|
1801
|
+
sourceKey: "codex"
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function toCachedParsedCodexFile(parsed) {
|
|
1805
|
+
return {
|
|
1806
|
+
session: parsed.session,
|
|
1807
|
+
rawEntries: parsed.rawEntries.map(toCachedCodexUsageEntry)
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function rehydrateParsedCodexFile(cached) {
|
|
1811
|
+
return {
|
|
1812
|
+
session: cached.session,
|
|
1813
|
+
rawEntries: cached.rawEntries.map(rehydrateCodexUsageEntry)
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function filterParsedCodexFileByFamilies(parsed, families) {
|
|
1817
|
+
if (!families || families.length === 0) {
|
|
1818
|
+
return parsed;
|
|
1819
|
+
}
|
|
1820
|
+
const filteredPerModelUsage = {};
|
|
1821
|
+
const summedUsage = {
|
|
1822
|
+
input_tokens: 0,
|
|
1823
|
+
cached_input_tokens: 0,
|
|
1824
|
+
output_tokens: 0,
|
|
1825
|
+
reasoning_output_tokens: 0,
|
|
1826
|
+
total_tokens: 0
|
|
1827
|
+
};
|
|
1828
|
+
let primaryModel = parsed.session.model;
|
|
1829
|
+
let maxTokens = 0;
|
|
1830
|
+
for (const [model, usage] of Object.entries(parsed.session.perModelUsage || {})) {
|
|
1831
|
+
if (!matchesModelFamilies(model, families)) continue;
|
|
1832
|
+
filteredPerModelUsage[model] = usage;
|
|
1833
|
+
summedUsage.input_tokens += usage.input_tokens;
|
|
1834
|
+
summedUsage.cached_input_tokens += usage.cached_input_tokens;
|
|
1835
|
+
summedUsage.output_tokens += usage.output_tokens;
|
|
1836
|
+
summedUsage.reasoning_output_tokens += usage.reasoning_output_tokens;
|
|
1837
|
+
summedUsage.total_tokens += usage.total_tokens;
|
|
1838
|
+
if (usage.total_tokens > maxTokens) {
|
|
1839
|
+
maxTokens = usage.total_tokens;
|
|
1840
|
+
primaryModel = model;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (Object.keys(filteredPerModelUsage).length === 0) {
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
session: {
|
|
1848
|
+
...parsed.session,
|
|
1849
|
+
model: primaryModel,
|
|
1850
|
+
tokenUsage: summedUsage,
|
|
1851
|
+
perModelUsage: filteredPerModelUsage
|
|
1852
|
+
},
|
|
1853
|
+
rawEntries: parsed.rawEntries.filter((entry) => matchesModelFamilies(entry.rawModel, families))
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function getCodexParseCacheKey(options) {
|
|
1445
1857
|
return JSON.stringify({
|
|
1446
1858
|
codexDir: getCodexDir(),
|
|
1447
|
-
families: families || []
|
|
1859
|
+
families: options.families || [],
|
|
1860
|
+
useCache: options.useCache !== false,
|
|
1861
|
+
refreshCache: options.refreshCache === true
|
|
1448
1862
|
});
|
|
1449
1863
|
}
|
|
1450
1864
|
async function parseSessionFile(filePath, families) {
|
|
1451
1865
|
try {
|
|
1452
|
-
const content = await
|
|
1866
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
1453
1867
|
const lines = content.trim().split("\n");
|
|
1454
1868
|
let sessionMeta = null;
|
|
1455
1869
|
let currentModel = "gpt-5";
|
|
@@ -1568,28 +1982,66 @@ async function parseSessionFile(filePath, families) {
|
|
|
1568
1982
|
}
|
|
1569
1983
|
}
|
|
1570
1984
|
async function collectParsedCodexFiles(options = {}) {
|
|
1571
|
-
const cacheKey = getCodexParseCacheKey(options
|
|
1985
|
+
const cacheKey = getCodexParseCacheKey(options);
|
|
1572
1986
|
const cached = codexParseCache.get(cacheKey);
|
|
1573
1987
|
if (cached) {
|
|
1574
1988
|
return cached;
|
|
1575
1989
|
}
|
|
1576
1990
|
const loadPromise = (async () => {
|
|
1577
1991
|
const codexDir = getCodexDir();
|
|
1578
|
-
const sessionsDir =
|
|
1579
|
-
const archivedDir =
|
|
1992
|
+
const sessionsDir = join3(codexDir, "sessions");
|
|
1993
|
+
const archivedDir = join3(codexDir, "archived_sessions");
|
|
1580
1994
|
const [sessionFiles, archivedFiles] = await Promise.all([
|
|
1581
1995
|
findJsonlFiles(sessionsDir),
|
|
1582
1996
|
findJsonlFiles(archivedDir)
|
|
1583
1997
|
]);
|
|
1584
1998
|
const jsonlFiles = [...sessionFiles, ...archivedFiles];
|
|
1585
1999
|
if (jsonlFiles.length === 0) {
|
|
2000
|
+
await updateUsageCache(options, (cache) => {
|
|
2001
|
+
for (const [key, record] of Object.entries(cache.entries)) {
|
|
2002
|
+
if (record.kind === CODEX_JSONL_CACHE_KIND && (isPathWithin2(record.fingerprint.path, sessionsDir) || isPathWithin2(record.fingerprint.path, archivedDir))) {
|
|
2003
|
+
delete cache.entries[key];
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
1586
2007
|
return [];
|
|
1587
2008
|
}
|
|
2009
|
+
const persistentCache = await readUsageCacheForLookup(options);
|
|
2010
|
+
const retainedKeys = /* @__PURE__ */ new Set();
|
|
2011
|
+
const updates = [];
|
|
1588
2012
|
const parsedFiles = await mapWithConcurrency(
|
|
1589
2013
|
jsonlFiles,
|
|
1590
2014
|
FILE_PARSE_CONCURRENCY2,
|
|
1591
|
-
(file) =>
|
|
2015
|
+
async (file) => {
|
|
2016
|
+
const fingerprint = await getSourceFingerprint(file);
|
|
2017
|
+
if (!fingerprint) return null;
|
|
2018
|
+
const key = getPersistentCacheKey2(CODEX_JSONL_CACHE_KIND, file);
|
|
2019
|
+
retainedKeys.add(key);
|
|
2020
|
+
const cached2 = getCachedUsageValue(persistentCache, key, fingerprint);
|
|
2021
|
+
if (cached2) {
|
|
2022
|
+
const parsed2 = rehydrateParsedCodexFile(cached2);
|
|
2023
|
+
return filterParsedCodexFileByFamilies(parsed2, options.families);
|
|
2024
|
+
}
|
|
2025
|
+
const parsed = await parseSessionFile(file, options.families);
|
|
2026
|
+
if (parsed) {
|
|
2027
|
+
const fullParsed = options.families && options.families.length > 0 ? await parseSessionFile(file) : parsed;
|
|
2028
|
+
if (fullParsed) {
|
|
2029
|
+
updates.push({ key, fingerprint, value: toCachedParsedCodexFile(fullParsed) });
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return parsed;
|
|
2033
|
+
}
|
|
1592
2034
|
);
|
|
2035
|
+
await updateUsageCache(options, (cache) => {
|
|
2036
|
+
for (const [key, record] of Object.entries(cache.entries)) {
|
|
2037
|
+
if (record.kind === CODEX_JSONL_CACHE_KIND && (isPathWithin2(record.fingerprint.path, sessionsDir) || isPathWithin2(record.fingerprint.path, archivedDir)) && !retainedKeys.has(key)) {
|
|
2038
|
+
delete cache.entries[key];
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
for (const update of updates) {
|
|
2042
|
+
setCachedUsageValue(cache, update.key, CODEX_JSONL_CACHE_KIND, update.fingerprint, update.value);
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
1593
2045
|
return parsedFiles.filter((entry) => entry !== null);
|
|
1594
2046
|
})();
|
|
1595
2047
|
codexParseCache.set(cacheKey, loadPromise);
|
|
@@ -1690,8 +2142,8 @@ async function loadCodexStats(options = {}) {
|
|
|
1690
2142
|
function normalizePresentableModelName(modelName) {
|
|
1691
2143
|
return getModelDisplayName(modelName).trim();
|
|
1692
2144
|
}
|
|
1693
|
-
function normalizeEntryModels(
|
|
1694
|
-
return
|
|
2145
|
+
function normalizeEntryModels(entries) {
|
|
2146
|
+
return entries.map((entry) => ({
|
|
1695
2147
|
...entry,
|
|
1696
2148
|
model: normalizePresentableModelName(entry.model)
|
|
1697
2149
|
}));
|
|
@@ -1703,9 +2155,9 @@ function toLocalDateString3(isoTimestamp) {
|
|
|
1703
2155
|
const day = String(date.getDate()).padStart(2, "0");
|
|
1704
2156
|
return `${year}-${month}-${day}`;
|
|
1705
2157
|
}
|
|
1706
|
-
async function parseClaudeJsonl(projectFilter, families) {
|
|
1707
|
-
const
|
|
1708
|
-
return
|
|
2158
|
+
async function parseClaudeJsonl(projectFilter, families, cacheOptions = {}) {
|
|
2159
|
+
const entries = await collectAnthropicUsageEntries({ projectFilter, families, ...cacheOptions });
|
|
2160
|
+
return entries.map((entry) => ({
|
|
1709
2161
|
date: entry.date,
|
|
1710
2162
|
model: entry.model,
|
|
1711
2163
|
rawModel: entry.rawModel,
|
|
@@ -1723,9 +2175,9 @@ async function parseClaudeJsonl(projectFilter, families) {
|
|
|
1723
2175
|
timestamp: entry.timestamp
|
|
1724
2176
|
}));
|
|
1725
2177
|
}
|
|
1726
|
-
async function parseCodexJsonl(families) {
|
|
1727
|
-
const
|
|
1728
|
-
return
|
|
2178
|
+
async function parseCodexJsonl(families, cacheOptions = {}) {
|
|
2179
|
+
const entries = await collectCodexUsageEntries({ families, ...cacheOptions });
|
|
2180
|
+
return entries.map((entry) => ({
|
|
1729
2181
|
date: entry.date,
|
|
1730
2182
|
model: entry.model,
|
|
1731
2183
|
rawModel: entry.rawModel,
|
|
@@ -1751,10 +2203,10 @@ function getSourceLabel(sourceKey, source) {
|
|
|
1751
2203
|
if (key === "codex") return "Codex CLI";
|
|
1752
2204
|
return key;
|
|
1753
2205
|
}
|
|
1754
|
-
function computeSessionCounts(
|
|
2206
|
+
function computeSessionCounts(entries) {
|
|
1755
2207
|
const mainSessions = /* @__PURE__ */ new Set();
|
|
1756
2208
|
const subagentSessions = /* @__PURE__ */ new Set();
|
|
1757
|
-
for (const entry of
|
|
2209
|
+
for (const entry of entries) {
|
|
1758
2210
|
const sourceKey = entry.sourceKey || entry.source;
|
|
1759
2211
|
if (entry.sessionId) {
|
|
1760
2212
|
mainSessions.add(`${sourceKey}:${entry.sessionId}`);
|
|
@@ -1769,8 +2221,8 @@ function computeSessionCounts(entries2) {
|
|
|
1769
2221
|
total: mainSessions.size + subagentSessions.size
|
|
1770
2222
|
};
|
|
1771
2223
|
}
|
|
1772
|
-
function filterByDateRange(
|
|
1773
|
-
return
|
|
2224
|
+
function filterByDateRange(entries, since, until) {
|
|
2225
|
+
return entries.filter((e) => {
|
|
1774
2226
|
if (since && e.date < since) return false;
|
|
1775
2227
|
if (until && e.date > until) return false;
|
|
1776
2228
|
return true;
|
|
@@ -1797,9 +2249,9 @@ function sortModelsByTier(models) {
|
|
|
1797
2249
|
return priorityB - priorityA;
|
|
1798
2250
|
});
|
|
1799
2251
|
}
|
|
1800
|
-
function aggregateByDay(
|
|
2252
|
+
function aggregateByDay(entries) {
|
|
1801
2253
|
const dayMap = /* @__PURE__ */ new Map();
|
|
1802
|
-
for (const e of
|
|
2254
|
+
for (const e of entries) {
|
|
1803
2255
|
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1804
2256
|
const existing = dayMap.get(e.date);
|
|
1805
2257
|
if (existing) {
|
|
@@ -1828,9 +2280,9 @@ function aggregateByDay(entries2) {
|
|
|
1828
2280
|
models: sortModelsByTier(Array.from(modelsSet))
|
|
1829
2281
|
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1830
2282
|
}
|
|
1831
|
-
function aggregateByMonth(
|
|
2283
|
+
function aggregateByMonth(entries) {
|
|
1832
2284
|
const monthMap = /* @__PURE__ */ new Map();
|
|
1833
|
-
for (const e of
|
|
2285
|
+
for (const e of entries) {
|
|
1834
2286
|
const month = e.date.slice(0, 7);
|
|
1835
2287
|
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1836
2288
|
const existing = monthMap.get(month);
|
|
@@ -1860,9 +2312,9 @@ function aggregateByMonth(entries2) {
|
|
|
1860
2312
|
models: sortModelsByTier(Array.from(modelsSet))
|
|
1861
2313
|
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1862
2314
|
}
|
|
1863
|
-
function aggregateByModel(
|
|
2315
|
+
function aggregateByModel(entries) {
|
|
1864
2316
|
const modelMap = /* @__PURE__ */ new Map();
|
|
1865
|
-
for (const e of
|
|
2317
|
+
for (const e of entries) {
|
|
1866
2318
|
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1867
2319
|
if (entryTotal <= 0) continue;
|
|
1868
2320
|
const existing = modelMap.get(e.model);
|
|
@@ -1887,9 +2339,9 @@ function aggregateByModel(entries2) {
|
|
|
1887
2339
|
}
|
|
1888
2340
|
return Array.from(modelMap.values()).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1889
2341
|
}
|
|
1890
|
-
function aggregateBySession(
|
|
2342
|
+
function aggregateBySession(entries) {
|
|
1891
2343
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
1892
|
-
for (const e of
|
|
2344
|
+
for (const e of entries) {
|
|
1893
2345
|
const sourceKey = e.sourceKey || e.source;
|
|
1894
2346
|
const sid = e.sessionId ? `${sourceKey}:${e.sessionId}` : "unknown";
|
|
1895
2347
|
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
@@ -1921,9 +2373,9 @@ function aggregateBySession(entries2) {
|
|
|
1921
2373
|
}
|
|
1922
2374
|
return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
|
|
1923
2375
|
const date = data.firstTimestamp ? toLocalDateString3(data.firstTimestamp) : "unknown";
|
|
1924
|
-
const
|
|
2376
|
+
const shortId2 = data.displaySessionId.slice(0, 12);
|
|
1925
2377
|
return {
|
|
1926
|
-
key: `${date} ${
|
|
2378
|
+
key: `${date} ${shortId2}`,
|
|
1927
2379
|
inputTokens: data.inputTokens,
|
|
1928
2380
|
outputTokens: data.outputTokens,
|
|
1929
2381
|
cacheWriteTokens: data.cacheWriteTokens,
|
|
@@ -1947,9 +2399,9 @@ function computeTotals(rows) {
|
|
|
1947
2399
|
{ inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalTokens: 0, cost: 0 }
|
|
1948
2400
|
);
|
|
1949
2401
|
}
|
|
1950
|
-
function computeModelBreakdown(
|
|
2402
|
+
function computeModelBreakdown(entries) {
|
|
1951
2403
|
const modelMap = /* @__PURE__ */ new Map();
|
|
1952
|
-
for (const e of
|
|
2404
|
+
for (const e of entries) {
|
|
1953
2405
|
const total = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1954
2406
|
if (total <= 0) continue;
|
|
1955
2407
|
const existing = modelMap.get(e.model);
|
|
@@ -1968,10 +2420,10 @@ function computeModelBreakdown(entries2) {
|
|
|
1968
2420
|
percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
|
|
1969
2421
|
})).sort((a, b) => b.tokens - a.tokens);
|
|
1970
2422
|
}
|
|
1971
|
-
function computeSourceBreakdown(
|
|
2423
|
+
function computeSourceBreakdown(entries) {
|
|
1972
2424
|
const sourceMap = /* @__PURE__ */ new Map();
|
|
1973
2425
|
const sessionsBySource = /* @__PURE__ */ new Map();
|
|
1974
|
-
for (const entry of
|
|
2426
|
+
for (const entry of entries) {
|
|
1975
2427
|
const sourceKey = entry.sourceKey || entry.source;
|
|
1976
2428
|
const label = getSourceLabel(entry.sourceKey, entry.source);
|
|
1977
2429
|
const total = entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
|
|
@@ -2001,9 +2453,9 @@ function computeSourceBreakdown(entries2) {
|
|
|
2001
2453
|
percentage: totalTokens > 0 ? Math.round(source.tokens / totalTokens * 100) : 0
|
|
2002
2454
|
})).sort((a, b) => b.tokens - a.tokens);
|
|
2003
2455
|
}
|
|
2004
|
-
function computeActivityStats(
|
|
2456
|
+
function computeActivityStats(entries, source) {
|
|
2005
2457
|
const dayMap = /* @__PURE__ */ new Map();
|
|
2006
|
-
for (const entry of
|
|
2458
|
+
for (const entry of entries) {
|
|
2007
2459
|
const existing = dayMap.get(entry.date);
|
|
2008
2460
|
if (existing) {
|
|
2009
2461
|
existing.inputTokens += entry.inputTokens;
|
|
@@ -2026,7 +2478,7 @@ function computeActivityStats(entries2, source) {
|
|
|
2026
2478
|
}
|
|
2027
2479
|
}
|
|
2028
2480
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
2029
|
-
for (const entry of
|
|
2481
|
+
for (const entry of entries) {
|
|
2030
2482
|
if (!entry.sessionId) continue;
|
|
2031
2483
|
const sessions = sessionMap.get(entry.date) || /* @__PURE__ */ new Set();
|
|
2032
2484
|
sessions.add(`${entry.source}:${entry.sessionId}`);
|
|
@@ -2065,33 +2517,34 @@ function computeActivityStats(entries2, source) {
|
|
|
2065
2517
|
},
|
|
2066
2518
|
days,
|
|
2067
2519
|
totals,
|
|
2068
|
-
modelBreakdown: computeModelBreakdown(
|
|
2069
|
-
sessionCounts: computeSessionCounts(
|
|
2070
|
-
sourceBreakdown: computeSourceBreakdown(
|
|
2520
|
+
modelBreakdown: computeModelBreakdown(entries),
|
|
2521
|
+
sessionCounts: computeSessionCounts(entries),
|
|
2522
|
+
sourceBreakdown: computeSourceBreakdown(entries)
|
|
2071
2523
|
};
|
|
2072
2524
|
}
|
|
2073
2525
|
async function loadUsageStats(options) {
|
|
2074
2526
|
const { aggregation, since, until, codexOnly, combined, projectFilter, families } = options;
|
|
2075
|
-
|
|
2527
|
+
const cacheOptions = { useCache: options.useCache, refreshCache: options.refreshCache };
|
|
2528
|
+
let entries = [];
|
|
2076
2529
|
if (!codexOnly) {
|
|
2077
|
-
|
|
2530
|
+
entries = entries.concat(await parseClaudeJsonl(projectFilter, families, cacheOptions));
|
|
2078
2531
|
}
|
|
2079
2532
|
if (codexOnly || combined) {
|
|
2080
|
-
|
|
2533
|
+
entries = entries.concat(await parseCodexJsonl(families, cacheOptions));
|
|
2081
2534
|
}
|
|
2082
|
-
if (
|
|
2535
|
+
if (entries.length === 0) {
|
|
2083
2536
|
return null;
|
|
2084
2537
|
}
|
|
2085
|
-
|
|
2538
|
+
entries = normalizeEntryModels(entries);
|
|
2086
2539
|
if (families && families.length > 0) {
|
|
2087
|
-
|
|
2540
|
+
entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
|
|
2088
2541
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (
|
|
2542
|
+
entries = filterByDateRange(entries, since, until);
|
|
2543
|
+
entries = entries.filter((e) => !e.model.toLowerCase().includes("synthetic"));
|
|
2544
|
+
if (entries.length === 0) {
|
|
2092
2545
|
return null;
|
|
2093
2546
|
}
|
|
2094
|
-
const dates =
|
|
2547
|
+
const dates = entries.map((e) => e.date).sort();
|
|
2095
2548
|
const dateRange = {
|
|
2096
2549
|
start: dates[0],
|
|
2097
2550
|
end: dates[dates.length - 1]
|
|
@@ -2099,26 +2552,26 @@ async function loadUsageStats(options) {
|
|
|
2099
2552
|
let rows;
|
|
2100
2553
|
switch (aggregation) {
|
|
2101
2554
|
case "monthly":
|
|
2102
|
-
rows = aggregateByMonth(
|
|
2555
|
+
rows = aggregateByMonth(entries);
|
|
2103
2556
|
break;
|
|
2104
2557
|
case "model":
|
|
2105
|
-
rows = aggregateByModel(
|
|
2558
|
+
rows = aggregateByModel(entries);
|
|
2106
2559
|
break;
|
|
2107
2560
|
case "session":
|
|
2108
|
-
rows = aggregateBySession(
|
|
2561
|
+
rows = aggregateBySession(entries);
|
|
2109
2562
|
break;
|
|
2110
2563
|
case "total":
|
|
2111
2564
|
rows = [];
|
|
2112
2565
|
break;
|
|
2113
2566
|
case "daily":
|
|
2114
2567
|
default:
|
|
2115
|
-
rows = aggregateByDay(
|
|
2568
|
+
rows = aggregateByDay(entries);
|
|
2116
2569
|
break;
|
|
2117
2570
|
}
|
|
2118
|
-
const totals = aggregation === "total" ? computeTotals(aggregateByDay(
|
|
2119
|
-
const modelBreakdown = computeModelBreakdown(
|
|
2120
|
-
const sessionCounts = computeSessionCounts(
|
|
2121
|
-
const sourceBreakdown = computeSourceBreakdown(
|
|
2571
|
+
const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
|
|
2572
|
+
const modelBreakdown = computeModelBreakdown(entries);
|
|
2573
|
+
const sessionCounts = computeSessionCounts(entries);
|
|
2574
|
+
const sourceBreakdown = computeSourceBreakdown(entries);
|
|
2122
2575
|
let source = "claude";
|
|
2123
2576
|
if (codexOnly) source = "codex";
|
|
2124
2577
|
else if (combined) source = "combined";
|
|
@@ -2136,29 +2589,30 @@ async function loadUsageStats(options) {
|
|
|
2136
2589
|
}
|
|
2137
2590
|
async function loadActivityStats(options) {
|
|
2138
2591
|
const { since, until, codexOnly, combined, projectFilter, families } = options;
|
|
2139
|
-
|
|
2592
|
+
const cacheOptions = { useCache: options.useCache, refreshCache: options.refreshCache };
|
|
2593
|
+
let entries = [];
|
|
2140
2594
|
if (!codexOnly) {
|
|
2141
|
-
|
|
2595
|
+
entries = entries.concat(await parseClaudeJsonl(projectFilter, families, cacheOptions));
|
|
2142
2596
|
}
|
|
2143
2597
|
if (codexOnly || combined) {
|
|
2144
|
-
|
|
2598
|
+
entries = entries.concat(await parseCodexJsonl(families, cacheOptions));
|
|
2145
2599
|
}
|
|
2146
|
-
if (
|
|
2600
|
+
if (entries.length === 0) {
|
|
2147
2601
|
return null;
|
|
2148
2602
|
}
|
|
2149
|
-
|
|
2603
|
+
entries = normalizeEntryModels(entries);
|
|
2150
2604
|
if (families && families.length > 0) {
|
|
2151
|
-
|
|
2605
|
+
entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
|
|
2152
2606
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
if (
|
|
2607
|
+
entries = filterByDateRange(entries, since, until);
|
|
2608
|
+
entries = entries.filter((entry) => !entry.model.toLowerCase().includes("synthetic"));
|
|
2609
|
+
if (entries.length === 0) {
|
|
2156
2610
|
return null;
|
|
2157
2611
|
}
|
|
2158
2612
|
let source = "claude";
|
|
2159
2613
|
if (codexOnly) source = "codex";
|
|
2160
2614
|
else if (combined) source = "combined";
|
|
2161
|
-
const stats = computeActivityStats(
|
|
2615
|
+
const stats = computeActivityStats(entries, source);
|
|
2162
2616
|
stats.scopeLabel = formatModelFamilyLabel(families);
|
|
2163
2617
|
return stats;
|
|
2164
2618
|
}
|
|
@@ -2428,12 +2882,20 @@ async function loadData(options) {
|
|
|
2428
2882
|
let codex = null;
|
|
2429
2883
|
if (!codexOnly) {
|
|
2430
2884
|
if (await claudeCompatibleDataExists()) {
|
|
2431
|
-
claude = await loadClaudeStatsFromJsonl({
|
|
2885
|
+
claude = await loadClaudeStatsFromJsonl({
|
|
2886
|
+
families,
|
|
2887
|
+
useCache: options.useCache,
|
|
2888
|
+
refreshCache: options.refreshCache
|
|
2889
|
+
});
|
|
2432
2890
|
}
|
|
2433
2891
|
}
|
|
2434
2892
|
if (codexOnly || combined) {
|
|
2435
2893
|
if (await codexDataExists()) {
|
|
2436
|
-
codex = await loadCodexStats({
|
|
2894
|
+
codex = await loadCodexStats({
|
|
2895
|
+
families,
|
|
2896
|
+
useCache: options.useCache,
|
|
2897
|
+
refreshCache: options.refreshCache
|
|
2898
|
+
});
|
|
2437
2899
|
}
|
|
2438
2900
|
}
|
|
2439
2901
|
let source = "claude";
|
|
@@ -3052,15 +3514,15 @@ async function publishArtifact(artifact, baseUrl, legacyUrl) {
|
|
|
3052
3514
|
}
|
|
3053
3515
|
|
|
3054
3516
|
// src/claude-inspector.ts
|
|
3055
|
-
import { promises as
|
|
3056
|
-
import { homedir as
|
|
3057
|
-
import { join as
|
|
3517
|
+
import { promises as fs4 } from "fs";
|
|
3518
|
+
import { homedir as homedir4 } from "os";
|
|
3519
|
+
import { join as join4 } from "path";
|
|
3058
3520
|
function getClaudeDir2() {
|
|
3059
|
-
return process.env.CLAUDE_HOME ||
|
|
3521
|
+
return process.env.CLAUDE_HOME || join4(homedir4(), ".claude");
|
|
3060
3522
|
}
|
|
3061
3523
|
async function pathExists3(path) {
|
|
3062
3524
|
try {
|
|
3063
|
-
await
|
|
3525
|
+
await fs4.access(path);
|
|
3064
3526
|
return true;
|
|
3065
3527
|
} catch {
|
|
3066
3528
|
return false;
|
|
@@ -3068,7 +3530,7 @@ async function pathExists3(path) {
|
|
|
3068
3530
|
}
|
|
3069
3531
|
async function readJsonFile(path) {
|
|
3070
3532
|
try {
|
|
3071
|
-
const content = await
|
|
3533
|
+
const content = await fs4.readFile(path, "utf-8");
|
|
3072
3534
|
return JSON.parse(content);
|
|
3073
3535
|
} catch {
|
|
3074
3536
|
return null;
|
|
@@ -3076,17 +3538,17 @@ async function readJsonFile(path) {
|
|
|
3076
3538
|
}
|
|
3077
3539
|
async function walkFiles(dir, visitor) {
|
|
3078
3540
|
if (!await pathExists3(dir)) return;
|
|
3079
|
-
let
|
|
3541
|
+
let entries;
|
|
3080
3542
|
try {
|
|
3081
|
-
|
|
3543
|
+
entries = await fs4.readdir(dir);
|
|
3082
3544
|
} catch {
|
|
3083
3545
|
return;
|
|
3084
3546
|
}
|
|
3085
|
-
for (const entry of
|
|
3086
|
-
const fullPath =
|
|
3547
|
+
for (const entry of entries) {
|
|
3548
|
+
const fullPath = join4(dir, entry);
|
|
3087
3549
|
let stat;
|
|
3088
3550
|
try {
|
|
3089
|
-
stat = await
|
|
3551
|
+
stat = await fs4.stat(fullPath);
|
|
3090
3552
|
} catch {
|
|
3091
3553
|
continue;
|
|
3092
3554
|
}
|
|
@@ -3104,8 +3566,8 @@ function daysSince(dateStr) {
|
|
|
3104
3566
|
const diffMs = Date.now() - parsed.getTime();
|
|
3105
3567
|
return Math.max(0, Math.floor(diffMs / (24 * 60 * 60 * 1e3)));
|
|
3106
3568
|
}
|
|
3107
|
-
async function inspectClaudeSystem(home =
|
|
3108
|
-
const path =
|
|
3569
|
+
async function inspectClaudeSystem(home = homedir4()) {
|
|
3570
|
+
const path = join4(home, ".claude.json");
|
|
3109
3571
|
const json = await readJsonFile(path);
|
|
3110
3572
|
return {
|
|
3111
3573
|
path,
|
|
@@ -3120,18 +3582,18 @@ async function inspectClaudeSystem(home = homedir3()) {
|
|
|
3120
3582
|
};
|
|
3121
3583
|
}
|
|
3122
3584
|
async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
|
|
3123
|
-
const statsCachePath =
|
|
3585
|
+
const statsCachePath = join4(claudeDir, "stats-cache.json");
|
|
3124
3586
|
const statsCache = await readJsonFile(statsCachePath);
|
|
3125
|
-
const facetsDir =
|
|
3126
|
-
const sessionMetaDir =
|
|
3127
|
-
const projectsDir =
|
|
3128
|
-
const facetFiles = await pathExists3(facetsDir) ? (await
|
|
3129
|
-
const sessionMetaFiles = await pathExists3(sessionMetaDir) ? (await
|
|
3587
|
+
const facetsDir = join4(claudeDir, "usage-data", "facets");
|
|
3588
|
+
const sessionMetaDir = join4(claudeDir, "usage-data", "session-meta");
|
|
3589
|
+
const projectsDir = join4(claudeDir, "projects");
|
|
3590
|
+
const facetFiles = await pathExists3(facetsDir) ? (await fs4.readdir(facetsDir)).filter((file) => file.endsWith(".json")) : [];
|
|
3591
|
+
const sessionMetaFiles = await pathExists3(sessionMetaDir) ? (await fs4.readdir(sessionMetaDir)).filter((file) => file.endsWith(".json")) : [];
|
|
3130
3592
|
let rateLimitHit = 0;
|
|
3131
3593
|
let apiTokenLimit = 0;
|
|
3132
3594
|
let contextLimit = 0;
|
|
3133
3595
|
for (const file of facetFiles) {
|
|
3134
|
-
const json = await readJsonFile(
|
|
3596
|
+
const json = await readJsonFile(join4(facetsDir, file));
|
|
3135
3597
|
const friction = json?.friction_counts || {};
|
|
3136
3598
|
rateLimitHit += friction.rate_limit_hit || 0;
|
|
3137
3599
|
apiTokenLimit += friction.api_token_limit || 0;
|
|
@@ -3141,7 +3603,7 @@ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
|
|
|
3141
3603
|
let inputTokens = 0;
|
|
3142
3604
|
let outputTokens = 0;
|
|
3143
3605
|
for (const file of sessionMetaFiles) {
|
|
3144
|
-
const json = await readJsonFile(
|
|
3606
|
+
const json = await readJsonFile(join4(sessionMetaDir, file));
|
|
3145
3607
|
if (!json) continue;
|
|
3146
3608
|
if (json.uses_task_agent) sessionsUsingTaskAgent += 1;
|
|
3147
3609
|
inputTokens += json.input_tokens || 0;
|
|
@@ -3204,6 +3666,359 @@ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
|
|
|
3204
3666
|
};
|
|
3205
3667
|
}
|
|
3206
3668
|
|
|
3669
|
+
// src/shared/args.ts
|
|
3670
|
+
var modelFamilyArgs = {
|
|
3671
|
+
claude: {
|
|
3672
|
+
type: "boolean",
|
|
3673
|
+
description: "Show only Claude family stats across local usage sources",
|
|
3674
|
+
default: false
|
|
3675
|
+
},
|
|
3676
|
+
kimi: {
|
|
3677
|
+
type: "boolean",
|
|
3678
|
+
description: "Show only Kimi family stats across local usage sources",
|
|
3679
|
+
default: false
|
|
3680
|
+
},
|
|
3681
|
+
minimax: {
|
|
3682
|
+
type: "boolean",
|
|
3683
|
+
description: "Show only MiniMax family stats across local usage sources",
|
|
3684
|
+
default: false
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
function getSelectedModelFamilies(args) {
|
|
3688
|
+
const families = [];
|
|
3689
|
+
if (args.claude === true) families.push("claude");
|
|
3690
|
+
if (args.kimi === true) families.push("kimi");
|
|
3691
|
+
if (args.minimax === true) families.push("minimax");
|
|
3692
|
+
return families;
|
|
3693
|
+
}
|
|
3694
|
+
var cacheArgs = {
|
|
3695
|
+
"no-cache": {
|
|
3696
|
+
type: "boolean",
|
|
3697
|
+
description: "Bypass the persistent local usage cache for this run",
|
|
3698
|
+
default: false
|
|
3699
|
+
},
|
|
3700
|
+
"refresh-cache": {
|
|
3701
|
+
type: "boolean",
|
|
3702
|
+
description: "Rebuild the persistent local usage cache before showing stats",
|
|
3703
|
+
default: false
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
function getUsageCacheOptions(args) {
|
|
3707
|
+
return {
|
|
3708
|
+
useCache: args["no-cache"] !== true,
|
|
3709
|
+
refreshCache: args["refresh-cache"] === true
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// src/dashboard/assets-path.ts
|
|
3714
|
+
import { fileURLToPath } from "url";
|
|
3715
|
+
import { dirname as dirname2, resolve, join as join5 } from "path";
|
|
3716
|
+
import { existsSync } from "fs";
|
|
3717
|
+
function resolveDashboardAssets(importMetaUrl) {
|
|
3718
|
+
const here = dirname2(fileURLToPath(importMetaUrl));
|
|
3719
|
+
const candidates = [
|
|
3720
|
+
// Published tarball (tsup single-file bundle): here === .../dist, assets at .../dist/web
|
|
3721
|
+
resolve(here, "web"),
|
|
3722
|
+
// Same tarball layout but caller is nested one deeper (e.g. dist/commands/...)
|
|
3723
|
+
resolve(here, "..", "web"),
|
|
3724
|
+
// Monorepo dev with tsx from packages/cli/src/dashboard/...
|
|
3725
|
+
resolve(here, "..", "..", "..", "web", ".next-static"),
|
|
3726
|
+
// Monorepo dev with tsx from packages/cli/src/commands/...
|
|
3727
|
+
resolve(here, "..", "..", "web", ".next-static")
|
|
3728
|
+
];
|
|
3729
|
+
for (const candidate of candidates) {
|
|
3730
|
+
if (existsSync(join5(candidate, "index.html"))) return candidate;
|
|
3731
|
+
}
|
|
3732
|
+
throw new Error(
|
|
3733
|
+
`dashboard assets not found. Tried:
|
|
3734
|
+
` + candidates.map((c) => ` - ${c}`).join("\n") + `
|
|
3735
|
+
Run \`pnpm --filter web build:static\` first.`
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
// src/dashboard/payloads.ts
|
|
3740
|
+
import { randomBytes } from "crypto";
|
|
3741
|
+
var MAX_INLINE_BYTES = 6e3;
|
|
3742
|
+
var STUB_BASE = "http://local";
|
|
3743
|
+
function extractEncodedQuery(urlWithQuery) {
|
|
3744
|
+
const url = new URL(urlWithQuery);
|
|
3745
|
+
const query = url.search.replace(/^\?/, "");
|
|
3746
|
+
return query || null;
|
|
3747
|
+
}
|
|
3748
|
+
function shortId() {
|
|
3749
|
+
return randomBytes(6).toString("hex");
|
|
3750
|
+
}
|
|
3751
|
+
function inlineOrFallback(paramKey, payload, inlineQuery, out) {
|
|
3752
|
+
if (!payload || !inlineQuery) return;
|
|
3753
|
+
if (Buffer.byteLength(inlineQuery, "utf-8") <= MAX_INLINE_BYTES) {
|
|
3754
|
+
out.hubParams.set(paramKey, inlineQuery);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const id = shortId();
|
|
3758
|
+
out.jsonStore.set(id, JSON.stringify(payload));
|
|
3759
|
+
out.hubParams.set(paramKey, `json:${id}`);
|
|
3760
|
+
}
|
|
3761
|
+
function buildDashboardPayloads(input) {
|
|
3762
|
+
const out = {
|
|
3763
|
+
hubParams: new URLSearchParams(),
|
|
3764
|
+
jsonStore: /* @__PURE__ */ new Map()
|
|
3765
|
+
};
|
|
3766
|
+
if (input.usage) {
|
|
3767
|
+
const full = encodeUsageToUrl(input.usage, STUB_BASE);
|
|
3768
|
+
inlineOrFallback("u", input.usage, extractEncodedQuery(full), out);
|
|
3769
|
+
}
|
|
3770
|
+
if (input.wrapped) {
|
|
3771
|
+
const full = encodeStatsToUrl(input.wrapped, STUB_BASE);
|
|
3772
|
+
inlineOrFallback("w", input.wrapped, extractEncodedQuery(full), out);
|
|
3773
|
+
}
|
|
3774
|
+
if (input.activity) {
|
|
3775
|
+
const full = encodeActivityToUrl(input.activity, STUB_BASE);
|
|
3776
|
+
inlineOrFallback("a", input.activity, extractEncodedQuery(full), out);
|
|
3777
|
+
}
|
|
3778
|
+
return out;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// src/dashboard/open-browser.ts
|
|
3782
|
+
import { spawn } from "child_process";
|
|
3783
|
+
import { platform } from "os";
|
|
3784
|
+
function openInBrowser(url) {
|
|
3785
|
+
const os = platform();
|
|
3786
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
|
|
3787
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
3788
|
+
try {
|
|
3789
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
3790
|
+
child.on("error", () => {
|
|
3791
|
+
});
|
|
3792
|
+
child.unref();
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
// src/dashboard/server.ts
|
|
3798
|
+
import { createServer as createServer2 } from "http";
|
|
3799
|
+
import { readFile } from "fs/promises";
|
|
3800
|
+
import { existsSync as existsSync2, realpathSync, statSync } from "fs";
|
|
3801
|
+
import { extname, join as join6, normalize, resolve as resolve2, sep } from "path";
|
|
3802
|
+
|
|
3803
|
+
// src/dashboard/port.ts
|
|
3804
|
+
import { createServer, createConnection } from "net";
|
|
3805
|
+
async function findFreePort(start, end) {
|
|
3806
|
+
for (let port = start; port <= end; port++) {
|
|
3807
|
+
if (await isPortFree(port)) return port;
|
|
3808
|
+
}
|
|
3809
|
+
throw new Error(`no free port available in range ${start}-${end}`);
|
|
3810
|
+
}
|
|
3811
|
+
function isPortFree(port) {
|
|
3812
|
+
return isConnectable(port).then((connectable) => {
|
|
3813
|
+
if (connectable) return false;
|
|
3814
|
+
return isBindable(port);
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
function isConnectable(port) {
|
|
3818
|
+
return new Promise((resolve3) => {
|
|
3819
|
+
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
3820
|
+
const done = (result) => {
|
|
3821
|
+
socket.removeAllListeners();
|
|
3822
|
+
socket.destroy();
|
|
3823
|
+
resolve3(result);
|
|
3824
|
+
};
|
|
3825
|
+
socket.once("connect", () => done(true));
|
|
3826
|
+
socket.once("error", () => done(false));
|
|
3827
|
+
const timer = setTimeout(() => done(false), 250);
|
|
3828
|
+
timer.unref();
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
function isBindable(port) {
|
|
3832
|
+
return new Promise((resolve3) => {
|
|
3833
|
+
const server = createServer();
|
|
3834
|
+
const onError = () => {
|
|
3835
|
+
server.removeListener("listening", onListening);
|
|
3836
|
+
resolve3(false);
|
|
3837
|
+
};
|
|
3838
|
+
const onListening = () => {
|
|
3839
|
+
server.removeListener("error", onError);
|
|
3840
|
+
server.close(() => {
|
|
3841
|
+
resolve3(true);
|
|
3842
|
+
});
|
|
3843
|
+
};
|
|
3844
|
+
server.on("error", onError);
|
|
3845
|
+
server.on("listening", onListening);
|
|
3846
|
+
server.listen(port, "127.0.0.1");
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// src/dashboard/server.ts
|
|
3851
|
+
var MIME = {
|
|
3852
|
+
".html": "text/html; charset=utf-8",
|
|
3853
|
+
".js": "application/javascript; charset=utf-8",
|
|
3854
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
3855
|
+
".css": "text/css; charset=utf-8",
|
|
3856
|
+
".json": "application/json; charset=utf-8",
|
|
3857
|
+
".svg": "image/svg+xml",
|
|
3858
|
+
".png": "image/png",
|
|
3859
|
+
".jpg": "image/jpeg",
|
|
3860
|
+
".jpeg": "image/jpeg",
|
|
3861
|
+
".webp": "image/webp",
|
|
3862
|
+
".ico": "image/x-icon",
|
|
3863
|
+
".woff": "font/woff",
|
|
3864
|
+
".woff2": "font/woff2",
|
|
3865
|
+
".txt": "text/plain; charset=utf-8",
|
|
3866
|
+
".map": "application/json; charset=utf-8"
|
|
3867
|
+
};
|
|
3868
|
+
async function startDashboardServer(options) {
|
|
3869
|
+
const root = realpathSync(resolve2(options.assetsDir));
|
|
3870
|
+
const preferred = options.preferredPort ?? 8080;
|
|
3871
|
+
const port = await findFreePort(preferred, preferred + 20);
|
|
3872
|
+
const server = createServer2(async (req, res) => {
|
|
3873
|
+
try {
|
|
3874
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
3875
|
+
let pathname = url.pathname;
|
|
3876
|
+
const dataMatch = /^\/data\/([A-Za-z0-9_-]+)\.json$/.exec(pathname);
|
|
3877
|
+
if (dataMatch) {
|
|
3878
|
+
const body2 = options.jsonStore.get(dataMatch[1]);
|
|
3879
|
+
if (!body2) {
|
|
3880
|
+
res.statusCode = 404;
|
|
3881
|
+
res.end("not found");
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3885
|
+
res.setHeader("Cache-Control", "no-store");
|
|
3886
|
+
res.end(body2);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
if (pathname === "/") pathname = "/index.html";
|
|
3890
|
+
if (pathname !== "/index.html" && pathname.endsWith("/")) {
|
|
3891
|
+
pathname = pathname.slice(0, -1);
|
|
3892
|
+
}
|
|
3893
|
+
const safePath = normalize(join6(root, pathname));
|
|
3894
|
+
if (!safePath.startsWith(root + sep) && safePath !== root) {
|
|
3895
|
+
res.statusCode = 400;
|
|
3896
|
+
res.end("bad request");
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
let filePath = safePath;
|
|
3900
|
+
const sibling = filePath + ".html";
|
|
3901
|
+
if (existsSync2(filePath) && statSync(filePath).isFile()) {
|
|
3902
|
+
} else if (existsSync2(sibling) && statSync(sibling).isFile()) {
|
|
3903
|
+
filePath = sibling;
|
|
3904
|
+
} else if (existsSync2(filePath) && statSync(filePath).isDirectory()) {
|
|
3905
|
+
filePath = join6(filePath, "index.html");
|
|
3906
|
+
}
|
|
3907
|
+
let realFilePath;
|
|
3908
|
+
try {
|
|
3909
|
+
realFilePath = realpathSync(filePath);
|
|
3910
|
+
} catch {
|
|
3911
|
+
res.statusCode = 404;
|
|
3912
|
+
res.end("not found");
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
if (!realFilePath.startsWith(root + sep) && realFilePath !== root) {
|
|
3916
|
+
res.statusCode = 400;
|
|
3917
|
+
res.end("bad request");
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
if (!existsSync2(realFilePath) || !statSync(realFilePath).isFile()) {
|
|
3921
|
+
res.statusCode = 404;
|
|
3922
|
+
res.end("not found");
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
const ext = extname(realFilePath).toLowerCase();
|
|
3926
|
+
const mime = MIME[ext] || "application/octet-stream";
|
|
3927
|
+
const body = await readFile(realFilePath);
|
|
3928
|
+
if (pathname.startsWith("/_next/static/")) {
|
|
3929
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
3930
|
+
} else {
|
|
3931
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
3932
|
+
}
|
|
3933
|
+
res.setHeader("Content-Type", mime);
|
|
3934
|
+
res.setHeader("Content-Length", String(body.length));
|
|
3935
|
+
res.end(body);
|
|
3936
|
+
} catch (err) {
|
|
3937
|
+
console.error("[dashboard] server error:", err);
|
|
3938
|
+
res.statusCode = 500;
|
|
3939
|
+
res.end("internal error");
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
await new Promise((resolveFn, rejectFn) => {
|
|
3943
|
+
const onError = (err) => rejectFn(err);
|
|
3944
|
+
server.once("error", onError);
|
|
3945
|
+
server.listen(port, "127.0.0.1", () => {
|
|
3946
|
+
server.removeListener("error", onError);
|
|
3947
|
+
resolveFn();
|
|
3948
|
+
});
|
|
3949
|
+
});
|
|
3950
|
+
return {
|
|
3951
|
+
server,
|
|
3952
|
+
port,
|
|
3953
|
+
async close() {
|
|
3954
|
+
await Promise.race([
|
|
3955
|
+
new Promise(
|
|
3956
|
+
(resolveFn, rejectFn) => server.close((err) => err ? rejectFn(err) : resolveFn())
|
|
3957
|
+
),
|
|
3958
|
+
new Promise((resolveFn) => {
|
|
3959
|
+
const t = setTimeout(resolveFn, 2e3);
|
|
3960
|
+
t.unref();
|
|
3961
|
+
})
|
|
3962
|
+
]);
|
|
3963
|
+
}
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
// src/commands/dashboard.ts
|
|
3968
|
+
async function runDashboard(args, config) {
|
|
3969
|
+
void config;
|
|
3970
|
+
const families = getSelectedModelFamilies(args);
|
|
3971
|
+
const scopeLabel = formatModelFamilyLabel(families);
|
|
3972
|
+
const cacheOptions = getUsageCacheOptions(args);
|
|
3973
|
+
const codexOnly = args.codex;
|
|
3974
|
+
const combined = args.combined;
|
|
3975
|
+
const [usage, activity, data] = await Promise.all([
|
|
3976
|
+
loadUsageStats({
|
|
3977
|
+
aggregation: "daily",
|
|
3978
|
+
codexOnly,
|
|
3979
|
+
combined,
|
|
3980
|
+
families,
|
|
3981
|
+
scopeLabel,
|
|
3982
|
+
...cacheOptions
|
|
3983
|
+
}),
|
|
3984
|
+
loadActivityStats({ codexOnly, combined, families, scopeLabel, ...cacheOptions }),
|
|
3985
|
+
loadData({ codexOnly, combined, families, scopeLabel, ...cacheOptions })
|
|
3986
|
+
]);
|
|
3987
|
+
validateData(data, { codexOnly, combined, families, scopeLabel, ...cacheOptions });
|
|
3988
|
+
let wrapped = null;
|
|
3989
|
+
if (combined) {
|
|
3990
|
+
const c = data.claude ? computeWrappedStats(data.claude) : null;
|
|
3991
|
+
const x = data.codex ? computeCodexWrappedStats(data.codex) : null;
|
|
3992
|
+
wrapped = combineWrappedStats(c, x);
|
|
3993
|
+
} else if (codexOnly) {
|
|
3994
|
+
wrapped = data.codex ? computeCodexWrappedStats(data.codex) : null;
|
|
3995
|
+
} else {
|
|
3996
|
+
wrapped = data.claude ? computeWrappedStats(data.claude) : null;
|
|
3997
|
+
}
|
|
3998
|
+
const activityPayload = activity ? buildActivityArtifact(activity, "tokens", 365).payload : null;
|
|
3999
|
+
const { hubParams, jsonStore } = buildDashboardPayloads({
|
|
4000
|
+
usage: usage ?? null,
|
|
4001
|
+
wrapped,
|
|
4002
|
+
activity: activityPayload
|
|
4003
|
+
});
|
|
4004
|
+
const assetsDir = resolveDashboardAssets(import.meta.url);
|
|
4005
|
+
const server = await startDashboardServer({ assetsDir, jsonStore });
|
|
4006
|
+
const url = `http://127.0.0.1:${server.port}/dashboard/?${hubParams.toString()}`;
|
|
4007
|
+
const orange = "\x1B[38;5;208m";
|
|
4008
|
+
const reset = "\x1B[0m";
|
|
4009
|
+
console.log(`${orange}vibestats dashboard${reset} \u2192 ${url}`);
|
|
4010
|
+
console.log("Press Ctrl+C to stop.");
|
|
4011
|
+
openInBrowser(url);
|
|
4012
|
+
await new Promise((resolveFn) => {
|
|
4013
|
+
const shutdown = async () => {
|
|
4014
|
+
await server.close();
|
|
4015
|
+
resolveFn();
|
|
4016
|
+
};
|
|
4017
|
+
process.once("SIGINT", shutdown);
|
|
4018
|
+
process.once("SIGTERM", shutdown);
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3207
4022
|
// src/cli-intent.ts
|
|
3208
4023
|
var COMMANDS = /* @__PURE__ */ new Set(["usage", "limits", "limit", "pace"]);
|
|
3209
4024
|
var SCOPES = /* @__PURE__ */ new Set(["claude", "codex", "all", "combined"]);
|
|
@@ -3216,7 +4031,9 @@ var USAGE_COMMAND_FLAGS = /* @__PURE__ */ new Set([
|
|
|
3216
4031
|
"metric",
|
|
3217
4032
|
"model",
|
|
3218
4033
|
"monthly",
|
|
4034
|
+
"no-cache",
|
|
3219
4035
|
"project",
|
|
4036
|
+
"refresh-cache",
|
|
3220
4037
|
"sessions",
|
|
3221
4038
|
"share",
|
|
3222
4039
|
"since",
|
|
@@ -3241,9 +4058,11 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
3241
4058
|
"minimax",
|
|
3242
4059
|
"model",
|
|
3243
4060
|
"monthly",
|
|
4061
|
+
"no-cache",
|
|
3244
4062
|
"no-short",
|
|
3245
4063
|
"project",
|
|
3246
4064
|
"quiet",
|
|
4065
|
+
"refresh-cache",
|
|
3247
4066
|
"sessions",
|
|
3248
4067
|
"share",
|
|
3249
4068
|
"total",
|
|
@@ -3390,8 +4209,8 @@ function applyCliIntent(args, intent) {
|
|
|
3390
4209
|
// src/limits/claude.ts
|
|
3391
4210
|
import { execFile as execFile2 } from "child_process";
|
|
3392
4211
|
import { randomUUID } from "crypto";
|
|
3393
|
-
import { dirname } from "path";
|
|
3394
|
-
import { fileURLToPath } from "url";
|
|
4212
|
+
import { dirname as dirname3 } from "path";
|
|
4213
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3395
4214
|
|
|
3396
4215
|
// src/limits/pace.ts
|
|
3397
4216
|
var PACE_EPSILON_PERCENT = 1;
|
|
@@ -3741,7 +4560,7 @@ function isClaudePromptReady(text) {
|
|
|
3741
4560
|
return /Claude Code v\d+\.\d+\.\d+/.test(normalized) && (normalized.includes("\u276F") || normalized.includes(">"));
|
|
3742
4561
|
}
|
|
3743
4562
|
function execFileCommand(command, args, options = {}) {
|
|
3744
|
-
return new Promise((
|
|
4563
|
+
return new Promise((resolve3, reject) => {
|
|
3745
4564
|
execFile2(
|
|
3746
4565
|
command,
|
|
3747
4566
|
[...args],
|
|
@@ -3755,18 +4574,18 @@ function execFileCommand(command, args, options = {}) {
|
|
|
3755
4574
|
reject(error);
|
|
3756
4575
|
return;
|
|
3757
4576
|
}
|
|
3758
|
-
|
|
4577
|
+
resolve3({ stdout, stderr });
|
|
3759
4578
|
}
|
|
3760
4579
|
);
|
|
3761
4580
|
});
|
|
3762
4581
|
}
|
|
3763
|
-
function
|
|
3764
|
-
return new Promise((
|
|
4582
|
+
function sleep2(ms) {
|
|
4583
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3765
4584
|
}
|
|
3766
4585
|
function resolveDefaultClaudeUsageCwd() {
|
|
3767
4586
|
const envCwd = process.env.VIBESTATS_CLAUDE_USAGE_CWD ?? process.env.VIBESTATS_USAGE_CWD;
|
|
3768
4587
|
if (envCwd?.trim()) return envCwd;
|
|
3769
|
-
return
|
|
4588
|
+
return dirname3(fileURLToPath2(import.meta.url));
|
|
3770
4589
|
}
|
|
3771
4590
|
async function captureUsageWithTmux(options) {
|
|
3772
4591
|
const sessionName = `${TMUX_SESSION_PREFIX}-${randomUUID().replaceAll("-", "").slice(0, 16)}`;
|
|
@@ -3834,7 +4653,7 @@ async function fetchClaudeLimits(options = {}) {
|
|
|
3834
4653
|
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
3835
4654
|
now,
|
|
3836
4655
|
runCommand: options.runCommand ?? execFileCommand,
|
|
3837
|
-
sleepFn: options.sleepFn ??
|
|
4656
|
+
sleepFn: options.sleepFn ?? sleep2
|
|
3838
4657
|
});
|
|
3839
4658
|
} catch (error) {
|
|
3840
4659
|
return {
|
|
@@ -3863,7 +4682,7 @@ async function fetchClaudeLimits(options = {}) {
|
|
|
3863
4682
|
}
|
|
3864
4683
|
|
|
3865
4684
|
// src/limits/codex.ts
|
|
3866
|
-
import { spawn } from "child_process";
|
|
4685
|
+
import { spawn as spawn2 } from "child_process";
|
|
3867
4686
|
function clampPercent3(value) {
|
|
3868
4687
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
3869
4688
|
if (!Number.isFinite(numeric)) return 0;
|
|
@@ -3996,8 +4815,8 @@ var JsonRpcClient = class {
|
|
|
3996
4815
|
method,
|
|
3997
4816
|
...params === void 0 ? {} : { params }
|
|
3998
4817
|
};
|
|
3999
|
-
const promise = new Promise((
|
|
4000
|
-
this.pending.set(id, { resolve, reject });
|
|
4818
|
+
const promise = new Promise((resolve3, reject) => {
|
|
4819
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
4001
4820
|
});
|
|
4002
4821
|
try {
|
|
4003
4822
|
this.child.stdin.write(`${JSON.stringify(message)}
|
|
@@ -4077,7 +4896,7 @@ function scriptArgsForCodexStatus(command) {
|
|
|
4077
4896
|
async function fetchCodexStatusFallback(options = {}) {
|
|
4078
4897
|
const command = options.command || "codex";
|
|
4079
4898
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
4080
|
-
const child =
|
|
4899
|
+
const child = spawn2("script", scriptArgsForCodexStatus(command), {
|
|
4081
4900
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4082
4901
|
env: process.env
|
|
4083
4902
|
});
|
|
@@ -4090,19 +4909,19 @@ async function fetchCodexStatusFallback(options = {}) {
|
|
|
4090
4909
|
child.stderr.on("data", (chunk) => {
|
|
4091
4910
|
output += chunk;
|
|
4092
4911
|
});
|
|
4093
|
-
await new Promise((
|
|
4912
|
+
await new Promise((resolve3) => {
|
|
4094
4913
|
const timer = setTimeout(() => {
|
|
4095
4914
|
child.kill("SIGTERM");
|
|
4096
|
-
|
|
4915
|
+
resolve3();
|
|
4097
4916
|
}, timeoutMs);
|
|
4098
4917
|
timer.unref();
|
|
4099
4918
|
child.on("error", () => {
|
|
4100
4919
|
clearTimeout(timer);
|
|
4101
|
-
|
|
4920
|
+
resolve3();
|
|
4102
4921
|
});
|
|
4103
4922
|
child.on("exit", () => {
|
|
4104
4923
|
clearTimeout(timer);
|
|
4105
|
-
|
|
4924
|
+
resolve3();
|
|
4106
4925
|
});
|
|
4107
4926
|
});
|
|
4108
4927
|
const windows = parseCodexStatusText(output);
|
|
@@ -4125,7 +4944,7 @@ async function fetchCodexStatusFallback(options = {}) {
|
|
|
4125
4944
|
async function fetchCodexLimits(options = {}) {
|
|
4126
4945
|
const command = options.command || "codex";
|
|
4127
4946
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
4128
|
-
const child =
|
|
4947
|
+
const child = spawn2(command, ["-s", "read-only", "-a", "untrusted", "app-server"], {
|
|
4129
4948
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4130
4949
|
env: process.env
|
|
4131
4950
|
});
|
|
@@ -4290,10 +5109,10 @@ function displayUsageLimits(limits, options = {}) {
|
|
|
4290
5109
|
}
|
|
4291
5110
|
|
|
4292
5111
|
// src/config.ts
|
|
4293
|
-
import { readFileSync, existsSync, writeFileSync } from "fs";
|
|
4294
|
-
import { homedir as
|
|
4295
|
-
import { join as
|
|
4296
|
-
var CONFIG_PATH =
|
|
5112
|
+
import { readFileSync, existsSync as existsSync3, writeFileSync } from "fs";
|
|
5113
|
+
import { homedir as homedir5 } from "os";
|
|
5114
|
+
import { join as join7 } from "path";
|
|
5115
|
+
var CONFIG_PATH = join7(homedir5(), ".vibestats.json");
|
|
4297
5116
|
var DEFAULT_CONFIG = {
|
|
4298
5117
|
baseUrl: "https://vibestats.wolfai.dev",
|
|
4299
5118
|
outputFormat: "normal",
|
|
@@ -4303,7 +5122,7 @@ var DEFAULT_CONFIG = {
|
|
|
4303
5122
|
hideCost: false
|
|
4304
5123
|
};
|
|
4305
5124
|
function loadConfig() {
|
|
4306
|
-
if (!
|
|
5125
|
+
if (!existsSync3(CONFIG_PATH)) {
|
|
4307
5126
|
return DEFAULT_CONFIG;
|
|
4308
5127
|
}
|
|
4309
5128
|
try {
|
|
@@ -4326,7 +5145,7 @@ function mergeConfig(defaults, user) {
|
|
|
4326
5145
|
};
|
|
4327
5146
|
}
|
|
4328
5147
|
function initConfig() {
|
|
4329
|
-
if (
|
|
5148
|
+
if (existsSync3(CONFIG_PATH)) {
|
|
4330
5149
|
console.log(`Config file already exists at ${CONFIG_PATH}`);
|
|
4331
5150
|
return;
|
|
4332
5151
|
}
|
|
@@ -4356,32 +5175,6 @@ function resolveOptions(cliArgs, config) {
|
|
|
4356
5175
|
};
|
|
4357
5176
|
}
|
|
4358
5177
|
|
|
4359
|
-
// src/shared/args.ts
|
|
4360
|
-
var modelFamilyArgs = {
|
|
4361
|
-
claude: {
|
|
4362
|
-
type: "boolean",
|
|
4363
|
-
description: "Show only Claude family stats across local usage sources",
|
|
4364
|
-
default: false
|
|
4365
|
-
},
|
|
4366
|
-
kimi: {
|
|
4367
|
-
type: "boolean",
|
|
4368
|
-
description: "Show only Kimi family stats across local usage sources",
|
|
4369
|
-
default: false
|
|
4370
|
-
},
|
|
4371
|
-
minimax: {
|
|
4372
|
-
type: "boolean",
|
|
4373
|
-
description: "Show only MiniMax family stats across local usage sources",
|
|
4374
|
-
default: false
|
|
4375
|
-
}
|
|
4376
|
-
};
|
|
4377
|
-
function getSelectedModelFamilies(args) {
|
|
4378
|
-
const families = [];
|
|
4379
|
-
if (args.claude === true) families.push("claude");
|
|
4380
|
-
if (args.kimi === true) families.push("kimi");
|
|
4381
|
-
if (args.minimax === true) families.push("minimax");
|
|
4382
|
-
return families;
|
|
4383
|
-
}
|
|
4384
|
-
|
|
4385
5178
|
// src/index.ts
|
|
4386
5179
|
function printCommandHelp(error) {
|
|
4387
5180
|
console.error(`Error: ${error}`);
|
|
@@ -4390,6 +5183,9 @@ function printCommandHelp(error) {
|
|
|
4390
5183
|
console.error(" vibestats limits all");
|
|
4391
5184
|
console.error(" vibestats limits claude");
|
|
4392
5185
|
console.error(" vibestats limits codex");
|
|
5186
|
+
console.error(" vibestats claude");
|
|
5187
|
+
console.error(" vibestats codex");
|
|
5188
|
+
console.error(" vibestats all");
|
|
4393
5189
|
console.error(" vibestats usage all");
|
|
4394
5190
|
console.error(" vibestats usage --combined --daily");
|
|
4395
5191
|
console.error(" vibestats usage codex --total");
|
|
@@ -4539,7 +5335,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
|
|
|
4539
5335
|
var main = defineCommand({
|
|
4540
5336
|
meta: {
|
|
4541
5337
|
name: "vibestats",
|
|
4542
|
-
version: "1.
|
|
5338
|
+
version: "1.4.1",
|
|
4543
5339
|
description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
|
|
4544
5340
|
},
|
|
4545
5341
|
args: {
|
|
@@ -4664,6 +5460,7 @@ var main = defineCommand({
|
|
|
4664
5460
|
description: "Disable shortlink generation",
|
|
4665
5461
|
default: false
|
|
4666
5462
|
},
|
|
5463
|
+
...cacheArgs,
|
|
4667
5464
|
// Config management
|
|
4668
5465
|
init: {
|
|
4669
5466
|
type: "boolean",
|
|
@@ -4675,6 +5472,11 @@ var main = defineCommand({
|
|
|
4675
5472
|
description: "Show current config location and values",
|
|
4676
5473
|
default: false
|
|
4677
5474
|
},
|
|
5475
|
+
dashboard: {
|
|
5476
|
+
type: "boolean",
|
|
5477
|
+
description: "Open a local web dashboard with usage, activity, and wrapped views",
|
|
5478
|
+
default: false
|
|
5479
|
+
},
|
|
4678
5480
|
"claude-system": {
|
|
4679
5481
|
type: "boolean",
|
|
4680
5482
|
description: "Inspect ~/.claude.json account and app state",
|
|
@@ -4715,6 +5517,10 @@ var main = defineCommand({
|
|
|
4715
5517
|
await runLiveLimits(normalizedArgs, config);
|
|
4716
5518
|
return;
|
|
4717
5519
|
}
|
|
5520
|
+
if (normalizedArgs.dashboard) {
|
|
5521
|
+
await runDashboard(normalizedArgs, config);
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
4718
5524
|
if (normalizedArgs.activity) {
|
|
4719
5525
|
await runActivity(normalizedArgs, config);
|
|
4720
5526
|
} else if (normalizedArgs.wrapped) {
|
|
@@ -4750,6 +5556,7 @@ async function runUsage(args, config) {
|
|
|
4750
5556
|
else if (args.model) aggregation = "model";
|
|
4751
5557
|
else if (args.total) aggregation = "total";
|
|
4752
5558
|
const shouldShowSpinner = !args.json && !args.quiet;
|
|
5559
|
+
const cacheOptions = getUsageCacheOptions(args);
|
|
4753
5560
|
const statsPromise = loadUsageStats({
|
|
4754
5561
|
aggregation,
|
|
4755
5562
|
since,
|
|
@@ -4758,7 +5565,8 @@ async function runUsage(args, config) {
|
|
|
4758
5565
|
combined: args.combined,
|
|
4759
5566
|
projectFilter: args.project ? process.cwd() : void 0,
|
|
4760
5567
|
families,
|
|
4761
|
-
scopeLabel
|
|
5568
|
+
scopeLabel,
|
|
5569
|
+
...cacheOptions
|
|
4762
5570
|
});
|
|
4763
5571
|
const stats = shouldShowSpinner ? await createSpinner("Loading vibestats...").whilePromise(statsPromise) : await statsPromise;
|
|
4764
5572
|
if (!stats) {
|
|
@@ -4865,14 +5673,15 @@ async function runWrapped(args, config) {
|
|
|
4865
5673
|
const scopeLabel = formatModelFamilyLabel(families);
|
|
4866
5674
|
const metric = parseActivityMetric(args.metric);
|
|
4867
5675
|
const days = parseActivityDays(args.days);
|
|
5676
|
+
const cacheOptions = getUsageCacheOptions(args);
|
|
4868
5677
|
const spinner = createSpinner("Preparing wrapped...");
|
|
4869
5678
|
const [data, activityStats] = await spinner.whilePromise(
|
|
4870
5679
|
Promise.all([
|
|
4871
|
-
loadData({ codexOnly: args.codex, combined: args.combined, families, scopeLabel }),
|
|
4872
|
-
loadActivityStats({ codexOnly: args.codex, combined: args.combined, families, scopeLabel })
|
|
5680
|
+
loadData({ codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions }),
|
|
5681
|
+
loadActivityStats({ codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions })
|
|
4873
5682
|
])
|
|
4874
5683
|
);
|
|
4875
|
-
validateData(data, { codexOnly: args.codex, combined: args.combined, families, scopeLabel });
|
|
5684
|
+
validateData(data, { codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions });
|
|
4876
5685
|
let claudeStats = null;
|
|
4877
5686
|
let codexStats = null;
|
|
4878
5687
|
if (data.claude) {
|
|
@@ -4933,6 +5742,7 @@ async function runActivity(args, config) {
|
|
|
4933
5742
|
const metric = parseActivityMetric(args.metric);
|
|
4934
5743
|
const days = parseActivityDays(args.days);
|
|
4935
5744
|
const spinner = createSpinner("Preparing activity graph...");
|
|
5745
|
+
const cacheOptions = getUsageCacheOptions(args);
|
|
4936
5746
|
const stats = await spinner.whilePromise(
|
|
4937
5747
|
loadActivityStats({
|
|
4938
5748
|
codexOnly: args.codex,
|
|
@@ -4941,7 +5751,8 @@ async function runActivity(args, config) {
|
|
|
4941
5751
|
since: args.since,
|
|
4942
5752
|
until: args.until,
|
|
4943
5753
|
families,
|
|
4944
|
-
scopeLabel
|
|
5754
|
+
scopeLabel,
|
|
5755
|
+
...cacheOptions
|
|
4945
5756
|
})
|
|
4946
5757
|
);
|
|
4947
5758
|
if (!stats) {
|