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.
Files changed (45) hide show
  1. package/README.ja.md +1 -0
  2. package/README.ko.md +1 -0
  3. package/README.md +1 -0
  4. package/README.zh-CN.md +1 -0
  5. package/dashboard/dist/assets/{Card-BfayTmBt.js → Card-Bm2ZOfFA.js} +1 -1
  6. package/dashboard/dist/assets/{DashboardPage-C_ExwqoB.js → DashboardPage-BCwneqYv.js} +2 -2
  7. package/dashboard/dist/assets/{DevicePage-Bzc-tOQ7.js → DevicePage-CtqTSoxC.js} +1 -1
  8. package/dashboard/dist/assets/{FadeIn-C1nCEQAI.js → FadeIn-BGmXw8rD.js} +1 -1
  9. package/dashboard/dist/assets/{HeaderGithubStar-CalgbIws.js → HeaderGithubStar-7oF7WLZx.js} +1 -1
  10. package/dashboard/dist/assets/{IpCheckPage-C2_FdWEa.js → IpCheckPage-DLGIFtxp.js} +1 -1
  11. package/dashboard/dist/assets/{LandingPage-Cab2gSJk.js → LandingPage-DblVIECc.js} +1 -1
  12. package/dashboard/dist/assets/{LeaderboardPage-BoWSvv8E.js → LeaderboardPage-BSIiXBXB.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-BxvvRB0p.js → LeaderboardProfilePage-rnzMxGm7.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-JTd7ZYkv.js → LimitsPage-BIIloMLr.js} +1 -1
  15. package/dashboard/dist/assets/{LoginPage-fU320alu.js → LoginPage-K0pFbm2P.js} +1 -1
  16. package/dashboard/dist/assets/{PopoverPopup-C76-ba7G.js → PopoverPopup-CdYV60oX.js} +1 -1
  17. package/dashboard/dist/assets/{ProviderIcon-xv4cUgTy.js → ProviderIcon-CyeZHlvD.js} +1 -1
  18. package/dashboard/dist/assets/{SettingsPage-UtWH1_mF.js → SettingsPage-CwNF9bwF.js} +1 -1
  19. package/dashboard/dist/assets/{SkillsPage-9W2jGGps.js → SkillsPage-DJtcsqCs.js} +1 -1
  20. package/dashboard/dist/assets/{WidgetsPage-BedujTKv.js → WidgetsPage-DnQgY7YQ.js} +1 -1
  21. package/dashboard/dist/assets/{WrappedPage-Bms62oTH.js → WrappedPage-Ci0XyXL_.js} +1 -1
  22. package/dashboard/dist/assets/check-CDwpiGWn.js +1 -0
  23. package/dashboard/dist/assets/{chevron-down-g6db-hJJ.js → chevron-down-BTjZUclo.js} +1 -1
  24. package/dashboard/dist/assets/{download-UDqrzLfH.js → download-TnEyNsSy.js} +1 -1
  25. package/dashboard/dist/assets/{info-BB9X3uhm.js → info-8F30IXmF.js} +1 -1
  26. package/dashboard/dist/assets/{leaderboard-columns-CTGzd-uH.js → leaderboard-columns-C6mnviZx.js} +1 -1
  27. package/dashboard/dist/assets/{main-QPJFCBQm.js → main-Ch22JpOj.js} +15 -15
  28. package/dashboard/dist/assets/main-D_iR1XAB.css +1 -0
  29. package/dashboard/dist/assets/{use-limits-display-prefs-DccYvUNZ.js → use-limits-display-prefs-Rfcf8uOp.js} +1 -1
  30. package/dashboard/dist/assets/{use-native-settings-DAbSUv-n.js → use-native-settings-7r5luMxr.js} +1 -1
  31. package/dashboard/dist/assets/{use-reduced-motion-B_GaKtWC.js → use-reduced-motion-lvUJO2cx.js} +1 -1
  32. package/dashboard/dist/assets/{use-usage-limits-3pTcFcoF.js → use-usage-limits-BDH7ZuiN.js} +1 -1
  33. package/dashboard/dist/assets/{useCurrency-CLZ2MqvV.js → useCurrency-9qQotA-7.js} +1 -1
  34. package/dashboard/dist/index.html +2 -2
  35. package/dashboard/dist/share.html +2 -2
  36. package/package.json +1 -1
  37. package/src/commands/serve.js +114 -14
  38. package/src/commands/status.js +13 -0
  39. package/src/commands/sync.js +35 -2
  40. package/src/lib/local-api.js +112 -0
  41. package/src/lib/pricing/matcher.js +54 -3
  42. package/src/lib/pricing/seed-snapshot.json +1 -1
  43. package/src/lib/rollout.js +474 -0
  44. package/dashboard/dist/assets/check-DHKWR9eH.js +0 -1
  45. package/dashboard/dist/assets/main-C8k06i2w.css +0 -1
@@ -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:",
@@ -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
- if (lower.includes(match.toLowerCase()) && curated.exact[ref]) {
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
  }