tokentracker-cli 0.14.6 → 0.15.1
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 +6 -4
- package/README.zh-CN.md +7 -5
- package/dashboard/dist/assets/{main-DZknCFji.js → main-DAAixEqh.js} +168 -168
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +26 -0
- package/src/commands/status.js +12 -0
- package/src/commands/sync.js +68 -0
- package/src/commands/uninstall.js +5 -0
- package/src/lib/diagnostics.js +13 -0
- package/src/lib/grok-hook.js +272 -0
- package/src/lib/pricing/curated-overrides.json +8 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +250 -0
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
};
|