tokentracker-cli 0.84.9 → 0.85.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 (102) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-BgBFpxmU.js → AchievementBadge-DEqbHlmn.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DHPD3Fnu.js → AchievementsPage-BOaEu1AQ.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DouWVD-3.js → AchievementsSection-C370ngO3.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-Df9eQiA3.js → BadgeDetailModal-D7Wkef0K.js} +1 -1
  5. package/dashboard/dist/assets/{Card-CQ9wBnz6.js → Card-mDEVjNdM.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-XIO-I-5v.js → ClawdAnimated-DdsX-z8n.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-Cy1cJ91B.js → CommandPalette-DhxRIuHC.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-CwKG4LQM.js → CommunityStatsModal-CG-9JPS6.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-BNcWqick.js → ConfirmModal-COhxErLm.js} +1 -1
  10. package/dashboard/dist/assets/DashboardPage-tX0DKgFE.js +1 -0
  11. package/dashboard/dist/assets/{DevicePage-pJ2jNkZV.js → DevicePage-BsSeHx84.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-C2SOtkJN.js → DialogClose-eyOW2cbx.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-CGSbB8G7.js → DialogTitle-H8sGiH7c.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-CZSiw7me.js → FadeIn-pq2ja50M.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-ZmQhX_kR.js → HeaderGithubStar-DWgcpDHr.js} +1 -1
  17. package/dashboard/dist/assets/{HoverTooltip-D_YdG369.js → HoverTooltip-CcJ_VR65.js} +1 -1
  18. package/dashboard/dist/assets/{IpCheckPage-du886dF0.js → IpCheckPage-CNUN346Q.js} +1 -1
  19. package/dashboard/dist/assets/{LandingPage-BuAak8lU.js → LandingPage-yaoRGsNI.js} +2 -2
  20. package/dashboard/dist/assets/{LeaderboardAvatar-nU0ARYnN.js → LeaderboardAvatar-Ddai4I12.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-CngwCvC7.js → LeaderboardPage-Bajkxf63.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-CXZJ0e-T.js → LeaderboardProfileModal-BGYKDgIi.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CZve6s6b.js → LeaderboardProfilePage-Ck5vPaeN.js} +1 -1
  24. package/dashboard/dist/assets/{LimitsPage-CWvoJPk8.js → LimitsPage-Bqobtsw2.js} +1 -1
  25. package/dashboard/dist/assets/{LocalOnlyNotice-DyUAAyyE.js → LocalOnlyNotice-e9KSE3wj.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-eEiRfbdv.js → LoginCard-BhTSm2tb.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-C16WdIQm.js → LoginModal-CRwuThde.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-B2WqpE1t.js → LoginPage-j1i74lcc.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-DtQWrGmI.js → PetPage-r62ViU1X.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-CRhqtFLB.js → PopoverPopup-DopNTyD4.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-DWNH_wYu.js → ProviderIcon-c1LkPhYM.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-DzCczcWZ.js → ResetPasswordPage-DJdDNvtq.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-CV9ccq6R.js → SegmentedControl-DhWVOTPN.js} +1 -1
  34. package/dashboard/dist/assets/{Select-BrogNNk0.js → Select-DeUmTmsJ.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-Bpu3swt0.js → SelectItemText-pX0B5f7h.js} +1 -1
  36. package/dashboard/dist/assets/{ServiceStatusPage-D_FIrMJP.js → ServiceStatusPage-ejIPz6oF.js} +1 -1
  37. package/dashboard/dist/assets/{SessionsPage-C8TvTy1o.js → SessionsPage-WEbn90GW.js} +2 -2
  38. package/dashboard/dist/assets/{SettingsPage-Bd-tzJAR.js → SettingsPage-B8t5gCeI.js} +1 -1
  39. package/dashboard/dist/assets/{SkillsPage-CKGU9wQ4.js → SkillsPage-BWMlzeAp.js} +1 -1
  40. package/dashboard/dist/assets/{TokenGalaxy-DhHW6L-U.js → TokenGalaxy-DIBRBvMj.js} +1 -1
  41. package/dashboard/dist/assets/{ToolbarRootContext-DQo-4pPB.js → ToolbarRootContext-C6KxLvnI.js} +1 -1
  42. package/dashboard/dist/assets/{TrendMonitor-Ejl8d45g.js → TrendMonitor-BaMNh67w.js} +1 -1
  43. package/dashboard/dist/assets/{WidgetsPage-CY7f4wCD.js → WidgetsPage-DDsy4M3x.js} +1 -1
  44. package/dashboard/dist/assets/{WrappedPage-B4-B2v-v.js → WrappedPage-CiNnUFRI.js} +1 -1
  45. package/dashboard/dist/assets/{__vite-browser-external-BNUFXCfp.js → __vite-browser-external-CM-H8-LW.js} +23 -16
  46. package/dashboard/dist/assets/{agent-logos-BRzutmeR.js → agent-logos-BjVa6oe7.js} +1 -1
  47. package/dashboard/dist/assets/{arrow-up-right-BcrbPk8c.js → arrow-up-right-CCum1mqX.js} +1 -1
  48. package/dashboard/dist/assets/{check-BMxeFPfQ.js → check-ySRQc1YP.js} +1 -1
  49. package/dashboard/dist/assets/{chevron-right-DOstgiRf.js → chevron-right-5vFloHew.js} +1 -1
  50. package/dashboard/dist/assets/{copy-BsKO524t.js → copy-mhfKVMKo.js} +1 -1
  51. package/dashboard/dist/assets/{download-dadqxej7.js → download-GdrXglSx.js} +1 -1
  52. package/dashboard/dist/assets/{external-link-CTu3BMeU.js → external-link-KGFaKc8s.js} +1 -1
  53. package/dashboard/dist/assets/{flame-B_IvWcMn.js → flame-BmNtNoS1.js} +1 -1
  54. package/dashboard/dist/assets/{format-tokens-DIT6hLSb.js → format-tokens-CCpCNw3d.js} +1 -1
  55. package/dashboard/dist/assets/{icons-mWku76bu.js → icons-XLKZavTf.js} +1 -1
  56. package/dashboard/dist/assets/{index-D-gQV-hF.js → index-CsywTeCQ.js} +1 -1
  57. package/dashboard/dist/assets/{index-CboN9E82.js → index-IKpmeZte.js} +1 -1
  58. package/dashboard/dist/assets/{index-D5-w-aGy.js → index-_xU5U5Ct.js} +1 -1
  59. package/dashboard/dist/assets/{info-x6l352SW.js → info-DUV5UpmT.js} +1 -1
  60. package/dashboard/dist/assets/{limits-providers-Bo12VlLJ.js → limits-providers-DFkCZCZj.js} +1 -1
  61. package/dashboard/dist/assets/{link-2-eo_xPxH1.js → link-2-D7c64yLY.js} +1 -1
  62. package/dashboard/dist/assets/{loader-circle-C4n1iGBv.js → loader-circle-sMONRK2M.js} +1 -1
  63. package/dashboard/dist/assets/{maximize-2-pR8xwsI_.js → maximize-2-CVWRgrk9.js} +1 -1
  64. package/dashboard/dist/assets/{provider-display-ErggQWu4.js → provider-display-DSKqVjgm.js} +1 -1
  65. package/dashboard/dist/assets/{react-DYKg02Aq.js → react-c1q4mGkS.js} +1 -1
  66. package/dashboard/dist/assets/{search-B_jbUwPS.js → search-CwD3s-_C.js} +1 -1
  67. package/dashboard/dist/assets/{skills-api-DY5ruGmw.js → skills-api-BRXj8kjT.js} +1 -1
  68. package/dashboard/dist/assets/{store-Cu8i1y26.js → store-lqTW2gfK.js} +1 -1
  69. package/dashboard/dist/assets/{terminal-D3j6X68s.js → terminal-DU1_KbKo.js} +1 -1
  70. package/dashboard/dist/assets/{trash-2-CFpwgJw5.js → trash-2-B8Y5Xc-K.js} +1 -1
  71. package/dashboard/dist/assets/{use-community-stats-C0fKq66G.js → use-community-stats-DYwwimvp.js} +1 -1
  72. package/dashboard/dist/assets/{use-limits-display-prefs-TOp4UAnI.js → use-limits-display-prefs-Bst6ybK3.js} +1 -1
  73. package/dashboard/dist/assets/{use-native-settings-BZie_7z1.js → use-native-settings-Cqle4lgw.js} +1 -1
  74. package/dashboard/dist/assets/{use-ordered-list-Bq92YyUp.js → use-ordered-list-Dtze0ahb.js} +1 -1
  75. package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +1 -0
  76. package/dashboard/dist/assets/{use-session-efficiency-pref-BcpytkES.js → use-session-efficiency-pref-gwbDMHXa.js} +1 -1
  77. package/dashboard/dist/assets/{use-transform-Dmmrne2z.js → use-transform-CU0yxXpX.js} +1 -1
  78. package/dashboard/dist/assets/{use-usage-limits-Cw0Z9Tew.js → use-usage-limits-5y1I1T5_.js} +1 -1
  79. package/dashboard/dist/assets/{useCurrency-DyyonINj.js → useCurrency-BfVNrywe.js} +1 -1
  80. package/dashboard/dist/assets/{useScrollLock-DvWw86lJ.js → useScrollLock-CF1m7AMJ.js} +1 -1
  81. package/dashboard/dist/assets/{useTokenFormat-CBkvGfJ1.js → useTokenFormat-DiRwVvRr.js} +1 -1
  82. package/dashboard/dist/assets/{zap-CnYA39Ug.js → zap-BZtlWtuN.js} +1 -1
  83. package/dashboard/dist/index.html +1 -1
  84. package/dashboard/dist/ip-check.html +1 -1
  85. package/dashboard/dist/leaderboard.html +1 -1
  86. package/dashboard/dist/share.html +1 -1
  87. package/package.json +1 -1
  88. package/src/commands/status.js +114 -9
  89. package/src/commands/sync.js +151 -46
  90. package/src/lib/claude-categorizer.js +36 -9
  91. package/src/lib/diagnostics.js +86 -13
  92. package/src/lib/doctor.js +17 -0
  93. package/src/lib/grok-context-breakdown.js +794 -0
  94. package/src/lib/install-resolver.js +10 -2
  95. package/src/lib/local-api.js +21 -3
  96. package/src/lib/multi-install-parser.js +9 -1
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +337 -44
  99. package/src/lib/session-analytics.js +332 -9
  100. package/dashboard/dist/assets/DashboardPage-Dns_1yXL.js +0 -1
  101. package/dashboard/dist/assets/DialogDescription-Bhfqqmll.js +0 -1
  102. package/dashboard/dist/assets/use-reduced-motion-ZiVeOkvb.js +0 -1
@@ -0,0 +1,794 @@
1
+ // Grok Build "Context Breakdown" — Codex-style tool-oriented view.
2
+ //
3
+ // Privacy: tokens + timestamps + tool names only. Never return prompt text,
4
+ // assistant text, tool arguments, or file contents.
5
+ //
6
+ // Data source: ~/.grok/sessions/**/updates.jsonl
7
+ // Authoritative billable totals come from turn_completed.usage. Tool/exec
8
+ // attribution is heuristic: the turn's non-reasoning tokens are split evenly
9
+ // across tools observed in that turn (same approach as Codex).
10
+
11
+ "use strict";
12
+
13
+ const crypto = require("node:crypto");
14
+ const fs = require("node:fs");
15
+ const os = require("node:os");
16
+ const path = require("node:path");
17
+ const readline = require("node:readline");
18
+
19
+ const {
20
+ emptyTotals,
21
+ addInto,
22
+ roundTotals,
23
+ buildExecStatsEntry,
24
+ categorizeTool,
25
+ inferExecCommandKind,
26
+ sanitizeCommandSignature,
27
+ getExecutableName,
28
+ } = require("./categorizer-utils");
29
+
30
+ const CACHE_SCHEMA_VERSION = "grok-context-v1";
31
+ const CACHE = new Map();
32
+ const CACHE_TTL_MS = 60_000;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Time helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function dayKeyToIsoBounds(from, to) {
39
+ if (!from && !to) return { fromIso: null, toIso: null };
40
+ const fromDate = from ? new Date(`${from}T00:00:00Z`) : null;
41
+ const toDate = to ? new Date(`${to}T23:59:59Z`) : null;
42
+ if (fromDate && Number.isFinite(fromDate.getTime())) {
43
+ fromDate.setUTCHours(fromDate.getUTCHours() - 14);
44
+ }
45
+ if (toDate && Number.isFinite(toDate.getTime())) {
46
+ toDate.setUTCHours(toDate.getUTCHours() + 14);
47
+ }
48
+ return {
49
+ fromIso: fromDate ? fromDate.toISOString() : null,
50
+ toIso: toDate ? toDate.toISOString() : null,
51
+ };
52
+ }
53
+
54
+ function timestampToIso(value) {
55
+ if (value == null) return null;
56
+ if (typeof value === "number") {
57
+ if (!Number.isFinite(value) || value <= 0) return null;
58
+ const millis = value < 10_000_000_000 ? value * 1000 : value;
59
+ const dt = new Date(millis);
60
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
61
+ }
62
+ if (typeof value === "string") {
63
+ const trimmed = value.trim();
64
+ if (!trimmed) return null;
65
+ if (/^[0-9]+(?:\.[0-9]+)?$/.test(trimmed)) return timestampToIso(Number(trimmed));
66
+ const dt = new Date(trimmed);
67
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function dayKeyFromIso(iso, timeZoneContext) {
73
+ if (!iso) return "";
74
+ const { timeZone, offsetMinutes } = timeZoneContext || {};
75
+ const dt = new Date(iso);
76
+ if (!Number.isFinite(dt.getTime())) return iso.slice(0, 10);
77
+
78
+ if (timeZone && typeof Intl !== "undefined" && Intl.DateTimeFormat) {
79
+ try {
80
+ const parts = new Intl.DateTimeFormat("en-CA", {
81
+ timeZone,
82
+ year: "numeric",
83
+ month: "2-digit",
84
+ day: "2-digit",
85
+ hourCycle: "h23",
86
+ }).formatToParts(dt);
87
+ const values = {};
88
+ for (const part of parts) {
89
+ if (part.type !== "literal") values[part.type] = part.value;
90
+ }
91
+ if (values.year && values.month && values.day) {
92
+ return `${values.year}-${values.month}-${values.day}`;
93
+ }
94
+ } catch {
95
+ // fall through
96
+ }
97
+ }
98
+ if (Number.isFinite(offsetMinutes)) {
99
+ const shifted = new Date(dt.getTime() - Number(offsetMinutes) * 60_000);
100
+ return `${String(shifted.getUTCFullYear()).padStart(4, "0")}-${String(shifted.getUTCMonth() + 1).padStart(2, "0")}-${String(shifted.getUTCDate()).padStart(2, "0")}`;
101
+ }
102
+ return iso.slice(0, 10);
103
+ }
104
+
105
+ function isIsoInRange(iso, { from, to, timeZoneContext } = {}) {
106
+ if (!from && !to) return true;
107
+ const day = dayKeyFromIso(iso, timeZoneContext);
108
+ if (!day) return false;
109
+ if (from && day < from) return false;
110
+ if (to && day > to) return false;
111
+ return true;
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Grok path discovery
116
+ // ---------------------------------------------------------------------------
117
+
118
+ function resolveGrokHome(env = process.env) {
119
+ if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
120
+ if (env.GROK_HOME) return env.GROK_HOME;
121
+ return path.join(os.homedir(), ".grok");
122
+ }
123
+
124
+ function discoverGrokSessionFiles(env = process.env) {
125
+ const home = resolveGrokHome(env);
126
+ const sessionsRoot = path.join(home, "sessions");
127
+ if (!fs.existsSync(sessionsRoot)) return [];
128
+
129
+ const out = [];
130
+ let cwdDirs = [];
131
+ try {
132
+ cwdDirs = fs.readdirSync(sessionsRoot);
133
+ } catch {
134
+ return [];
135
+ }
136
+
137
+ for (const cwdDir of cwdDirs) {
138
+ const cwdPath = path.join(sessionsRoot, cwdDir);
139
+ let st;
140
+ try {
141
+ st = fs.statSync(cwdPath);
142
+ } catch {
143
+ continue;
144
+ }
145
+ if (!st.isDirectory()) continue;
146
+
147
+ let sessionIds = [];
148
+ try {
149
+ sessionIds = fs.readdirSync(cwdPath);
150
+ } catch {
151
+ continue;
152
+ }
153
+
154
+ for (const sid of sessionIds) {
155
+ const sessionDir = path.join(cwdPath, sid);
156
+ const updatesPath = path.join(sessionDir, "updates.jsonl");
157
+ let ust;
158
+ try {
159
+ ust = fs.statSync(updatesPath);
160
+ } catch {
161
+ continue;
162
+ }
163
+ if (!ust.isFile() || ust.size <= 0) continue;
164
+ out.push({
165
+ sessionId: sid,
166
+ sessionDir,
167
+ updatesPath,
168
+ mtimeMs: ust.mtimeMs,
169
+ size: ust.size,
170
+ });
171
+ }
172
+ }
173
+ out.sort((a, b) => a.updatesPath.localeCompare(b.updatesPath));
174
+ return out;
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Tool / exec naming
179
+ // ---------------------------------------------------------------------------
180
+
181
+ function mapGrokToolName(rawName) {
182
+ const name = String(rawName || "").trim();
183
+ if (!name) return "unknown";
184
+ const lower = name.toLowerCase();
185
+ if (lower === "read_file" || lower === "read") return "Read";
186
+ if (lower === "write" || lower === "write_file") return "Write";
187
+ if (lower === "search_replace" || lower === "str_replace" || lower === "edit") return "Edit";
188
+ if (lower === "grep") return "Grep";
189
+ if (lower === "list_dir" || lower === "glob" || lower === "find") return "Glob";
190
+ if (lower === "run_terminal_command" || lower === "bash" || lower === "shell") return "Bash";
191
+ if (lower === "todo_write" || lower === "todowrite") return "TodoWrite";
192
+ if (lower === "web_search" || lower === "websearch") return "WebSearch";
193
+ if (lower === "web_fetch" || lower === "webfetch") return "WebFetch";
194
+ if (lower.startsWith("mcp_") || lower.startsWith("mcp-")) {
195
+ return name.replace(/^mcp[_-]/i, "mcp__").replace(/-/g, "_");
196
+ }
197
+ return name;
198
+ }
199
+
200
+ function extractToolName(update, meta) {
201
+ const fromMetaTool =
202
+ update?._meta?.["x.ai/tool"]?.name ||
203
+ update?._meta?.["x.ai/tool"]?.label ||
204
+ null;
205
+ const fromUpdateParams = meta?.updateParams?.title || meta?.updateParams?.name || null;
206
+ const fromUpdate = update?.title || update?.name || null;
207
+ return mapGrokToolName(fromMetaTool || fromUpdateParams || fromUpdate || "unknown");
208
+ }
209
+
210
+ function extractExecCommand(update) {
211
+ const raw = update?.rawInput;
212
+ if (!raw || typeof raw !== "object") return null;
213
+ const command =
214
+ typeof raw.command === "string"
215
+ ? raw.command
216
+ : typeof raw.cmd === "string"
217
+ ? raw.cmd
218
+ : null;
219
+ if (!command || !command.trim()) return null;
220
+ return command.trim();
221
+ }
222
+
223
+ function durationBucket(ms) {
224
+ const n = Number(ms || 0);
225
+ if (n < 100) return "<100ms";
226
+ if (n < 500) return "100-500ms";
227
+ if (n < 2000) return "0.5-2s";
228
+ if (n < 10000) return "2-10s";
229
+ if (n < 60000) return "10-60s";
230
+ return ">=60s";
231
+ }
232
+
233
+ function outputSizeBucket(lines, chars) {
234
+ if (lines >= 500 || chars >= 50_000) return "huge";
235
+ if (lines >= 100 || chars >= 10_000) return "large";
236
+ if (lines >= 20 || chars >= 2_000) return "medium";
237
+ if (lines > 0 || chars > 0) return "small";
238
+ return "empty";
239
+ }
240
+
241
+ function readUsageTotals(usage) {
242
+ if (!usage || typeof usage !== "object") return null;
243
+ const inputRaw = Math.max(0, Number(usage.inputTokens ?? usage.input_tokens ?? 0) || 0);
244
+ const cached = Math.max(
245
+ 0,
246
+ Number(usage.cachedReadTokens ?? usage.cache_read_input_tokens ?? usage.cached_input_tokens ?? 0) || 0,
247
+ );
248
+ const output = Math.max(0, Number(usage.outputTokens ?? usage.output_tokens ?? 0) || 0);
249
+ const reasoning = Math.max(
250
+ 0,
251
+ Number(usage.reasoningTokens ?? usage.reasoning_output_tokens ?? 0) || 0,
252
+ );
253
+ const nonCachedInput = Math.max(0, inputRaw - cached);
254
+ // Prefer the reported totalTokens (authoritative billable total). Fallback
255
+ // includes reasoning so a missing total still matches usage-parser shape.
256
+ let total = Math.max(0, Number(usage.totalTokens ?? usage.total_tokens ?? 0) || 0);
257
+ if (total <= 0) total = nonCachedInput + cached + output + reasoning;
258
+ if (total <= 0) return null;
259
+ return {
260
+ input_tokens: nonCachedInput,
261
+ cached_input_tokens: cached,
262
+ cache_creation_input_tokens: 0,
263
+ output_tokens: output,
264
+ reasoning_output_tokens: reasoning,
265
+ total_tokens: total,
266
+ };
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Per-session parse
271
+ // ---------------------------------------------------------------------------
272
+
273
+ function ensureTool(map, name) {
274
+ if (!map.has(name)) map.set(name, { name, raw_name: name, calls: 0, totals: emptyTotals() });
275
+ return map.get(name);
276
+ }
277
+
278
+ function ensureExec(map, key) {
279
+ if (!map.has(key)) map.set(key, { name: key, ...buildExecStatsEntry() });
280
+ return map.get(key);
281
+ }
282
+
283
+ function absorbExecStats(map, key, details) {
284
+ const row = ensureExec(map, key);
285
+ row.calls += 1;
286
+ row.duration_ms += details.dur;
287
+ row.max_duration_ms = Math.max(row.max_duration_ms, details.dur);
288
+ row.output_chars += details.outputChars;
289
+ row.output_lines += details.outputLines;
290
+ if (details.failed) row.failures += 1;
291
+ }
292
+
293
+ function finalizeToolRows(map) {
294
+ return Array.from(map.values())
295
+ .map((row) => ({
296
+ name: row.name,
297
+ raw_name: row.raw_name || row.name,
298
+ calls: row.calls,
299
+ totals: roundTotals(row.totals),
300
+ }))
301
+ .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
302
+ }
303
+
304
+ function finalizeExecRows(map) {
305
+ return Array.from(map.values())
306
+ .map((row) => ({
307
+ name: row.name,
308
+ calls: row.calls,
309
+ failures: row.failures,
310
+ duration_ms: row.duration_ms,
311
+ max_duration_ms: row.max_duration_ms,
312
+ output_chars: row.output_chars,
313
+ output_lines: row.output_lines,
314
+ totals: roundTotals(row.totals),
315
+ }))
316
+ .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
317
+ }
318
+
319
+ async function parseGrokUpdatesFile(updatesPath, { from, to, timeZoneContext } = {}) {
320
+ const byTool = new Map();
321
+ const byExecKind = new Map();
322
+ const byExecExit = new Map();
323
+ const byExecExecutable = new Map();
324
+ const byExecCommand = new Map();
325
+ const byExecDuration = new Map();
326
+ const byExecOutput = new Map();
327
+ const totals = emptyTotals();
328
+ let turnCount = 0;
329
+
330
+ // promptId -> { tools: string[], execs: details[], sawThought: bool }
331
+ const turns = new Map();
332
+
333
+ function getTurn(key) {
334
+ if (!turns.has(key)) {
335
+ turns.set(key, { tools: [], execs: [], sawThought: false, sawMessage: false });
336
+ }
337
+ return turns.get(key);
338
+ }
339
+
340
+ function buildExecDetails(command, status) {
341
+ const kind = inferExecCommandKind(command);
342
+ const failed = String(status || "").toLowerCase() === "failed" || String(status || "").toLowerCase() === "error";
343
+ return {
344
+ kind,
345
+ exitKey: failed ? "failed:unknown" : "completed:0",
346
+ executable: getExecutableName(command),
347
+ command: sanitizeCommandSignature(command),
348
+ duration: durationBucket(0),
349
+ output: outputSizeBucket(0, 0),
350
+ dur: 0,
351
+ outputChars: 0,
352
+ outputLines: 0,
353
+ failed,
354
+ };
355
+ }
356
+
357
+ function attributeTurn(delta, turnState) {
358
+ if (!delta || delta.total_tokens <= 0) return;
359
+ turnCount += 1;
360
+
361
+ const uniqueTools = [...new Set((turnState?.tools || []).filter(Boolean))];
362
+ const tools = uniqueTools.length > 0 ? uniqueTools : ["text_response"];
363
+ const share = 1 / tools.length;
364
+
365
+ for (const name of tools) {
366
+ const row = ensureTool(byTool, name);
367
+ row.calls += share;
368
+ addInto(row.totals, {
369
+ input_tokens: delta.input_tokens * share,
370
+ cached_input_tokens: delta.cached_input_tokens * share,
371
+ cache_creation_input_tokens: delta.cache_creation_input_tokens * share,
372
+ output_tokens: delta.output_tokens * share,
373
+ reasoning_output_tokens: delta.reasoning_output_tokens * share,
374
+ total_tokens: delta.total_tokens * share,
375
+ });
376
+ }
377
+
378
+ // Prefer Bash bucket share when present (Codex attaches exec tokens only to
379
+ // the shell tool's fraction of the turn).
380
+ const bashShare = tools.includes("Bash") ? share : 0;
381
+ const execDelta =
382
+ bashShare > 0
383
+ ? {
384
+ input_tokens: delta.input_tokens * bashShare,
385
+ cached_input_tokens: delta.cached_input_tokens * bashShare,
386
+ cache_creation_input_tokens: delta.cache_creation_input_tokens * bashShare,
387
+ output_tokens: delta.output_tokens * bashShare,
388
+ reasoning_output_tokens: delta.reasoning_output_tokens * bashShare,
389
+ total_tokens: delta.total_tokens * bashShare,
390
+ }
391
+ : null;
392
+
393
+ const pendingExecDetails = turnState?.execs || [];
394
+ if (execDelta && pendingExecDetails.length > 0) {
395
+ const per = 1 / pendingExecDetails.length;
396
+ for (const details of pendingExecDetails) {
397
+ const attributed = {
398
+ input_tokens: execDelta.input_tokens * per,
399
+ cached_input_tokens: execDelta.cached_input_tokens * per,
400
+ cache_creation_input_tokens: execDelta.cache_creation_input_tokens * per,
401
+ output_tokens: execDelta.output_tokens * per,
402
+ reasoning_output_tokens: execDelta.reasoning_output_tokens * per,
403
+ total_tokens: execDelta.total_tokens * per,
404
+ };
405
+ addInto(ensureExec(byExecKind, details.kind).totals, attributed);
406
+ addInto(ensureExec(byExecExit, details.exitKey).totals, attributed);
407
+ addInto(ensureExec(byExecExecutable, details.executable).totals, attributed);
408
+ addInto(ensureExec(byExecCommand, details.command).totals, attributed);
409
+ addInto(ensureExec(byExecDuration, details.duration).totals, attributed);
410
+ addInto(ensureExec(byExecOutput, details.output).totals, attributed);
411
+ absorbExecStats(byExecKind, details.kind, details);
412
+ absorbExecStats(byExecExit, details.exitKey, details);
413
+ absorbExecStats(byExecExecutable, details.executable, details);
414
+ absorbExecStats(byExecCommand, details.command, details);
415
+ absorbExecStats(byExecDuration, details.duration, details);
416
+ absorbExecStats(byExecOutput, details.output, details);
417
+ }
418
+ } else {
419
+ for (const details of pendingExecDetails) {
420
+ absorbExecStats(byExecKind, details.kind, details);
421
+ absorbExecStats(byExecExit, details.exitKey, details);
422
+ absorbExecStats(byExecExecutable, details.executable, details);
423
+ absorbExecStats(byExecCommand, details.command, details);
424
+ absorbExecStats(byExecDuration, details.duration, details);
425
+ absorbExecStats(byExecOutput, details.output, details);
426
+ }
427
+ }
428
+
429
+ addInto(totals, delta);
430
+ }
431
+
432
+ if (!fs.existsSync(updatesPath)) {
433
+ return emptySessionResult();
434
+ }
435
+
436
+ const input = fs.createReadStream(updatesPath, { encoding: "utf8" });
437
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
438
+ try {
439
+ for await (const line of rl) {
440
+ if (!line || !line.trim()) continue;
441
+ let record;
442
+ try {
443
+ record = JSON.parse(line);
444
+ } catch {
445
+ continue;
446
+ }
447
+ const params = record?.params || {};
448
+ const meta = params._meta || record?._meta || {};
449
+ const update = params.update;
450
+ if (!update || typeof update !== "object") continue;
451
+
452
+ const sessionUpdate = update.sessionUpdate || meta.updateType || "";
453
+ const promptId =
454
+ meta.promptId ||
455
+ update.prompt_id ||
456
+ (meta.turnStartMs != null ? `turn-${meta.turnStartMs}` : null) ||
457
+ "unknown";
458
+ const ts =
459
+ timestampToIso(meta.agentTimestampMs) ||
460
+ timestampToIso(meta.timestampMs) ||
461
+ timestampToIso(record.timestamp) ||
462
+ null;
463
+
464
+ if (sessionUpdate === "tool_call" || meta.updateType === "ToolCall") {
465
+ const turn = getTurn(promptId);
466
+ const toolName = extractToolName(update, meta);
467
+ turn.tools.push(toolName);
468
+ if (toolName === "Bash") {
469
+ const cmd = extractExecCommand(update);
470
+ if (cmd) {
471
+ const status = update.status || meta.updateParams?.status || "completed";
472
+ turn.execs.push(buildExecDetails(cmd, status));
473
+ }
474
+ }
475
+ continue;
476
+ }
477
+
478
+ if (sessionUpdate === "agent_thought_chunk" || meta.updateType === "AgentThoughtChunk") {
479
+ getTurn(promptId).sawThought = true;
480
+ continue;
481
+ }
482
+
483
+ if (
484
+ sessionUpdate === "agent_message_chunk" ||
485
+ sessionUpdate === "user_message_chunk" ||
486
+ meta.updateType === "AgentMessageChunk"
487
+ ) {
488
+ getTurn(promptId).sawMessage = true;
489
+ continue;
490
+ }
491
+
492
+ if (sessionUpdate !== "turn_completed") continue;
493
+ if (ts && !isIsoInRange(ts, { from, to, timeZoneContext })) {
494
+ turns.delete(promptId);
495
+ continue;
496
+ }
497
+
498
+ const usage = readUsageTotals(update.usage);
499
+ if (!usage) {
500
+ turns.delete(promptId);
501
+ continue;
502
+ }
503
+
504
+ // Prefer per-model split when present (sum models into one turn total —
505
+ // already aggregated in usage.totalTokens).
506
+ const turnState = turns.get(promptId) || { tools: [], execs: [], sawThought: false, sawMessage: false };
507
+ attributeTurn(usage, turnState);
508
+ turns.delete(promptId);
509
+ }
510
+ } finally {
511
+ rl.close();
512
+ input.destroy?.();
513
+ }
514
+
515
+ return {
516
+ totals: roundTotals(totals),
517
+ turnCount,
518
+ toolBreakdown: { tool_rows: finalizeToolRows(byTool) },
519
+ execCommandBreakdown: {
520
+ by_type: finalizeExecRows(byExecKind),
521
+ by_executable: finalizeExecRows(byExecExecutable),
522
+ by_command: finalizeExecRows(byExecCommand),
523
+ by_duration: finalizeExecRows(byExecDuration),
524
+ by_output: finalizeExecRows(byExecOutput),
525
+ by_exit: finalizeExecRows(byExecExit),
526
+ },
527
+ };
528
+ }
529
+
530
+ function emptySessionResult() {
531
+ return {
532
+ totals: emptyTotals(),
533
+ turnCount: 0,
534
+ toolBreakdown: { tool_rows: [] },
535
+ execCommandBreakdown: {
536
+ by_type: [],
537
+ by_executable: [],
538
+ by_command: [],
539
+ by_duration: [],
540
+ by_output: [],
541
+ by_exit: [],
542
+ },
543
+ };
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Merge + display assembly (mirrors codex-context-breakdown)
548
+ // ---------------------------------------------------------------------------
549
+
550
+ function mergeRows(map, rows) {
551
+ for (const row of rows || []) {
552
+ const name = row?.name ? String(row.name) : "";
553
+ const rawName = row?.raw_name ? String(row.raw_name) : name;
554
+ const key = rawName || name;
555
+ if (!key) continue;
556
+ if (!map.has(key)) {
557
+ map.set(key, { name, raw_name: rawName, calls: 0, totals: emptyTotals() });
558
+ }
559
+ const cur = map.get(key);
560
+ cur.name = name;
561
+ cur.raw_name = rawName;
562
+ cur.calls += Number(row.calls || 0);
563
+ addInto(cur.totals, row.totals || {});
564
+ }
565
+ }
566
+
567
+ function mergeExecRows(map, rows) {
568
+ for (const row of rows || []) {
569
+ const name = row?.name ? String(row.name) : "";
570
+ if (!name) continue;
571
+ if (!map.has(name)) map.set(name, { name, ...buildExecStatsEntry() });
572
+ const cur = map.get(name);
573
+ cur.calls += Number(row.calls || 0);
574
+ cur.failures += Number(row.failures || 0);
575
+ cur.duration_ms += Number(row.duration_ms || 0);
576
+ cur.max_duration_ms = Math.max(cur.max_duration_ms, Number(row.max_duration_ms || 0));
577
+ cur.output_chars += Number(row.output_chars || 0);
578
+ cur.output_lines += Number(row.output_lines || 0);
579
+ addInto(cur.totals, row.totals || {});
580
+ }
581
+ }
582
+
583
+ async function computeGrokContextBreakdown({
584
+ from = null,
585
+ to = null,
586
+ top = 50,
587
+ timeZoneContext = null,
588
+ env = process.env,
589
+ } = {}) {
590
+ const limitedTop = Number.isFinite(top) && top > 0 ? Math.min(Math.floor(top), 200) : 50;
591
+ const sessions = discoverGrokSessionFiles(env);
592
+ const inventorySig = crypto
593
+ .createHash("sha256")
594
+ .update(
595
+ sessions
596
+ .map((s) => `${s.updatesPath}:${s.size}:${Math.floor(s.mtimeMs)}`)
597
+ .join("|"),
598
+ )
599
+ .digest("hex");
600
+ const cacheKey = [
601
+ CACHE_SCHEMA_VERSION,
602
+ from || "",
603
+ to || "",
604
+ limitedTop,
605
+ timeZoneContext?.timeZone || "",
606
+ timeZoneContext?.offsetMinutes ?? "",
607
+ inventorySig,
608
+ ].join("|");
609
+
610
+ const cached = CACHE.get(cacheKey);
611
+ if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
612
+ return cached.value;
613
+ }
614
+
615
+ const grand = emptyTotals();
616
+ const byTool = new Map();
617
+ const byExecKind = new Map();
618
+ const byExecExit = new Map();
619
+ const byExecExecutable = new Map();
620
+ const byExecCommand = new Map();
621
+ const byExecDuration = new Map();
622
+ const byExecOutput = new Map();
623
+ let messageCount = 0;
624
+ let sessionCount = 0;
625
+
626
+ for (const sess of sessions) {
627
+ const parsed = await parseGrokUpdatesFile(sess.updatesPath, {
628
+ from,
629
+ to,
630
+ timeZoneContext,
631
+ });
632
+ if (!parsed?.totals?.total_tokens) continue;
633
+ sessionCount += 1;
634
+ messageCount += Number(parsed.turnCount || 0);
635
+ addInto(grand, parsed.totals);
636
+ mergeRows(byTool, parsed.toolBreakdown?.tool_rows);
637
+ mergeExecRows(byExecKind, parsed.execCommandBreakdown?.by_type);
638
+ mergeExecRows(byExecExecutable, parsed.execCommandBreakdown?.by_executable);
639
+ mergeExecRows(byExecCommand, parsed.execCommandBreakdown?.by_command);
640
+ mergeExecRows(byExecDuration, parsed.execCommandBreakdown?.by_duration);
641
+ mergeExecRows(byExecOutput, parsed.execCommandBreakdown?.by_output);
642
+ mergeExecRows(byExecExit, parsed.execCommandBreakdown?.by_exit);
643
+ }
644
+
645
+ const toolRows = finalizeToolRows(byTool);
646
+ const toolRowsLimited = toolRows.slice(0, limitedTop);
647
+
648
+ // Category rows from tool names (File Ops / Search / Execution / ...)
649
+ const byCategory = new Map();
650
+ for (const row of toolRows) {
651
+ if (row.name === "text_response") continue;
652
+ const cat = categorizeTool(row.raw_name || row.name);
653
+ if (!byCategory.has(cat)) {
654
+ byCategory.set(cat, { name: cat, calls: 0, totals: emptyTotals(), tools: [] });
655
+ }
656
+ const target = byCategory.get(cat);
657
+ target.calls += Number(row.calls || 0);
658
+ addInto(target.totals, row.totals || {});
659
+ target.tools.push({
660
+ name: row.name,
661
+ calls: row.calls,
662
+ totals: roundTotals(row.totals),
663
+ });
664
+ }
665
+ const categoryRows = Array.from(byCategory.values())
666
+ .map((c) => ({
667
+ name: c.name,
668
+ calls: Math.round(c.calls || 0),
669
+ totals: roundTotals(c.totals),
670
+ tools: (c.tools || [])
671
+ .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0))
672
+ .slice(0, limitedTop),
673
+ }))
674
+ .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
675
+
676
+ // Message breakdown: residual "text_response" tools + reasoning/output split
677
+ const textResponse = toolRows.find((r) => r.name === "text_response");
678
+ const textTotals = textResponse?.totals || emptyTotals();
679
+ const reasoning = Number(grand.reasoning_output_tokens || 0);
680
+ const assistantOut = Math.max(0, Number(grand.output_tokens || 0) - Math.min(reasoning, Number(grand.output_tokens || 0)));
681
+ // For Grok, cached reads dominate conversation history; non-cached input is
682
+ // closer to "current user/context injection". Heuristic only — matches Codex
683
+ // message_breakdown intent without reading message bodies.
684
+ const historyTokens = Number(grand.cached_input_tokens || 0);
685
+ const userish = Number(grand.input_tokens || 0);
686
+ const messageBreakdown = [
687
+ {
688
+ key: "user_input",
689
+ name: "User input",
690
+ totals: roundTotals({
691
+ input_tokens: userish,
692
+ cached_input_tokens: 0,
693
+ cache_creation_input_tokens: 0,
694
+ output_tokens: 0,
695
+ reasoning_output_tokens: 0,
696
+ total_tokens: userish,
697
+ }),
698
+ },
699
+ {
700
+ key: "conversation_history",
701
+ name: "Conversation history",
702
+ totals: roundTotals({
703
+ input_tokens: 0,
704
+ cached_input_tokens: historyTokens,
705
+ cache_creation_input_tokens: 0,
706
+ output_tokens: 0,
707
+ reasoning_output_tokens: 0,
708
+ total_tokens: historyTokens,
709
+ }),
710
+ },
711
+ {
712
+ key: "assistant_response",
713
+ name: "Assistant response",
714
+ totals: roundTotals({
715
+ input_tokens: 0,
716
+ cached_input_tokens: 0,
717
+ cache_creation_input_tokens: 0,
718
+ output_tokens: assistantOut,
719
+ reasoning_output_tokens: 0,
720
+ total_tokens: assistantOut,
721
+ }),
722
+ },
723
+ ].sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
724
+
725
+ const serializeExecRows = (rows) =>
726
+ (rows || []).slice(0, limitedTop).map((r) => ({
727
+ name: r.name,
728
+ calls: r.calls,
729
+ failures: r.failures,
730
+ duration_ms: r.duration_ms,
731
+ max_duration_ms: r.max_duration_ms,
732
+ output_chars: r.output_chars,
733
+ output_lines: r.output_lines,
734
+ totals: roundTotals(r.totals),
735
+ }));
736
+
737
+ const result = {
738
+ source: "grok",
739
+ scope: "supported",
740
+ breakdown_status: "ok",
741
+ totals: roundTotals(grand),
742
+ session_count: sessionCount,
743
+ message_count: messageCount,
744
+ message_breakdown: {
745
+ categories: messageBreakdown,
746
+ privacy: {
747
+ includes_content: false,
748
+ note: "Aggregated message token categories only; prompt and assistant text are never returned.",
749
+ },
750
+ },
751
+ tool_calls_breakdown: {
752
+ total_calls: Math.round(toolRows.reduce((a, r) => a + Number(r.calls || 0), 0)),
753
+ tools: toolRowsLimited,
754
+ categories: categoryRows.slice(0, limitedTop),
755
+ tools_total: toolRows.reduce((a, r) => a + Math.round(r.totals?.total_tokens || 0), 0),
756
+ privacy: {
757
+ includes_inputs: false,
758
+ note: "Aggregated tool names only; no tool arguments or outputs are included.",
759
+ },
760
+ },
761
+ skills_breakdown: {
762
+ total_calls: 0,
763
+ skills: [],
764
+ privacy: {
765
+ includes_inputs: false,
766
+ note: "Grok Build does not currently expose a dedicated skill usage channel in turn logs.",
767
+ },
768
+ },
769
+ exec_command_breakdown: {
770
+ by_type: serializeExecRows(finalizeExecRows(byExecKind)),
771
+ by_executable: serializeExecRows(finalizeExecRows(byExecExecutable)),
772
+ by_command: serializeExecRows(finalizeExecRows(byExecCommand)),
773
+ by_duration: serializeExecRows(finalizeExecRows(byExecDuration)),
774
+ by_output: serializeExecRows(finalizeExecRows(byExecOutput)),
775
+ by_exit: serializeExecRows(finalizeExecRows(byExecExit)),
776
+ },
777
+ };
778
+
779
+ CACHE.set(cacheKey, { at: Date.now(), value: result });
780
+ while (CACHE.size > 32) {
781
+ const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];
782
+ if (oldest) CACHE.delete(oldest[0]);
783
+ }
784
+ return result;
785
+ }
786
+
787
+ module.exports = {
788
+ computeGrokContextBreakdown,
789
+ // test helpers
790
+ mapGrokToolName,
791
+ readUsageTotals,
792
+ discoverGrokSessionFiles,
793
+ parseGrokUpdatesFile,
794
+ };