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",
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": {
@@ -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
- renderLocalSuccess();
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
- spawnInitSync({ trackerBinPath, packageName: "tokentracker" });
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 spawn failed: ${msg}\n`);
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}Welcome to Token Tracker${RESET}`,
222
+ `${BOLD}Token Tracker${RESET} ${color("Local-first usage across " + providerCount + " AI CLIs", DIM)}`,
182
223
  DIVIDER,
183
- `${CYAN}Privacy First: Your data stays local. Only token counts are tracked — never prompts or responses.${RESET}`,
224
+ `${CYAN}Nothing leaves your machine token counts only, never prompts or responses.${RESET}`,
184
225
  DIVIDER,
185
226
  "",
186
- "This tool will:",
187
- " - Detect your AI CLI tools (Codex, Claude, Gemini, OpenCode, Cursor, OpenClaw)",
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
- process.stdout.write(
199
- [
200
- "",
201
- `${BOLD}Setup complete!${RESET}`,
202
- "",
203
- " Token data will be collected automatically via hooks.",
204
- " Launching dashboard...",
205
- "",
206
- // One-shot, post-success star CTA. `init` is run once per machine, so
207
- // this is the only place a CLI user naturally sees the project's
208
- // GitHub URL — and they're at peak satisfaction. No prompts in
209
- // status/doctor/sync/etc, which run in scripts and would be noisy.
210
- ` ${color("⭐ Liking it? Star us at https://github.com/mm7894215/TokenTracker", DIM)}`,
211
- "",
212
- ].join("\n"),
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
- function spawnInitSync({ trackerBinPath, packageName }) {
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
- const child = cp.spawn(cmd[0], cmd.slice(1), {
1025
- detached: true,
1026
- stdio: "ignore",
1027
- env: process.env,
1028
- });
1029
- child.on("error", (err) => {
1030
- const msg = err && err.message ? err.message : "unknown error";
1031
- const detail = isDebugEnabled() ? ` (${msg})` : "";
1032
- process.stderr.write(`Minor issue: Background sync could not start${detail}.\n`);
1033
- process.stderr.write("Run: npx --yes tokentracker-cli sync\n");
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
- child.unref();
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 }) {