tokentracker-cli 0.77.2 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +21 -0
- package/README.md +21 -0
- package/dashboard/dist/achievements/README.md +9 -0
- package/dashboard/dist/achievements/arctic-code-vault-contributor.png +0 -0
- package/dashboard/dist/achievements/galaxy-brain.png +0 -0
- package/dashboard/dist/achievements/heart-on-your-sleeve.png +0 -0
- package/dashboard/dist/achievements/open-sourcerer.png +0 -0
- package/dashboard/dist/achievements/pair-extraordinaire.png +0 -0
- package/dashboard/dist/achievements/proxima-pioneer.png +0 -0
- package/dashboard/dist/achievements/proxima-staffshipper.png +0 -0
- package/dashboard/dist/achievements/proxima-staffuser.png +0 -0
- package/dashboard/dist/achievements/public-sponsor.png +0 -0
- package/dashboard/dist/achievements/pull-shark.png +0 -0
- package/dashboard/dist/achievements/quickdraw.png +0 -0
- package/dashboard/dist/achievements/starstruck.png +0 -0
- package/dashboard/dist/assets/AchievementBadge-BxETKM1a.js +1 -0
- package/dashboard/dist/assets/AchievementsPage-BnsI7sbt.js +2 -0
- package/dashboard/dist/assets/AchievementsSection-BjKtfATU.js +1 -0
- package/dashboard/dist/assets/{ActivityHeatmap-CH6m_ZJM.js → ActivityHeatmap-B2NacPjc.js} +1 -1
- package/dashboard/dist/assets/BadgeDetailModal-BMNUpuro.js +1 -0
- package/dashboard/dist/assets/{Card-D_aKGDr7.js → Card-Y5og8UqF.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-vRiBiNhb.js → ClawdAnimated-4EGVuTbr.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CjetJZ-5.js → CommunityStatsModal-DgQ5Cv9h.js} +1 -1
- package/dashboard/dist/assets/ConfirmModal-DNDMMsf7.js +1 -0
- package/dashboard/dist/assets/Controls-BzQP6AmV.js +1 -0
- package/dashboard/dist/assets/DashboardPage-C5nQV_pg.js +19 -0
- package/dashboard/dist/assets/DevicePage-Hc411Mif.js +1 -0
- package/dashboard/dist/assets/{DialogClose-hhTDeRp1.js → DialogClose-DG2e5iky.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CD7W4GGZ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CA5znwms.js → DialogTitle-BlZXQboq.js} +1 -1
- package/dashboard/dist/assets/{FadeIn--ff4ZbC-.js → FadeIn-bR4Q4FyK.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CTZvanhz.js → HeaderGithubStar-iujOMm58.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Bg47gNVu.js → IpCheckPage-BVb5O-Nc.js} +4 -4
- package/dashboard/dist/assets/{LandingPage-DZ8Mz38n.js → LandingPage-B5JCSXIZ.js} +1 -1
- package/dashboard/dist/assets/LeaderboardAvatar-C5o48_SY.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-CNOPBahO.js +2 -0
- package/dashboard/dist/assets/LeaderboardProfileModal-Buo6oZZk.js +73 -0
- package/dashboard/dist/assets/{LeaderboardProfilePage-C4bGXJCE.js → LeaderboardProfilePage-DljcZHf7.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-BAQ10X55.js → LimitsPage-BOaJ4wfo.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-gtHCn7Mx.js → LocalOnlyNotice-BBxetUgv.js} +1 -1
- package/dashboard/dist/assets/LoginPage-LYriDN1G.js +1 -0
- package/dashboard/dist/assets/PetPage-CPPVItLo.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CQxxgUXk.js → PopoverPopup-iP8jGLgj.js} +1 -1
- package/dashboard/dist/assets/ResetPasswordPage-BqAwHe1w.js +1 -0
- package/dashboard/dist/assets/Select-DdpTxWI0.js +1 -0
- package/dashboard/dist/assets/{SelectItemText-2LjzGvo5.js → SelectItemText-Cpno_HPs.js} +1 -1
- package/dashboard/dist/assets/SettingsPage-DtKCHKrt.js +1 -0
- package/dashboard/dist/assets/SkillsPage-D5vXnEab.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-BIPU747m.js +1 -0
- package/dashboard/dist/assets/{WrappedPage-YxshTScX.js → WrappedPage-VfTcO6cY.js} +1 -1
- package/dashboard/dist/assets/{agent-logos-SeuxwQsp.js → agent-logos-sYidWnQM.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Bm3t-iA0.js → arrow-up-right-Dt35aRs9.js} +1 -1
- package/dashboard/dist/assets/check-B2kgoEGi.js +1 -0
- package/dashboard/dist/assets/{copy-DUNz-xWm.js → copy-BdlX1Dcz.js} +1 -1
- package/dashboard/dist/assets/{download-COSLX73O.js → download-C4750cH0.js} +1 -1
- package/dashboard/dist/assets/flame-cYIuY1Z6.js +1 -0
- package/dashboard/dist/assets/{format-tokens-CR28AiQO.js → format-tokens-Ci6ia4_b.js} +1 -1
- package/dashboard/dist/assets/{info-Cm7V_KDO.js → info-Hy8GSxzK.js} +1 -1
- package/dashboard/dist/assets/{link-2-CYqDGrkv.js → link-2-CHfhV9M_.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-6XAdy2ec.js → loader-circle-OYZpObE-.js} +1 -1
- package/dashboard/dist/assets/main--YQ6HWXP.js +1237 -0
- package/dashboard/dist/assets/main-BY13exWE.css +1 -0
- package/dashboard/dist/assets/{trash-2-DKLewOQb.js → trash-2-DFV8X7qB.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-AdPaun74.js → use-community-stats-BzlENlsN.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Bau4ykMw.js → use-limits-display-prefs-Bgxvo849.js} +1 -1
- package/dashboard/dist/assets/use-native-settings-Cz5XgWcy.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-BVaEJaWa.js → use-ordered-list-N591-3J4.js} +2 -2
- package/dashboard/dist/assets/{use-quality-per-dollar-pref-Htlc-uFe.js → use-quality-per-dollar-pref-CW332Qtp.js} +1 -1
- package/dashboard/dist/assets/use-transform-BNrGbsUe.js +1 -0
- package/dashboard/dist/assets/{use-usage-limits-BnzWTJQS.js → use-usage-limits-BL1n1H9j.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-bd6TLRsc.js → useCurrency-B5QegAgS.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-ajoWrfku.js → useScrollLock-CcorrDyq.js} +1 -1
- package/dashboard/dist/assets/zap-C_DbBJ3s.js +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/status.js +119 -12
- package/src/commands/sync.js +158 -83
- package/src/lib/install-resolver.js +1 -0
- package/src/lib/local-api.js +315 -56
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +18 -2
- package/dashboard/dist/assets/ConfirmModal-C6bZw6FZ.js +0 -1
- package/dashboard/dist/assets/Controls-Cj22oGL3.js +0 -1
- package/dashboard/dist/assets/DashboardPage-DPxiQkQw.js +0 -19
- package/dashboard/dist/assets/DevicePage-YAo4ert4.js +0 -1
- package/dashboard/dist/assets/DialogDescription-gxEpcUbJ.js +0 -1
- package/dashboard/dist/assets/LeaderboardAvatar-3T57GKKK.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-wP_yCU9I.js +0 -2
- package/dashboard/dist/assets/LeaderboardProfileModal-CfVMENby.js +0 -72
- package/dashboard/dist/assets/LoginPage-TXvIBzoW.js +0 -1
- package/dashboard/dist/assets/PetPage-Dj1jrxYe.js +0 -1
- package/dashboard/dist/assets/ResetPasswordPage-tN0tmSfP.js +0 -1
- package/dashboard/dist/assets/Select-DnbHYPS4.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DosKS_L6.js +0 -1
- package/dashboard/dist/assets/SkillsPage-DaCJ25vT.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-CCGemQVD.js +0 -1
- package/dashboard/dist/assets/check-D8Owfjyw.js +0 -1
- package/dashboard/dist/assets/main-BCstrDYE.js +0 -1172
- package/dashboard/dist/assets/main-BYdJ5vWR.css +0 -1
- package/dashboard/dist/assets/use-native-settings-CidwlprT.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -44,6 +44,11 @@ const {
|
|
|
44
44
|
|
|
45
45
|
const { computeCodexContextBreakdown } = require("./codex-context-breakdown");
|
|
46
46
|
|
|
47
|
+
const {
|
|
48
|
+
deriveProjectKeyFromRef,
|
|
49
|
+
CLAUDE_MEM_OBSERVER_PROJECT_REF,
|
|
50
|
+
} = require("./rollout");
|
|
51
|
+
|
|
47
52
|
// ---------------------------------------------------------------------------
|
|
48
53
|
// Queue data helpers
|
|
49
54
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +58,38 @@ function resolveQueuePath() {
|
|
|
53
58
|
return path.join(home, ".tokentracker", "tracker", "queue.jsonl");
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
// Pseudo-project written by the pre-fix claude-mem observer attribution;
|
|
62
|
+
// mirrored Claude Code sessions, not a real repository. Excluded at read
|
|
63
|
+
// time so historical rows never surface on the Project Usage panel. Derived
|
|
64
|
+
// from rollout.js's ref so a rename there cannot silently desync the filter.
|
|
65
|
+
const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
|
|
66
|
+
CLAUDE_MEM_OBSERVER_PROJECT_REF,
|
|
67
|
+
);
|
|
68
|
+
const PROJECT_USAGE_MAX_ENTRIES = 10;
|
|
69
|
+
|
|
70
|
+
// Shared by project-usage-summary and project-usage-detail: reads the
|
|
71
|
+
// deduped project bucket log, drops the claude-mem pseudo project, and
|
|
72
|
+
// builds the from/to day-range predicate. Callers compute each row's day
|
|
73
|
+
// key ONCE (rowDayKey constructs an Intl.DateTimeFormat per call — the
|
|
74
|
+
// dominant per-row cost) and reuse it for both the range check and any
|
|
75
|
+
// day bucketing.
|
|
76
|
+
function readProjectUsageContext(qp, url) {
|
|
77
|
+
const from = url.searchParams.get("from") || "";
|
|
78
|
+
const to = url.searchParams.get("to") || "";
|
|
79
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
80
|
+
const hasRange = Boolean(from || to);
|
|
81
|
+
const dayInRange = (day) => {
|
|
82
|
+
if (!hasRange) return true;
|
|
83
|
+
if (!day) return false;
|
|
84
|
+
return (!from || day >= from) && (!to || day <= to);
|
|
85
|
+
};
|
|
86
|
+
const projectQueuePath = path.join(path.dirname(qp), "project.queue.jsonl");
|
|
87
|
+
const projectRows = readProjectQueueData(projectQueuePath).filter(
|
|
88
|
+
(row) => row.project_key !== CLAUDE_MEM_OBSERVER_PROJECT_KEY,
|
|
89
|
+
);
|
|
90
|
+
return { from, to, timeZoneContext, hasRange, dayInRange, projectRows };
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
function readProjectQueueData(projectQueuePath) {
|
|
57
94
|
let raw;
|
|
58
95
|
try {
|
|
@@ -327,6 +364,104 @@ function scopedQueueRows(queuePath, url) {
|
|
|
327
364
|
};
|
|
328
365
|
}
|
|
329
366
|
|
|
367
|
+
// ── Local achievements ───────────────────────────────────────────────────────
|
|
368
|
+
// Local-only badges (the cloud nine live in scripts/ops/user-badges.sql).
|
|
369
|
+
// Thresholds are ordered bronze → silver → gold → diamond. This module is the
|
|
370
|
+
// single server-side home for LOCAL thresholds; the dashboard renders whatever
|
|
371
|
+
// the payload says and embeds none of these numbers.
|
|
372
|
+
const LOCAL_BADGE_THRESHOLDS = {
|
|
373
|
+
project_hopper: [3, 5, 10, 20], // distinct projects
|
|
374
|
+
project_devotion: [1000000, 10000000, 100000000, 1000000000], // max tokens in one project
|
|
375
|
+
night_owl: [5, 20, 60, 150], // active hour buckets between 00:00–05:59 local
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const LOCAL_TIER_KEYS = ["bronze", "silver", "gold", "diamond"];
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Compute the local badge set from deduped queue rows.
|
|
382
|
+
* Rows are replayed in hour_start order so each tier's `achieved` timestamp is
|
|
383
|
+
* the hour at which the running metric first crossed that threshold. Local
|
|
384
|
+
* time (night_owl) follows the caller's tz query params like every other
|
|
385
|
+
* usage endpoint.
|
|
386
|
+
*/
|
|
387
|
+
function computeLocalAchievements(queueRows, projectRows, { timeZoneContext } = {}) {
|
|
388
|
+
const sortByHour = (rows) =>
|
|
389
|
+
rows
|
|
390
|
+
.filter((row) => row && row.hour_start)
|
|
391
|
+
.slice()
|
|
392
|
+
.sort((a, b) => String(a.hour_start).localeCompare(String(b.hour_start)));
|
|
393
|
+
|
|
394
|
+
const trackers = {
|
|
395
|
+
project_hopper: { value: 0, achieved: {}, meta: {} },
|
|
396
|
+
project_devotion: { value: 0, achieved: {}, meta: {} },
|
|
397
|
+
night_owl: { value: 0, achieved: {}, meta: {} },
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const bump = (badgeId, newValue, atIso, meta) => {
|
|
401
|
+
const tracker = trackers[badgeId];
|
|
402
|
+
if (newValue <= tracker.value) return;
|
|
403
|
+
tracker.value = newValue;
|
|
404
|
+
if (meta) tracker.meta = meta;
|
|
405
|
+
const thresholds = LOCAL_BADGE_THRESHOLDS[badgeId];
|
|
406
|
+
for (let i = 0; i < thresholds.length; i += 1) {
|
|
407
|
+
const tierKey = LOCAL_TIER_KEYS[i];
|
|
408
|
+
if (newValue >= thresholds[i] && !tracker.achieved[tierKey]) {
|
|
409
|
+
tracker.achieved[tierKey] = atIso;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const seenProjects = new Set();
|
|
415
|
+
const perProjectTokens = new Map();
|
|
416
|
+
for (const row of sortByHour(projectRows || [])) {
|
|
417
|
+
const projectKey = row.project_key;
|
|
418
|
+
const tokens = Number(row.total_tokens || 0);
|
|
419
|
+
if (!projectKey || tokens <= 0) continue;
|
|
420
|
+
if (!seenProjects.has(projectKey)) {
|
|
421
|
+
seenProjects.add(projectKey);
|
|
422
|
+
bump("project_hopper", seenProjects.size, row.hour_start);
|
|
423
|
+
}
|
|
424
|
+
const running = (perProjectTokens.get(projectKey) || 0) + tokens;
|
|
425
|
+
perProjectTokens.set(projectKey, running);
|
|
426
|
+
if (running > trackers.project_devotion.value) {
|
|
427
|
+
bump("project_devotion", running, row.hour_start, { project_key: projectKey });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const nightHours = new Set();
|
|
432
|
+
for (const row of sortByHour(queueRows || [])) {
|
|
433
|
+
if (Number(row.total_tokens || 0) <= 0) continue;
|
|
434
|
+
if (nightHours.has(row.hour_start)) continue;
|
|
435
|
+
const parts = getZonedParts(new Date(row.hour_start), timeZoneContext || {});
|
|
436
|
+
if (!parts || parts.hour >= 6) continue;
|
|
437
|
+
nightHours.add(row.hour_start);
|
|
438
|
+
bump("night_owl", nightHours.size, row.hour_start);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return Object.entries(LOCAL_BADGE_THRESHOLDS).map(([badgeId, thresholds]) => {
|
|
442
|
+
const tracker = trackers[badgeId];
|
|
443
|
+
let tier = 0;
|
|
444
|
+
for (let i = 0; i < thresholds.length; i += 1) {
|
|
445
|
+
if (tracker.value >= thresholds[i]) tier = i + 1;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
id: badgeId,
|
|
449
|
+
tier,
|
|
450
|
+
metric_value: tracker.value,
|
|
451
|
+
thresholds: thresholds.slice(),
|
|
452
|
+
lower_is_better: false,
|
|
453
|
+
next_threshold: tier >= 4 ? null : thresholds[tier],
|
|
454
|
+
achieved: {
|
|
455
|
+
bronze: tracker.achieved.bronze || null,
|
|
456
|
+
silver: tracker.achieved.silver || null,
|
|
457
|
+
gold: tracker.achieved.gold || null,
|
|
458
|
+
diamond: tracker.achieved.diamond || null,
|
|
459
|
+
},
|
|
460
|
+
meta: tracker.meta,
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
330
465
|
function getTimeZoneContext(url) {
|
|
331
466
|
const tz = String(url.searchParams.get("tz") || "").trim();
|
|
332
467
|
const rawOffset = Number(url.searchParams.get("tz_offset_minutes"));
|
|
@@ -1912,76 +2047,185 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1912
2047
|
// hour_start). Falling back to "session-file count × total tokens"
|
|
1913
2048
|
// (the old behavior) produced pure fiction: every short-and-hot
|
|
1914
2049
|
// project got the same weight as every long-and-cold one.
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const projectRows =
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
if (!byProject.has(key)) {
|
|
1925
|
-
byProject.set(key, {
|
|
1926
|
-
project_key: key,
|
|
1927
|
-
project_ref: row.project_ref || key,
|
|
1928
|
-
total_tokens: 0,
|
|
1929
|
-
billable_total_tokens: 0,
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
const agg = byProject.get(key);
|
|
1933
|
-
agg.total_tokens += Number(row.total_tokens || 0);
|
|
1934
|
-
agg.billable_total_tokens += Number(row.total_tokens || 0);
|
|
1935
|
-
if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
// If no project-attributed rows exist yet (user hasn't synced project
|
|
1939
|
-
// attribution, or never used a project-capable CLI), fall back to
|
|
1940
|
-
// per-source aggregation over the main queue so the panel isn't
|
|
1941
|
-
// totally empty. This path used to also exist for the non-empty case
|
|
1942
|
-
// and produce wrong numbers; keep it only as the empty fallback.
|
|
1943
|
-
let entries;
|
|
1944
|
-
if (byProject.size === 0) {
|
|
1945
|
-
const rows = readQueueData(qp);
|
|
1946
|
-
const bySrc = new Map();
|
|
2050
|
+
const limitParam = Number(url.searchParams.get("limit"));
|
|
2051
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0
|
|
2052
|
+
? Math.min(Math.floor(limitParam), PROJECT_USAGE_MAX_ENTRIES)
|
|
2053
|
+
: PROJECT_USAGE_MAX_ENTRIES;
|
|
2054
|
+
const { timeZoneContext, hasRange, dayInRange, projectRows } =
|
|
2055
|
+
readProjectUsageContext(qp, url);
|
|
2056
|
+
|
|
2057
|
+
const aggregateEntries = (rows, keyOf, refOf) => {
|
|
2058
|
+
const byKey = new Map();
|
|
1947
2059
|
for (const row of rows) {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
// domains (e.g. codex.ai, cursor.ai) and was sent to the
|
|
1955
|
-
// dashboard as a clickable href before v0.11.1 / this commit.
|
|
1956
|
-
project_ref: "",
|
|
2060
|
+
if (hasRange && !dayInRange(rowDayKey(row, timeZoneContext))) continue;
|
|
2061
|
+
const key = keyOf(row);
|
|
2062
|
+
if (!byKey.has(key)) {
|
|
2063
|
+
byKey.set(key, {
|
|
2064
|
+
project_key: key,
|
|
2065
|
+
project_ref: refOf(row),
|
|
1957
2066
|
total_tokens: 0,
|
|
1958
2067
|
billable_total_tokens: 0,
|
|
2068
|
+
input_tokens: 0,
|
|
2069
|
+
output_tokens: 0,
|
|
2070
|
+
cached_input_tokens: 0,
|
|
2071
|
+
cache_creation_input_tokens: 0,
|
|
2072
|
+
reasoning_output_tokens: 0,
|
|
2073
|
+
conversation_count: 0,
|
|
2074
|
+
sourceTotals: new Map(),
|
|
1959
2075
|
});
|
|
1960
2076
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
2077
|
+
const agg = byKey.get(key);
|
|
2078
|
+
agg.total_tokens += Number(row.total_tokens || 0);
|
|
2079
|
+
agg.billable_total_tokens += Number(
|
|
2080
|
+
row.billable_total_tokens ?? row.total_tokens ?? 0,
|
|
2081
|
+
);
|
|
2082
|
+
agg.input_tokens += Number(row.input_tokens || 0);
|
|
2083
|
+
agg.output_tokens += Number(row.output_tokens || 0);
|
|
2084
|
+
agg.cached_input_tokens += Number(row.cached_input_tokens || 0);
|
|
2085
|
+
agg.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
|
|
2086
|
+
agg.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
|
|
2087
|
+
agg.conversation_count += Number(row.conversation_count || 0);
|
|
2088
|
+
if (!agg.project_ref) agg.project_ref = refOf(row);
|
|
2089
|
+
const src = row.source || "unknown";
|
|
2090
|
+
agg.sourceTotals.set(
|
|
2091
|
+
src,
|
|
2092
|
+
(agg.sourceTotals.get(src) || 0) + Number(row.total_tokens || 0),
|
|
2093
|
+
);
|
|
1963
2094
|
}
|
|
1964
|
-
|
|
1965
|
-
.sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
|
|
1966
|
-
.map((e) => ({
|
|
1967
|
-
...e,
|
|
1968
|
-
total_tokens: String(e.total_tokens),
|
|
1969
|
-
billable_total_tokens: String(e.billable_total_tokens),
|
|
1970
|
-
}));
|
|
1971
|
-
} else {
|
|
1972
|
-
entries = Array.from(byProject.values())
|
|
2095
|
+
return Array.from(byKey.values())
|
|
1973
2096
|
.sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
|
|
1974
|
-
.
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2097
|
+
.slice(0, limit)
|
|
2098
|
+
.map(({ sourceTotals, ...entry }) => ({
|
|
2099
|
+
...entry,
|
|
2100
|
+
total_tokens: String(entry.total_tokens),
|
|
2101
|
+
billable_total_tokens: String(entry.billable_total_tokens),
|
|
2102
|
+
sources: Array.from(sourceTotals.entries())
|
|
2103
|
+
.sort((a, b) => b[1] - a[1])
|
|
2104
|
+
.map(([source, totalTokens]) => ({ source, total_tokens: totalTokens })),
|
|
1978
2105
|
}));
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
let entries = aggregateEntries(
|
|
2109
|
+
projectRows,
|
|
2110
|
+
(row) => row.project_key || "unknown",
|
|
2111
|
+
(row) => row.project_ref || "",
|
|
2112
|
+
);
|
|
2113
|
+
|
|
2114
|
+
// If no project-attributed rows exist yet (user hasn't synced project
|
|
2115
|
+
// attribution, or never used a project-capable CLI), fall back to
|
|
2116
|
+
// per-source aggregation over the main queue so the panel isn't
|
|
2117
|
+
// totally empty. This path used to also exist for the non-empty case
|
|
2118
|
+
// and produce wrong numbers; keep it only as the empty fallback.
|
|
2119
|
+
if (entries.length === 0 && projectRows.length === 0) {
|
|
2120
|
+
entries = aggregateEntries(
|
|
2121
|
+
readQueueData(qp),
|
|
2122
|
+
(row) => row.source || "unknown",
|
|
2123
|
+
// Synthetic source-only row: leave project_ref empty rather than
|
|
2124
|
+
// fabricating `https://${src}.ai`, which resolves to unrelated
|
|
2125
|
+
// domains (e.g. codex.ai, cursor.ai) and was sent to the
|
|
2126
|
+
// dashboard as a clickable href before v0.11.1 / this commit.
|
|
2127
|
+
() => "",
|
|
2128
|
+
);
|
|
1979
2129
|
}
|
|
1980
2130
|
|
|
1981
2131
|
json(res, { generated_at: new Date().toISOString(), entries });
|
|
1982
2132
|
return true;
|
|
1983
2133
|
}
|
|
1984
2134
|
|
|
2135
|
+
// --- project-usage-detail ---
|
|
2136
|
+
// Per-project drill-down for the Project Usage modal: daily series,
|
|
2137
|
+
// per-source breakdown, and aggregate stats. Everything comes from the
|
|
2138
|
+
// local project bucket log — no external requests, no git access.
|
|
2139
|
+
if (p === "/functions/tokentracker-project-usage-detail") {
|
|
2140
|
+
const projectKey = url.searchParams.get("project_key") || "";
|
|
2141
|
+
if (!projectKey) {
|
|
2142
|
+
json(res, { error: "missing_project_key" }, 400);
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
const { from, to, timeZoneContext, dayInRange, projectRows } =
|
|
2146
|
+
readProjectUsageContext(qp, url);
|
|
2147
|
+
|
|
2148
|
+
const emptyMeasures = () => ({
|
|
2149
|
+
total_tokens: 0,
|
|
2150
|
+
billable_total_tokens: 0,
|
|
2151
|
+
input_tokens: 0,
|
|
2152
|
+
output_tokens: 0,
|
|
2153
|
+
cached_input_tokens: 0,
|
|
2154
|
+
cache_creation_input_tokens: 0,
|
|
2155
|
+
reasoning_output_tokens: 0,
|
|
2156
|
+
conversation_count: 0,
|
|
2157
|
+
});
|
|
2158
|
+
const addMeasures = (acc, row) => {
|
|
2159
|
+
acc.total_tokens += Number(row.total_tokens || 0);
|
|
2160
|
+
acc.billable_total_tokens += Number(
|
|
2161
|
+
row.billable_total_tokens ?? row.total_tokens ?? 0,
|
|
2162
|
+
);
|
|
2163
|
+
acc.input_tokens += Number(row.input_tokens || 0);
|
|
2164
|
+
acc.output_tokens += Number(row.output_tokens || 0);
|
|
2165
|
+
acc.cached_input_tokens += Number(row.cached_input_tokens || 0);
|
|
2166
|
+
acc.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
|
|
2167
|
+
acc.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
|
|
2168
|
+
acc.conversation_count += Number(row.conversation_count || 0);
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
let projectRef = "";
|
|
2172
|
+
const totals = emptyMeasures();
|
|
2173
|
+
const byDay = new Map();
|
|
2174
|
+
const bySource = new Map();
|
|
2175
|
+
let rangeTotalTokens = 0;
|
|
2176
|
+
|
|
2177
|
+
for (const row of projectRows) {
|
|
2178
|
+
const day = rowDayKey(row, timeZoneContext);
|
|
2179
|
+
if (!dayInRange(day)) continue;
|
|
2180
|
+
rangeTotalTokens += Number(row.total_tokens || 0);
|
|
2181
|
+
if (row.project_key !== projectKey) continue;
|
|
2182
|
+
if (!projectRef && row.project_ref) projectRef = row.project_ref;
|
|
2183
|
+
addMeasures(totals, row);
|
|
2184
|
+
|
|
2185
|
+
const src = row.source || "unknown";
|
|
2186
|
+
if (day) {
|
|
2187
|
+
// `models` keyed by source (project rows carry no model column) —
|
|
2188
|
+
// it feeds TrendMonitor's stacked-segment slot so the modal chart
|
|
2189
|
+
// colors by provider exactly like the dashboard's Usage Trend.
|
|
2190
|
+
if (!byDay.has(day)) byDay.set(day, { day, ...emptyMeasures(), models: {} });
|
|
2191
|
+
const dayAgg = byDay.get(day);
|
|
2192
|
+
addMeasures(dayAgg, row);
|
|
2193
|
+
dayAgg.models[src] = (dayAgg.models[src] || 0) + Number(row.total_tokens || 0);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (!bySource.has(src)) {
|
|
2197
|
+
bySource.set(src, {
|
|
2198
|
+
source: src,
|
|
2199
|
+
total_tokens: 0,
|
|
2200
|
+
conversation_count: 0,
|
|
2201
|
+
dayKeys: new Set(),
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
const srcAgg = bySource.get(src);
|
|
2205
|
+
srcAgg.total_tokens += Number(row.total_tokens || 0);
|
|
2206
|
+
srcAgg.conversation_count += Number(row.conversation_count || 0);
|
|
2207
|
+
if (day) srcAgg.dayKeys.add(day);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
json(res, {
|
|
2211
|
+
generated_at: new Date().toISOString(),
|
|
2212
|
+
from,
|
|
2213
|
+
to,
|
|
2214
|
+
project_key: projectKey,
|
|
2215
|
+
project_ref: projectRef,
|
|
2216
|
+
totals,
|
|
2217
|
+
days_active: byDay.size,
|
|
2218
|
+
// All-project total over the same range so the client can render
|
|
2219
|
+
// "share of everything you tracked" without a second request.
|
|
2220
|
+
range_total_tokens: rangeTotalTokens,
|
|
2221
|
+
daily: Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day)),
|
|
2222
|
+
sources: Array.from(bySource.values())
|
|
2223
|
+
.sort((a, b) => b.total_tokens - a.total_tokens)
|
|
2224
|
+
.map(({ dayKeys, ...src }) => ({ ...src, days_active: dayKeys.size })),
|
|
2225
|
+
});
|
|
2226
|
+
return true;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1985
2229
|
// --- user-status (stub) ---
|
|
1986
2230
|
if (p === "/functions/tokentracker-user-status") {
|
|
1987
2231
|
json(res, {
|
|
@@ -2312,6 +2556,18 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
2312
2556
|
return true;
|
|
2313
2557
|
}
|
|
2314
2558
|
|
|
2559
|
+
// --- achievements (local badges) ---
|
|
2560
|
+
if (p === "/functions/tokentracker-achievements") {
|
|
2561
|
+
const timeZoneContext = getTimeZoneContext(url);
|
|
2562
|
+
const queueRows = readQueueData(qp);
|
|
2563
|
+
const { projectRows } = readProjectUsageContext(qp, url);
|
|
2564
|
+
json(res, {
|
|
2565
|
+
generated_at: new Date().toISOString(),
|
|
2566
|
+
achievements: computeLocalAchievements(queueRows, projectRows, { timeZoneContext }),
|
|
2567
|
+
});
|
|
2568
|
+
return true;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2315
2571
|
// --- usage-limits ---
|
|
2316
2572
|
if (p === "/functions/tokentracker-usage-limits") {
|
|
2317
2573
|
const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
|
|
@@ -2353,4 +2609,7 @@ module.exports = {
|
|
|
2353
2609
|
// to the machine, not the hostname.
|
|
2354
2610
|
getOrCreateMachineId,
|
|
2355
2611
|
computeStableMachineId,
|
|
2612
|
+
// Local achievement compute — exported for test/local-achievements.test.js.
|
|
2613
|
+
computeLocalAchievements,
|
|
2614
|
+
LOCAL_BADGE_THRESHOLDS,
|
|
2356
2615
|
};
|