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.
- package/LICENSE +1 -1
- package/dashboard/dist/assets/main-Bst6S3yM.css +1 -0
- package/dashboard/dist/assets/{main-CBVhF0BE.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 +167 -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.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": {
|
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,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 ||= {});
|