tokentracker-cli 0.5.53 → 0.5.54
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/package.json +2 -2
- package/src/commands/sync.js +27 -2
- package/src/lib/rollout.js +113 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw)",
|
|
3
|
+
"version": "0.5.54",
|
|
4
|
+
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Kiro, Gemini, OpenCode, OpenClaw, Hermes)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tokentracker-cli": "bin/tracker.js",
|
package/src/commands/sync.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
readOpencodeDbMessages,
|
|
14
14
|
resolveKiroDbPath,
|
|
15
15
|
resolveKiroJsonlPath,
|
|
16
|
+
resolveHermesDbPath,
|
|
16
17
|
parseRolloutIncremental,
|
|
17
18
|
parseClaudeIncremental,
|
|
18
19
|
parseGeminiIncremental,
|
|
@@ -21,6 +22,7 @@ const {
|
|
|
21
22
|
parseOpenclawIncremental,
|
|
22
23
|
parseCursorApiIncremental,
|
|
23
24
|
parseKiroIncremental,
|
|
25
|
+
parseHermesIncremental,
|
|
24
26
|
} = require("../lib/rollout");
|
|
25
27
|
const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
|
|
26
28
|
const {
|
|
@@ -320,6 +322,27 @@ async function cmdSync(argv) {
|
|
|
320
322
|
});
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
// ── Hermes Agent (SQLite-based) ──
|
|
326
|
+
let hermesResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
327
|
+
const hermesDbPath = resolveHermesDbPath();
|
|
328
|
+
if (fssync.existsSync(hermesDbPath)) {
|
|
329
|
+
if (progress?.enabled) {
|
|
330
|
+
progress.start(`Parsing Hermes ${renderBar(0)} | buckets 0`);
|
|
331
|
+
}
|
|
332
|
+
hermesResult = await parseHermesIncremental({
|
|
333
|
+
dbPath: hermesDbPath,
|
|
334
|
+
cursors,
|
|
335
|
+
queuePath,
|
|
336
|
+
onProgress: (p) => {
|
|
337
|
+
if (!progress?.enabled) return;
|
|
338
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
339
|
+
progress.update(
|
|
340
|
+
`Parsing Hermes ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} sessions | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
323
346
|
if (cursors?.projectHourly?.projects && projectQueuePath && projectQueueStatePath) {
|
|
324
347
|
for (const [projectKey, meta] of Object.entries(cursors.projectHourly.projects)) {
|
|
325
348
|
if (!meta || typeof meta !== "object") continue;
|
|
@@ -395,7 +418,8 @@ async function cmdSync(argv) {
|
|
|
395
418
|
geminiResult.filesProcessed +
|
|
396
419
|
opencodeResult.filesProcessed +
|
|
397
420
|
cursorResult.recordsProcessed +
|
|
398
|
-
kiroResult.recordsProcessed
|
|
421
|
+
kiroResult.recordsProcessed +
|
|
422
|
+
hermesResult.recordsProcessed;
|
|
399
423
|
const totalBuckets =
|
|
400
424
|
parseResult.bucketsQueued +
|
|
401
425
|
openclawResult.bucketsQueued +
|
|
@@ -403,7 +427,8 @@ async function cmdSync(argv) {
|
|
|
403
427
|
geminiResult.bucketsQueued +
|
|
404
428
|
opencodeResult.bucketsQueued +
|
|
405
429
|
cursorResult.bucketsQueued +
|
|
406
|
-
kiroResult.bucketsQueued
|
|
430
|
+
kiroResult.bucketsQueued +
|
|
431
|
+
hermesResult.bucketsQueued;
|
|
407
432
|
process.stdout.write(
|
|
408
433
|
[
|
|
409
434
|
"Sync finished:",
|
package/src/lib/rollout.js
CHANGED
|
@@ -2888,6 +2888,117 @@ async function parseKiroIncremental({ dbPath, jsonlPath, cursors, queuePath, onP
|
|
|
2888
2888
|
return { recordsProcessed: rows.length, eventsAggregated, bucketsQueued };
|
|
2889
2889
|
}
|
|
2890
2890
|
|
|
2891
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2892
|
+
// Hermes Agent — SQLite-based (sessions table in ~/.hermes/state.db)
|
|
2893
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2894
|
+
|
|
2895
|
+
function resolveHermesDbPath() {
|
|
2896
|
+
const home = require("node:os").homedir();
|
|
2897
|
+
return path.join(home, ".hermes", "state.db");
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function readHermesSessions(dbPath, sinceEpoch) {
|
|
2901
|
+
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
2902
|
+
const since = Number.isFinite(sinceEpoch) && sinceEpoch > 0 ? sinceEpoch : 0;
|
|
2903
|
+
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} AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR reasoning_tokens > 0) ORDER BY started_at ASC`;
|
|
2904
|
+
let raw;
|
|
2905
|
+
try {
|
|
2906
|
+
raw = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
|
|
2907
|
+
encoding: "utf8",
|
|
2908
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2909
|
+
timeout: 15_000,
|
|
2910
|
+
});
|
|
2911
|
+
} catch (_e) {
|
|
2912
|
+
return [];
|
|
2913
|
+
}
|
|
2914
|
+
if (!raw || !raw.trim()) return [];
|
|
2915
|
+
let rows;
|
|
2916
|
+
try {
|
|
2917
|
+
rows = JSON.parse(raw);
|
|
2918
|
+
} catch (_e) {
|
|
2919
|
+
return [];
|
|
2920
|
+
}
|
|
2921
|
+
return Array.isArray(rows) ? rows : [];
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
async function parseHermesIncremental({ dbPath, cursors, queuePath, onProgress }) {
|
|
2925
|
+
await ensureDir(path.dirname(queuePath));
|
|
2926
|
+
const hermesState = cursors.hermes && typeof cursors.hermes === "object" ? cursors.hermes : {};
|
|
2927
|
+
const lastStartedAt =
|
|
2928
|
+
typeof hermesState.lastStartedAt === "number" ? hermesState.lastStartedAt : 0;
|
|
2929
|
+
|
|
2930
|
+
const resolvedDbPath = dbPath || resolveHermesDbPath();
|
|
2931
|
+
if (!fssync.existsSync(resolvedDbPath)) {
|
|
2932
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
const rows = readHermesSessions(resolvedDbPath, lastStartedAt);
|
|
2936
|
+
if (rows.length === 0) {
|
|
2937
|
+
cursors.hermes = { ...hermesState, lastStartedAt, updatedAt: new Date().toISOString() };
|
|
2938
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
2942
|
+
const touchedBuckets = new Set();
|
|
2943
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
2944
|
+
let eventsAggregated = 0;
|
|
2945
|
+
let maxStartedAt = lastStartedAt;
|
|
2946
|
+
|
|
2947
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2948
|
+
const row = rows[i];
|
|
2949
|
+
const inputTokens = toNonNegativeInt(row.input_tokens);
|
|
2950
|
+
const outputTokens = toNonNegativeInt(row.output_tokens);
|
|
2951
|
+
const cacheRead = toNonNegativeInt(row.cache_read_tokens);
|
|
2952
|
+
const cacheWrite = toNonNegativeInt(row.cache_write_tokens);
|
|
2953
|
+
const reasoning = toNonNegativeInt(row.reasoning_tokens);
|
|
2954
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && reasoning === 0) continue;
|
|
2955
|
+
|
|
2956
|
+
// Prefer ended_at for bucket placement; fall back to started_at
|
|
2957
|
+
const epochSec = row.ended_at || row.started_at;
|
|
2958
|
+
if (!epochSec || !Number.isFinite(epochSec)) continue;
|
|
2959
|
+
const tsIso = new Date(epochSec * 1000).toISOString();
|
|
2960
|
+
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
2961
|
+
if (!bucketStart) continue;
|
|
2962
|
+
|
|
2963
|
+
const model = normalizeModelInput(row.model) || "hermes-agent";
|
|
2964
|
+
|
|
2965
|
+
const delta = {
|
|
2966
|
+
input_tokens: inputTokens,
|
|
2967
|
+
cached_input_tokens: cacheRead,
|
|
2968
|
+
cache_creation_input_tokens: cacheWrite,
|
|
2969
|
+
output_tokens: outputTokens,
|
|
2970
|
+
reasoning_output_tokens: reasoning,
|
|
2971
|
+
total_tokens: inputTokens + outputTokens + cacheRead + cacheWrite + reasoning,
|
|
2972
|
+
conversation_count: toNonNegativeInt(row.message_count) || 1,
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
const bucket = getHourlyBucket(hourlyState, "hermes", model, bucketStart);
|
|
2976
|
+
addTotals(bucket.totals, delta);
|
|
2977
|
+
touchedBuckets.add(bucketKey("hermes", model, bucketStart));
|
|
2978
|
+
eventsAggregated++;
|
|
2979
|
+
|
|
2980
|
+
if (row.started_at > maxStartedAt) maxStartedAt = row.started_at;
|
|
2981
|
+
|
|
2982
|
+
if (cb) {
|
|
2983
|
+
cb({
|
|
2984
|
+
index: i + 1,
|
|
2985
|
+
total: rows.length,
|
|
2986
|
+
recordsProcessed: i + 1,
|
|
2987
|
+
eventsAggregated,
|
|
2988
|
+
bucketsQueued: touchedBuckets.size,
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
2994
|
+
const updatedAt = new Date().toISOString();
|
|
2995
|
+
hourlyState.updatedAt = updatedAt;
|
|
2996
|
+
cursors.hourly = hourlyState;
|
|
2997
|
+
cursors.hermes = { ...hermesState, lastStartedAt: maxStartedAt, updatedAt };
|
|
2998
|
+
|
|
2999
|
+
return { recordsProcessed: rows.length, eventsAggregated, bucketsQueued };
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2891
3002
|
module.exports = {
|
|
2892
3003
|
listRolloutFiles,
|
|
2893
3004
|
listClaudeProjectFiles,
|
|
@@ -2896,6 +3007,7 @@ module.exports = {
|
|
|
2896
3007
|
readOpencodeDbMessages,
|
|
2897
3008
|
resolveKiroDbPath,
|
|
2898
3009
|
resolveKiroJsonlPath,
|
|
3010
|
+
resolveHermesDbPath,
|
|
2899
3011
|
parseRolloutIncremental,
|
|
2900
3012
|
parseClaudeIncremental,
|
|
2901
3013
|
parseGeminiIncremental,
|
|
@@ -2904,4 +3016,5 @@ module.exports = {
|
|
|
2904
3016
|
parseOpenclawIncremental,
|
|
2905
3017
|
parseCursorApiIncremental,
|
|
2906
3018
|
parseKiroIncremental,
|
|
3019
|
+
parseHermesIncremental,
|
|
2907
3020
|
};
|