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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dashboard/dist/assets/{Card-ANC9ZmIT.js → Card-jA08WeEw.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DpZaCksZ.js → DashboardPage-chDVOYmG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-WXGyOn0H.js → FadeIn-DqSYXuUL.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DUkE0Dwd.js → HeaderGithubStar-C11rWv0B.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BPF8eGpg.js → IpCheckPage-CkEZ9yLK.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-0uTpqpAU.js → LandingPage-BgckTHRQ.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-DzxRJEzb.js → LeaderboardPage-BCNW7UWp.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-C3_oUxhG.js → LeaderboardProfilePage-BLATxMt-.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BVuvoeY9.js → LimitsPage-arF--WgR.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CfkNRmT6.js → LoginPage-DpoFP0va.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CfxiYbJm.js → PopoverPopup-kdgc2H6C.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DiPzAed2.js → ProviderIcon-DV5r9qqP.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Devu7beE.js → SettingsPage-Bb22ORmU.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CSe8fW4V.js → SkillsPage-xhtBqVKC.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BrLp5YLk.js → WidgetsPage-CUoSVDET.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-nFF6Yj_r.js → chevron-down-DYb2EChD.js} +1 -1
- package/dashboard/dist/assets/{download-DhSZ--68.js → download-C-_8o6dh.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-CvFdXrw5.js → leaderboard-columns-BgzBlYo7.js} +1 -1
- package/dashboard/dist/assets/{main-DtrPNYb7.js → main-11hApDak.js} +3 -3
- package/dashboard/dist/assets/{use-limits-display-prefs-Yy8t7tbB.js → use-limits-display-prefs-BeGKWUuk.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-uemf9RSH.js → use-native-settings-nTTHktn0.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-DH8DxE18.js → use-reduced-motion-DU8Gm6j1.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-C3vUT6PH.js → use-usage-limits-DTPmEB8Y.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +9 -5
- package/src/commands/serve.js +0 -12
- package/src/commands/sync.js +370 -7
- package/src/lib/grok-hook.js +86 -7
- package/src/lib/pricing/curated-overrides.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +403 -140
- package/src/lib/subscriptions.js +92 -40
- package/src/lib/usage-limits.js +1 -1
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
|
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"
|
|
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
|
|
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
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
-
|
|
3070
|
-
const nextSnapshots = {};
|
|
3106
|
+
const touchedBuckets = new Set();
|
|
3071
3107
|
|
|
3072
|
-
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
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
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
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
|
-
|
|
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
|
|
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/**/
|
|
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
|
-
//
|
|
5899
|
-
//
|
|
5900
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
6275
|
+
for (let index = 0; index < sessionList.length; index++) {
|
|
6276
|
+
const sess = sessionList[index];
|
|
6066
6277
|
const sessionId = grokSessionIdFor(sess);
|
|
6067
|
-
if (!sessionId)
|
|
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
|
-
|
|
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
|
|
6092
|
-
|
|
6093
|
-
const
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
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,
|