tokentracker-cli 0.5.98 → 0.5.100
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/README.md +6 -5
- package/README.zh-CN.md +7 -5
- package/bin/tracker.js +14 -0
- package/dashboard/dist/assets/{main-io8wJG1t.js → main-BYMjcXxR.js} +202 -194
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +3 -2
- package/src/commands/init.js +20 -1
- package/src/commands/serve.js +7 -0
- package/src/commands/status.js +63 -15
- package/src/commands/sync.js +100 -1
- package/src/lib/cursor-config.js +15 -0
- package/src/lib/local-api.js +39 -16
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/proxy-env.js +81 -0
- package/src/lib/rollout.js +299 -20
- package/src/lib/source-metadata.js +46 -0
- package/src/lib/usage-limits.js +312 -40
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
]
|
|
136
136
|
}
|
|
137
137
|
</script>
|
|
138
|
-
<script type="module" crossorigin src="/assets/main-
|
|
138
|
+
<script type="module" crossorigin src="/assets/main-BYMjcXxR.js"></script>
|
|
139
139
|
<link rel="stylesheet" crossorigin href="/assets/main-HLMqEvtH.css">
|
|
140
140
|
</head>
|
|
141
141
|
<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-BYMjcXxR.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.5.
|
|
4
|
-
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor,
|
|
3
|
+
"version": "0.5.100",
|
|
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)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tokentracker-cli": "bin/tracker.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dashboard:open": "bash scripts/open-dashboard.sh",
|
|
30
30
|
"dashboard:preview": "npm --prefix dashboard run preview",
|
|
31
31
|
"dev:shim": "node scripts/dev-bin-shim.cjs",
|
|
32
|
+
"audit:tokens": "node scripts/audit-token-correctness.cjs",
|
|
32
33
|
"graph:auto-index": "node scripts/graph/auto-index.cjs",
|
|
33
34
|
"graph:scip": "node scripts/graph/generate-scip.cjs",
|
|
34
35
|
"pricing:build-seed": "node scripts/build-pricing-seed.cjs",
|
package/src/commands/init.js
CHANGED
|
@@ -425,6 +425,18 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
// oh-my-pi: passive reader — no hook installation needed.
|
|
429
|
+
// TokenTracker reads ~/.omp/agent/sessions/**/*.jsonl directly.
|
|
430
|
+
{
|
|
431
|
+
const ompHome = process.env.OMP_HOME ||
|
|
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");
|
|
435
|
+
if (fssync.existsSync(ompSessions)) {
|
|
436
|
+
summary.push({ label: "oh-my-pi", status: "detected", detail: "Passive reader (no hook needed)" });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
// CodeBuddy: Claude-Code fork. Install the SessionEnd hook so finished
|
|
429
441
|
// sessions trigger notify.cjs → tracker sync; passive scan still runs as a
|
|
430
442
|
// safety net for sessions that don't fire SessionEnd cleanly.
|
|
@@ -879,7 +891,7 @@ function isRunnableCommand(command) {
|
|
|
879
891
|
`;
|
|
880
892
|
}
|
|
881
893
|
|
|
882
|
-
module.exports = { cmdInit, buildNotifyHandler };
|
|
894
|
+
module.exports = { cmdInit, buildNotifyHandler, installLocalTrackerApp };
|
|
883
895
|
|
|
884
896
|
async function probeFile(p) {
|
|
885
897
|
try {
|
|
@@ -908,7 +920,9 @@ async function installLocalTrackerApp({ appDir }) {
|
|
|
908
920
|
const packageRoot = path.resolve(__dirname, "../..");
|
|
909
921
|
const srcFrom = path.join(packageRoot, "src");
|
|
910
922
|
const binFrom = path.join(packageRoot, "bin", "tracker.js");
|
|
923
|
+
const packageJsonFrom = path.join(packageRoot, "package.json");
|
|
911
924
|
const nodeModulesFrom = path.join(packageRoot, "node_modules");
|
|
925
|
+
const dashboardDistFrom = path.join(packageRoot, "dashboard", "dist");
|
|
912
926
|
|
|
913
927
|
// When running from the installed local runtime (or when appDir is symlinked to this package),
|
|
914
928
|
// source and destination resolve to the same place. Do not delete appDir in that case.
|
|
@@ -920,6 +934,7 @@ async function installLocalTrackerApp({ appDir }) {
|
|
|
920
934
|
const binToDir = path.join(appDir, "bin");
|
|
921
935
|
const binTo = path.join(binToDir, "tracker.js");
|
|
922
936
|
const nodeModulesTo = path.join(appDir, "node_modules");
|
|
937
|
+
const dashboardDistTo = path.join(appDir, "dashboard", "dist");
|
|
923
938
|
|
|
924
939
|
await fs.rm(appDir, { recursive: true, force: true }).catch(() => {});
|
|
925
940
|
await ensureDir(appDir);
|
|
@@ -927,6 +942,10 @@ async function installLocalTrackerApp({ appDir }) {
|
|
|
927
942
|
await ensureDir(binToDir);
|
|
928
943
|
await fs.copyFile(binFrom, binTo);
|
|
929
944
|
await fs.chmod(binTo, 0o755).catch(() => {});
|
|
945
|
+
await fs.copyFile(packageJsonFrom, path.join(appDir, "package.json")).catch(() => {});
|
|
946
|
+
if (await isDir(dashboardDistFrom)) {
|
|
947
|
+
await fs.cp(dashboardDistFrom, dashboardDistTo, { recursive: true });
|
|
948
|
+
}
|
|
930
949
|
await copyRuntimeDependencies({ from: nodeModulesFrom, to: nodeModulesTo });
|
|
931
950
|
}
|
|
932
951
|
|
package/src/commands/serve.js
CHANGED
|
@@ -37,6 +37,13 @@ async function cmdServe(argv) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
try {
|
|
41
|
+
const { installLocalTrackerApp } = require("./init");
|
|
42
|
+
await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
// 0.1 Ensure config.json baseUrl matches the canonical default
|
|
41
48
|
try {
|
|
42
49
|
const { DEFAULT_BASE_URL } = require("../lib/runtime-config");
|
package/src/commands/status.js
CHANGED
|
@@ -16,19 +16,31 @@ const {
|
|
|
16
16
|
isGeminiHookConfigured,
|
|
17
17
|
buildGeminiHookCommand,
|
|
18
18
|
} = require("../lib/gemini-config");
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
resolveOpencodeConfigDir,
|
|
21
|
+
isOpencodePluginInstalled,
|
|
22
|
+
} = require("../lib/opencode-config");
|
|
20
23
|
const { collectLocalSubscriptions } = require("../lib/subscriptions");
|
|
21
|
-
const {
|
|
22
|
-
|
|
24
|
+
const {
|
|
25
|
+
describeCopilotOtelStatus,
|
|
26
|
+
readCopilotOauthToken,
|
|
27
|
+
} = require("../lib/usage-limits");
|
|
28
|
+
const {
|
|
29
|
+
normalizeState: normalizeUploadState,
|
|
30
|
+
} = require("../lib/upload-throttle");
|
|
23
31
|
const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
24
32
|
const { probeOpenclawHookState } = require("../lib/openclaw-hook");
|
|
25
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
probeOpenclawSessionPluginState,
|
|
35
|
+
} = require("../lib/openclaw-session-plugin");
|
|
26
36
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
27
37
|
const {
|
|
28
38
|
resolveKimiWireFiles,
|
|
29
39
|
resolveKiroCliDbPath,
|
|
30
40
|
resolveCodebuddyHome,
|
|
31
41
|
resolveCodebuddyProjectFiles,
|
|
42
|
+
resolveOmpSessionFiles,
|
|
43
|
+
resolveOmpAgentDir,
|
|
32
44
|
} = require("../lib/rollout");
|
|
33
45
|
|
|
34
46
|
async function cmdStatus(argv = []) {
|
|
@@ -60,8 +72,13 @@ async function cmdStatus(argv = []) {
|
|
|
60
72
|
"settings.json",
|
|
61
73
|
);
|
|
62
74
|
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
63
|
-
const geminiSettingsPath = resolveGeminiSettingsPath({
|
|
64
|
-
|
|
75
|
+
const geminiSettingsPath = resolveGeminiSettingsPath({
|
|
76
|
+
configDir: geminiConfigDir,
|
|
77
|
+
});
|
|
78
|
+
const opencodeConfigDir = resolveOpencodeConfigDir({
|
|
79
|
+
home,
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
65
82
|
const notifyPath = path.join(binDir, "notify.cjs");
|
|
66
83
|
const claudeHookCommand = buildClaudeHookCommand(notifyPath);
|
|
67
84
|
const codebuddyHookCommand = buildHookCommand(notifyPath, "codebuddy");
|
|
@@ -70,20 +87,26 @@ async function cmdStatus(argv = []) {
|
|
|
70
87
|
const config = await readJson(configPath);
|
|
71
88
|
const cursors = await readJson(cursorsPath);
|
|
72
89
|
const queueState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
73
|
-
const uploadThrottle = normalizeUploadState(
|
|
90
|
+
const uploadThrottle = normalizeUploadState(
|
|
91
|
+
await readJson(uploadThrottlePath),
|
|
92
|
+
);
|
|
74
93
|
const autoRetry = await readJson(autoRetryPath);
|
|
75
94
|
|
|
76
95
|
const queueSize = await safeStatSize(queuePath);
|
|
77
96
|
const pendingBytes = Math.max(0, queueSize - (queueState.offset || 0));
|
|
78
97
|
|
|
79
98
|
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
80
|
-
const lastOpenclawSync =
|
|
81
|
-
|
|
99
|
+
const lastOpenclawSync =
|
|
100
|
+
(await safeReadText(openclawSignalPath))?.trim() || null;
|
|
101
|
+
const lastNotifySpawn = parseEpochMsToIso(
|
|
102
|
+
(await safeReadText(throttlePath))?.trim() || null,
|
|
103
|
+
);
|
|
82
104
|
|
|
83
105
|
const codexNotify = await readCodexNotify(codexConfigPath);
|
|
84
106
|
const notifyConfigured = Array.isArray(codexNotify) && codexNotify.length > 0;
|
|
85
107
|
const everyCodeNotify = await readEveryCodeNotify(codeConfigPath);
|
|
86
|
-
const everyCodeConfigured =
|
|
108
|
+
const everyCodeConfigured =
|
|
109
|
+
Array.isArray(everyCodeNotify) && everyCodeNotify.length > 0;
|
|
87
110
|
const claudeHookConfigured = await isClaudeHookConfigured({
|
|
88
111
|
settingsPath: claudeSettingsPath,
|
|
89
112
|
hookCommand: claudeHookCommand,
|
|
@@ -104,7 +127,11 @@ async function cmdStatus(argv = []) {
|
|
|
104
127
|
trackerDir,
|
|
105
128
|
env: process.env,
|
|
106
129
|
});
|
|
107
|
-
const openclawHookState = await probeOpenclawHookState({
|
|
130
|
+
const openclawHookState = await probeOpenclawHookState({
|
|
131
|
+
home,
|
|
132
|
+
trackerDir,
|
|
133
|
+
env: process.env,
|
|
134
|
+
});
|
|
108
135
|
|
|
109
136
|
const lastUpload = uploadThrottle.lastSuccessMs
|
|
110
137
|
? parseEpochMsToIso(uploadThrottle.lastSuccessMs)
|
|
@@ -151,9 +178,17 @@ async function cmdStatus(argv = []) {
|
|
|
151
178
|
? resolveCodebuddyProjectFiles(process.env)
|
|
152
179
|
: [];
|
|
153
180
|
|
|
181
|
+
// oh-my-pi — passive scan only (no hooks).
|
|
182
|
+
const ompAgentDir = resolveOmpAgentDir(process.env);
|
|
183
|
+
const ompInstalled = fssync.existsSync(path.join(ompAgentDir, "sessions"));
|
|
184
|
+
const ompFiles = ompInstalled ? resolveOmpSessionFiles(process.env) : [];
|
|
185
|
+
|
|
154
186
|
const copilotToken = readCopilotOauthToken({ home });
|
|
155
187
|
const copilotOtel = describeCopilotOtelStatus({ home, env: process.env });
|
|
156
|
-
const copilotLines = formatCopilotLines({
|
|
188
|
+
const copilotLines = formatCopilotLines({
|
|
189
|
+
token: copilotToken,
|
|
190
|
+
otel: copilotOtel,
|
|
191
|
+
});
|
|
157
192
|
|
|
158
193
|
process.stdout.write(
|
|
159
194
|
[
|
|
@@ -186,6 +221,9 @@ async function cmdStatus(argv = []) {
|
|
|
186
221
|
codebuddyInstalled
|
|
187
222
|
? `- CodeBuddy hooks: ${codebuddyHookConfigured ? "set" : "unset"} (${codebuddyFiles.length} session jsonl file${codebuddyFiles.length !== 1 ? "s" : ""} found)`
|
|
188
223
|
: null,
|
|
224
|
+
ompInstalled
|
|
225
|
+
? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found)`
|
|
226
|
+
: null,
|
|
189
227
|
...copilotLines,
|
|
190
228
|
...subscriptionLines,
|
|
191
229
|
"",
|
|
@@ -197,7 +235,9 @@ async function cmdStatus(argv = []) {
|
|
|
197
235
|
|
|
198
236
|
function formatCopilotLines({ token, otel }) {
|
|
199
237
|
if (!token && !otel.otel_has_files) return [];
|
|
200
|
-
const limitsState = token
|
|
238
|
+
const limitsState = token
|
|
239
|
+
? "set (via GitHub OAuth)"
|
|
240
|
+
: "unset (no Copilot OAuth token found)";
|
|
201
241
|
const usageState = otel.otel_has_files
|
|
202
242
|
? `set (${otel.otel_path || otel.otel_default_dir})`
|
|
203
243
|
: otel.otel_enabled
|
|
@@ -235,7 +275,11 @@ function formatSubscriptionLine(entry = {}) {
|
|
|
235
275
|
|
|
236
276
|
if (!planType) return null;
|
|
237
277
|
|
|
238
|
-
if (
|
|
278
|
+
if (
|
|
279
|
+
tool === "claude" &&
|
|
280
|
+
provider === "anthropic" &&
|
|
281
|
+
product === "subscription"
|
|
282
|
+
) {
|
|
239
283
|
const suffix = rateLimitTier ? ` (rate limit tier: ${rateLimitTier})` : "";
|
|
240
284
|
return `- ${toolLabel} subscription: ${planType}${suffix}`;
|
|
241
285
|
}
|
|
@@ -249,7 +293,11 @@ function formatSubscriptionLine(entry = {}) {
|
|
|
249
293
|
}
|
|
250
294
|
|
|
251
295
|
function parseArgs(argv) {
|
|
252
|
-
const out = {
|
|
296
|
+
const out = {
|
|
297
|
+
diagnostics: false,
|
|
298
|
+
probeKeychain: false,
|
|
299
|
+
probeKeychainDetails: false,
|
|
300
|
+
};
|
|
253
301
|
|
|
254
302
|
for (let i = 0; i < argv.length; i++) {
|
|
255
303
|
const a = argv[i];
|
package/src/commands/sync.js
CHANGED
|
@@ -27,6 +27,8 @@ const {
|
|
|
27
27
|
parseCopilotIncremental,
|
|
28
28
|
resolveKimiWireFiles,
|
|
29
29
|
parseKimiIncremental,
|
|
30
|
+
resolveOmpSessionFiles,
|
|
31
|
+
parseOmpIncremental,
|
|
30
32
|
resolveCodebuddyProjectFiles,
|
|
31
33
|
parseCodebuddyIncremental,
|
|
32
34
|
resolveKiroCliSessionFiles,
|
|
@@ -52,6 +54,7 @@ const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
|
52
54
|
const { resolveRuntimeConfig } = require("../lib/runtime-config");
|
|
53
55
|
|
|
54
56
|
const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
|
|
57
|
+
const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
|
|
55
58
|
|
|
56
59
|
async function cmdSync(argv) {
|
|
57
60
|
const opts = parseArgs(argv);
|
|
@@ -114,6 +117,8 @@ async function cmdSync(argv) {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
await migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles });
|
|
121
|
+
|
|
117
122
|
const openclawFiles = openclawSignal?.sessionFile
|
|
118
123
|
? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
|
|
119
124
|
: [];
|
|
@@ -444,6 +449,28 @@ async function cmdSync(argv) {
|
|
|
444
449
|
});
|
|
445
450
|
}
|
|
446
451
|
|
|
452
|
+
// ── oh-my-pi (passive ~/.omp/agent/sessions/**/*.jsonl reader) ──
|
|
453
|
+
let ompResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
454
|
+
const ompFiles = resolveOmpSessionFiles(process.env);
|
|
455
|
+
if (ompFiles.length > 0) {
|
|
456
|
+
if (progress?.enabled) {
|
|
457
|
+
progress.start(`Parsing oh-my-pi ${renderBar(0)} | buckets 0`);
|
|
458
|
+
}
|
|
459
|
+
ompResult = await parseOmpIncremental({
|
|
460
|
+
sessionFiles: ompFiles,
|
|
461
|
+
cursors,
|
|
462
|
+
queuePath,
|
|
463
|
+
env: process.env,
|
|
464
|
+
onProgress: (p) => {
|
|
465
|
+
if (!progress?.enabled) return;
|
|
466
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
467
|
+
progress.update(
|
|
468
|
+
`Parsing oh-my-pi ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
469
|
+
);
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
447
474
|
// ── GitHub Copilot CLI (OTEL JSONL files) ──
|
|
448
475
|
let copilotResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
449
476
|
const copilotPaths = resolveCopilotOtelPaths(process.env);
|
|
@@ -563,6 +590,7 @@ async function cmdSync(argv) {
|
|
|
563
590
|
hermesResult.recordsProcessed +
|
|
564
591
|
kimiResult.recordsProcessed +
|
|
565
592
|
codebuddyResult.recordsProcessed +
|
|
593
|
+
ompResult.recordsProcessed +
|
|
566
594
|
copilotResult.recordsProcessed;
|
|
567
595
|
const totalBuckets =
|
|
568
596
|
parseResult.bucketsQueued +
|
|
@@ -576,6 +604,7 @@ async function cmdSync(argv) {
|
|
|
576
604
|
hermesResult.bucketsQueued +
|
|
577
605
|
kimiResult.bucketsQueued +
|
|
578
606
|
codebuddyResult.bucketsQueued +
|
|
607
|
+
ompResult.bucketsQueued +
|
|
579
608
|
copilotResult.bucketsQueued;
|
|
580
609
|
process.stdout.write(
|
|
581
610
|
[
|
|
@@ -621,7 +650,13 @@ function parseArgs(argv) {
|
|
|
621
650
|
return out;
|
|
622
651
|
}
|
|
623
652
|
|
|
624
|
-
module.exports = {
|
|
653
|
+
module.exports = {
|
|
654
|
+
cmdSync,
|
|
655
|
+
migrateCursorUnknownBuckets,
|
|
656
|
+
migrateRolloutCumulativeDeltaBuckets,
|
|
657
|
+
CURSOR_UNKNOWN_MIGRATION_KEY,
|
|
658
|
+
ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
|
|
659
|
+
};
|
|
625
660
|
|
|
626
661
|
function normalizeString(value) {
|
|
627
662
|
if (typeof value !== "string") return null;
|
|
@@ -1034,3 +1069,67 @@ async function migrateCursorUnknownBuckets({ cursors, queuePath }) {
|
|
|
1034
1069
|
|
|
1035
1070
|
cursors.migrations[CURSOR_UNKNOWN_MIGRATION_KEY] = new Date().toISOString();
|
|
1036
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rolloutFiles }) {
|
|
1074
|
+
if (!cursors || typeof cursors !== "object") return;
|
|
1075
|
+
cursors.migrations = cursors.migrations || {};
|
|
1076
|
+
if (cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY]) return;
|
|
1077
|
+
|
|
1078
|
+
const rolloutPathSources = new Map();
|
|
1079
|
+
for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
|
|
1080
|
+
const filePath = typeof entry === "string" ? entry : entry?.path;
|
|
1081
|
+
const source = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
|
|
1082
|
+
if (!filePath) continue;
|
|
1083
|
+
if (source === "codex" || source === "every-code") {
|
|
1084
|
+
rolloutPathSources.set(filePath, source);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (cursors.files && typeof cursors.files === "object") {
|
|
1089
|
+
for (const filePath of rolloutPathSources.keys()) {
|
|
1090
|
+
delete cursors.files[filePath];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const buckets = cursors.hourly?.buckets;
|
|
1095
|
+
const retractions = [];
|
|
1096
|
+
if (buckets && typeof buckets === "object") {
|
|
1097
|
+
for (const key of Object.keys(buckets)) {
|
|
1098
|
+
const [source, model, ...hourParts] = key.split("|");
|
|
1099
|
+
if (source !== "codex" && source !== "every-code") continue;
|
|
1100
|
+
const hourStart = hourParts.join("|");
|
|
1101
|
+
retractions.push(
|
|
1102
|
+
JSON.stringify({
|
|
1103
|
+
source,
|
|
1104
|
+
model: model || "unknown",
|
|
1105
|
+
hour_start: hourStart,
|
|
1106
|
+
input_tokens: 0,
|
|
1107
|
+
cached_input_tokens: 0,
|
|
1108
|
+
cache_creation_input_tokens: 0,
|
|
1109
|
+
output_tokens: 0,
|
|
1110
|
+
reasoning_output_tokens: 0,
|
|
1111
|
+
total_tokens: 0,
|
|
1112
|
+
billable_total_tokens: 0,
|
|
1113
|
+
conversation_count: 0,
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
delete buckets[key];
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const groupQueued = cursors.hourly?.groupQueued;
|
|
1121
|
+
if (groupQueued && typeof groupQueued === "object") {
|
|
1122
|
+
for (const key of Object.keys(groupQueued)) {
|
|
1123
|
+
if (key.startsWith("codex|") || key.startsWith("every-code|")) {
|
|
1124
|
+
delete groupQueued[key];
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (retractions.length > 0) {
|
|
1130
|
+
await ensureDir(path.dirname(queuePath));
|
|
1131
|
+
await fs.appendFile(queuePath, retractions.join("\n") + "\n");
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
cursors.migrations[ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY] = new Date().toISOString();
|
|
1135
|
+
}
|
package/src/lib/cursor-config.js
CHANGED
|
@@ -107,6 +107,7 @@ function extractUserIdFromJwt(jwt) {
|
|
|
107
107
|
|
|
108
108
|
const CURSOR_CSV_URL = "https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens";
|
|
109
109
|
const CURSOR_SUMMARY_URL = "https://cursor.com/api/usage-summary";
|
|
110
|
+
const CURSOR_SOURCE_SCOPE = "account";
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
113
|
* Fetch full usage CSV from Cursor API.
|
|
@@ -280,6 +281,10 @@ function parseCursorCsv(csvText) {
|
|
|
280
281
|
kind: kindIdx !== undefined ? stripQuotes(fields[kindIdx]) : "unknown",
|
|
281
282
|
model: stripQuotes(fields[modelIdx]),
|
|
282
283
|
maxMode: maxModeIdx !== undefined ? stripQuotes(fields[maxModeIdx]) : "No",
|
|
284
|
+
sourceScope: CURSOR_SOURCE_SCOPE,
|
|
285
|
+
billableKind: isCursorBillableKind(kindIdx !== undefined ? fields[kindIdx] : "unknown")
|
|
286
|
+
? "billable"
|
|
287
|
+
: "non_billable",
|
|
283
288
|
inputTokens: inputWithoutCache,
|
|
284
289
|
cacheWriteTokens: Math.max(0, inputWithCache - inputWithoutCache),
|
|
285
290
|
cacheReadTokens: toNum(fields[cacheReadIdx]),
|
|
@@ -312,9 +317,18 @@ function normalizeCursorUsage(record) {
|
|
|
312
317
|
output_tokens: outputTokens,
|
|
313
318
|
reasoning_output_tokens: 0,
|
|
314
319
|
total_tokens: totalTokens,
|
|
320
|
+
billable_total_tokens: isCursorBillableKind(record?.kind) ? totalTokens : 0,
|
|
315
321
|
};
|
|
316
322
|
}
|
|
317
323
|
|
|
324
|
+
function isCursorBillableKind(kind) {
|
|
325
|
+
const normalized = String(kind || "").trim().toLowerCase();
|
|
326
|
+
if (!normalized) return true;
|
|
327
|
+
if (normalized.includes("no charge")) return false;
|
|
328
|
+
if (normalized === "free") return false;
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
// ── CSV helpers ──
|
|
319
333
|
|
|
320
334
|
function parseCsvLine(line) {
|
|
@@ -364,5 +378,6 @@ module.exports = {
|
|
|
364
378
|
fetchCursorUsageCsv,
|
|
365
379
|
fetchCursorUsageSummary,
|
|
366
380
|
parseCursorCsv,
|
|
381
|
+
isCursorBillableKind,
|
|
367
382
|
normalizeCursorUsage,
|
|
368
383
|
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -4,6 +4,12 @@ const path = require("node:path");
|
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
6
|
const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
|
|
7
|
+
const {
|
|
8
|
+
filterRowsByUsageScope,
|
|
9
|
+
getSourceScope,
|
|
10
|
+
listExcludedSources,
|
|
11
|
+
normalizeUsageScope,
|
|
12
|
+
} = require("./source-metadata");
|
|
7
13
|
|
|
8
14
|
const SYNC_TIMEOUT_MS = 120_000;
|
|
9
15
|
const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
|
|
@@ -154,7 +160,7 @@ function aggregateByDay(rows, timeZoneContext = null) {
|
|
|
154
160
|
}
|
|
155
161
|
const a = byDay.get(day);
|
|
156
162
|
a.total_tokens += row.total_tokens || 0;
|
|
157
|
-
a.billable_total_tokens += row.total_tokens
|
|
163
|
+
a.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
|
|
158
164
|
a.total_cost_usd += computeRowCost(row);
|
|
159
165
|
a.input_tokens += row.input_tokens || 0;
|
|
160
166
|
a.output_tokens += row.output_tokens || 0;
|
|
@@ -166,6 +172,22 @@ function aggregateByDay(rows, timeZoneContext = null) {
|
|
|
166
172
|
return Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
function getRequestedUsageScope(url) {
|
|
176
|
+
if (url.searchParams.get("include_account_level") === "1") return "all";
|
|
177
|
+
return normalizeUsageScope(url.searchParams.get("scope"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scopedQueueRows(queuePath, url) {
|
|
181
|
+
const scope = getRequestedUsageScope(url);
|
|
182
|
+
const allRows = readQueueData(queuePath);
|
|
183
|
+
return {
|
|
184
|
+
scope,
|
|
185
|
+
allRows,
|
|
186
|
+
rows: filterRowsByUsageScope(allRows, scope),
|
|
187
|
+
excludedSources: listExcludedSources(allRows, scope),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
function getTimeZoneContext(url) {
|
|
170
192
|
const tz = String(url.searchParams.get("tz") || "").trim();
|
|
171
193
|
const rawOffset = Number(url.searchParams.get("tz_offset_minutes"));
|
|
@@ -796,7 +818,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
796
818
|
const from = url.searchParams.get("from") || "";
|
|
797
819
|
const to = url.searchParams.get("to") || "";
|
|
798
820
|
const timeZoneContext = getTimeZoneContext(url);
|
|
799
|
-
const rows =
|
|
821
|
+
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
800
822
|
const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
|
|
801
823
|
const totals = daily.reduce(
|
|
802
824
|
(acc, r) => {
|
|
@@ -848,7 +870,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
848
870
|
const l30fromStr = shiftDay(todayStr, -29);
|
|
849
871
|
|
|
850
872
|
json(res, {
|
|
851
|
-
from, to, days: daily.length,
|
|
873
|
+
from, to, days: daily.length, scope, excluded_sources: excludedSources,
|
|
852
874
|
totals: { ...totals, total_cost_usd: totalCost.toFixed(6) },
|
|
853
875
|
rolling: {
|
|
854
876
|
last_7d: { from: l7fromStr, to: todayStr, active_days: l7.length, totals: l7t },
|
|
@@ -863,9 +885,9 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
863
885
|
const from = url.searchParams.get("from") || "";
|
|
864
886
|
const to = url.searchParams.get("to") || "";
|
|
865
887
|
const timeZoneContext = getTimeZoneContext(url);
|
|
866
|
-
const rows =
|
|
888
|
+
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
867
889
|
const daily = aggregateByDay(rows, timeZoneContext).filter((d) => d.day >= from && d.day <= to);
|
|
868
|
-
json(res, { from, to, data: daily });
|
|
890
|
+
json(res, { from, to, scope, excluded_sources: excludedSources, data: daily });
|
|
869
891
|
return true;
|
|
870
892
|
}
|
|
871
893
|
|
|
@@ -873,7 +895,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
873
895
|
if (p === "/functions/tokentracker-usage-heatmap") {
|
|
874
896
|
const weeks = parseInt(url.searchParams.get("weeks") || "52", 10);
|
|
875
897
|
const timeZoneContext = getTimeZoneContext(url);
|
|
876
|
-
const rows =
|
|
898
|
+
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
877
899
|
const daily = aggregateByDay(rows, timeZoneContext);
|
|
878
900
|
const todayParts = getZonedParts(new Date(), timeZoneContext);
|
|
879
901
|
const todayStr = formatPartsDayKey(todayParts) || new Date().toISOString().slice(0, 10);
|
|
@@ -910,7 +932,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
910
932
|
for (let i = 0; i < cells.length; i += 7) {
|
|
911
933
|
weeksArr.push(cells.slice(i, i + 7));
|
|
912
934
|
}
|
|
913
|
-
json(res, { from, to, week_starts_on: "sun", active_days: cells.filter((c) => c.billable_total_tokens > 0).length, streak_days: 0, weeks: weeksArr });
|
|
935
|
+
json(res, { from, to, scope, excluded_sources: excludedSources, week_starts_on: "sun", active_days: cells.filter((c) => c.billable_total_tokens > 0).length, streak_days: 0, weeks: weeksArr });
|
|
914
936
|
return true;
|
|
915
937
|
}
|
|
916
938
|
|
|
@@ -919,7 +941,8 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
919
941
|
const from = url.searchParams.get("from") || "";
|
|
920
942
|
const to = url.searchParams.get("to") || "";
|
|
921
943
|
const timeZoneContext = getTimeZoneContext(url);
|
|
922
|
-
const rows =
|
|
944
|
+
const { rows: scopedRows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
945
|
+
const rows = scopedRows.filter((r) => {
|
|
923
946
|
if (!r.hour_start) return false;
|
|
924
947
|
const d = rowDayKey(r, timeZoneContext);
|
|
925
948
|
return d >= from && d <= to;
|
|
@@ -930,10 +953,10 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
930
953
|
const src = row.source || "unknown";
|
|
931
954
|
const mdl = row.model || "unknown";
|
|
932
955
|
if (!bySource.has(src))
|
|
933
|
-
bySource.set(src, { source: src, totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" }, models: new Map() });
|
|
956
|
+
bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" }, models: new Map() });
|
|
934
957
|
const sa = bySource.get(src);
|
|
935
958
|
sa.totals.total_tokens += row.total_tokens || 0;
|
|
936
|
-
sa.totals.billable_total_tokens += row.total_tokens
|
|
959
|
+
sa.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
|
|
937
960
|
sa.totals.input_tokens += row.input_tokens || 0;
|
|
938
961
|
sa.totals.output_tokens += row.output_tokens || 0;
|
|
939
962
|
sa.totals.cached_input_tokens += row.cached_input_tokens || 0;
|
|
@@ -943,7 +966,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
943
966
|
sa.models.set(mdl, { model: mdl, model_id: mdl, totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" } });
|
|
944
967
|
const ma = sa.models.get(mdl);
|
|
945
968
|
ma.totals.total_tokens += row.total_tokens || 0;
|
|
946
|
-
ma.totals.billable_total_tokens += row.total_tokens
|
|
969
|
+
ma.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
|
|
947
970
|
ma.totals.input_tokens += row.input_tokens || 0;
|
|
948
971
|
ma.totals.output_tokens += row.output_tokens || 0;
|
|
949
972
|
ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
|
|
@@ -968,7 +991,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
968
991
|
});
|
|
969
992
|
|
|
970
993
|
json(res, {
|
|
971
|
-
from, to, days: 0, sources,
|
|
994
|
+
from, to, days: 0, scope, excluded_sources: excludedSources, sources,
|
|
972
995
|
pricing: { model: "per-model", pricing_mode: "per_token_type", source: "litellm", effective_from: new Date().toISOString().slice(0, 10) },
|
|
973
996
|
});
|
|
974
997
|
return true;
|
|
@@ -1061,9 +1084,9 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1061
1084
|
if (p === "/functions/tokentracker-usage-hourly") {
|
|
1062
1085
|
const day = url.searchParams.get("day") || new Date().toISOString().slice(0, 10);
|
|
1063
1086
|
const timeZoneContext = getTimeZoneContext(url);
|
|
1064
|
-
const rows =
|
|
1087
|
+
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
1065
1088
|
const data = aggregateHourlyByDay(rows, day, timeZoneContext);
|
|
1066
|
-
json(res, { day, data });
|
|
1089
|
+
json(res, { day, scope, excluded_sources: excludedSources, data });
|
|
1067
1090
|
return true;
|
|
1068
1091
|
}
|
|
1069
1092
|
|
|
@@ -1072,7 +1095,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1072
1095
|
const from = url.searchParams.get("from") || "";
|
|
1073
1096
|
const to = url.searchParams.get("to") || "";
|
|
1074
1097
|
const timeZoneContext = getTimeZoneContext(url);
|
|
1075
|
-
const rows =
|
|
1098
|
+
const { rows, scope, excludedSources } = scopedQueueRows(qp, url);
|
|
1076
1099
|
const byMonth = new Map();
|
|
1077
1100
|
for (const row of rows) {
|
|
1078
1101
|
if (!row.hour_start) continue;
|
|
@@ -1091,7 +1114,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1091
1114
|
a.reasoning_output_tokens += row.reasoning_output_tokens || 0;
|
|
1092
1115
|
a.conversation_count += row.conversation_count || 0;
|
|
1093
1116
|
}
|
|
1094
|
-
json(res, { from, to, data: Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)) });
|
|
1117
|
+
json(res, { from, to, scope, excluded_sources: excludedSources, data: Array.from(byMonth.values()).sort((a, b) => a.month.localeCompare(b.month)) });
|
|
1095
1118
|
return true;
|
|
1096
1119
|
}
|
|
1097
1120
|
|