tokentracker-cli 0.9.0 → 0.10.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.
@@ -210,8 +210,8 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-DDgbwHx4.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-DDgbwHx4.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.9.0",
3
+ "version": "0.10.1",
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,11 @@ 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
+ // v1 had a cursor-format bug (wrote plain integer instead of {inode, offset,
71
+ // updatedAt}), which made parseClaudeIncremental reread every jsonl from
72
+ // byte 0 on the next sync and double everything. v2 fixes the format and
73
+ // re-runs the repair regardless of whether v1 already applied.
74
+ const CLAUDE_GROUND_TRUTH_REPAIR_KEY = "claudeGroundTruthRepair_2026_05_v2";
66
75
 
67
76
  async function cmdSync(argv) {
68
77
  const opts = parseArgs(argv);
@@ -178,6 +187,7 @@ async function cmdSync(argv) {
178
187
 
179
188
  const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
180
189
  await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath });
190
+ await repairClaudeQueueFromGroundTruth({ cursors, queuePath, queueStatePath });
181
191
  let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
182
192
  if (claudeFiles.length > 0) {
183
193
  if (progress?.enabled) {
@@ -1196,6 +1206,177 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
1196
1206
  cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
1197
1207
  }
1198
1208
 
1209
+ // One-time repair migration: rebuild source=claude rows in queue.jsonl from
1210
+ // the actual jsonl files using ccusage's algorithm (msgId+reqId global
1211
+ // dedup). Earlier `reincludeClaudeMemObserverFiles` versions (v1/v2/v3) each
1212
+ // reset the hash set and re-read observer jsonls, which silently inflated
1213
+ // queue.jsonl's claude totals by ~40%. We do an atomic rewrite — keep all
1214
+ // non-claude rows verbatim, replace every claude/claude-mem row with the
1215
+ // ground-truth set — then reset cursors so the next incremental sync stays
1216
+ // in sync, and reset the cloud upload offset so the corrected rows actually
1217
+ // reach the cloud (the ingest endpoint upserts by (source, model,
1218
+ // hour_start), so re-uploading other sources is idempotent).
1219
+ async function repairClaudeQueueFromGroundTruth({ cursors, queuePath, queueStatePath = null }) {
1220
+ if (!cursors || typeof cursors !== "object") return false;
1221
+ const migrations = (cursors.migrations ||= {});
1222
+ if (migrations[CLAUDE_GROUND_TRUTH_REPAIR_KEY]) return false;
1223
+
1224
+ let result;
1225
+ try {
1226
+ result = await computeClaudeGroundTruthBuckets();
1227
+ } catch (e) {
1228
+ console.error("[sync] claude ground-truth repair: scan failed:", e?.message || e);
1229
+ return false;
1230
+ }
1231
+ const { rows, seenHashes, fileList } = result;
1232
+
1233
+ // 1. Atomic rewrite of queue.jsonl: keep non-claude rows, drop existing
1234
+ // claude/claude-mem rows, append truth rows. Atomic via tmp + rename.
1235
+ let claudeRowsRemoved = 0;
1236
+ if (typeof queuePath === "string" && queuePath) {
1237
+ let raw = "";
1238
+ try {
1239
+ raw = await fs.readFile(queuePath, "utf8");
1240
+ } catch (e) {
1241
+ if (e?.code !== "ENOENT") throw e;
1242
+ }
1243
+ const keptLines = [];
1244
+ for (const line of raw.split("\n")) {
1245
+ if (!line.trim()) continue;
1246
+ let row;
1247
+ try {
1248
+ row = JSON.parse(line);
1249
+ } catch (_e) {
1250
+ // Preserve unparseable lines verbatim — operator may want to
1251
+ // recover them later.
1252
+ keptLines.push(line);
1253
+ continue;
1254
+ }
1255
+ if (row?.source === "claude" || row?.source === "claude-mem") {
1256
+ claudeRowsRemoved += 1;
1257
+ continue;
1258
+ }
1259
+ keptLines.push(line);
1260
+ }
1261
+
1262
+ const truthLines = rows.map((r) =>
1263
+ JSON.stringify({
1264
+ source: "claude",
1265
+ model: r.model,
1266
+ hour_start: r.hour_start,
1267
+ input_tokens: r.input_tokens,
1268
+ cached_input_tokens: r.cached_input_tokens,
1269
+ cache_creation_input_tokens: r.cache_creation_input_tokens,
1270
+ output_tokens: r.output_tokens,
1271
+ reasoning_output_tokens: r.reasoning_output_tokens,
1272
+ total_tokens: r.total_tokens,
1273
+ billable_total_tokens: r.billable_total_tokens,
1274
+ conversation_count: r.conversation_count,
1275
+ }),
1276
+ );
1277
+
1278
+ await ensureDir(path.dirname(queuePath));
1279
+ const out = keptLines.concat(truthLines).join("\n") + "\n";
1280
+ const tmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
1281
+ await fs.writeFile(tmp, out, "utf8");
1282
+ await fs.rename(tmp, queuePath);
1283
+ }
1284
+
1285
+ // 2. Reset cursors.hourly.buckets / groupQueued for source=claude (and the
1286
+ // dead source=claude-mem buckets) so incremental sync's in-memory state
1287
+ // matches the truth.
1288
+ const hourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
1289
+ hourly.buckets ||= {};
1290
+ hourly.groupQueued ||= {};
1291
+
1292
+ let bucketsCleared = 0;
1293
+ for (const k of Object.keys(hourly.buckets)) {
1294
+ if (k.startsWith("claude|") || k.startsWith("claude-mem|")) {
1295
+ delete hourly.buckets[k];
1296
+ bucketsCleared += 1;
1297
+ }
1298
+ }
1299
+ for (const k of Object.keys(hourly.groupQueued)) {
1300
+ if (k.startsWith("claude|") || k.startsWith("claude-mem|")) {
1301
+ delete hourly.groupQueued[k];
1302
+ }
1303
+ }
1304
+
1305
+ for (const r of rows) {
1306
+ const totals = {
1307
+ input_tokens: r.input_tokens,
1308
+ cached_input_tokens: r.cached_input_tokens,
1309
+ cache_creation_input_tokens: r.cache_creation_input_tokens,
1310
+ output_tokens: r.output_tokens,
1311
+ reasoning_output_tokens: r.reasoning_output_tokens,
1312
+ total_tokens: r.total_tokens,
1313
+ billable_total_tokens: r.billable_total_tokens,
1314
+ conversation_count: r.conversation_count,
1315
+ };
1316
+ const key = bucketKey("claude", r.model, r.hour_start);
1317
+ hourly.buckets[key] = {
1318
+ totals,
1319
+ queuedKey: totalsKey(totals),
1320
+ source: "claude",
1321
+ hour_start: r.hour_start,
1322
+ };
1323
+ hourly.groupQueued[groupBucketKey("claude", r.hour_start)] = totalsKey(totals);
1324
+ }
1325
+
1326
+ // 3. Reset per-file cursors so future incremental sync only reads genuinely
1327
+ // new tail content. Format must match what rollout.js expects:
1328
+ // { inode, offset, updatedAt }. Setting a plain integer here breaks
1329
+ // the inode-equality check inside parseClaudeFile, which would treat
1330
+ // the file as untracked and re-read it from byte 0 — silently doubling
1331
+ // everything. (That was the actual cause of the regression after the
1332
+ // first repair attempt.)
1333
+ cursors.files ||= {};
1334
+ let filesReset = 0;
1335
+ const nowIso = new Date().toISOString();
1336
+ for (const fp of fileList) {
1337
+ let st;
1338
+ try {
1339
+ st = fssync.statSync(fp);
1340
+ } catch (_e) {
1341
+ continue;
1342
+ }
1343
+ cursors.files[fp] = {
1344
+ inode: st.ino || 0,
1345
+ offset: st.size,
1346
+ updatedAt: nowIso,
1347
+ };
1348
+ filesReset += 1;
1349
+ }
1350
+ cursors.claudeHashes = seenHashes;
1351
+
1352
+ // 4. Reset cloud-upload offset so the corrected rows are re-sent. Other
1353
+ // sources are upserted idempotently by the ingest endpoint, so this is
1354
+ // safe — just costs one extra round of bandwidth.
1355
+ if (typeof queueStatePath === "string" && queueStatePath) {
1356
+ let uploadState = {};
1357
+ try {
1358
+ uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8"));
1359
+ } catch (_e) {
1360
+ uploadState = {};
1361
+ }
1362
+ uploadState.offset = 0;
1363
+ uploadState.updatedAt = new Date().toISOString();
1364
+ uploadState.note = "reset_after_claude_repair_2026_05_v1";
1365
+ await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
1366
+ }
1367
+
1368
+ migrations[CLAUDE_GROUND_TRUTH_REPAIR_KEY] = {
1369
+ appliedAt: new Date().toISOString(),
1370
+ bucketsWritten: rows.length,
1371
+ bucketsCleared,
1372
+ rowsRemoved: claudeRowsRemoved,
1373
+ filesReset,
1374
+ hashesRetained: seenHashes.length,
1375
+ uploadOffsetReset: typeof queueStatePath === "string" && !!queueStatePath,
1376
+ };
1377
+ return true;
1378
+ }
1379
+
1199
1380
  async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath }) {
1200
1381
  if (!cursors || typeof cursors !== "object") return false;
1201
1382
  const migrations = (cursors.migrations ||= {});