tokentracker-cli 0.6.7 → 0.8.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/dashboard/dist/assets/{main-CuwxhzW4.js → main-CBVhF0BE.js} +169 -168
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +16 -4
- package/src/commands/status.js +13 -0
- package/src/commands/sync.js +154 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +285 -1
- package/src/lib/source-metadata.js +5 -3
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
]
|
|
211
211
|
}
|
|
212
212
|
</script>
|
|
213
|
-
<script type="module" crossorigin src="/assets/main-
|
|
213
|
+
<script type="module" crossorigin src="/assets/main-CBVhF0BE.js"></script>
|
|
214
214
|
<link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
|
|
215
215
|
</head>
|
|
216
216
|
<body>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"description": "Shareable Token Tracker dashboard snapshot."
|
|
52
52
|
}
|
|
53
53
|
</script>
|
|
54
|
-
<script type="module" crossorigin src="/assets/main-
|
|
54
|
+
<script type="module" crossorigin src="/assets/main-CBVhF0BE.js"></script>
|
|
55
55
|
<link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
|
|
56
56
|
</head>
|
|
57
57
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.
|
|
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, Craft Agents)",
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
7
7
|
"tokentracker-cli": "bin/tracker.js",
|
package/src/commands/init.js
CHANGED
|
@@ -44,6 +44,11 @@ const {
|
|
|
44
44
|
probeOpenclawSessionPluginState,
|
|
45
45
|
} = require("../lib/openclaw-session-plugin");
|
|
46
46
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
47
|
+
const {
|
|
48
|
+
resolveOmpAgentDir,
|
|
49
|
+
resolvePiAgentDir,
|
|
50
|
+
piAgentDirCollidesWithOmp,
|
|
51
|
+
} = require("../lib/rollout");
|
|
47
52
|
const { resolveRuntimeConfig, DEFAULT_BASE_URL } = require("../lib/runtime-config");
|
|
48
53
|
const {
|
|
49
54
|
BOLD,
|
|
@@ -428,15 +433,22 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
428
433
|
// oh-my-pi: passive reader — no hook installation needed.
|
|
429
434
|
// TokenTracker reads ~/.omp/agent/sessions/**/*.jsonl directly.
|
|
430
435
|
{
|
|
431
|
-
const
|
|
432
|
-
(process.env.PI_CONFIG_DIR ? path.join(home, process.env.PI_CONFIG_DIR) : path.join(home, ".omp"));
|
|
433
|
-
const ompAgentDir = process.env.PI_CODING_AGENT_DIR || path.join(ompHome, "agent");
|
|
434
|
-
const ompSessions = path.join(ompAgentDir, "sessions");
|
|
436
|
+
const ompSessions = path.join(resolveOmpAgentDir(process.env), "sessions");
|
|
435
437
|
if (fssync.existsSync(ompSessions)) {
|
|
436
438
|
summary.push({ label: "oh-my-pi", status: "detected", detail: "Passive reader (no hook needed)" });
|
|
437
439
|
}
|
|
438
440
|
}
|
|
439
441
|
|
|
442
|
+
// pi (@mariozechner/pi-coding-agent): passive reader — no hook installation needed.
|
|
443
|
+
// TokenTracker reads ~/.pi/agent/sessions/**/*.jsonl directly. Skip when its
|
|
444
|
+
// agent dir collides with omp's so the summary matches what sync will scan.
|
|
445
|
+
if (!piAgentDirCollidesWithOmp(process.env)) {
|
|
446
|
+
const piSessions = path.join(resolvePiAgentDir(process.env), "sessions");
|
|
447
|
+
if (fssync.existsSync(piSessions)) {
|
|
448
|
+
summary.push({ label: "pi", status: "detected", detail: "Passive reader (no hook needed)" });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
440
452
|
// Craft Agents: passive reader — no hook installation needed.
|
|
441
453
|
// TokenTracker reads ~/.craft-agent/workspaces/<id>/sessions/**/session.jsonl
|
|
442
454
|
// (and any user-relocated workspace listed in ~/.craft-agent/config.json).
|
package/src/commands/status.js
CHANGED
|
@@ -41,6 +41,9 @@ const {
|
|
|
41
41
|
resolveCodebuddyProjectFiles,
|
|
42
42
|
resolveOmpSessionFiles,
|
|
43
43
|
resolveOmpAgentDir,
|
|
44
|
+
resolvePiSessionFiles,
|
|
45
|
+
resolvePiAgentDir,
|
|
46
|
+
piAgentDirCollidesWithOmp,
|
|
44
47
|
resolveCraftSessionFiles,
|
|
45
48
|
resolveCraftConfigDir,
|
|
46
49
|
} = require("../lib/rollout");
|
|
@@ -185,6 +188,13 @@ async function cmdStatus(argv = []) {
|
|
|
185
188
|
const ompInstalled = fssync.existsSync(path.join(ompAgentDir, "sessions"));
|
|
186
189
|
const ompFiles = ompInstalled ? resolveOmpSessionFiles(process.env) : [];
|
|
187
190
|
|
|
191
|
+
// pi (@mariozechner/pi-coding-agent) — passive scan only (no hooks).
|
|
192
|
+
// Skip when its agent dir collides with omp's; sync would dedupe anyway.
|
|
193
|
+
const piCollides = piAgentDirCollidesWithOmp(process.env);
|
|
194
|
+
const piAgentDir = resolvePiAgentDir(process.env);
|
|
195
|
+
const piInstalled = !piCollides && fssync.existsSync(path.join(piAgentDir, "sessions"));
|
|
196
|
+
const piFiles = piInstalled ? resolvePiSessionFiles(process.env) : [];
|
|
197
|
+
|
|
188
198
|
// Craft Agents — passive scan only (no hooks).
|
|
189
199
|
const craftConfigDir = resolveCraftConfigDir(process.env);
|
|
190
200
|
const craftInstalled = fssync.existsSync(craftConfigDir);
|
|
@@ -231,6 +241,9 @@ async function cmdStatus(argv = []) {
|
|
|
231
241
|
ompInstalled
|
|
232
242
|
? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found)`
|
|
233
243
|
: null,
|
|
244
|
+
piInstalled
|
|
245
|
+
? `- pi: passive reader (${piFiles.length} session jsonl file${piFiles.length !== 1 ? "s" : ""} found)`
|
|
246
|
+
: null,
|
|
234
247
|
craftInstalled
|
|
235
248
|
? `- Craft Agents: passive reader (${craftFiles.length} session jsonl file${craftFiles.length !== 1 ? "s" : ""} found)`
|
|
236
249
|
: null,
|
package/src/commands/sync.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("node:path");
|
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
4
|
const fssync = require("node:fs");
|
|
5
5
|
const cp = require("node:child_process");
|
|
6
|
+
const readline = require("node:readline");
|
|
6
7
|
|
|
7
8
|
const { ensureDir, readJson, writeJson, openLock } = require("../lib/fs");
|
|
8
9
|
const {
|
|
@@ -29,6 +30,9 @@ const {
|
|
|
29
30
|
parseKimiIncremental,
|
|
30
31
|
resolveOmpSessionFiles,
|
|
31
32
|
parseOmpIncremental,
|
|
33
|
+
resolvePiSessionFiles,
|
|
34
|
+
parsePiIncremental,
|
|
35
|
+
piAgentDirCollidesWithOmp,
|
|
32
36
|
resolveCraftSessionFiles,
|
|
33
37
|
parseCraftIncremental,
|
|
34
38
|
resolveCodebuddyProjectFiles,
|
|
@@ -57,6 +61,8 @@ const { resolveRuntimeConfig } = require("../lib/runtime-config");
|
|
|
57
61
|
|
|
58
62
|
const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
|
|
59
63
|
const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
|
|
64
|
+
const CLAUDE_MEM_OBSERVER_REINCLUDE_KEY = "claudeMemObserverReinclude_2026_05_v3";
|
|
65
|
+
const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
|
|
60
66
|
|
|
61
67
|
async function cmdSync(argv) {
|
|
62
68
|
const opts = parseArgs(argv);
|
|
@@ -171,6 +177,7 @@ async function cmdSync(argv) {
|
|
|
171
177
|
openclawResult.bucketsQueued += openclawFallback.bucketsQueued;
|
|
172
178
|
|
|
173
179
|
const claudeFiles = await listClaudeProjectFiles(claudeProjectsDir);
|
|
180
|
+
await reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath });
|
|
174
181
|
let claudeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
175
182
|
if (claudeFiles.length > 0) {
|
|
176
183
|
if (progress?.enabled) {
|
|
@@ -473,6 +480,33 @@ async function cmdSync(argv) {
|
|
|
473
480
|
});
|
|
474
481
|
}
|
|
475
482
|
|
|
483
|
+
// ── pi (@mariozechner/pi-coding-agent) — passive ~/.pi/agent/sessions/**/*.jsonl reader ──
|
|
484
|
+
// Skip pi parse if its agent dir resolves to the same path as omp's. This
|
|
485
|
+
// prevents double-counting when explicit overrides (TOKENTRACKER_OMP_AGENT_DIR /
|
|
486
|
+
// TOKENTRACKER_PI_AGENT_DIR) bypass the install-signal disambiguator.
|
|
487
|
+
let piResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
488
|
+
const piFiles = piAgentDirCollidesWithOmp(process.env)
|
|
489
|
+
? []
|
|
490
|
+
: resolvePiSessionFiles(process.env);
|
|
491
|
+
if (piFiles.length > 0) {
|
|
492
|
+
if (progress?.enabled) {
|
|
493
|
+
progress.start(`Parsing pi ${renderBar(0)} | buckets 0`);
|
|
494
|
+
}
|
|
495
|
+
piResult = await parsePiIncremental({
|
|
496
|
+
sessionFiles: piFiles,
|
|
497
|
+
cursors,
|
|
498
|
+
queuePath,
|
|
499
|
+
env: process.env,
|
|
500
|
+
onProgress: (p) => {
|
|
501
|
+
if (!progress?.enabled) return;
|
|
502
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
503
|
+
progress.update(
|
|
504
|
+
`Parsing pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
505
|
+
);
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
476
510
|
// ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
|
|
477
511
|
let craftResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
478
512
|
const craftFiles = resolveCraftSessionFiles(process.env);
|
|
@@ -615,6 +649,7 @@ async function cmdSync(argv) {
|
|
|
615
649
|
kimiResult.recordsProcessed +
|
|
616
650
|
codebuddyResult.recordsProcessed +
|
|
617
651
|
ompResult.recordsProcessed +
|
|
652
|
+
piResult.recordsProcessed +
|
|
618
653
|
craftResult.recordsProcessed +
|
|
619
654
|
copilotResult.recordsProcessed;
|
|
620
655
|
const totalBuckets =
|
|
@@ -630,6 +665,7 @@ async function cmdSync(argv) {
|
|
|
630
665
|
kimiResult.bucketsQueued +
|
|
631
666
|
codebuddyResult.bucketsQueued +
|
|
632
667
|
ompResult.bucketsQueued +
|
|
668
|
+
piResult.bucketsQueued +
|
|
633
669
|
craftResult.bucketsQueued +
|
|
634
670
|
copilotResult.bucketsQueued;
|
|
635
671
|
process.stdout.write(
|
|
@@ -680,8 +716,10 @@ module.exports = {
|
|
|
680
716
|
cmdSync,
|
|
681
717
|
migrateCursorUnknownBuckets,
|
|
682
718
|
migrateRolloutCumulativeDeltaBuckets,
|
|
719
|
+
reincludeClaudeMemObserverFiles,
|
|
683
720
|
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
684
721
|
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
722
|
+
CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
|
|
685
723
|
};
|
|
686
724
|
|
|
687
725
|
function normalizeString(value) {
|
|
@@ -959,8 +997,6 @@ async function writeOpenclawSignal(trackerDir) {
|
|
|
959
997
|
const AUTO_RETRY_FILENAME = "auto.retry.json";
|
|
960
998
|
const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000;
|
|
961
999
|
|
|
962
|
-
const readline = require("node:readline");
|
|
963
|
-
|
|
964
1000
|
const INGEST_SLUG = "tokentracker-ingest";
|
|
965
1001
|
const MAX_INGEST_BUCKETS = 500;
|
|
966
1002
|
|
|
@@ -1159,3 +1195,119 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
|
|
|
1159
1195
|
|
|
1160
1196
|
cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
|
|
1161
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
async function reincludeClaudeMemObserverFiles({ cursors, claudeFiles, queuePath }) {
|
|
1200
|
+
if (!cursors || typeof cursors !== "object") return false;
|
|
1201
|
+
const migrations = (cursors.migrations ||= {});
|
|
1202
|
+
if (migrations[CLAUDE_MEM_OBSERVER_REINCLUDE_KEY]) return false;
|
|
1203
|
+
|
|
1204
|
+
const observerPaths = (Array.isArray(claudeFiles) ? claudeFiles : [])
|
|
1205
|
+
.map((entry) => (typeof entry === "string" ? entry : entry?.path))
|
|
1206
|
+
.filter((p) => typeof p === "string" && p.includes(CLAUDE_MEM_OBSERVER_PATH_SEGMENT));
|
|
1207
|
+
|
|
1208
|
+
if (!cursors.files || typeof cursors.files !== "object") {
|
|
1209
|
+
cursors.files = {};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
let filesReset = 0;
|
|
1213
|
+
for (const filePath of observerPaths) {
|
|
1214
|
+
if (cursors.files[filePath]) {
|
|
1215
|
+
delete cursors.files[filePath];
|
|
1216
|
+
filesReset += 1;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const hashesToRemove = observerPaths.length > 0
|
|
1221
|
+
? await collectClaudeMessageHashes(observerPaths)
|
|
1222
|
+
: new Set();
|
|
1223
|
+
let hashesRemoved = 0;
|
|
1224
|
+
if (Array.isArray(cursors.claudeHashes) && hashesToRemove.size > 0) {
|
|
1225
|
+
const nextHashes = [];
|
|
1226
|
+
for (const hash of cursors.claudeHashes) {
|
|
1227
|
+
if (hashesToRemove.has(hash)) {
|
|
1228
|
+
hashesRemoved += 1;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
nextHashes.push(hash);
|
|
1232
|
+
}
|
|
1233
|
+
cursors.claudeHashes = nextHashes;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const queueRowsRelabeled = typeof queuePath === "string" && queuePath
|
|
1237
|
+
? await relabelClaudeMemQueueRows(queuePath)
|
|
1238
|
+
: 0;
|
|
1239
|
+
|
|
1240
|
+
migrations[CLAUDE_MEM_OBSERVER_REINCLUDE_KEY] = {
|
|
1241
|
+
appliedAt: new Date().toISOString(),
|
|
1242
|
+
filesReset,
|
|
1243
|
+
hashesRemoved,
|
|
1244
|
+
queueRowsRelabeled,
|
|
1245
|
+
};
|
|
1246
|
+
return filesReset > 0 || hashesRemoved > 0 || queueRowsRelabeled > 0;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async function relabelClaudeMemQueueRows(queuePath) {
|
|
1250
|
+
let raw;
|
|
1251
|
+
try {
|
|
1252
|
+
raw = await fs.readFile(queuePath, "utf8");
|
|
1253
|
+
} catch (_e) {
|
|
1254
|
+
return 0;
|
|
1255
|
+
}
|
|
1256
|
+
if (!raw || !raw.includes('"claude-mem"')) return 0;
|
|
1257
|
+
|
|
1258
|
+
const lines = raw.split("\n");
|
|
1259
|
+
const out = [];
|
|
1260
|
+
let relabeled = 0;
|
|
1261
|
+
for (const line of lines) {
|
|
1262
|
+
if (!line) {
|
|
1263
|
+
out.push(line);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
let obj;
|
|
1267
|
+
try {
|
|
1268
|
+
obj = JSON.parse(line);
|
|
1269
|
+
} catch (_e) {
|
|
1270
|
+
out.push(line);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (obj && obj.source === "claude-mem") {
|
|
1274
|
+
obj.source = "claude";
|
|
1275
|
+
relabeled += 1;
|
|
1276
|
+
out.push(JSON.stringify(obj));
|
|
1277
|
+
} else {
|
|
1278
|
+
out.push(line);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (relabeled === 0) return 0;
|
|
1282
|
+
|
|
1283
|
+
await fs.writeFile(queuePath, out.join("\n"), "utf8");
|
|
1284
|
+
return relabeled;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function collectClaudeMessageHashes(filePaths) {
|
|
1288
|
+
const hashes = new Set();
|
|
1289
|
+
for (const filePath of filePaths) {
|
|
1290
|
+
let stream;
|
|
1291
|
+
try {
|
|
1292
|
+
stream = fssync.createReadStream(filePath, { encoding: "utf8" });
|
|
1293
|
+
} catch (_e) {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1297
|
+
for await (const line of rl) {
|
|
1298
|
+
if (!line.includes('"usage"')) continue;
|
|
1299
|
+
let obj;
|
|
1300
|
+
try {
|
|
1301
|
+
obj = JSON.parse(line);
|
|
1302
|
+
} catch (_e) {
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
const msgId = obj?.message?.id;
|
|
1306
|
+
const reqId = obj?.requestId;
|
|
1307
|
+
if (msgId && reqId) hashes.add(`${msgId}:${reqId}`);
|
|
1308
|
+
}
|
|
1309
|
+
rl.close();
|
|
1310
|
+
stream.close?.();
|
|
1311
|
+
}
|
|
1312
|
+
return hashes;
|
|
1313
|
+
}
|