tokentracker-cli 0.77.2 → 0.78.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.
Files changed (80) hide show
  1. package/README.de.md +21 -0
  2. package/README.md +21 -0
  3. package/dashboard/dist/assets/ActivityHeatmap-5352XxLR.js +42 -0
  4. package/dashboard/dist/assets/{Card-D_aKGDr7.js → Card-Ca_hYHhx.js} +1 -1
  5. package/dashboard/dist/assets/{ClawdAnimated-vRiBiNhb.js → ClawdAnimated-DSgOS5ND.js} +1 -1
  6. package/dashboard/dist/assets/{CommunityStatsModal-CjetJZ-5.js → CommunityStatsModal-o8uQVQxu.js} +1 -1
  7. package/dashboard/dist/assets/ConfirmModal-D1hi9XUf.js +1 -0
  8. package/dashboard/dist/assets/Controls-CwnKP56V.js +1 -0
  9. package/dashboard/dist/assets/DashboardPage-CDv0TIOL.js +19 -0
  10. package/dashboard/dist/assets/{DevicePage-YAo4ert4.js → DevicePage-BlfIHcZX.js} +1 -1
  11. package/dashboard/dist/assets/DialogClose-CismfYwe.js +1 -0
  12. package/dashboard/dist/assets/DialogDescription-DWKp1yez.js +1 -0
  13. package/dashboard/dist/assets/{DialogTitle-CA5znwms.js → DialogTitle-DuuTVbUb.js} +1 -1
  14. package/dashboard/dist/assets/{FadeIn--ff4ZbC-.js → FadeIn-DpnitRKo.js} +1 -1
  15. package/dashboard/dist/assets/{HeaderGithubStar-CTZvanhz.js → HeaderGithubStar-D6zy-ADl.js} +1 -1
  16. package/dashboard/dist/assets/{IpCheckPage-Bg47gNVu.js → IpCheckPage-yUNp0VNe.js} +1 -1
  17. package/dashboard/dist/assets/{LandingPage-DZ8Mz38n.js → LandingPage-BvmpAaVn.js} +1 -1
  18. package/dashboard/dist/assets/LeaderboardAvatar-2WTIeXye.js +1 -0
  19. package/dashboard/dist/assets/LeaderboardPage-Bq3-VO7w.js +2 -0
  20. package/dashboard/dist/assets/{LeaderboardProfileModal-CfVMENby.js → LeaderboardProfileModal-DH0EEK6q.js} +3 -3
  21. package/dashboard/dist/assets/{LeaderboardProfilePage-C4bGXJCE.js → LeaderboardProfilePage-uJAng2JE.js} +3 -3
  22. package/dashboard/dist/assets/{LimitsPage-BAQ10X55.js → LimitsPage-D8UohgJm.js} +2 -2
  23. package/dashboard/dist/assets/{LocalOnlyNotice-gtHCn7Mx.js → LocalOnlyNotice-Dv_BnDVV.js} +1 -1
  24. package/dashboard/dist/assets/{LoginPage-TXvIBzoW.js → LoginPage-D6b6DZU3.js} +1 -1
  25. package/dashboard/dist/assets/PetPage-DfpBkuCa.js +1 -0
  26. package/dashboard/dist/assets/{PopoverPopup-CQxxgUXk.js → PopoverPopup-CjzdZn-9.js} +1 -1
  27. package/dashboard/dist/assets/{ResetPasswordPage-tN0tmSfP.js → ResetPasswordPage-BnOCFkwe.js} +1 -1
  28. package/dashboard/dist/assets/Select-CHtQAiLD.js +1 -0
  29. package/dashboard/dist/assets/{SelectItemText-2LjzGvo5.js → SelectItemText-Vjqvf8wO.js} +1 -1
  30. package/dashboard/dist/assets/SettingsPage-B-iHGL4q.js +1 -0
  31. package/dashboard/dist/assets/SkillsPage-DtG2aV-q.js +1 -0
  32. package/dashboard/dist/assets/WidgetsPage-4kqTtWfK.js +1 -0
  33. package/dashboard/dist/assets/{WrappedPage-YxshTScX.js → WrappedPage-DMMRpani.js} +1 -1
  34. package/dashboard/dist/assets/agent-logos-D0vYkEpX.js +1 -0
  35. package/dashboard/dist/assets/{arrow-up-right-Bm3t-iA0.js → arrow-up-right-BAQYGZkR.js} +1 -1
  36. package/dashboard/dist/assets/check-C3RPBZiS.js +1 -0
  37. package/dashboard/dist/assets/{copy-DUNz-xWm.js → copy-gG_BLBs8.js} +1 -1
  38. package/dashboard/dist/assets/{download-COSLX73O.js → download-BQOcg3gT.js} +1 -1
  39. package/dashboard/dist/assets/{format-tokens-CR28AiQO.js → format-tokens-CGK6lQAf.js} +1 -1
  40. package/dashboard/dist/assets/{info-Cm7V_KDO.js → info-C_BrDrcq.js} +1 -1
  41. package/dashboard/dist/assets/{link-2-CYqDGrkv.js → link-2-DFwCBbIZ.js} +1 -1
  42. package/dashboard/dist/assets/{loader-circle-6XAdy2ec.js → loader-circle-BMFI_BJ8.js} +1 -1
  43. package/dashboard/dist/assets/main-Dy8Y_qst.css +1 -0
  44. package/dashboard/dist/assets/main-w0hAaCsy.js +1190 -0
  45. package/dashboard/dist/assets/{trash-2-DKLewOQb.js → trash-2-GeZ6IzgV.js} +1 -1
  46. package/dashboard/dist/assets/{use-community-stats-AdPaun74.js → use-community-stats-pC8cmVof.js} +1 -1
  47. package/dashboard/dist/assets/{use-limits-display-prefs-Bau4ykMw.js → use-limits-display-prefs-DnFpPcA3.js} +1 -1
  48. package/dashboard/dist/assets/use-native-settings-CvcOSUhn.js +1 -0
  49. package/dashboard/dist/assets/{use-ordered-list-BVaEJaWa.js → use-ordered-list-CbMroVZ8.js} +2 -2
  50. package/dashboard/dist/assets/{use-quality-per-dollar-pref-Htlc-uFe.js → use-quality-per-dollar-pref-Bn3Al6yX.js} +1 -1
  51. package/dashboard/dist/assets/{use-usage-limits-BnzWTJQS.js → use-usage-limits-D08q_Dr8.js} +1 -1
  52. package/dashboard/dist/assets/{useCurrency-bd6TLRsc.js → useCurrency-h3kqF52v.js} +1 -1
  53. package/dashboard/dist/assets/{useScrollLock-ajoWrfku.js → useScrollLock-9au2731u.js} +1 -1
  54. package/dashboard/dist/index.html +2 -2
  55. package/dashboard/dist/share.html +2 -2
  56. package/package.json +1 -1
  57. package/src/commands/status.js +119 -12
  58. package/src/commands/sync.js +158 -83
  59. package/src/lib/install-resolver.js +1 -0
  60. package/src/lib/local-api.js +202 -56
  61. package/src/lib/pricing/seed-snapshot.json +1 -1
  62. package/src/lib/rollout.js +18 -2
  63. package/dashboard/dist/assets/ActivityHeatmap-CH6m_ZJM.js +0 -42
  64. package/dashboard/dist/assets/ConfirmModal-C6bZw6FZ.js +0 -1
  65. package/dashboard/dist/assets/Controls-Cj22oGL3.js +0 -1
  66. package/dashboard/dist/assets/DashboardPage-DPxiQkQw.js +0 -19
  67. package/dashboard/dist/assets/DialogClose-hhTDeRp1.js +0 -1
  68. package/dashboard/dist/assets/DialogDescription-gxEpcUbJ.js +0 -1
  69. package/dashboard/dist/assets/LeaderboardAvatar-3T57GKKK.js +0 -1
  70. package/dashboard/dist/assets/LeaderboardPage-wP_yCU9I.js +0 -2
  71. package/dashboard/dist/assets/PetPage-Dj1jrxYe.js +0 -1
  72. package/dashboard/dist/assets/Select-DnbHYPS4.js +0 -1
  73. package/dashboard/dist/assets/SettingsPage-DosKS_L6.js +0 -1
  74. package/dashboard/dist/assets/SkillsPage-DaCJ25vT.js +0 -1
  75. package/dashboard/dist/assets/WidgetsPage-CCGemQVD.js +0 -1
  76. package/dashboard/dist/assets/agent-logos-SeuxwQsp.js +0 -1
  77. package/dashboard/dist/assets/check-D8Owfjyw.js +0 -1
  78. package/dashboard/dist/assets/main-BCstrDYE.js +0 -1172
  79. package/dashboard/dist/assets/main-BYdJ5vWR.css +0 -1
  80. package/dashboard/dist/assets/use-native-settings-CidwlprT.js +0 -1
@@ -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 {
@@ -1912,76 +1949,185 @@ function createLocalApiHandler({ queuePath }) {
1912
1949
  // hour_start). Falling back to "session-file count × total tokens"
1913
1950
  // (the old behavior) produced pure fiction: every short-and-hot
1914
1951
  // project got the same weight as every long-and-cold one.
1915
- const projectQueuePath = path.join(
1916
- path.dirname(qp),
1917
- "project.queue.jsonl",
1918
- );
1919
- const projectRows = readProjectQueueData(projectQueuePath);
1920
-
1921
- const byProject = new Map();
1922
- for (const row of projectRows) {
1923
- const key = row.project_key || "unknown";
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();
1952
+ const limitParam = Number(url.searchParams.get("limit"));
1953
+ const limit = Number.isFinite(limitParam) && limitParam > 0
1954
+ ? Math.min(Math.floor(limitParam), PROJECT_USAGE_MAX_ENTRIES)
1955
+ : PROJECT_USAGE_MAX_ENTRIES;
1956
+ const { timeZoneContext, hasRange, dayInRange, projectRows } =
1957
+ readProjectUsageContext(qp, url);
1958
+
1959
+ const aggregateEntries = (rows, keyOf, refOf) => {
1960
+ const byKey = new Map();
1947
1961
  for (const row of rows) {
1948
- const src = row.source || "unknown";
1949
- if (!bySrc.has(src)) {
1950
- bySrc.set(src, {
1951
- project_key: src,
1952
- // Synthetic source-only row: leave project_ref empty rather than
1953
- // fabricating `https://${src}.ai`, which resolves to unrelated
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: "",
1962
+ if (hasRange && !dayInRange(rowDayKey(row, timeZoneContext))) continue;
1963
+ const key = keyOf(row);
1964
+ if (!byKey.has(key)) {
1965
+ byKey.set(key, {
1966
+ project_key: key,
1967
+ project_ref: refOf(row),
1957
1968
  total_tokens: 0,
1958
1969
  billable_total_tokens: 0,
1970
+ input_tokens: 0,
1971
+ output_tokens: 0,
1972
+ cached_input_tokens: 0,
1973
+ cache_creation_input_tokens: 0,
1974
+ reasoning_output_tokens: 0,
1975
+ conversation_count: 0,
1976
+ sourceTotals: new Map(),
1959
1977
  });
1960
1978
  }
1961
- bySrc.get(src).total_tokens += row.total_tokens || 0;
1962
- bySrc.get(src).billable_total_tokens += row.total_tokens || 0;
1979
+ const agg = byKey.get(key);
1980
+ agg.total_tokens += Number(row.total_tokens || 0);
1981
+ agg.billable_total_tokens += Number(
1982
+ row.billable_total_tokens ?? row.total_tokens ?? 0,
1983
+ );
1984
+ agg.input_tokens += Number(row.input_tokens || 0);
1985
+ agg.output_tokens += Number(row.output_tokens || 0);
1986
+ agg.cached_input_tokens += Number(row.cached_input_tokens || 0);
1987
+ agg.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
1988
+ agg.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
1989
+ agg.conversation_count += Number(row.conversation_count || 0);
1990
+ if (!agg.project_ref) agg.project_ref = refOf(row);
1991
+ const src = row.source || "unknown";
1992
+ agg.sourceTotals.set(
1993
+ src,
1994
+ (agg.sourceTotals.get(src) || 0) + Number(row.total_tokens || 0),
1995
+ );
1963
1996
  }
1964
- entries = Array.from(bySrc.values())
1997
+ return Array.from(byKey.values())
1965
1998
  .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())
1973
- .sort((a, b) => b.billable_total_tokens - a.billable_total_tokens)
1974
- .map((e) => ({
1975
- ...e,
1976
- total_tokens: String(e.total_tokens),
1977
- billable_total_tokens: String(e.billable_total_tokens),
1999
+ .slice(0, limit)
2000
+ .map(({ sourceTotals, ...entry }) => ({
2001
+ ...entry,
2002
+ total_tokens: String(entry.total_tokens),
2003
+ billable_total_tokens: String(entry.billable_total_tokens),
2004
+ sources: Array.from(sourceTotals.entries())
2005
+ .sort((a, b) => b[1] - a[1])
2006
+ .map(([source, totalTokens]) => ({ source, total_tokens: totalTokens })),
1978
2007
  }));
2008
+ };
2009
+
2010
+ let entries = aggregateEntries(
2011
+ projectRows,
2012
+ (row) => row.project_key || "unknown",
2013
+ (row) => row.project_ref || "",
2014
+ );
2015
+
2016
+ // If no project-attributed rows exist yet (user hasn't synced project
2017
+ // attribution, or never used a project-capable CLI), fall back to
2018
+ // per-source aggregation over the main queue so the panel isn't
2019
+ // totally empty. This path used to also exist for the non-empty case
2020
+ // and produce wrong numbers; keep it only as the empty fallback.
2021
+ if (entries.length === 0 && projectRows.length === 0) {
2022
+ entries = aggregateEntries(
2023
+ readQueueData(qp),
2024
+ (row) => row.source || "unknown",
2025
+ // Synthetic source-only row: leave project_ref empty rather than
2026
+ // fabricating `https://${src}.ai`, which resolves to unrelated
2027
+ // domains (e.g. codex.ai, cursor.ai) and was sent to the
2028
+ // dashboard as a clickable href before v0.11.1 / this commit.
2029
+ () => "",
2030
+ );
1979
2031
  }
1980
2032
 
1981
2033
  json(res, { generated_at: new Date().toISOString(), entries });
1982
2034
  return true;
1983
2035
  }
1984
2036
 
2037
+ // --- project-usage-detail ---
2038
+ // Per-project drill-down for the Project Usage modal: daily series,
2039
+ // per-source breakdown, and aggregate stats. Everything comes from the
2040
+ // local project bucket log — no external requests, no git access.
2041
+ if (p === "/functions/tokentracker-project-usage-detail") {
2042
+ const projectKey = url.searchParams.get("project_key") || "";
2043
+ if (!projectKey) {
2044
+ json(res, { error: "missing_project_key" }, 400);
2045
+ return true;
2046
+ }
2047
+ const { from, to, timeZoneContext, dayInRange, projectRows } =
2048
+ readProjectUsageContext(qp, url);
2049
+
2050
+ const emptyMeasures = () => ({
2051
+ total_tokens: 0,
2052
+ billable_total_tokens: 0,
2053
+ input_tokens: 0,
2054
+ output_tokens: 0,
2055
+ cached_input_tokens: 0,
2056
+ cache_creation_input_tokens: 0,
2057
+ reasoning_output_tokens: 0,
2058
+ conversation_count: 0,
2059
+ });
2060
+ const addMeasures = (acc, row) => {
2061
+ acc.total_tokens += Number(row.total_tokens || 0);
2062
+ acc.billable_total_tokens += Number(
2063
+ row.billable_total_tokens ?? row.total_tokens ?? 0,
2064
+ );
2065
+ acc.input_tokens += Number(row.input_tokens || 0);
2066
+ acc.output_tokens += Number(row.output_tokens || 0);
2067
+ acc.cached_input_tokens += Number(row.cached_input_tokens || 0);
2068
+ acc.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
2069
+ acc.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
2070
+ acc.conversation_count += Number(row.conversation_count || 0);
2071
+ };
2072
+
2073
+ let projectRef = "";
2074
+ const totals = emptyMeasures();
2075
+ const byDay = new Map();
2076
+ const bySource = new Map();
2077
+ let rangeTotalTokens = 0;
2078
+
2079
+ for (const row of projectRows) {
2080
+ const day = rowDayKey(row, timeZoneContext);
2081
+ if (!dayInRange(day)) continue;
2082
+ rangeTotalTokens += Number(row.total_tokens || 0);
2083
+ if (row.project_key !== projectKey) continue;
2084
+ if (!projectRef && row.project_ref) projectRef = row.project_ref;
2085
+ addMeasures(totals, row);
2086
+
2087
+ const src = row.source || "unknown";
2088
+ if (day) {
2089
+ // `models` keyed by source (project rows carry no model column) —
2090
+ // it feeds TrendMonitor's stacked-segment slot so the modal chart
2091
+ // colors by provider exactly like the dashboard's Usage Trend.
2092
+ if (!byDay.has(day)) byDay.set(day, { day, ...emptyMeasures(), models: {} });
2093
+ const dayAgg = byDay.get(day);
2094
+ addMeasures(dayAgg, row);
2095
+ dayAgg.models[src] = (dayAgg.models[src] || 0) + Number(row.total_tokens || 0);
2096
+ }
2097
+
2098
+ if (!bySource.has(src)) {
2099
+ bySource.set(src, {
2100
+ source: src,
2101
+ total_tokens: 0,
2102
+ conversation_count: 0,
2103
+ dayKeys: new Set(),
2104
+ });
2105
+ }
2106
+ const srcAgg = bySource.get(src);
2107
+ srcAgg.total_tokens += Number(row.total_tokens || 0);
2108
+ srcAgg.conversation_count += Number(row.conversation_count || 0);
2109
+ if (day) srcAgg.dayKeys.add(day);
2110
+ }
2111
+
2112
+ json(res, {
2113
+ generated_at: new Date().toISOString(),
2114
+ from,
2115
+ to,
2116
+ project_key: projectKey,
2117
+ project_ref: projectRef,
2118
+ totals,
2119
+ days_active: byDay.size,
2120
+ // All-project total over the same range so the client can render
2121
+ // "share of everything you tracked" without a second request.
2122
+ range_total_tokens: rangeTotalTokens,
2123
+ daily: Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day)),
2124
+ sources: Array.from(bySource.values())
2125
+ .sort((a, b) => b.total_tokens - a.total_tokens)
2126
+ .map(({ dayKeys, ...src }) => ({ ...src, days_active: dayKeys.size })),
2127
+ });
2128
+ return true;
2129
+ }
2130
+
1985
2131
  // --- user-status (stub) ---
1986
2132
  if (p === "/functions/tokentracker-user-status") {
1987
2133
  json(res, {