tokentracker-cli 0.19.0 → 0.20.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 (37) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dashboard/dist/assets/{Card-ANC9ZmIT.js → Card-jA08WeEw.js} +1 -1
  4. package/dashboard/dist/assets/{DashboardPage-DpZaCksZ.js → DashboardPage-chDVOYmG.js} +1 -1
  5. package/dashboard/dist/assets/{FadeIn-WXGyOn0H.js → FadeIn-DqSYXuUL.js} +1 -1
  6. package/dashboard/dist/assets/{HeaderGithubStar-DUkE0Dwd.js → HeaderGithubStar-C11rWv0B.js} +1 -1
  7. package/dashboard/dist/assets/{IpCheckPage-BPF8eGpg.js → IpCheckPage-CkEZ9yLK.js} +1 -1
  8. package/dashboard/dist/assets/{LandingPage-0uTpqpAU.js → LandingPage-BgckTHRQ.js} +1 -1
  9. package/dashboard/dist/assets/{LeaderboardPage-DzxRJEzb.js → LeaderboardPage-BCNW7UWp.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardProfilePage-C3_oUxhG.js → LeaderboardProfilePage-BLATxMt-.js} +1 -1
  11. package/dashboard/dist/assets/{LimitsPage-BVuvoeY9.js → LimitsPage-arF--WgR.js} +1 -1
  12. package/dashboard/dist/assets/{LoginPage-CfkNRmT6.js → LoginPage-DpoFP0va.js} +1 -1
  13. package/dashboard/dist/assets/{PopoverPopup-CfxiYbJm.js → PopoverPopup-kdgc2H6C.js} +1 -1
  14. package/dashboard/dist/assets/{ProviderIcon-DiPzAed2.js → ProviderIcon-DV5r9qqP.js} +1 -1
  15. package/dashboard/dist/assets/{SettingsPage-Devu7beE.js → SettingsPage-Bb22ORmU.js} +1 -1
  16. package/dashboard/dist/assets/{SkillsPage-CSe8fW4V.js → SkillsPage-xhtBqVKC.js} +1 -1
  17. package/dashboard/dist/assets/{WidgetsPage-BrLp5YLk.js → WidgetsPage-CUoSVDET.js} +1 -1
  18. package/dashboard/dist/assets/{chevron-down-nFF6Yj_r.js → chevron-down-DYb2EChD.js} +1 -1
  19. package/dashboard/dist/assets/{download-DhSZ--68.js → download-C-_8o6dh.js} +1 -1
  20. package/dashboard/dist/assets/{leaderboard-columns-CvFdXrw5.js → leaderboard-columns-BgzBlYo7.js} +1 -1
  21. package/dashboard/dist/assets/{main-DtrPNYb7.js → main-11hApDak.js} +3 -3
  22. package/dashboard/dist/assets/{use-limits-display-prefs-Yy8t7tbB.js → use-limits-display-prefs-BeGKWUuk.js} +1 -1
  23. package/dashboard/dist/assets/{use-native-settings-uemf9RSH.js → use-native-settings-nTTHktn0.js} +1 -1
  24. package/dashboard/dist/assets/{use-reduced-motion-DH8DxE18.js → use-reduced-motion-DU8Gm6j1.js} +1 -1
  25. package/dashboard/dist/assets/{use-usage-limits-C3vUT6PH.js → use-usage-limits-DTPmEB8Y.js} +1 -1
  26. package/dashboard/dist/index.html +1 -1
  27. package/dashboard/dist/share.html +1 -1
  28. package/package.json +1 -1
  29. package/src/commands/init.js +9 -5
  30. package/src/commands/serve.js +0 -12
  31. package/src/commands/sync.js +370 -7
  32. package/src/lib/grok-hook.js +86 -7
  33. package/src/lib/pricing/curated-overrides.json +1 -1
  34. package/src/lib/pricing/seed-snapshot.json +1 -1
  35. package/src/lib/rollout.js +403 -140
  36. package/src/lib/subscriptions.js +92 -40
  37. package/src/lib/usage-limits.js +1 -1
@@ -3005,18 +3005,61 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
3005
3005
  // Hermes Agent — SQLite-based (sessions table in ~/.hermes/state.db)
3006
3006
  // ─────────────────────────────────────────────────────────────────────────────
3007
3007
 
3008
- function resolveHermesDbPath() {
3008
+ function resolveHermesPath(env = process.env) {
3009
+ const override = env.TOKENTRACKER_HERMES_HOME;
3010
+ if (typeof override === "string" && override.trim().length > 0) {
3011
+ return override.trim();
3012
+ }
3009
3013
  const home = require("node:os").homedir();
3010
- return path.join(home, ".hermes", "state.db");
3014
+ return path.join(home, ".hermes");
3015
+ }
3016
+
3017
+ function resolveHermesDbPath(env = process.env) {
3018
+ return path.join(resolveHermesPath(env), "state.db");
3011
3019
  }
3012
3020
 
3013
- function readHermesSessions(dbPath, lastCompletedEpoch) {
3021
+ function resolveAllHermesDBPaths({ hermesPath, dbPath } = {}) {
3022
+ const hermesDir = hermesPath ?? (dbPath ? path.dirname(dbPath) : resolveHermesPath());
3023
+ const defaultDbPath = dbPath ?? path.join(hermesDir, "state.db");
3024
+ const profilePaths = {};
3025
+ try {
3026
+ const profilesDir = path.join(hermesDir, "profiles");
3027
+ const profiles = fssync.readdirSync(profilesDir, { withFileTypes: true })
3028
+ .filter((entry) => entry.isDirectory())
3029
+ .sort((a, b) => a.name.localeCompare(b.name));
3030
+
3031
+ for (const entry of profiles) {
3032
+ const dbPath = path.join(profilesDir, entry.name, "state.db");
3033
+ if (fssync.existsSync(dbPath)) {
3034
+ profilePaths[entry.name] = dbPath;
3035
+ }
3036
+ }
3037
+ } catch (_e) { }
3038
+
3039
+ return {
3040
+ default: fssync.existsSync(defaultDbPath) ? defaultDbPath : null,
3041
+ profiles: profilePaths,
3042
+ }
3043
+ }
3044
+
3045
+ function sqliteStringLiteral(value) {
3046
+ return `'${String(value).replace(/'/g, "''")}'`;
3047
+ }
3048
+
3049
+ function readHermesSessions(dbPath, lastCompletedEpoch, unfinishedSessionIds = []) {
3014
3050
  if (!dbPath || !fssync.existsSync(dbPath)) return [];
3015
3051
  const since = Number.isFinite(lastCompletedEpoch) && lastCompletedEpoch > 0 ? lastCompletedEpoch : 0;
3016
- // Fetch sessions that started after the cursor, OR sessions that are still
3017
- // in-progress (ended_at IS NULL). Hermes updates token counts in real-time,
3018
- // so an active session keeps growing and must be re-read on every sync.
3019
- const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at > ${since} OR ended_at IS NULL) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
3052
+ const forceIds = Array.isArray(unfinishedSessionIds)
3053
+ ? [...new Set(unfinishedSessionIds.filter((id) => typeof id === "string" && id.length > 0))]
3054
+ : [];
3055
+ const forceIncludeSql = forceIds.length > 0
3056
+ ? ` OR id IN (${forceIds.map(sqliteStringLiteral).join(",")})`
3057
+ : "";
3058
+ // Fetch sessions that started at/after the cursor, sessions that are still
3059
+ // in-progress (ended_at IS NULL), OR sessions that were previously observed
3060
+ // unfinished. Hermes updates token counts in real-time, including a final
3061
+ // delta when an active session later gets ended_at set.
3062
+ const sql = `SELECT id, model, started_at, ended_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, message_count FROM sessions WHERE (started_at >= ${since} OR ended_at IS NULL${forceIncludeSql}) AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
3020
3063
  let raw;
3021
3064
  try {
3022
3065
  raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
@@ -3037,122 +3080,173 @@ function readHermesSessions(dbPath, lastCompletedEpoch) {
3037
3080
  return Array.isArray(rows) ? rows : [];
3038
3081
  }
3039
3082
 
3040
- async function parseHermesIncremental({ dbPath, cursors, queuePath, onProgress }) {
3083
+ function hasLegacyHermesDefaultState(hermesState) {
3084
+ return (
3085
+ typeof hermesState.lastStartedAt === "number" ||
3086
+ typeof hermesState.lastCompletedStartedAt === "number" ||
3087
+ (hermesState.snapshots && typeof hermesState.snapshots === "object")
3088
+ );
3089
+ }
3090
+
3091
+ async function parseHermesIncremental({ hermesPath, dbPath, cursors, queuePath, onProgress }) {
3041
3092
  await ensureDir(path.dirname(queuePath));
3042
3093
  const hermesState = cursors.hermes && typeof cursors.hermes === "object" ? cursors.hermes : {};
3043
3094
 
3044
- // Only advance past sessions that have fully ended. Active sessions
3045
- // (ended_at IS NULL) must be re-read every sync because Hermes updates
3046
- // their token counts in real-time after each turn.
3047
- const lastCompletedStartedAt =
3048
- typeof hermesState.lastCompletedStartedAt === "number" ? hermesState.lastCompletedStartedAt : 0;
3049
-
3050
- // Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
3051
- const prevSnapshots = (hermesState.snapshots && typeof hermesState.snapshots === "object")
3052
- ? hermesState.snapshots : {};
3053
-
3054
- const resolvedDbPath = dbPath || resolveHermesDbPath();
3055
- if (!fssync.existsSync(resolvedDbPath)) {
3056
- return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
3057
- }
3058
-
3059
- const rows = readHermesSessions(resolvedDbPath, lastCompletedStartedAt);
3060
- if (rows.length === 0) {
3061
- cursors.hermes = { ...hermesState, lastCompletedStartedAt, updatedAt: new Date().toISOString() };
3095
+ const dbPaths = resolveAllHermesDBPaths({ hermesPath, dbPath });
3096
+ if (dbPaths.default === null && Object.keys(dbPaths.profiles).length === 0) {
3097
+ // No state in any profile
3062
3098
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
3063
3099
  }
3064
3100
 
3065
3101
  const hourlyState = normalizeHourlyState(cursors?.hourly);
3066
- const touchedBuckets = new Set();
3067
3102
  const cb = typeof onProgress === "function" ? onProgress : null;
3103
+ const updatedAt = new Date().toISOString();
3104
+ let recordsProcessed = 0;
3068
3105
  let eventsAggregated = 0;
3069
- let maxCompletedStartedAt = lastCompletedStartedAt;
3070
- const nextSnapshots = {};
3106
+ const touchedBuckets = new Set();
3071
3107
 
3072
- for (let i = 0; i < rows.length; i++) {
3073
- const row = rows[i];
3074
- const inputTokens = toNonNegativeInt(row.input_tokens);
3075
- const outputTokens = toNonNegativeInt(row.output_tokens);
3076
- const cacheRead = toNonNegativeInt(row.cache_read_tokens);
3077
- const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
3078
- const reasoning = toNonNegativeInt(row.reasoning_tokens);
3079
- if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
3080
-
3081
- // Save current snapshot for next sync
3082
- nextSnapshots[row.id] = { in: inputTokens, out: outputTokens, cacheRead, cacheWrite, reasoning };
3083
-
3084
- // Compute delta from previous snapshot (if any) so that we only count
3085
- // new tokens since the last sync. First time we see a session the
3086
- // previous snapshot is absent, so the full amount is the delta.
3087
- const prev = prevSnapshots[row.id];
3088
- let dInput = inputTokens;
3089
- let dOutput = outputTokens;
3090
- let dCacheRead = cacheRead;
3091
- let dCacheWrite = cacheWrite;
3092
- let dReasoning = reasoning;
3093
- if (prev) {
3094
- dInput = Math.max(0, inputTokens - (prev.in || 0));
3095
- dOutput = Math.max(0, outputTokens - (prev.out || 0));
3096
- dCacheRead = Math.max(0, cacheRead - (prev.cacheRead || 0));
3097
- dCacheWrite = Math.max(0, cacheWrite - (prev.cacheWrite || 0));
3098
- dReasoning = Math.max(0, reasoning - (prev.reasoning || 0));
3099
- }
3100
- // Skip if delta is zero (session unchanged since last sync)
3101
- if (dInput === 0 && dOutput === 0 && dCacheRead === 0 && dCacheWrite === 0 && dReasoning === 0) continue;
3102
-
3103
- // Prefer ended_at for bucket placement; fall back to started_at
3104
- const epochSec = row.ended_at || row.started_at;
3105
- if (!epochSec || !Number.isFinite(epochSec)) continue;
3106
- const tsIso = new Date(epochSec * 1000).toISOString();
3107
- const bucketStart = toUtcHalfHourStart(tsIso);
3108
- if (!bucketStart) continue;
3108
+ function ingestProfile(dbPath, dbState) {
3109
+ const trackedUnfinishedSessionIds = Array.isArray(dbState.unfinishedSessionIds)
3110
+ ? dbState.unfinishedSessionIds
3111
+ : [];
3112
+ const rows = readHermesSessions(dbPath, dbState.lastCompletedStartedAt, trackedUnfinishedSessionIds);
3113
+ recordsProcessed += rows.length;
3114
+ if (rows.length === 0) {
3115
+ dbState.updatedAt = updatedAt;
3116
+ return;
3117
+ }
3118
+
3119
+ // Per-session snapshot from the previous sync: { [sessionId]: { in, out, cacheRead, cacheWrite, reasoning } }
3120
+ const prevSnapshots = (dbState.snapshots && typeof dbState.snapshots === "object")
3121
+ ? dbState.snapshots : {};
3122
+
3123
+ // Only advance past sessions that have fully ended. Active sessions
3124
+ // (ended_at IS NULL) must be re-read every sync because Hermes updates
3125
+ // their token counts in real-time after each turn.
3126
+ const lastCompletedStartedAt =
3127
+ typeof dbState.lastCompletedStartedAt === "number" ? dbState.lastCompletedStartedAt : 0;
3128
+
3129
+ let maxCompletedStartedAt = lastCompletedStartedAt;
3130
+ let oldestUnfinishedStartedAt = Infinity;
3131
+ const nextUnfinishedSessionIds = new Set();
3132
+ const nextSnapshots = {};
3133
+
3134
+ for (let i = 0; i < rows.length; i++) {
3135
+ const row = rows[i];
3136
+ const inputTokens = toNonNegativeInt(row.input_tokens);
3137
+ const outputTokens = toNonNegativeInt(row.output_tokens);
3138
+ const cacheRead = toNonNegativeInt(row.cache_read_tokens);
3139
+ const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
3140
+ const reasoning = toNonNegativeInt(row.reasoning_tokens);
3141
+ const messageCount = toNonNegativeInt(row.message_count);
3142
+ if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
3143
+
3144
+ // Save current snapshot for next sync
3145
+ nextSnapshots[row.id] = { in: inputTokens, out: outputTokens, cacheRead, cacheWrite, reasoning, message_count: messageCount };
3146
+
3147
+ const startedAt = Number(row.started_at);
3148
+ const endedAt = row.ended_at == null ? null : Number(row.ended_at);
3149
+ if (endedAt == null) {
3150
+ if (row.id && Number.isFinite(startedAt)) {
3151
+ nextUnfinishedSessionIds.add(row.id);
3152
+ oldestUnfinishedStartedAt = Math.min(oldestUnfinishedStartedAt, startedAt);
3153
+ }
3154
+ } else if (Number.isFinite(startedAt) && startedAt > maxCompletedStartedAt) {
3155
+ maxCompletedStartedAt = startedAt;
3156
+ }
3109
3157
 
3110
- const model = normalizeModelInput(row.model) || "hermes-agent";
3158
+ // Compute delta from previous snapshot (if any) so that we only count
3159
+ // new usage since the last sync. First time we see a session the
3160
+ // previous snapshot is absent, so the full amount is the delta.
3161
+ const prev = prevSnapshots[row.id];
3162
+ let dInput = inputTokens;
3163
+ let dOutput = outputTokens;
3164
+ let dCacheRead = cacheRead;
3165
+ let dCacheWrite = cacheWrite;
3166
+ let dReasoning = reasoning;
3167
+ let dMessageCount = messageCount;
3168
+ if (prev) {
3169
+ dInput = Math.max(0, inputTokens - (prev.in || 0));
3170
+ dOutput = Math.max(0, outputTokens - (prev.out || 0));
3171
+ dCacheRead = Math.max(0, cacheRead - (prev.cacheRead || 0));
3172
+ dCacheWrite = Math.max(0, cacheWrite - (prev.cacheWrite || 0));
3173
+ dReasoning = Math.max(0, reasoning - (prev.reasoning || 0));
3174
+ dMessageCount = Math.max(0, messageCount - (prev.message_count || 0));
3175
+ }
3176
+ // Skip if delta is zero (session unchanged since last sync)
3177
+ if (dInput === 0 && dOutput === 0 && dCacheRead === 0 && dCacheWrite === 0 && dReasoning === 0) continue;
3111
3178
 
3112
- const delta = {
3113
- input_tokens: dInput,
3114
- cached_input_tokens: dCacheRead,
3115
- cache_creation_input_tokens: dCacheWrite,
3116
- output_tokens: dOutput,
3117
- reasoning_output_tokens: dReasoning,
3118
- total_tokens: dInput + dOutput + dCacheRead + dCacheWrite + dReasoning,
3119
- conversation_count: toNonNegativeInt(row.message_count) || 1,
3120
- };
3179
+ // Prefer ended_at for bucket placement; fall back to started_at
3180
+ const epochSec = endedAt ?? startedAt;
3181
+ if (!epochSec || !Number.isFinite(epochSec)) continue;
3182
+ const tsIso = new Date(epochSec * 1000).toISOString();
3183
+ const bucketStart = toUtcHalfHourStart(tsIso);
3184
+ if (!bucketStart) continue;
3121
3185
 
3122
- const bucket = getHourlyBucket(hourlyState, "hermes", model, bucketStart);
3123
- addTotals(bucket.totals, delta);
3124
- touchedBuckets.add(bucketKey("hermes", model, bucketStart));
3125
- eventsAggregated++;
3186
+ const model = normalizeModelInput(row.model) || "hermes-agent";
3126
3187
 
3127
- // Only advance cursor past sessions that have ended
3128
- if (row.ended_at && row.started_at > maxCompletedStartedAt) {
3129
- maxCompletedStartedAt = row.started_at;
3130
- }
3188
+ const delta = {
3189
+ input_tokens: dInput,
3190
+ cached_input_tokens: dCacheRead,
3191
+ cache_creation_input_tokens: dCacheWrite,
3192
+ output_tokens: dOutput,
3193
+ reasoning_output_tokens: dReasoning,
3194
+ total_tokens: dInput + dOutput + dCacheRead + dCacheWrite + dReasoning,
3195
+ conversation_count: dMessageCount,
3196
+ };
3131
3197
 
3132
- if (cb) {
3133
- cb({
3134
- index: i + 1,
3135
- total: rows.length,
3136
- recordsProcessed: i + 1,
3137
- eventsAggregated,
3138
- bucketsQueued: touchedBuckets.size,
3139
- });
3198
+ const bucket = getHourlyBucket(hourlyState, "hermes", model, bucketStart);
3199
+ addTotals(bucket.totals, delta);
3200
+ touchedBuckets.add(bucketKey("hermes", model, bucketStart));
3201
+ eventsAggregated++;
3202
+
3203
+ if (cb) {
3204
+ cb({
3205
+ index: i + 1,
3206
+ total: rows.length,
3207
+ recordsProcessed: i + 1,
3208
+ eventsAggregated,
3209
+ bucketsQueued: touchedBuckets.size,
3210
+ });
3211
+ }
3140
3212
  }
3213
+
3214
+ const nextLastCompletedStartedAt = Number.isFinite(oldestUnfinishedStartedAt)
3215
+ ? Math.min(maxCompletedStartedAt, oldestUnfinishedStartedAt)
3216
+ : maxCompletedStartedAt;
3217
+
3218
+ Object.assign(dbState, {
3219
+ lastStartedAt: nextLastCompletedStartedAt,
3220
+ lastCompletedStartedAt: nextLastCompletedStartedAt,
3221
+ unfinishedSessionIds: Array.from(nextUnfinishedSessionIds),
3222
+ snapshots: nextSnapshots,
3223
+ updatedAt,
3224
+ });
3225
+ }
3226
+
3227
+ if (dbPaths.default) {
3228
+ ingestProfile(dbPaths.default, hermesState);
3229
+ }
3230
+
3231
+ hermesState.profiles = hermesState.profiles && typeof hermesState.profiles === "object" ? hermesState.profiles : {};
3232
+
3233
+ for (const [profileName, dbPath] of Object.entries(dbPaths.profiles)) {
3234
+ const profileState = hermesState.profiles[profileName] && typeof hermesState.profiles[profileName] === "object"
3235
+ ? hermesState.profiles[profileName]
3236
+ : {};
3237
+ hermesState.profiles[profileName] = profileState;
3238
+ ingestProfile(dbPath, profileState);
3141
3239
  }
3142
3240
 
3143
3241
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
3144
- const updatedAt = new Date().toISOString();
3145
3242
  hourlyState.updatedAt = updatedAt;
3146
3243
  cursors.hourly = hourlyState;
3147
3244
  cursors.hermes = {
3148
3245
  ...hermesState,
3149
- lastStartedAt: maxCompletedStartedAt, // keep for backward compat
3150
- lastCompletedStartedAt: maxCompletedStartedAt,
3151
- snapshots: nextSnapshots,
3152
- updatedAt,
3246
+ updatedAt, // Update the overall profile state timestamp even if the DB doesn't exist for the fast-path check
3153
3247
  };
3154
3248
 
3155
- return { recordsProcessed: rows.length, eventsAggregated, bucketsQueued };
3249
+ return { recordsProcessed, eventsAggregated, bucketsQueued };
3156
3250
  }
3157
3251
 
3158
3252
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5893,15 +5987,15 @@ async function parseCopilotIncremental({ otelPaths, cursors, queuePath, onProgre
5893
5987
  }
5894
5988
 
5895
5989
  // ─────────────────────────────────────────────────────────────────────────────
5896
- // Grok Build (xAI) — passive reader for ~/.grok/sessions/**/signals.json + summary.json
5990
+ // Grok Build (xAI) — passive reader for ~/.grok/sessions/**/updates.jsonl + signals.json
5897
5991
  // 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.
5992
+ // updates.jsonl exposes cumulative totalTokens metadata. Grok still does not
5993
+ // expose a stable prompt/output/cache split locally, so these rows keep the
5994
+ // estimated input/output split while using better local telemetry for totals.
5901
5995
  // ─────────────────────────────────────────────────────────────────────────────
5902
5996
 
5903
5997
  const GROK_ESTIMATED_INPUT_RATIO = 0.8;
5904
- const GROK_CURSOR_VERSION = 2;
5998
+ const GROK_CURSOR_VERSION = 3;
5905
5999
 
5906
6000
  function resolveGrokBuildHome(env = process.env) {
5907
6001
  return (
@@ -5936,9 +6030,11 @@ function resolveGrokBuildSessions(env = process.env) {
5936
6030
  for (const sid of sessionIds) {
5937
6031
  const sessionDir = path.join(cwdPath, sid);
5938
6032
  const signalsPath = path.join(sessionDir, "signals.json");
5939
- if (fssync.existsSync(signalsPath)) {
6033
+ const updatesPath = path.join(sessionDir, "updates.jsonl");
6034
+ if (fssync.existsSync(signalsPath) || fssync.existsSync(updatesPath)) {
5940
6035
  results.push({
5941
6036
  sessionDir,
6037
+ updatesPath,
5942
6038
  signalsPath,
5943
6039
  summaryPath: path.join(sessionDir, "summary.json"),
5944
6040
  sessionId: sid,
@@ -5961,7 +6057,11 @@ function normalizeGrokSessionSnapshots(grokState) {
5961
6057
  totalTokens,
5962
6058
  messageCount: normalizeNonNegativeNumber(snapshot.messageCount),
5963
6059
  model: normalizeModelInput(snapshot.model) || null,
6060
+ source: normalizeModelInput(snapshot.source) || null,
6061
+ lastEventId: normalizeModelInput(snapshot.lastEventId) || null,
6062
+ lastEventTimestamp: normalizeModelInput(snapshot.lastEventTimestamp) || null,
5964
6063
  updatedAt: normalizeModelInput(snapshot.updatedAt) || null,
6064
+ legacySeen: snapshot.legacySeen === true,
5965
6065
  };
5966
6066
  }
5967
6067
  }
@@ -5971,7 +6071,7 @@ function normalizeGrokSessionSnapshots(grokState) {
5971
6071
  const safeSessionId = normalizeModelInput(sessionId);
5972
6072
  if (!safeSessionId || snapshots[safeSessionId]) continue;
5973
6073
  snapshots[safeSessionId] = {
5974
- totalTokens: Number.MAX_SAFE_INTEGER,
6074
+ totalTokens: 0,
5975
6075
  messageCount: 0,
5976
6076
  model: null,
5977
6077
  updatedAt: normalizeModelInput(grokState.updatedAt) || null,
@@ -5998,6 +6098,14 @@ function readGrokJsonFile(filePath) {
5998
6098
  }
5999
6099
  }
6000
6100
 
6101
+ function grokUpdatesPathForSession(sess) {
6102
+ if (typeof sess?.updatesPath === "string" && sess.updatesPath.trim()) return sess.updatesPath;
6103
+ if (typeof sess?.sessionDir === "string" && sess.sessionDir.trim()) {
6104
+ return path.join(sess.sessionDir, "updates.jsonl");
6105
+ }
6106
+ return null;
6107
+ }
6108
+
6001
6109
  function grokSessionIdFor(sess) {
6002
6110
  return (
6003
6111
  normalizeModelInput(sess?.sessionId) ||
@@ -6025,11 +6133,113 @@ function grokLastActiveFromSignals(signals, summary) {
6025
6133
  );
6026
6134
  }
6027
6135
 
6028
- function estimateGrokTokenDelta(totalTokens, conversationCount) {
6136
+ function grokMessageCountFromSignals(signals) {
6137
+ return normalizeNonNegativeNumber(
6138
+ signals?.assistantMessageCount ??
6139
+ signals?.turnCount ??
6140
+ signals?.num_chat_messages ??
6141
+ signals?.messageCount,
6142
+ );
6143
+ }
6144
+
6145
+ function grokEffectiveTotalFromSignals(signals) {
6146
+ if (!signals || typeof signals !== "object") return 0;
6147
+ const beforeCompaction = normalizeNonNegativeNumber(signals.totalTokensBeforeCompaction);
6148
+ const totalTokens = normalizeNonNegativeNumber(signals.totalTokens);
6149
+ if (signals.contextTokensUsed == null) {
6150
+ return beforeCompaction + totalTokens;
6151
+ }
6152
+ return Math.max(
6153
+ totalTokens,
6154
+ beforeCompaction + normalizeNonNegativeNumber(signals.contextTokensUsed),
6155
+ );
6156
+ }
6157
+
6158
+ function grokTimestampToIso(value) {
6159
+ if (value == null) return null;
6160
+ if (typeof value === "number") {
6161
+ if (!Number.isFinite(value) || value <= 0) return null;
6162
+ const millis = value < 10_000_000_000 ? value * 1000 : value;
6163
+ const dt = new Date(millis);
6164
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
6165
+ }
6166
+ if (typeof value === "string") {
6167
+ const trimmed = value.trim();
6168
+ if (!trimmed) return null;
6169
+ if (/^[0-9]+(?:\.[0-9]+)?$/.test(trimmed)) {
6170
+ return grokTimestampToIso(Number(trimmed));
6171
+ }
6172
+ const dt = new Date(trimmed);
6173
+ return Number.isFinite(dt.getTime()) ? dt.toISOString() : null;
6174
+ }
6175
+ return null;
6176
+ }
6177
+
6178
+ function grokTimestampFromUpdate(meta, record, fallback) {
6179
+ return (
6180
+ grokTimestampToIso(meta?.agentTimestampMs) ||
6181
+ grokTimestampToIso(meta?.timestampMs) ||
6182
+ grokTimestampToIso(record?.timestamp_ms) ||
6183
+ grokTimestampToIso(record?.timestamp) ||
6184
+ grokTimestampToIso(record?.time) ||
6185
+ fallback ||
6186
+ null
6187
+ );
6188
+ }
6189
+
6190
+ function grokEventId(value, fallback) {
6191
+ if (typeof value === "string" && value.trim()) return value.trim();
6192
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
6193
+ return fallback;
6194
+ }
6195
+
6196
+ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp) {
6197
+ if (!updatesPath) return { events: [], recordsProcessed: 0 };
6198
+ try {
6199
+ const stat = fssync.statSync(updatesPath);
6200
+ if (!stat.isFile()) return { events: [], recordsProcessed: 0 };
6201
+ } catch {
6202
+ return { events: [], recordsProcessed: 0 };
6203
+ }
6204
+
6205
+ const events = [];
6206
+ let lineIndex = 0;
6207
+ const input = fssync.createReadStream(updatesPath, { encoding: "utf8" });
6208
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
6209
+ try {
6210
+ for await (const line of rl) {
6211
+ lineIndex++;
6212
+ if (!line || !line.trim()) continue;
6213
+ let record;
6214
+ try {
6215
+ record = JSON.parse(line);
6216
+ } catch {
6217
+ continue;
6218
+ }
6219
+ const meta = record?.params?._meta || record?._meta;
6220
+ if (!meta || typeof meta !== "object") continue;
6221
+ const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
6222
+ if (totalTokens <= 0) continue;
6223
+ const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
6224
+ events.push({
6225
+ totalTokens,
6226
+ timestamp,
6227
+ eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
6228
+ });
6229
+ }
6230
+ } catch {
6231
+ return { events, recordsProcessed: events.length };
6232
+ }
6233
+
6234
+ return { events, recordsProcessed: events.length };
6235
+ }
6236
+
6237
+ function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
6029
6238
  const total = Math.trunc(normalizeNonNegativeNumber(totalTokens));
6030
6239
  const inputTokens = Math.round(total * GROK_ESTIMATED_INPUT_RATIO);
6031
6240
  const outputTokens = Math.max(0, total - inputTokens);
6032
- const conversations = Math.max(1, Math.trunc(normalizeNonNegativeNumber(conversationCount)));
6241
+ const rawConversations = Math.trunc(normalizeNonNegativeNumber(conversationCount));
6242
+ const conversations = options.allowZeroConversationCount ? rawConversations : Math.max(1, rawConversations);
6033
6243
 
6034
6244
  return {
6035
6245
  input_tokens: inputTokens,
@@ -6062,51 +6272,103 @@ async function parseGrokBuildIncremental({
6062
6272
 
6063
6273
  let eventsAggregated = 0;
6064
6274
 
6065
- for (const sess of sessionList) {
6275
+ for (let index = 0; index < sessionList.length; index++) {
6276
+ const sess = sessionList[index];
6066
6277
  const sessionId = grokSessionIdFor(sess);
6067
- if (!sessionId) continue;
6278
+ if (!sessionId) {
6279
+ if (onProgress) onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
6280
+ continue;
6281
+ }
6068
6282
 
6069
6283
  const signals = sess?.signals && typeof sess.signals === "object"
6070
6284
  ? sess.signals
6071
6285
  : readGrokJsonFile(sess?.signalsPath);
6072
- if (!signals || typeof signals !== "object") continue;
6286
+ const safeSignals = signals && typeof signals === "object" ? signals : {};
6073
6287
 
6074
6288
  const summary = sess?.summary && typeof sess.summary === "object"
6075
6289
  ? sess.summary
6076
6290
  : readGrokJsonFile(sess?.summaryPath) || {};
6077
- const totalTokens = normalizeNonNegativeNumber(signals.contextTokensUsed ?? signals.totalTokens);
6078
- if (totalTokens <= 0) {
6079
- continue;
6080
- }
6081
-
6082
6291
  const previous = sessionSnapshots[sessionId] || {};
6083
6292
  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
6293
  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(),
6294
+ const messageCount = grokMessageCountFromSignals(safeSignals);
6295
+ const model = grokModelFromSignals(safeSignals);
6296
+ const lastActive = grokLastActiveFromSignals(safeSignals, summary);
6297
+
6298
+ let highWatermark = previousTotal;
6299
+ let observedTotal = previousTotal;
6300
+ let tokenDeltaForSession = 0;
6301
+ let finalTouchedHourStart = null;
6302
+ let source = previous.source || null;
6303
+ let lastEventId = previous.lastEventId || null;
6304
+ let lastEventTimestamp = previous.lastEventTimestamp || null;
6305
+ const pendingTokenDeltas = [];
6306
+
6307
+ const recordTokenDelta = (deltaTokens, timestamp, deltaSource) => {
6308
+ const hourStartStr = toUtcHalfHourStart(timestamp) || toUtcHalfHourStart(Date.now());
6309
+ if (!hourStartStr) return false;
6310
+ pendingTokenDeltas.push({ deltaTokens, hourStartStr });
6311
+ tokenDeltaForSession += deltaTokens;
6312
+ finalTouchedHourStart = hourStartStr;
6313
+ source = deltaSource;
6314
+ lastEventTimestamp = timestamp || lastEventTimestamp;
6315
+ return true;
6109
6316
  };
6317
+
6318
+ const updates = await readGrokUpdateTokenEvents(grokUpdatesPathForSession(sess), lastActive);
6319
+ for (const event of updates.events) {
6320
+ observedTotal = Math.max(observedTotal, event.totalTokens);
6321
+ lastEventId = event.eventId || lastEventId;
6322
+ lastEventTimestamp = event.timestamp || lastEventTimestamp;
6323
+ if (event.totalTokens <= highWatermark) continue;
6324
+ const deltaTokens = event.totalTokens - highWatermark;
6325
+ highWatermark = event.totalTokens;
6326
+ recordTokenDelta(deltaTokens, event.timestamp || lastActive, "updates");
6327
+ }
6328
+
6329
+ const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
6330
+ observedTotal = Math.max(observedTotal, effectiveSignalTotal);
6331
+ if (effectiveSignalTotal > highWatermark) {
6332
+ const deltaTokens = effectiveSignalTotal - highWatermark;
6333
+ highWatermark = effectiveSignalTotal;
6334
+ recordTokenDelta(deltaTokens, lastActive, "signals");
6335
+ }
6336
+
6337
+ const finalTotal = Math.max(previousTotal, highWatermark, observedTotal);
6338
+ const legacyBaselineOnly = previous.legacySeen && previousTotal === 0 && finalTotal > 0;
6339
+ if (!legacyBaselineOnly) {
6340
+ for (const pending of pendingTokenDeltas) {
6341
+ const delta = estimateGrokTokenDelta(pending.deltaTokens, 0, { allowZeroConversationCount: true });
6342
+ const bucket = getHourlyBucket(hourlyState, "grok", model, pending.hourStartStr);
6343
+ addTotals(bucket.totals, delta);
6344
+ touchedBuckets.add(bucketKey("grok", model, pending.hourStartStr));
6345
+ eventsAggregated++;
6346
+ }
6347
+ }
6348
+
6349
+ if (!legacyBaselineOnly && tokenDeltaForSession > 0 && finalTouchedHourStart) {
6350
+ const deltaMessageCount =
6351
+ messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
6352
+ const bucket = getHourlyBucket(hourlyState, "grok", model, finalTouchedHourStart);
6353
+ addTotals(bucket.totals, { conversation_count: deltaMessageCount });
6354
+ touchedBuckets.add(bucketKey("grok", model, finalTouchedHourStart));
6355
+ }
6356
+
6357
+ if (finalTotal > 0 && (tokenDeltaForSession > 0 || previousTotal > 0 || legacyBaselineOnly)) {
6358
+ sessionSnapshots[sessionId] = {
6359
+ totalTokens: finalTotal,
6360
+ messageCount: Math.max(previousMessageCount, messageCount),
6361
+ model,
6362
+ source: source || previous.source || null,
6363
+ lastEventId,
6364
+ lastEventTimestamp,
6365
+ updatedAt: new Date().toISOString(),
6366
+ };
6367
+ }
6368
+
6369
+ if (onProgress) {
6370
+ onProgress({ index: index + 1, total: sessionList.length, bucketsQueued: touchedBuckets.size });
6371
+ }
6110
6372
  }
6111
6373
 
6112
6374
  const bucketsQueued = queuePath
@@ -6532,6 +6794,7 @@ module.exports = {
6532
6794
  readOpencodeDbMessages,
6533
6795
  resolveKiroDbPath,
6534
6796
  resolveKiroJsonlPath,
6797
+ resolveHermesPath,
6535
6798
  resolveHermesDbPath,
6536
6799
  resolveCopilotOtelPaths,
6537
6800
  parseRolloutIncremental,
@@ -6589,7 +6852,7 @@ module.exports = {
6589
6852
  canonicalizeProjectRef,
6590
6853
  deriveProjectKeyFromRef,
6591
6854
 
6592
- // Grok Build (xAI) — SessionEnd hook + passive signals.json reader
6855
+ // Grok Build (xAI) — SessionEnd hook + passive updates.jsonl/signals.json reader
6593
6856
  resolveGrokBuildHome,
6594
6857
  resolveGrokBuildSessions,
6595
6858
  parseGrokBuildIncremental,