tokentracker-cli 0.14.5 → 0.15.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.
@@ -1773,6 +1773,12 @@ function toUtcHalfHourStart(ts) {
1773
1773
  return bucketStart.toISOString();
1774
1774
  }
1775
1775
 
1776
+ function normalizeNonNegativeNumber(value) {
1777
+ const n = Number(value || 0);
1778
+ if (!Number.isFinite(n) || n <= 0) return 0;
1779
+ return n;
1780
+ }
1781
+
1776
1782
  function bucketKey(source, model, hourStart) {
1777
1783
  const safeSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
1778
1784
  const safeModel = normalizeModelInput(model) || DEFAULT_MODEL;
@@ -5886,6 +5892,245 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
5886
5892
  return { recordsProcessed, eventsAggregated, bucketsQueued };
5887
5893
  }
5888
5894
 
5895
+ // ─────────────────────────────────────────────────────────────────────────────
5896
+ // Grok Build (xAI) — passive reader for ~/.grok/sessions/**/signals.json + summary.json
5897
+ // Triggered either by full scan in sync or by the SessionEnd hook writing a signal.
5898
+ // Grok exposes contextTokensUsed, which appears to be a snapshot rather than
5899
+ // per-call telemetry, so these rows are estimates and only enqueue observed
5900
+ // increases for a session.
5901
+ // ─────────────────────────────────────────────────────────────────────────────
5902
+
5903
+ const GROK_ESTIMATED_INPUT_RATIO = 0.8;
5904
+ const GROK_CURSOR_VERSION = 2;
5905
+
5906
+ function resolveGrokBuildHome(env = process.env) {
5907
+ return (
5908
+ env.TOKENTRACKER_GROK_HOME ||
5909
+ env.GROK_HOME ||
5910
+ path.join(require("node:os").homedir(), ".grok")
5911
+ );
5912
+ }
5913
+
5914
+ function resolveGrokBuildSessions(env = process.env) {
5915
+ const home = resolveGrokBuildHome(env);
5916
+ const sessionsRoot = path.join(home, "sessions");
5917
+ if (!fssync.existsSync(sessionsRoot)) return [];
5918
+
5919
+ const results = [];
5920
+ let cwdDirs = [];
5921
+ try {
5922
+ cwdDirs = fssync.readdirSync(sessionsRoot);
5923
+ } catch {
5924
+ return [];
5925
+ }
5926
+
5927
+ for (const cwdDir of cwdDirs) {
5928
+ const cwdPath = path.join(sessionsRoot, cwdDir);
5929
+ let stat;
5930
+ try { stat = fssync.statSync(cwdPath); } catch { continue; }
5931
+ if (!stat.isDirectory()) continue;
5932
+
5933
+ let sessionIds = [];
5934
+ try { sessionIds = fssync.readdirSync(cwdPath); } catch { continue; }
5935
+
5936
+ for (const sid of sessionIds) {
5937
+ const sessionDir = path.join(cwdPath, sid);
5938
+ const signalsPath = path.join(sessionDir, "signals.json");
5939
+ if (fssync.existsSync(signalsPath)) {
5940
+ results.push({
5941
+ sessionDir,
5942
+ signalsPath,
5943
+ summaryPath: path.join(sessionDir, "summary.json"),
5944
+ sessionId: sid,
5945
+ encodedCwd: cwdDir
5946
+ });
5947
+ }
5948
+ }
5949
+ }
5950
+ return results;
5951
+ }
5952
+
5953
+ function normalizeGrokSessionSnapshots(grokState) {
5954
+ const snapshots = {};
5955
+ if (grokState?.sessionSnapshots && typeof grokState.sessionSnapshots === "object") {
5956
+ for (const [sessionId, snapshot] of Object.entries(grokState.sessionSnapshots)) {
5957
+ const safeSessionId = normalizeModelInput(sessionId);
5958
+ if (!safeSessionId || !snapshot || typeof snapshot !== "object") continue;
5959
+ const totalTokens = normalizeNonNegativeNumber(snapshot.totalTokens);
5960
+ snapshots[safeSessionId] = {
5961
+ totalTokens,
5962
+ messageCount: normalizeNonNegativeNumber(snapshot.messageCount),
5963
+ model: normalizeModelInput(snapshot.model) || null,
5964
+ updatedAt: normalizeModelInput(snapshot.updatedAt) || null,
5965
+ };
5966
+ }
5967
+ }
5968
+
5969
+ if (Array.isArray(grokState?.seenSessions)) {
5970
+ for (const sessionId of grokState.seenSessions) {
5971
+ const safeSessionId = normalizeModelInput(sessionId);
5972
+ if (!safeSessionId || snapshots[safeSessionId]) continue;
5973
+ snapshots[safeSessionId] = {
5974
+ totalTokens: Number.MAX_SAFE_INTEGER,
5975
+ messageCount: 0,
5976
+ model: null,
5977
+ updatedAt: normalizeModelInput(grokState.updatedAt) || null,
5978
+ legacySeen: true,
5979
+ };
5980
+ }
5981
+ }
5982
+
5983
+ return snapshots;
5984
+ }
5985
+
5986
+ function capGrokSessionSnapshots(sessionSnapshots) {
5987
+ const entries = Object.entries(sessionSnapshots);
5988
+ if (entries.length <= 10_000) return sessionSnapshots;
5989
+ return Object.fromEntries(entries.slice(entries.length - 10_000));
5990
+ }
5991
+
5992
+ function readGrokJsonFile(filePath) {
5993
+ if (!filePath) return null;
5994
+ try {
5995
+ return JSON.parse(fssync.readFileSync(filePath, "utf8"));
5996
+ } catch {
5997
+ return null;
5998
+ }
5999
+ }
6000
+
6001
+ function grokSessionIdFor(sess) {
6002
+ return (
6003
+ normalizeModelInput(sess?.sessionId) ||
6004
+ (normalizeModelInput(sess?.sessionDir) ? path.basename(sess.sessionDir) : null)
6005
+ );
6006
+ }
6007
+
6008
+ function grokModelFromSignals(signals) {
6009
+ return (
6010
+ normalizeModelInput(signals?.primaryModelId) ||
6011
+ normalizeModelInput(Array.isArray(signals?.modelsUsed) ? signals.modelsUsed[0] : null) ||
6012
+ normalizeModelInput(signals?.model) ||
6013
+ "grok-build"
6014
+ );
6015
+ }
6016
+
6017
+ function grokLastActiveFromSignals(signals, summary) {
6018
+ return (
6019
+ normalizeModelInput(signals?.lastActiveAt) ||
6020
+ normalizeModelInput(signals?.updatedAt) ||
6021
+ normalizeModelInput(signals?.lastActive) ||
6022
+ normalizeModelInput(summary?.updated_at) ||
6023
+ normalizeModelInput(summary?.updatedAt) ||
6024
+ new Date().toISOString()
6025
+ );
6026
+ }
6027
+
6028
+ function estimateGrokTokenDelta(totalTokens, conversationCount) {
6029
+ const total = Math.trunc(normalizeNonNegativeNumber(totalTokens));
6030
+ const inputTokens = Math.round(total * GROK_ESTIMATED_INPUT_RATIO);
6031
+ const outputTokens = Math.max(0, total - inputTokens);
6032
+ const conversations = Math.max(1, Math.trunc(normalizeNonNegativeNumber(conversationCount)));
6033
+
6034
+ return {
6035
+ input_tokens: inputTokens,
6036
+ cached_input_tokens: 0,
6037
+ cache_creation_input_tokens: 0,
6038
+ output_tokens: outputTokens,
6039
+ reasoning_output_tokens: 0,
6040
+ total_tokens: total,
6041
+ billable_total_tokens: total,
6042
+ conversation_count: conversations,
6043
+ };
6044
+ }
6045
+
6046
+ async function parseGrokBuildIncremental({
6047
+ sessions,
6048
+ cursors = {},
6049
+ queuePath,
6050
+ onProgress,
6051
+ env = process.env
6052
+ } = {}) {
6053
+ if (queuePath) await ensureDir(path.dirname(queuePath));
6054
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
6055
+ const grokState = cursors.grok && typeof cursors.grok === "object" ? { ...cursors.grok } : {};
6056
+ let sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
6057
+ const touchedBuckets = new Set();
6058
+
6059
+ const sessionList = Array.isArray(sessions) && sessions.length > 0
6060
+ ? sessions
6061
+ : resolveGrokBuildSessions(env);
6062
+
6063
+ let eventsAggregated = 0;
6064
+
6065
+ for (const sess of sessionList) {
6066
+ const sessionId = grokSessionIdFor(sess);
6067
+ if (!sessionId) continue;
6068
+
6069
+ const signals = sess?.signals && typeof sess.signals === "object"
6070
+ ? sess.signals
6071
+ : readGrokJsonFile(sess?.signalsPath);
6072
+ if (!signals || typeof signals !== "object") continue;
6073
+
6074
+ const summary = sess?.summary && typeof sess.summary === "object"
6075
+ ? sess.summary
6076
+ : readGrokJsonFile(sess?.summaryPath) || {};
6077
+ const totalTokens = normalizeNonNegativeNumber(signals.contextTokensUsed ?? signals.totalTokens);
6078
+ if (totalTokens <= 0) {
6079
+ continue;
6080
+ }
6081
+
6082
+ const previous = sessionSnapshots[sessionId] || {};
6083
+ const previousTotal = normalizeNonNegativeNumber(previous.totalTokens);
6084
+ const deltaTokens = totalTokens > previousTotal ? totalTokens - previousTotal : 0;
6085
+ if (deltaTokens <= 0) continue;
6086
+
6087
+ const messageCount = normalizeNonNegativeNumber(
6088
+ signals.assistantMessageCount ?? signals.num_chat_messages ?? signals.messageCount,
6089
+ );
6090
+ const previousMessageCount = normalizeNonNegativeNumber(previous.messageCount);
6091
+ const deltaMessageCount =
6092
+ messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
6093
+ const model = grokModelFromSignals(signals);
6094
+ const lastActive = grokLastActiveFromSignals(signals, summary);
6095
+ const hourStartStr = toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
6096
+ if (!hourStartStr) continue;
6097
+
6098
+ const delta = estimateGrokTokenDelta(deltaTokens, deltaMessageCount);
6099
+ const bucket = getHourlyBucket(hourlyState, "grok", model, hourStartStr);
6100
+ addTotals(bucket.totals, delta);
6101
+ touchedBuckets.add(bucketKey("grok", model, hourStartStr));
6102
+
6103
+ eventsAggregated++;
6104
+ sessionSnapshots[sessionId] = {
6105
+ totalTokens: Math.max(previousTotal, totalTokens),
6106
+ messageCount: Math.max(previousMessageCount, messageCount),
6107
+ model,
6108
+ updatedAt: new Date().toISOString(),
6109
+ };
6110
+ }
6111
+
6112
+ const bucketsQueued = queuePath
6113
+ ? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
6114
+ : 0;
6115
+ hourlyState.updatedAt = new Date().toISOString();
6116
+ cursors.hourly = hourlyState;
6117
+ sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
6118
+
6119
+ cursors.grok = {
6120
+ ...grokState,
6121
+ version: GROK_CURSOR_VERSION,
6122
+ sessionSnapshots,
6123
+ seenSessions: Object.keys(sessionSnapshots),
6124
+ updatedAt: new Date().toISOString()
6125
+ };
6126
+
6127
+ return {
6128
+ recordsProcessed: eventsAggregated,
6129
+ eventsAggregated,
6130
+ bucketsQueued
6131
+ };
6132
+ }
6133
+
5889
6134
  module.exports = {
5890
6135
  listRolloutFiles,
5891
6136
  listClaudeProjectFiles,
@@ -5950,4 +6195,9 @@ module.exports = {
5950
6195
  // Exposed for regression tests covering nested-group remote URLs.
5951
6196
  canonicalizeProjectRef,
5952
6197
  deriveProjectKeyFromRef,
6198
+
6199
+ // Grok Build (xAI) — SessionEnd hook + passive signals.json reader
6200
+ resolveGrokBuildHome,
6201
+ resolveGrokBuildSessions,
6202
+ parseGrokBuildIncremental,
5953
6203
  };