tokentracker-cli 0.8.1 → 0.10.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.
@@ -210,8 +210,8 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-CBVhF0BE.js"></script>
214
- <link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
213
+ <script type="module" crossorigin src="/assets/main-Cw4csGy9.js"></script>
214
+ <link rel="stylesheet" crossorigin href="/assets/main-Bst6S3yM.css">
215
215
  </head>
216
216
  <body>
217
217
  <main class="aeo-seed-content" aria-label="Token Tracker AI-readable summary">
@@ -51,8 +51,8 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-CBVhF0BE.js"></script>
55
- <link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
54
+ <script type="module" crossorigin src="/assets/main-Cw4csGy9.js"></script>
55
+ <link rel="stylesheet" crossorigin href="/assets/main-Bst6S3yM.css">
56
56
  </head>
57
57
  <body>
58
58
  <main class="aeo-seed-content" aria-label="Token Tracker share page summary">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi, pi, Craft Agents)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -40,7 +40,11 @@ const {
40
40
  resolveKiroCliSessionFiles,
41
41
  resolveKiroCliDbPath,
42
42
  parseKiroCliIncremental,
43
+ bucketKey,
44
+ totalsKey,
45
+ groupBucketKey,
43
46
  } = require("../lib/rollout");
47
+ const { computeClaudeGroundTruthBuckets } = require("../lib/claude-categorizer");
44
48
  const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
45
49
  const {
46
50
  normalizeState: normalizeUploadState,
@@ -63,6 +67,7 @@ const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
63
67
  const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
64
68
  const CLAUDE_MEM_OBSERVER_REINCLUDE_KEY = "claudeMemObserverReinclude_2026_05_v3";
65
69
  const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
70
+ const CLAUDE_GROUND_TRUTH_REPAIR_KEY = "claudeGroundTruthRepair_2026_05_v1";
66
71
 
67
72
  async function cmdSync(argv) {
68
73
  const opts = parseArgs(argv);
@@ -178,6 +183,7 @@ async function cmdSync(argv) {
178
183
 
179
184
  const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
180
185
  await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath });
186
+ await repairClaudeQueueFromGroundTruth({ cursors, queuePath, queueStatePath });
181
187
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
182
188
  if (claudeFiles.length > 0) {
183
189
  if (progress?.enabled) {
@@ -1196,6 +1202,167 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
1196
1202
  cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
1197
1203
  }
1198
1204
 
1205
+ // One-time repair migration: rebuild source=claude rows in queue.jsonl from
1206
+ // the actual jsonl files using ccusage's algorithm (msgId+reqId global
1207
+ // dedup). Earlier `reincludeClaudeMemObserverFiles` versions (v1/v2/v3) each
1208
+ // reset the hash set and re-read observer jsonls, which silently inflated
1209
+ // queue.jsonl's claude totals by ~40%. We do an atomic rewrite — keep all
1210
+ // non-claude rows verbatim, replace every claude/claude-mem row with the
1211
+ // ground-truth set — then reset cursors so the next incremental sync stays
1212
+ // in sync, and reset the cloud upload offset so the corrected rows actually
1213
+ // reach the cloud (the ingest endpoint upserts by (source, model,
1214
+ // hour_start), so re-uploading other sources is idempotent).
1215
+ async function repairClaudeQueueFromGroundTruth({ cursors, queuePath, queueStatePath = null }) {
1216
+ if (!cursors || typeof cursors !== "object") return false;
1217
+ const migrations = (cursors.migrations ||= {});
1218
+ if (migrations[CLAUDE_GROUND_TRUTH_REPAIR_KEY]) return false;
1219
+
1220
+ let result;
1221
+ try {
1222
+ result = await computeClaudeGroundTruthBuckets();
1223
+ } catch (e) {
1224
+ console.error("[sync] claude ground-truth repair: scan failed:", e?.message || e);
1225
+ return false;
1226
+ }
1227
+ const { rows, seenHashes, fileList } = result;
1228
+
1229
+ // 1. Atomic rewrite of queue.jsonl: keep non-claude rows, drop existing
1230
+ // claude/claude-mem rows, append truth rows. Atomic via tmp + rename.
1231
+ let claudeRowsRemoved = 0;
1232
+ if (typeof queuePath === "string" && queuePath) {
1233
+ let raw = "";
1234
+ try {
1235
+ raw = await fs.readFile(queuePath, "utf8");
1236
+ } catch (e) {
1237
+ if (e?.code !== "ENOENT") throw e;
1238
+ }
1239
+ const keptLines = [];
1240
+ for (const line of raw.split("\n")) {
1241
+ if (!line.trim()) continue;
1242
+ let row;
1243
+ try {
1244
+ row = JSON.parse(line);
1245
+ } catch (_e) {
1246
+ // Preserve unparseable lines verbatim — operator may want to
1247
+ // recover them later.
1248
+ keptLines.push(line);
1249
+ continue;
1250
+ }
1251
+ if (row?.source === "claude" || row?.source === "claude-mem") {
1252
+ claudeRowsRemoved += 1;
1253
+ continue;
1254
+ }
1255
+ keptLines.push(line);
1256
+ }
1257
+
1258
+ const truthLines = rows.map((r) =>
1259
+ JSON.stringify({
1260
+ source: "claude",
1261
+ model: r.model,
1262
+ hour_start: r.hour_start,
1263
+ input_tokens: r.input_tokens,
1264
+ cached_input_tokens: r.cached_input_tokens,
1265
+ cache_creation_input_tokens: r.cache_creation_input_tokens,
1266
+ output_tokens: r.output_tokens,
1267
+ reasoning_output_tokens: r.reasoning_output_tokens,
1268
+ total_tokens: r.total_tokens,
1269
+ billable_total_tokens: r.billable_total_tokens,
1270
+ conversation_count: r.conversation_count,
1271
+ }),
1272
+ );
1273
+
1274
+ await ensureDir(path.dirname(queuePath));
1275
+ const out = keptLines.concat(truthLines).join("\n") + "\n";
1276
+ const tmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
1277
+ await fs.writeFile(tmp, out, "utf8");
1278
+ await fs.rename(tmp, queuePath);
1279
+ }
1280
+
1281
+ // 2. Reset cursors.hourly.buckets / groupQueued for source=claude (and the
1282
+ // dead source=claude-mem buckets) so incremental sync's in-memory state
1283
+ // matches the truth.
1284
+ const hourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
1285
+ hourly.buckets ||= {};
1286
+ hourly.groupQueued ||= {};
1287
+
1288
+ let bucketsCleared = 0;
1289
+ for (const k of Object.keys(hourly.buckets)) {
1290
+ if (k.startsWith("claude|") || k.startsWith("claude-mem|")) {
1291
+ delete hourly.buckets[k];
1292
+ bucketsCleared += 1;
1293
+ }
1294
+ }
1295
+ for (const k of Object.keys(hourly.groupQueued)) {
1296
+ if (k.startsWith("claude|") || k.startsWith("claude-mem|")) {
1297
+ delete hourly.groupQueued[k];
1298
+ }
1299
+ }
1300
+
1301
+ for (const r of rows) {
1302
+ const totals = {
1303
+ input_tokens: r.input_tokens,
1304
+ cached_input_tokens: r.cached_input_tokens,
1305
+ cache_creation_input_tokens: r.cache_creation_input_tokens,
1306
+ output_tokens: r.output_tokens,
1307
+ reasoning_output_tokens: r.reasoning_output_tokens,
1308
+ total_tokens: r.total_tokens,
1309
+ billable_total_tokens: r.billable_total_tokens,
1310
+ conversation_count: r.conversation_count,
1311
+ };
1312
+ const key = bucketKey("claude", r.model, r.hour_start);
1313
+ hourly.buckets[key] = {
1314
+ totals,
1315
+ queuedKey: totalsKey(totals),
1316
+ source: "claude",
1317
+ hour_start: r.hour_start,
1318
+ };
1319
+ hourly.groupQueued[groupBucketKey("claude", r.hour_start)] = totalsKey(totals);
1320
+ }
1321
+
1322
+ // 3. Reset per-file cursors so future incremental sync only reads genuinely
1323
+ // new tail content.
1324
+ cursors.files ||= {};
1325
+ let filesReset = 0;
1326
+ for (const fp of fileList) {
1327
+ let size = 0;
1328
+ try {
1329
+ size = fssync.statSync(fp).size;
1330
+ } catch (_e) {
1331
+ continue;
1332
+ }
1333
+ cursors.files[fp] = size;
1334
+ filesReset += 1;
1335
+ }
1336
+ cursors.claudeHashes = seenHashes;
1337
+
1338
+ // 4. Reset cloud-upload offset so the corrected rows are re-sent. Other
1339
+ // sources are upserted idempotently by the ingest endpoint, so this is
1340
+ // safe — just costs one extra round of bandwidth.
1341
+ if (typeof queueStatePath === "string" && queueStatePath) {
1342
+ let uploadState = {};
1343
+ try {
1344
+ uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8"));
1345
+ } catch (_e) {
1346
+ uploadState = {};
1347
+ }
1348
+ uploadState.offset = 0;
1349
+ uploadState.updatedAt = new Date().toISOString();
1350
+ uploadState.note = "reset_after_claude_repair_2026_05_v1";
1351
+ await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
1352
+ }
1353
+
1354
+ migrations[CLAUDE_GROUND_TRUTH_REPAIR_KEY] = {
1355
+ appliedAt: new Date().toISOString(),
1356
+ bucketsWritten: rows.length,
1357
+ bucketsCleared,
1358
+ rowsRemoved: claudeRowsRemoved,
1359
+ filesReset,
1360
+ hashesRetained: seenHashes.length,
1361
+ uploadOffsetReset: typeof queueStatePath === "string" && !!queueStatePath,
1362
+ };
1363
+ return true;
1364
+ }
1365
+
1199
1366
  async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath }) {
1200
1367
  if (!cursors || typeof cursors !== "object") return false;
1201
1368
  const migrations = (cursors.migrations ||= {});