tokentracker-cli 0.10.1 → 0.11.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.
@@ -33,6 +33,8 @@ const {
33
33
  unsupportedSourcePayload: unsupportedCategoryPayload,
34
34
  } = require("./claude-categorizer");
35
35
 
36
+ const { computeCodexContextBreakdown } = require("./codex-context-breakdown");
37
+
36
38
  // ---------------------------------------------------------------------------
37
39
  // Queue data helpers
38
40
  // ---------------------------------------------------------------------------
@@ -177,6 +179,101 @@ function aggregateByDay(rows, timeZoneContext = null) {
177
179
  return Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
178
180
  }
179
181
 
182
+ function buildCodexCategoryFallbackFromQueue(queueRows, { from, to, timeZoneContext }) {
183
+ const totals = {
184
+ input_tokens: 0,
185
+ cached_input_tokens: 0,
186
+ cache_creation_input_tokens: 0,
187
+ output_tokens: 0,
188
+ reasoning_output_tokens: 0,
189
+ total_tokens: 0,
190
+ };
191
+ let conversationCount = 0;
192
+
193
+ for (const row of queueRows || []) {
194
+ if ((row?.source || "") !== "codex") continue;
195
+ if (!row.hour_start) continue;
196
+ const day = rowDayKey(row, timeZoneContext);
197
+ if (from && day < from) continue;
198
+ if (to && day > to) continue;
199
+ totals.input_tokens += Number(row.input_tokens || 0);
200
+ totals.cached_input_tokens += Number(row.cached_input_tokens || 0);
201
+ totals.cache_creation_input_tokens += Number(row.cache_creation_input_tokens || 0);
202
+ totals.output_tokens += Number(row.output_tokens || 0);
203
+ totals.reasoning_output_tokens += Number(row.reasoning_output_tokens || 0);
204
+ totals.total_tokens += Number(row.total_tokens || 0);
205
+ conversationCount += Number(row.conversation_count || 0);
206
+ }
207
+
208
+ return {
209
+ source: "codex",
210
+ scope: "supported",
211
+ breakdown_status: "queue_fallback",
212
+ totals,
213
+ session_count: 0,
214
+ message_count: conversationCount,
215
+ fallback: "queue_totals",
216
+ message_breakdown: {
217
+ categories: [
218
+ {
219
+ key: "user_input",
220
+ name: "User input",
221
+ totals: {
222
+ input_tokens: totals.input_tokens,
223
+ cached_input_tokens: 0,
224
+ cache_creation_input_tokens: 0,
225
+ output_tokens: 0,
226
+ reasoning_output_tokens: 0,
227
+ total_tokens: totals.input_tokens,
228
+ },
229
+ },
230
+ {
231
+ key: "conversation_history",
232
+ name: "Conversation history",
233
+ totals: {
234
+ input_tokens: 0,
235
+ cached_input_tokens: totals.cached_input_tokens,
236
+ cache_creation_input_tokens: totals.cache_creation_input_tokens,
237
+ output_tokens: 0,
238
+ reasoning_output_tokens: 0,
239
+ total_tokens: totals.cached_input_tokens + totals.cache_creation_input_tokens,
240
+ },
241
+ },
242
+ {
243
+ key: "assistant_response",
244
+ name: "Assistant response",
245
+ totals: {
246
+ input_tokens: 0,
247
+ cached_input_tokens: 0,
248
+ cache_creation_input_tokens: 0,
249
+ output_tokens: Math.max(0, totals.output_tokens - totals.reasoning_output_tokens),
250
+ reasoning_output_tokens: 0,
251
+ total_tokens: Math.max(0, totals.output_tokens - totals.reasoning_output_tokens),
252
+ },
253
+ },
254
+ ].sort((a, b) => Number(b.totals.total_tokens || 0) - Number(a.totals.total_tokens || 0)),
255
+ privacy: {
256
+ includes_content: false,
257
+ note: "Queue fallback includes aggregated token categories only; message text is never returned.",
258
+ },
259
+ },
260
+ tool_calls_breakdown: {
261
+ total_calls: 0,
262
+ tools: [],
263
+ categories: [],
264
+ tools_total: 0,
265
+ privacy: {
266
+ includes_inputs: false,
267
+ note: "Codex rollout sessions were unavailable; totals come from TokenTracker queue rows.",
268
+ },
269
+ },
270
+ exec_command_breakdown: {
271
+ by_type: [],
272
+ by_exit: [],
273
+ },
274
+ };
275
+ }
276
+
180
277
  function getRequestedUsageScope(url) {
181
278
  if (url.searchParams.get("include_account_level") === "1") return "all";
182
279
  return normalizeUsageScope(url.searchParams.get("scope"));
@@ -1002,27 +1099,53 @@ function createLocalApiHandler({ queuePath }) {
1002
1099
  return true;
1003
1100
  }
1004
1101
 
1005
- // --- usage-category-breakdown (Claude Code only) ---
1006
- // Splits historical Claude usage into seven semantic categories that
1007
- // mirror the Claude Code /context view: system_prefix /
1008
- // conversation_history / user_input / tool_calls / subagents /
1009
- // reasoning / assistant_response. Other sources don't carry the
1010
- // per-message role data this requires, so they return scope:unsupported.
1102
+ // --- usage-category-breakdown (Claude + Codex) ---
1103
+ // Claude: splits historical Claude usage into seven semantic categories
1104
+ // mirroring Claude Code's /context view (approx).
1105
+ // Codex: provides a tool-oriented breakdown, attributing per-turn token
1106
+ // deltas to observed tool calls (heuristic).
1011
1107
  if (p === "/functions/tokentracker-usage-category-breakdown") {
1012
1108
  const from = url.searchParams.get("from") || "";
1013
1109
  const to = url.searchParams.get("to") || "";
1014
1110
  const requestedSource = (url.searchParams.get("source") || "claude").trim().toLowerCase();
1015
- if (requestedSource !== "claude") {
1016
- json(res, { from, to, ...unsupportedCategoryPayload(requestedSource) });
1111
+ if (requestedSource === "claude") {
1112
+ try {
1113
+ const result = await computeClaudeCategoryBreakdown({ from, to, projectDir: process.cwd() });
1114
+ json(res, { from, to, ...result });
1115
+ } catch (e) {
1116
+ console.error("[LocalAPI] usage-category-breakdown:", e?.message || e);
1117
+ json(res, { from, to, ...unsupportedCategoryPayload("claude"), error: "compute_failed" }, 500);
1118
+ }
1017
1119
  return true;
1018
1120
  }
1019
- try {
1020
- const result = await computeClaudeCategoryBreakdown({ from, to, projectDir: process.cwd() });
1021
- json(res, { from, to, ...result });
1022
- } catch (e) {
1023
- console.error("[LocalAPI] usage-category-breakdown:", e?.message || e);
1024
- json(res, { from, to, ...unsupportedCategoryPayload("claude"), error: "compute_failed" }, 500);
1121
+
1122
+ if (requestedSource === "codex") {
1123
+ try {
1124
+ const timeZoneContext = getTimeZoneContext(url);
1125
+ const result = await computeCodexContextBreakdown({
1126
+ from,
1127
+ to,
1128
+ top: 50,
1129
+ timeZoneContext,
1130
+ });
1131
+ if (!Number(result?.totals?.total_tokens || 0)) {
1132
+ const fallback = buildCodexCategoryFallbackFromQueue(readQueueData(qp), {
1133
+ from,
1134
+ to,
1135
+ timeZoneContext,
1136
+ });
1137
+ json(res, { from, to, ...fallback });
1138
+ return true;
1139
+ }
1140
+ json(res, { from, to, ...result });
1141
+ } catch (e) {
1142
+ console.error("[LocalAPI] usage-category-breakdown(codex):", e?.message || e);
1143
+ json(res, { from, to, ...unsupportedCategoryPayload("codex"), error: "compute_failed" }, 500);
1144
+ }
1145
+ return true;
1025
1146
  }
1147
+
1148
+ json(res, { from, to, ...unsupportedCategoryPayload(requestedSource) });
1026
1149
  return true;
1027
1150
  }
1028
1151