tokentracker-cli 0.14.3 → 0.14.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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, Kilo CLI, Kilo Code)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
package/src/commands/init.js
CHANGED
|
@@ -80,6 +80,26 @@ const ASCII_LOGO = [
|
|
|
80
80
|
const DIVIDER = "----------------------------------------------";
|
|
81
81
|
const DEFAULT_DASHBOARD_URL = "https://www.tokentracker.cc";
|
|
82
82
|
|
|
83
|
+
// Single source of truth for the welcome screen's provider count + sample list.
|
|
84
|
+
// Keep in sync with the supported-tools table in CLAUDE.md.
|
|
85
|
+
const SUPPORTED_PROVIDERS = [
|
|
86
|
+
"Claude Code",
|
|
87
|
+
"Codex CLI",
|
|
88
|
+
"Cursor",
|
|
89
|
+
"Gemini CLI",
|
|
90
|
+
"OpenCode",
|
|
91
|
+
"OpenClaw",
|
|
92
|
+
"Every Code",
|
|
93
|
+
"Kiro",
|
|
94
|
+
"Hermes Agent",
|
|
95
|
+
"GitHub Copilot",
|
|
96
|
+
"Kimi Code",
|
|
97
|
+
"oh-my-pi",
|
|
98
|
+
"CodeBuddy",
|
|
99
|
+
"Kilo CLI",
|
|
100
|
+
"Kilo Code",
|
|
101
|
+
];
|
|
102
|
+
|
|
83
103
|
async function cmdInit(argv) {
|
|
84
104
|
const opts = parseArgs(argv);
|
|
85
105
|
const home = os.homedir();
|
|
@@ -163,54 +183,81 @@ async function cmdInit(argv) {
|
|
|
163
183
|
|
|
164
184
|
renderLocalReport({ summary: setup.summary, isDryRun: false });
|
|
165
185
|
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
// Run first sync inline (with a generous timeout) so we can render the
|
|
187
|
+
// *actual* token total in the success message — the aha moment. If the
|
|
188
|
+
// sync exceeds the timeout we surrender the wait but leave it running, so
|
|
189
|
+
// the dashboard still picks up data shortly after.
|
|
190
|
+
const ahaSpinner = createSpinner({ text: "Running first sync..." });
|
|
191
|
+
ahaSpinner.start();
|
|
192
|
+
let firstSync = null;
|
|
168
193
|
try {
|
|
169
|
-
|
|
194
|
+
firstSync = await runFirstSyncAndRead({
|
|
195
|
+
trackerBinPath,
|
|
196
|
+
trackerDir,
|
|
197
|
+
packageName: "tokentracker",
|
|
198
|
+
});
|
|
170
199
|
} catch (err) {
|
|
171
200
|
const msg = err && err.message ? err.message : "unknown error";
|
|
172
|
-
process.stderr.write(`Initial sync
|
|
201
|
+
process.stderr.write(`Initial sync issue: ${msg}\n`);
|
|
202
|
+
} finally {
|
|
203
|
+
ahaSpinner.stop();
|
|
173
204
|
}
|
|
205
|
+
|
|
206
|
+
renderLocalSuccess({ firstSync });
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
function renderWelcome() {
|
|
210
|
+
const providerCount = SUPPORTED_PROVIDERS.length;
|
|
211
|
+
// Show first 5 by name for grounding, then "+N more" so the line stays one row.
|
|
212
|
+
const previewNames = SUPPORTED_PROVIDERS.slice(0, 5).join(", ");
|
|
213
|
+
const remaining = providerCount - 5;
|
|
214
|
+
const providerLine =
|
|
215
|
+
remaining > 0
|
|
216
|
+
? `${previewNames} +${remaining} more`
|
|
217
|
+
: previewNames;
|
|
177
218
|
process.stdout.write(
|
|
178
219
|
[
|
|
179
220
|
ASCII_LOGO,
|
|
180
221
|
"",
|
|
181
|
-
`${BOLD}
|
|
222
|
+
`${BOLD}Token Tracker${RESET} ${color("Local-first usage across " + providerCount + " AI CLIs", DIM)}`,
|
|
182
223
|
DIVIDER,
|
|
183
|
-
`${CYAN}
|
|
224
|
+
`${CYAN}Nothing leaves your machine — token counts only, never prompts or responses.${RESET}`,
|
|
184
225
|
DIVIDER,
|
|
185
226
|
"",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
" - Set up lightweight hooks to track token usage",
|
|
189
|
-
" - View your dashboard at http://localhost:7680",
|
|
190
|
-
"",
|
|
191
|
-
"(Nothing will be changed until you confirm below)",
|
|
227
|
+
` Tracks: ${providerLine}`,
|
|
228
|
+
` Dashboard: http://localhost:7680`,
|
|
192
229
|
"",
|
|
193
230
|
].join("\n"),
|
|
194
231
|
);
|
|
195
232
|
}
|
|
196
233
|
|
|
197
|
-
function renderLocalSuccess() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
234
|
+
function renderLocalSuccess({ firstSync } = {}) {
|
|
235
|
+
const lines = ["", `${BOLD}Setup complete!${RESET}`, ""];
|
|
236
|
+
|
|
237
|
+
if (firstSync && firstSync.totalTokens > 0) {
|
|
238
|
+
const tokens = firstSync.totalTokens.toLocaleString("en-US");
|
|
239
|
+
const sourceCount = firstSync.sources.length;
|
|
240
|
+
const sourceWord = sourceCount === 1 ? "provider" : "providers";
|
|
241
|
+
lines.push(
|
|
242
|
+
` ${BOLD}${tokens}${RESET} tokens tracked across ${sourceCount} ${sourceWord}.`,
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(
|
|
246
|
+
" No usage history yet — run any AI CLI and tokens appear within a minute.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(
|
|
251
|
+
"",
|
|
252
|
+
` Dashboard: ${CYAN}http://localhost:7680${RESET}`,
|
|
253
|
+
"",
|
|
254
|
+
// One-shot, post-success star CTA. `init` is run once per machine, so
|
|
255
|
+
// this is the only place a CLI user naturally sees the project's GitHub
|
|
256
|
+
// URL — and they're at peak satisfaction.
|
|
257
|
+
` ${color("⭐ Star us if useful: https://github.com/mm7894215/TokenTracker", DIM)}`,
|
|
258
|
+
"",
|
|
213
259
|
);
|
|
260
|
+
process.stdout.write(lines.join("\n"));
|
|
214
261
|
}
|
|
215
262
|
|
|
216
263
|
function renderAccountNotLinked({ context } = {}) {
|
|
@@ -1014,25 +1061,88 @@ async function safeRealpath(p) {
|
|
|
1014
1061
|
}
|
|
1015
1062
|
}
|
|
1016
1063
|
|
|
1017
|
-
|
|
1064
|
+
// Run the first sync inline so we can show the user their real token total
|
|
1065
|
+
// immediately. Caps wall-time at FIRST_SYNC_TIMEOUT_MS — past that we let the
|
|
1066
|
+
// child continue detached and surrender the wait. Returns aggregate stats
|
|
1067
|
+
// derived from queue.jsonl after the wait window closes.
|
|
1068
|
+
const FIRST_SYNC_TIMEOUT_MS = 15_000;
|
|
1069
|
+
|
|
1070
|
+
async function runFirstSyncAndRead({ trackerBinPath, trackerDir, packageName }) {
|
|
1018
1071
|
const fallbackPkg = packageName || "tokentracker-cli";
|
|
1019
1072
|
const argv = ["sync", "--drain"];
|
|
1020
1073
|
const hasLocalRuntime = typeof trackerBinPath === "string" && fssync.existsSync(trackerBinPath);
|
|
1021
1074
|
const cmd = hasLocalRuntime
|
|
1022
1075
|
? [process.execPath, trackerBinPath, ...argv]
|
|
1023
1076
|
: ["npx", "--yes", fallbackPkg, ...argv];
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1077
|
+
|
|
1078
|
+
await new Promise((resolve) => {
|
|
1079
|
+
let settled = false;
|
|
1080
|
+
let timer = null;
|
|
1081
|
+
const settle = () => {
|
|
1082
|
+
if (settled) return;
|
|
1083
|
+
settled = true;
|
|
1084
|
+
if (timer) clearTimeout(timer);
|
|
1085
|
+
resolve();
|
|
1086
|
+
};
|
|
1087
|
+
let child;
|
|
1088
|
+
try {
|
|
1089
|
+
child = cp.spawn(cmd[0], cmd.slice(1), {
|
|
1090
|
+
// detached so we can let it keep running past our timeout — the user
|
|
1091
|
+
// still gets data later via dashboard auto-refresh.
|
|
1092
|
+
detached: true,
|
|
1093
|
+
stdio: "ignore",
|
|
1094
|
+
env: process.env,
|
|
1095
|
+
});
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
if (isDebugEnabled()) {
|
|
1098
|
+
process.stderr.write(`first-sync spawn failed: ${err?.message || err}\n`);
|
|
1099
|
+
}
|
|
1100
|
+
settle();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
child.on("error", () => settle());
|
|
1104
|
+
child.on("exit", () => settle());
|
|
1105
|
+
timer = setTimeout(() => {
|
|
1106
|
+
try {
|
|
1107
|
+
child.unref();
|
|
1108
|
+
} catch (_e) {}
|
|
1109
|
+
settle();
|
|
1110
|
+
}, FIRST_SYNC_TIMEOUT_MS);
|
|
1034
1111
|
});
|
|
1035
|
-
|
|
1112
|
+
|
|
1113
|
+
return readFirstSyncTotals(trackerDir);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function readFirstSyncTotals(trackerDir) {
|
|
1117
|
+
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
1118
|
+
let raw;
|
|
1119
|
+
try {
|
|
1120
|
+
raw = fssync.readFileSync(queuePath, "utf8");
|
|
1121
|
+
} catch (_e) {
|
|
1122
|
+
return { totalTokens: 0, sources: [] };
|
|
1123
|
+
}
|
|
1124
|
+
let totalTokens = 0;
|
|
1125
|
+
const sources = new Set();
|
|
1126
|
+
// Each sync appends cumulative totals per (source, model, hour_start); keep
|
|
1127
|
+
// the last entry per bucket to match what the dashboard shows.
|
|
1128
|
+
const latest = new Map();
|
|
1129
|
+
for (const line of raw.split("\n")) {
|
|
1130
|
+
const trimmed = line.trim();
|
|
1131
|
+
if (!trimmed) continue;
|
|
1132
|
+
try {
|
|
1133
|
+
const row = JSON.parse(trimmed);
|
|
1134
|
+
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
1135
|
+
latest.set(key, row);
|
|
1136
|
+
} catch {
|
|
1137
|
+
// skip malformed
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
for (const row of latest.values()) {
|
|
1141
|
+
const n = Number(row.total_tokens);
|
|
1142
|
+
if (Number.isFinite(n) && n > 0) totalTokens += n;
|
|
1143
|
+
if (row.source) sources.add(row.source);
|
|
1144
|
+
}
|
|
1145
|
+
return { totalTokens, sources: Array.from(sources) };
|
|
1036
1146
|
}
|
|
1037
1147
|
|
|
1038
1148
|
async function copyRuntimeDependencies({ from, to }) {
|