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.
- package/dashboard/dist/assets/main-Bst6S3yM.css +1 -0
- package/dashboard/dist/assets/{main-DDgbwHx4.js → main-Cw4csGy9.js} +204 -190
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/sync.js +181 -0
- package/src/lib/claude-categorizer.js +759 -0
- package/src/lib/local-api.js +29 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +5 -0
- package/dashboard/dist/assets/main-HLMqEvtH.css +0 -1
|
@@ -210,8 +210,8 @@
|
|
|
210
210
|
]
|
|
211
211
|
}
|
|
212
212
|
</script>
|
|
213
|
-
<script type="module" crossorigin src="/assets/main-
|
|
214
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
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-
|
|
55
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
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.
|
|
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": {
|
package/src/commands/sync.js
CHANGED
|
@@ -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 ||= {});
|