tokentracker-cli 0.24.6 → 0.24.8
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.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dashboard/dist/assets/{Card-BfayTmBt.js → Card-Bm2ZOfFA.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-C_ExwqoB.js → DashboardPage-BCwneqYv.js} +2 -2
- package/dashboard/dist/assets/{DevicePage-Bzc-tOQ7.js → DevicePage-CtqTSoxC.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-C1nCEQAI.js → FadeIn-BGmXw8rD.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CalgbIws.js → HeaderGithubStar-7oF7WLZx.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C2_FdWEa.js → IpCheckPage-DLGIFtxp.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Cab2gSJk.js → LandingPage-DblVIECc.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BoWSvv8E.js → LeaderboardPage-BSIiXBXB.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-BxvvRB0p.js → LeaderboardProfilePage-rnzMxGm7.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-JTd7ZYkv.js → LimitsPage-BIIloMLr.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-fU320alu.js → LoginPage-K0pFbm2P.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-C76-ba7G.js → PopoverPopup-CdYV60oX.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-xv4cUgTy.js → ProviderIcon-CyeZHlvD.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-UtWH1_mF.js → SettingsPage-CwNF9bwF.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-9W2jGGps.js → SkillsPage-DJtcsqCs.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BedujTKv.js → WidgetsPage-DnQgY7YQ.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Bms62oTH.js → WrappedPage-Ci0XyXL_.js} +1 -1
- package/dashboard/dist/assets/check-CDwpiGWn.js +1 -0
- package/dashboard/dist/assets/{chevron-down-g6db-hJJ.js → chevron-down-BTjZUclo.js} +1 -1
- package/dashboard/dist/assets/{download-UDqrzLfH.js → download-TnEyNsSy.js} +1 -1
- package/dashboard/dist/assets/{info-BB9X3uhm.js → info-8F30IXmF.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-CTGzd-uH.js → leaderboard-columns-C6mnviZx.js} +1 -1
- package/dashboard/dist/assets/{main-QPJFCBQm.js → main-Ch22JpOj.js} +15 -15
- package/dashboard/dist/assets/main-D_iR1XAB.css +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-DccYvUNZ.js → use-limits-display-prefs-Rfcf8uOp.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-DAbSUv-n.js → use-native-settings-7r5luMxr.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-B_GaKtWC.js → use-reduced-motion-lvUJO2cx.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-3pTcFcoF.js → use-usage-limits-BDH7ZuiN.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CLZ2MqvV.js → useCurrency-9qQotA-7.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +114 -14
- package/src/commands/status.js +13 -0
- package/src/commands/sync.js +35 -2
- package/src/lib/local-api.js +112 -0
- package/src/lib/pricing/matcher.js +54 -3
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +474 -0
- package/dashboard/dist/assets/check-DHKWR9eH.js +0 -1
- package/dashboard/dist/assets/main-C8k06i2w.css +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -52,6 +52,8 @@ const {
|
|
|
52
52
|
parseZedIncremental,
|
|
53
53
|
resolveGooseDbPath,
|
|
54
54
|
parseGooseIncremental,
|
|
55
|
+
listDroidSettingsFiles,
|
|
56
|
+
parseDroidIncremental,
|
|
55
57
|
bucketKey,
|
|
56
58
|
totalsKey,
|
|
57
59
|
claudeMessageDedupKey,
|
|
@@ -456,6 +458,35 @@ async function cmdSync(argv) {
|
|
|
456
458
|
});
|
|
457
459
|
}
|
|
458
460
|
|
|
461
|
+
// ── Droid (Factory CLI) — passive reader for ~/.factory/sessions/*.settings.json ──
|
|
462
|
+
const droidSettingsFiles = listDroidSettingsFiles(process.env);
|
|
463
|
+
let droidResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
464
|
+
if (droidSettingsFiles.length > 0) {
|
|
465
|
+
if (progress?.enabled) {
|
|
466
|
+
progress.start(
|
|
467
|
+
`Parsing Droid ${renderBar(0)} 0/${formatNumber(droidSettingsFiles.length)} sessions | buckets 0`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
droidResult = await parseDroidIncremental({
|
|
471
|
+
settingsFiles: droidSettingsFiles,
|
|
472
|
+
cursors,
|
|
473
|
+
queuePath,
|
|
474
|
+
// Full-scan sync: drop cursor entries for any session whose
|
|
475
|
+
// settings.json has disappeared off disk so cursors.droid stays
|
|
476
|
+
// bounded by the actual on-disk session count.
|
|
477
|
+
prune: true,
|
|
478
|
+
onProgress: (p) => {
|
|
479
|
+
if (!progress?.enabled) return;
|
|
480
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
481
|
+
progress.update(
|
|
482
|
+
`Parsing Droid ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
|
|
483
|
+
p.total,
|
|
484
|
+
)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
485
|
+
);
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
459
490
|
// ── Zed Agent (hosted models only; cumulative-delta over SQLite threads) ──
|
|
460
491
|
const zedDbPath = resolveZedDbPath(process.env);
|
|
461
492
|
let zedResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
@@ -948,7 +979,8 @@ async function cmdSync(argv) {
|
|
|
948
979
|
kilocodeResult.recordsProcessed +
|
|
949
980
|
roocodeResult.recordsProcessed +
|
|
950
981
|
zedResult.recordsProcessed +
|
|
951
|
-
gooseResult.recordsProcessed
|
|
982
|
+
gooseResult.recordsProcessed +
|
|
983
|
+
droidResult.recordsProcessed;
|
|
952
984
|
const totalBuckets =
|
|
953
985
|
parseResult.bucketsQueued +
|
|
954
986
|
openclawResult.bucketsQueued +
|
|
@@ -971,7 +1003,8 @@ async function cmdSync(argv) {
|
|
|
971
1003
|
kilocodeResult.bucketsQueued +
|
|
972
1004
|
roocodeResult.bucketsQueued +
|
|
973
1005
|
zedResult.bucketsQueued +
|
|
974
|
-
gooseResult.bucketsQueued
|
|
1006
|
+
gooseResult.bucketsQueued +
|
|
1007
|
+
droidResult.bucketsQueued;
|
|
975
1008
|
process.stdout.write(
|
|
976
1009
|
[
|
|
977
1010
|
"Sync finished:",
|
package/src/lib/local-api.js
CHANGED
|
@@ -14,6 +14,13 @@ const {
|
|
|
14
14
|
const SYNC_TIMEOUT_MS = 120_000;
|
|
15
15
|
const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
|
|
16
16
|
|
|
17
|
+
// Avatar proxy (see /api/avatar-proxy below). In-memory LRU; survives the
|
|
18
|
+
// CLI server lifetime, which is good enough — the dashboard reloads cheaply.
|
|
19
|
+
const AVATAR_PROXY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
20
|
+
const AVATAR_PROXY_MAX_BYTES = 512 * 1024; // 512 KiB per image
|
|
21
|
+
const AVATAR_PROXY_MAX_ENTRIES = 64;
|
|
22
|
+
const avatarProxyCache = new Map();
|
|
23
|
+
|
|
17
24
|
// ---------------------------------------------------------------------------
|
|
18
25
|
// Per-model pricing — delegated to src/lib/pricing/
|
|
19
26
|
// - CURATED overrides (kiro-*, hy3-*, composer-*, kimi-for-coding, etc.)
|
|
@@ -1100,6 +1107,111 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1100
1107
|
return true;
|
|
1101
1108
|
}
|
|
1102
1109
|
|
|
1110
|
+
// --- avatar proxy: fetch third-party avatars server-side ---
|
|
1111
|
+
// Why: WKWebView in TokenTrackerBar fails to load some users' Google /
|
|
1112
|
+
// GitHub avatars directly (network-stack / proxy / TLS quirks vary by
|
|
1113
|
+
// environment), even when the same URL renders fine in Safari. Proxying
|
|
1114
|
+
// through Node's fetch — which honors system proxy + cookies-of-none —
|
|
1115
|
+
// produces a same-origin <img> the WKWebView always accepts.
|
|
1116
|
+
// Lock-down: GET only; host allowlist of well-known avatar CDNs (no open
|
|
1117
|
+
// proxy); strip cookies/auth; small in-memory cache.
|
|
1118
|
+
if (p === "/api/avatar-proxy") {
|
|
1119
|
+
const method = String(req.method || "GET").toUpperCase();
|
|
1120
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1121
|
+
json(res, { error: "Method Not Allowed" }, 405);
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
const target = url.searchParams.get("url");
|
|
1125
|
+
if (!target) {
|
|
1126
|
+
json(res, { error: "Missing url" }, 400);
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
let parsed;
|
|
1130
|
+
try {
|
|
1131
|
+
parsed = new URL(target);
|
|
1132
|
+
} catch {
|
|
1133
|
+
json(res, { error: "Invalid url" }, 400);
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1137
|
+
json(res, { error: "Only http(s) allowed" }, 400);
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
const AVATAR_HOST_ALLOWLIST = [
|
|
1141
|
+
"lh3.googleusercontent.com",
|
|
1142
|
+
"lh4.googleusercontent.com",
|
|
1143
|
+
"lh5.googleusercontent.com",
|
|
1144
|
+
"lh6.googleusercontent.com",
|
|
1145
|
+
"avatars.githubusercontent.com",
|
|
1146
|
+
"secure.gravatar.com",
|
|
1147
|
+
"www.gravatar.com",
|
|
1148
|
+
"gravatar.com",
|
|
1149
|
+
"cdn.discordapp.com",
|
|
1150
|
+
"pbs.twimg.com",
|
|
1151
|
+
"abs.twimg.com",
|
|
1152
|
+
"api.dicebear.com",
|
|
1153
|
+
];
|
|
1154
|
+
const hostOk = AVATAR_HOST_ALLOWLIST.some(
|
|
1155
|
+
(h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
|
|
1156
|
+
);
|
|
1157
|
+
if (!hostOk) {
|
|
1158
|
+
json(res, { error: "Host not allowed" }, 403);
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const cacheKey = parsed.toString();
|
|
1163
|
+
const now = Date.now();
|
|
1164
|
+
const cached = avatarProxyCache.get(cacheKey);
|
|
1165
|
+
if (cached && now - cached.fetchedAt < AVATAR_PROXY_TTL_MS) {
|
|
1166
|
+
res.writeHead(200, {
|
|
1167
|
+
"Content-Type": cached.contentType,
|
|
1168
|
+
"Cache-Control": "public, max-age=3600",
|
|
1169
|
+
"X-Avatar-Cache": "HIT",
|
|
1170
|
+
});
|
|
1171
|
+
res.end(method === "HEAD" ? undefined : cached.body);
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
const upstream = await fetch(cacheKey, {
|
|
1177
|
+
method,
|
|
1178
|
+
redirect: "follow",
|
|
1179
|
+
headers: {
|
|
1180
|
+
accept: req.headers["accept"] || "image/*",
|
|
1181
|
+
"accept-language": req.headers["accept-language"] || "en",
|
|
1182
|
+
"user-agent": "TokenTracker/AvatarProxy (https://www.tokentracker.cc)",
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
if (!upstream.ok) {
|
|
1186
|
+
json(res, { error: `Upstream ${upstream.status}` }, upstream.status);
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
const contentType = upstream.headers.get("content-type") || "image/png";
|
|
1190
|
+
if (!contentType.toLowerCase().startsWith("image/")) {
|
|
1191
|
+
json(res, { error: "Not an image" }, 415);
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
const body = Buffer.from(await upstream.arrayBuffer());
|
|
1195
|
+
if (body.length <= AVATAR_PROXY_MAX_BYTES) {
|
|
1196
|
+
// Simple LRU: drop oldest if over capacity.
|
|
1197
|
+
if (avatarProxyCache.size >= AVATAR_PROXY_MAX_ENTRIES) {
|
|
1198
|
+
const oldestKey = avatarProxyCache.keys().next().value;
|
|
1199
|
+
if (oldestKey) avatarProxyCache.delete(oldestKey);
|
|
1200
|
+
}
|
|
1201
|
+
avatarProxyCache.set(cacheKey, { body, contentType, fetchedAt: now });
|
|
1202
|
+
}
|
|
1203
|
+
res.writeHead(200, {
|
|
1204
|
+
"Content-Type": contentType,
|
|
1205
|
+
"Cache-Control": "public, max-age=3600",
|
|
1206
|
+
"X-Avatar-Cache": "MISS",
|
|
1207
|
+
});
|
|
1208
|
+
res.end(method === "HEAD" ? undefined : body);
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
json(res, { error: `Avatar proxy error: ${e?.message || e}` }, 502);
|
|
1211
|
+
}
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1103
1215
|
// --- local-sync (POST) ---
|
|
1104
1216
|
if (p === "/functions/tokentracker-local-sync") {
|
|
1105
1217
|
if (String(req.method || "GET").toUpperCase() !== "POST") {
|
|
@@ -72,6 +72,33 @@ function getSortedKeys(litellm) {
|
|
|
72
72
|
return cached;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function buildDotRestoredModel(model) {
|
|
76
|
+
if (typeof model !== "string") return "";
|
|
77
|
+
const lower = model.toLowerCase();
|
|
78
|
+
const restored = lower.replace(/(\d+)-(\d+)/g, "$1.$2");
|
|
79
|
+
return restored === lower ? "" : restored;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function lookupExactCaseInsensitive(table, model) {
|
|
83
|
+
if (!table || !model) return null;
|
|
84
|
+
if (table[model]) return table[model];
|
|
85
|
+
const lower = model.toLowerCase();
|
|
86
|
+
for (const key of Object.keys(table)) {
|
|
87
|
+
if (key.toLowerCase() === lower) return table[key];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lookupContainedExactCaseInsensitive(table, model) {
|
|
93
|
+
if (!table || !model) return null;
|
|
94
|
+
const lower = model.toLowerCase();
|
|
95
|
+
const keys = Object.keys(table).sort((a, b) => b.length - a.length);
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
if (lower.includes(key.toLowerCase())) return table[key];
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
76
103
|
if (!model || typeof model !== "string") {
|
|
77
104
|
return { hit: false, source: "empty", value: null };
|
|
@@ -80,16 +107,29 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
|
80
107
|
? normalizeAntigravityModel(model)
|
|
81
108
|
: model;
|
|
82
109
|
const lower = lookupModel.toLowerCase();
|
|
110
|
+
const dotForm = buildDotRestoredModel(lookupModel);
|
|
83
111
|
|
|
84
112
|
// 1. CURATED exact
|
|
85
113
|
if (curated.exact && curated.exact[lookupModel]) {
|
|
86
114
|
return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
|
|
87
115
|
}
|
|
116
|
+
const curatedDotExact = lookupExactCaseInsensitive(curated.exact, dotForm);
|
|
117
|
+
if (curatedDotExact) {
|
|
118
|
+
return { hit: true, source: "curated:exact-dot", value: curatedDotExact };
|
|
119
|
+
}
|
|
120
|
+
const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
|
|
121
|
+
if (curatedDotContainedExact) {
|
|
122
|
+
return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };
|
|
123
|
+
}
|
|
88
124
|
|
|
89
125
|
// 2. LiteLLM exact
|
|
90
126
|
if (litellm && litellm[lookupModel]) {
|
|
91
127
|
return { hit: true, source: "litellm:exact", value: litellm[lookupModel] };
|
|
92
128
|
}
|
|
129
|
+
const litellmDotExact = lookupExactCaseInsensitive(litellm, dotForm);
|
|
130
|
+
if (litellmDotExact) {
|
|
131
|
+
return { hit: true, source: "litellm:exact-dot", value: litellmDotExact };
|
|
132
|
+
}
|
|
93
133
|
|
|
94
134
|
// 3. CURATED alias (literal mapping like "auto" -> "composer-1")
|
|
95
135
|
if (curated.alias && curated.alias[lookupModel] && curated.exact[curated.alias[lookupModel]]) {
|
|
@@ -100,11 +140,22 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
|
100
140
|
};
|
|
101
141
|
}
|
|
102
142
|
|
|
103
|
-
// 4. CURATED fuzzy substring
|
|
143
|
+
// 4. CURATED fuzzy substring. Also try a dot-restored variant of the input
|
|
144
|
+
// (digits separated by `-` rejoined as `.`) so providers that dash-normalize
|
|
145
|
+
// numeric segments — Droid emits `glm-5-1-0` for upstream `GLM-5.1` — still
|
|
146
|
+
// resolve against dot-keyed curated entries like `glm-5.1`, `glm-4.6`, etc.
|
|
147
|
+
// The regex only fires on digit-dash-digit, so `claude-3-7-sonnet`,
|
|
148
|
+
// `gpt-5-codex`, `gemini-2-5-pro` are unaffected (no digit-pair to rejoin or
|
|
149
|
+
// no matching curated key).
|
|
104
150
|
if (Array.isArray(curated.fuzzy)) {
|
|
105
151
|
for (const { match, ref } of curated.fuzzy) {
|
|
106
152
|
if (!match || !ref) continue;
|
|
107
|
-
|
|
153
|
+
const needle = match.toLowerCase();
|
|
154
|
+
if (!curated.exact[ref]) continue;
|
|
155
|
+
if (lower.includes(needle)) {
|
|
156
|
+
return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
|
|
157
|
+
}
|
|
158
|
+
if (dotForm && dotForm.includes(needle)) {
|
|
108
159
|
return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
|
|
109
160
|
}
|
|
110
161
|
}
|
|
@@ -125,7 +176,7 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
|
125
176
|
const keyLower = key.toLowerCase();
|
|
126
177
|
// Only accept if model is a superset of key (model contains key), to
|
|
127
178
|
// avoid e.g. "gpt-5" matching "gpt-5-pro" in the wrong direction.
|
|
128
|
-
if (lower.includes(keyLower)) {
|
|
179
|
+
if (lower.includes(keyLower) || (dotForm && dotForm.includes(keyLower))) {
|
|
129
180
|
return { hit: true, source: "litellm:fuzzy", value: litellm[key] };
|
|
130
181
|
}
|
|
131
182
|
}
|