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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.5.53",
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",
@@ -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:",
@@ -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
  };