@tokz/cli 0.2.3 → 0.2.5

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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp.ts
4
+ import { readFile } from "fs/promises";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ async function readJson(file) {
8
+ try {
9
+ return JSON.parse(await readFile(file, "utf8"));
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ }
14
+ function serverNames(obj) {
15
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) return Object.keys(obj);
16
+ return [];
17
+ }
18
+ async function findMcpServers(projectPath, home = homedir()) {
19
+ const out = /* @__PURE__ */ new Map();
20
+ const add = (names, source) => {
21
+ for (const name of names) if (!out.has(name)) out.set(name, { name, source });
22
+ };
23
+ const projectFile = join(projectPath, ".mcp.json");
24
+ const projectCfg = await readJson(projectFile);
25
+ add(serverNames(projectCfg?.mcpServers), projectFile);
26
+ const globalFile = join(home, ".claude.json");
27
+ const globalCfg = await readJson(globalFile);
28
+ add(serverNames(globalCfg?.mcpServers), globalFile);
29
+ const projects = globalCfg?.projects;
30
+ if (projects && typeof projects === "object") {
31
+ const entry = projects[projectPath];
32
+ add(serverNames(entry?.mcpServers), `${globalFile} (project entry)`);
33
+ }
34
+ return [...out.values()];
35
+ }
36
+
37
+ export {
38
+ findMcpServers
39
+ };
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addUsage,
4
+ compact,
5
+ costUsd,
6
+ emptyUsage,
7
+ shortModel,
8
+ usd
9
+ } from "./chunk-Z5W5QH2Z.js";
10
+
11
+ // src/blocks.ts
12
+ var BLOCK_LENGTH_MS = 5 * 60 * 60 * 1e3;
13
+ var HOUR_MS = 60 * 60 * 1e3;
14
+ function finalize(b) {
15
+ for (const [model, u] of Object.entries(b.usageByModel)) {
16
+ b.totalTokens += u.inputTokens + u.cacheReadTokens + u.cacheCreationTokens + u.outputTokens;
17
+ b.costUsd += costUsd(u, model).total;
18
+ }
19
+ return b;
20
+ }
21
+ function buildBlocks(events, opts) {
22
+ const len = opts?.sessionLengthMs ?? BLOCK_LENGTH_MS;
23
+ const now = opts?.now ?? Date.now();
24
+ const sorted = [...events].sort((a, b) => a.ts - b.ts);
25
+ const blocks = [];
26
+ let cur;
27
+ for (const e of sorted) {
28
+ if (!cur || e.ts >= cur.end) {
29
+ if (cur) blocks.push(finalize(cur));
30
+ const start = Math.floor(e.ts / HOUR_MS) * HOUR_MS;
31
+ cur = {
32
+ start,
33
+ end: start + len,
34
+ firstTs: e.ts,
35
+ lastTs: e.ts,
36
+ usageByModel: {},
37
+ totalTokens: 0,
38
+ costUsd: 0,
39
+ active: false
40
+ };
41
+ }
42
+ cur.lastTs = Math.max(cur.lastTs, e.ts);
43
+ addUsage(cur.usageByModel[e.model] ??= emptyUsage(), e.usage);
44
+ }
45
+ if (cur) {
46
+ cur.active = now < cur.end;
47
+ blocks.push(finalize(cur));
48
+ }
49
+ return blocks;
50
+ }
51
+ function burnRate(b, now = Date.now()) {
52
+ if (!b.active) return void 0;
53
+ const elapsed = Math.max(6e4, now - b.firstTs);
54
+ const remainingMs = Math.max(0, b.end - now);
55
+ const tokensPerMinute = b.totalTokens / (elapsed / 6e4);
56
+ const costPerHour = b.costUsd / (elapsed / HOUR_MS);
57
+ return {
58
+ tokensPerMinute,
59
+ costPerHour,
60
+ projectedTokens: Math.round(b.totalTokens + tokensPerMinute * (remainingMs / 6e4)),
61
+ projectedCostUsd: b.costUsd + costPerHour * (remainingMs / HOUR_MS),
62
+ remainingMs
63
+ };
64
+ }
65
+ function maxBlockTokens(blocks) {
66
+ return blocks.filter((b) => !b.active).reduce((m, b) => Math.max(m, b.totalTokens), 0);
67
+ }
68
+
69
+ // src/blocksReport.ts
70
+ import Table from "cli-table3";
71
+ import pc from "picocolors";
72
+ function fmtMs(ms) {
73
+ const mins = Math.round(ms / 6e4);
74
+ if (mins < 1) return "<1m";
75
+ if (mins < 60) return `${mins}m`;
76
+ const h = Math.floor(mins / 60);
77
+ const m = mins % 60;
78
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
79
+ }
80
+ function fmtStart(ms) {
81
+ const d = new Date(ms);
82
+ return `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 16)} UTC`;
83
+ }
84
+ function renderBlocksReport(blocks, opts) {
85
+ if (blocks.length === 0) return "No Claude Code activity found.";
86
+ const now = opts?.now ?? Date.now();
87
+ const limit = opts?.tokenLimit === "max" ? maxBlockTokens(blocks) : opts?.tokenLimit;
88
+ const parts = [];
89
+ parts.push(
90
+ pc.bold(`tokz blocks \u2014 ${blocks.length} \xD7 5-hour billing windows`) + pc.dim(" (Claude usage limits reset per rolling 5h block)")
91
+ );
92
+ const table = new Table({ head: ["Block start", "Status", "Models", "Tokens", "Cost"] });
93
+ for (const b of blocks) {
94
+ const models = [...new Set(Object.keys(b.usageByModel).map(shortModel))].join(", ");
95
+ const status = b.active ? pc.green(`ACTIVE \xB7 ${fmtMs(b.end - now)} left`) : pc.dim(`done \xB7 ${fmtMs(b.lastTs - b.firstTs)}`);
96
+ const tokens = limit && b.totalTokens > limit ? pc.red(`${compact(b.totalTokens)} \u{1F6A8}`) : limit && b.totalTokens > limit * 0.8 ? pc.yellow(`${compact(b.totalTokens)} \u26A0\uFE0F`) : compact(b.totalTokens);
97
+ table.push([fmtStart(b.start), status, models, tokens, usd(b.costUsd)]);
98
+ }
99
+ parts.push(table.toString());
100
+ const active = blocks.find((b) => b.active);
101
+ const rate = active ? burnRate(active, now) : void 0;
102
+ if (active && rate) {
103
+ const lines = [
104
+ pc.bold("Active block") + ` \u2014 started ${fmtStart(active.start)}, ${pc.green(fmtMs(rate.remainingMs))} remaining`,
105
+ ` burn rate ${compact(Math.round(rate.tokensPerMinute))} tok/min \xB7 ${usd(rate.costPerHour)}/hr`,
106
+ ` projected ${compact(rate.projectedTokens)} tokens \xB7 ${usd(rate.projectedCostUsd)} by block end`
107
+ ];
108
+ if (limit) {
109
+ const over = rate.projectedTokens > limit;
110
+ const nearing = !over && active.totalTokens > limit * 0.8;
111
+ lines.push(
112
+ ` limit ${compact(limit)} tokens \u2014 ` + (over ? pc.red(`\u{1F6A8} projected to exceed (${compact(rate.projectedTokens)})`) : nearing ? pc.yellow(`\u26A0\uFE0F ${Math.round(active.totalTokens / limit * 100)}% used`) : pc.green(`${Math.round(active.totalTokens / limit * 100)}% used`))
113
+ );
114
+ }
115
+ parts.push(lines.join("\n"));
116
+ }
117
+ return parts.join("\n\n");
118
+ }
119
+
120
+ export {
121
+ buildBlocks,
122
+ burnRate,
123
+ fmtMs,
124
+ renderBlocksReport
125
+ };
@@ -12,6 +12,11 @@ var PRICES = {
12
12
  "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
13
13
  "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
14
14
  // OpenAI (no cache-write charge; cached input 0.1x)
15
+ "gpt-5.6-sol": { inputPerMTok: 5, outputPerMTok: 30, cacheWriteMult: 0 },
16
+ "gpt-5.6-terra": { inputPerMTok: 2.5, outputPerMTok: 15, cacheWriteMult: 0 },
17
+ "gpt-5.6-luna": { inputPerMTok: 1, outputPerMTok: 6, cacheWriteMult: 0 },
18
+ "gpt-5.5-pro": { inputPerMTok: 30, outputPerMTok: 180, cacheWriteMult: 0, cacheReadMult: 1 },
19
+ "gpt-5.5": { inputPerMTok: 5, outputPerMTok: 30, cacheWriteMult: 0 },
15
20
  "gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
16
21
  "gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2, cacheWriteMult: 0 },
17
22
  "gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4, cacheWriteMult: 0 },
@@ -22,6 +27,8 @@ var PRICES = {
22
27
  "o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4, cacheWriteMult: 0, cacheReadMult: 0.25 },
23
28
  o3: { inputPerMTok: 2, outputPerMTok: 8, cacheWriteMult: 0, cacheReadMult: 0.25 },
24
29
  // Google
30
+ "gemini-3.1-pro": { inputPerMTok: 2, outputPerMTok: 12, cacheWriteMult: 0 },
31
+ "gemini-3.5-flash": { inputPerMTok: 1.5, outputPerMTok: 9, cacheWriteMult: 0 },
25
32
  "gemini-2.5-pro": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
26
33
  "gemini-2.5-flash": { inputPerMTok: 0.3, outputPerMTok: 2.5, cacheWriteMult: 0 }
27
34
  };
@@ -29,17 +36,29 @@ var DEFAULT_CACHE_READ_MULT = 0.1;
29
36
  var DEFAULT_CACHE_WRITE_MULT = 1.25;
30
37
  var CLAUDE_FALLBACK = PRICES["claude-opus-4-8"];
31
38
  var UNKNOWN = { inputPerMTok: 0, outputPerMTok: 0 };
32
- function resolvePrice(modelId) {
39
+ var livePrices = {};
40
+ var resolveCache = /* @__PURE__ */ new Map();
41
+ function setLivePrices(prices) {
42
+ livePrices = prices;
43
+ resolveCache.clear();
44
+ }
45
+ function longestPrefix(modelId, table) {
33
46
  let best;
34
47
  let bestLen = -1;
35
- for (const [prefix, price] of Object.entries(PRICES)) {
48
+ for (const [prefix, price] of Object.entries(table)) {
36
49
  if (modelId.startsWith(prefix) && prefix.length > bestLen) {
37
50
  best = price;
38
51
  bestLen = prefix.length;
39
52
  }
40
53
  }
41
- if (best) return best;
42
- return modelId.startsWith("claude") ? CLAUDE_FALLBACK : UNKNOWN;
54
+ return best;
55
+ }
56
+ function resolvePrice(modelId) {
57
+ const hit = resolveCache.get(modelId);
58
+ if (hit) return hit;
59
+ const price = livePrices[modelId] ?? longestPrefix(modelId, PRICES) ?? longestPrefix(modelId, livePrices) ?? (modelId.startsWith("claude") ? CLAUDE_FALLBACK : UNKNOWN);
60
+ resolveCache.set(modelId, price);
61
+ return price;
43
62
  }
44
63
  function emptyUsage() {
45
64
  return { inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, outputTokens: 0, turns: 0 };
@@ -53,7 +72,8 @@ function costUsd(usage, modelId) {
53
72
  const cacheRead = usage.cacheReadTokens / 1e6 * p.inputPerMTok * readMult;
54
73
  const write1h = Math.min(usage.cacheCreation1hTokens ?? 0, usage.cacheCreationTokens);
55
74
  const write5m = usage.cacheCreationTokens - write1h;
56
- const cacheWrite = write5m / 1e6 * p.inputPerMTok * writeMult + write1h / 1e6 * p.inputPerMTok * (p.cacheWriteMult ?? CACHE_WRITE_1H_MULT);
75
+ const write1hMult = p.cacheWrite1hMult ?? (p.cacheWriteMult === 0 ? 0 : CACHE_WRITE_1H_MULT);
76
+ const cacheWrite = write5m / 1e6 * p.inputPerMTok * writeMult + write1h / 1e6 * p.inputPerMTok * write1hMult;
57
77
  const output = usage.outputTokens / 1e6 * p.outputPerMTok;
58
78
  return { input, cacheRead, cacheWrite, output, total: input + cacheRead + cacheWrite + output };
59
79
  }
@@ -220,6 +240,85 @@ function buildReport(sessions, servers, range) {
220
240
  };
221
241
  }
222
242
 
243
+ // src/format.ts
244
+ var usd = (n) => `$${n.toFixed(2)}`;
245
+ var tok = (n) => n.toLocaleString("en-US");
246
+ function compact(n) {
247
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
248
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
249
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
250
+ return String(n);
251
+ }
252
+ var pct = (fraction) => `${Math.round(fraction * 100)}%`;
253
+ var pct1 = (fraction) => `${(fraction * 100).toFixed(1)}%`;
254
+ var shortModel = (id) => id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
255
+ function duration(startIso, endIso) {
256
+ if (!startIso || !endIso) return "\u2014";
257
+ const mins = Math.round((Date.parse(endIso) - Date.parse(startIso)) / 6e4);
258
+ if (mins < 1) return "<1m";
259
+ if (mins < 60) return `${mins}m`;
260
+ const h = Math.floor(mins / 60);
261
+ const m = mins % 60;
262
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
263
+ }
264
+ function relativeDate(iso, now = Date.now()) {
265
+ if (!iso) return "\u2014";
266
+ const date = iso.slice(0, 10);
267
+ const days = Math.floor((now - Date.parse(date)) / 864e5);
268
+ if (days <= 0) return "today";
269
+ if (days === 1) return "yesterday";
270
+ if (days < 30) return `${days}d ago`;
271
+ return date;
272
+ }
273
+
274
+ // src/dates.ts
275
+ var zone;
276
+ var formatter;
277
+ function setTimezone(tz) {
278
+ zone = tz === "utc" || tz === void 0 ? void 0 : tz;
279
+ formatter = zone ? new Intl.DateTimeFormat("en-CA", {
280
+ timeZone: zone === "local" ? void 0 : zone,
281
+ year: "numeric",
282
+ month: "2-digit",
283
+ day: "2-digit"
284
+ }) : void 0;
285
+ }
286
+ function dayKey(ts) {
287
+ if (!formatter) return typeof ts === "string" ? ts.slice(0, 10) : new Date(ts).toISOString().slice(0, 10);
288
+ return formatter.format(typeof ts === "string" ? new Date(ts) : ts);
289
+ }
290
+ function weekKey(day) {
291
+ const d = /* @__PURE__ */ new Date(`${day}T00:00:00Z`);
292
+ const dow = (d.getUTCDay() + 6) % 7;
293
+ d.setUTCDate(d.getUTCDate() - dow);
294
+ return d.toISOString().slice(0, 10);
295
+ }
296
+ function monthKey(day) {
297
+ return day.slice(0, 7);
298
+ }
299
+ function groupDaily(daily, unit) {
300
+ if (unit === "day") return daily;
301
+ const keyOf = unit === "week" ? weekKey : monthKey;
302
+ const out = /* @__PURE__ */ new Map();
303
+ for (const d of daily) {
304
+ const key = keyOf(d.date);
305
+ const acc = out.get(key) ?? { date: key, costUsd: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, outputTokens: 0, turns: 0 };
306
+ acc.costUsd += d.costUsd;
307
+ acc.inputTokens += d.inputTokens;
308
+ acc.cacheReadTokens += d.cacheReadTokens;
309
+ acc.cacheCreationTokens += d.cacheCreationTokens;
310
+ acc.outputTokens += d.outputTokens;
311
+ acc.turns += d.turns;
312
+ out.set(key, acc);
313
+ }
314
+ return [...out.values()].sort((a, b) => a.date < b.date ? -1 : 1);
315
+ }
316
+ function parseDateArg(raw) {
317
+ if (!raw) return void 0;
318
+ const m = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(raw.trim());
319
+ return m ? `${m[1]}-${m[2]}-${m[3]}` : void 0;
320
+ }
321
+
223
322
  // src/discover.ts
224
323
  import { homedir } from "os";
225
324
  import { join } from "path";
@@ -235,40 +334,6 @@ async function findTranscripts(projectPath, home = homedir()) {
235
334
  return glob(["**/*.jsonl"], { cwd, absolute: true }).catch(() => []);
236
335
  }
237
336
 
238
- // src/mcp.ts
239
- import { readFile } from "fs/promises";
240
- import { homedir as homedir2 } from "os";
241
- import { join as join2 } from "path";
242
- async function readJson(file) {
243
- try {
244
- return JSON.parse(await readFile(file, "utf8"));
245
- } catch {
246
- return void 0;
247
- }
248
- }
249
- function serverNames(obj) {
250
- if (obj && typeof obj === "object" && !Array.isArray(obj)) return Object.keys(obj);
251
- return [];
252
- }
253
- async function findMcpServers(projectPath, home = homedir2()) {
254
- const out = /* @__PURE__ */ new Map();
255
- const add = (names, source) => {
256
- for (const name of names) if (!out.has(name)) out.set(name, { name, source });
257
- };
258
- const projectFile = join2(projectPath, ".mcp.json");
259
- const projectCfg = await readJson(projectFile);
260
- add(serverNames(projectCfg?.mcpServers), projectFile);
261
- const globalFile = join2(home, ".claude.json");
262
- const globalCfg = await readJson(globalFile);
263
- add(serverNames(globalCfg?.mcpServers), globalFile);
264
- const projects = globalCfg?.projects;
265
- if (projects && typeof projects === "object") {
266
- const entry = projects[projectPath];
267
- add(serverNames(entry?.mcpServers), `${globalFile} (project entry)`);
268
- }
269
- return [...out.values()];
270
- }
271
-
272
337
  // src/transcript.ts
273
338
  import { createReadStream } from "fs";
274
339
  import { createInterface } from "readline";
@@ -293,7 +358,7 @@ var AssistantLine = z.object({
293
358
  content: z.array(z.object({ type: z.string(), id: z.string().optional(), name: z.string().optional() }).passthrough()).optional()
294
359
  })
295
360
  });
296
- async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set()) {
361
+ async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set(), events) {
297
362
  const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
298
363
  const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
299
364
  for await (const line of rl) {
@@ -345,8 +410,9 @@ async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), s
345
410
  if (hasDelta) {
346
411
  const accs = [stats.usageByModel[model] ??= emptyUsage()];
347
412
  if (timestamp) {
348
- const day = stats.dailyUsage[timestamp.slice(0, 10)] ??= {};
413
+ const day = stats.dailyUsage[dayKey(timestamp)] ??= {};
349
414
  accs.push(day[model] ??= emptyUsage());
415
+ events?.push({ ts: Date.parse(timestamp), model, usage: delta });
350
416
  }
351
417
  for (const acc of accs) {
352
418
  acc.inputTokens += delta.inputTokens;
@@ -379,47 +445,14 @@ async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), s
379
445
  return stats;
380
446
  }
381
447
 
382
- // src/format.ts
383
- var usd = (n) => `$${n.toFixed(2)}`;
384
- var tok = (n) => n.toLocaleString("en-US");
385
- function compact(n) {
386
- if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
387
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
388
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
389
- return String(n);
390
- }
391
- var pct = (fraction) => `${Math.round(fraction * 100)}%`;
392
- var pct1 = (fraction) => `${(fraction * 100).toFixed(1)}%`;
393
- var shortModel = (id) => id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
394
- function duration(startIso, endIso) {
395
- if (!startIso || !endIso) return "\u2014";
396
- const mins = Math.round((Date.parse(endIso) - Date.parse(startIso)) / 6e4);
397
- if (mins < 1) return "<1m";
398
- if (mins < 60) return `${mins}m`;
399
- const h = Math.floor(mins / 60);
400
- const m = mins % 60;
401
- return m > 0 ? `${h}h ${m}m` : `${h}h`;
402
- }
403
- function relativeDate(iso, now = Date.now()) {
404
- if (!iso) return "\u2014";
405
- const date = iso.slice(0, 10);
406
- const days = Math.floor((now - Date.parse(date)) / 864e5);
407
- if (days <= 0) return "today";
408
- if (days === 1) return "yesterday";
409
- if (days < 30) return `${days}d ago`;
410
- return date;
411
- }
412
-
413
448
  export {
449
+ setLivePrices,
414
450
  emptyUsage,
415
451
  costUsd,
416
452
  addUsage,
417
453
  cacheSavings,
418
454
  cacheHitRate,
419
455
  buildReport,
420
- sanitizeProjectPath,
421
- findTranscripts,
422
- findMcpServers,
423
456
  usd,
424
457
  tok,
425
458
  compact,
@@ -428,5 +461,11 @@ export {
428
461
  shortModel,
429
462
  duration,
430
463
  relativeDate,
464
+ setTimezone,
465
+ dayKey,
466
+ groupDaily,
467
+ parseDateArg,
468
+ sanitizeProjectPath,
469
+ findTranscripts,
431
470
  parseTranscript
432
471
  };
package/dist/cli.js CHANGED
@@ -1,21 +1,107 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ buildBlocks,
4
+ renderBlocksReport
5
+ } from "./chunk-6Y6MKFHZ.js";
6
+ import {
7
+ findMcpServers
8
+ } from "./chunk-65BQE6TI.js";
2
9
  import {
3
10
  buildReport,
4
- findMcpServers,
5
11
  findTranscripts,
12
+ groupDaily,
13
+ parseDateArg,
6
14
  parseTranscript,
7
15
  pct1,
16
+ setLivePrices,
17
+ setTimezone,
8
18
  tok,
9
19
  usd
10
- } from "./chunk-JAZOB7NC.js";
20
+ } from "./chunk-Z5W5QH2Z.js";
11
21
 
12
22
  // src/cli.ts
13
23
  import { createRequire } from "module";
14
24
  import { Command } from "commander";
15
25
 
26
+ // src/livePricing.ts
27
+ import { mkdir, readFile, writeFile } from "fs/promises";
28
+ import { homedir } from "os";
29
+ import { join } from "path";
30
+ var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
31
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
32
+ var FETCH_TIMEOUT_MS = 3500;
33
+ function mapLitellmPrices(raw) {
34
+ const out = {};
35
+ for (const [key, value] of Object.entries(raw)) {
36
+ if (!value || typeof value !== "object") continue;
37
+ const e = value;
38
+ const inCost = e.input_cost_per_token;
39
+ const outCost = e.output_cost_per_token;
40
+ if (typeof inCost !== "number" || typeof outCost !== "number" || inCost <= 0) continue;
41
+ const name = key.includes("/") ? key.slice(key.lastIndexOf("/") + 1) : key;
42
+ if (key.includes("/") && out[name]) continue;
43
+ const p = { inputPerMTok: inCost * 1e6, outputPerMTok: outCost * 1e6 };
44
+ if (typeof e.cache_read_input_token_cost === "number")
45
+ p.cacheReadMult = e.cache_read_input_token_cost / inCost;
46
+ if (typeof e.cache_creation_input_token_cost === "number")
47
+ p.cacheWriteMult = e.cache_creation_input_token_cost / inCost;
48
+ else if (!name.startsWith("claude"))
49
+ p.cacheWriteMult = 0;
50
+ if (typeof e.cache_creation_input_token_cost_above_1hr === "number")
51
+ p.cacheWrite1hMult = e.cache_creation_input_token_cost_above_1hr / inCost;
52
+ out[name] = p;
53
+ }
54
+ return out;
55
+ }
56
+ async function initPricing(opts) {
57
+ const dir = opts?.cacheDir ?? join(homedir(), ".tokz");
58
+ const file = join(dir, "litellm-prices.json");
59
+ let cached;
60
+ try {
61
+ cached = JSON.parse(await readFile(file, "utf8"));
62
+ } catch {
63
+ }
64
+ const fresh = cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS;
65
+ if (cached && (fresh || opts?.offline)) {
66
+ setLivePrices(cached.prices);
67
+ return "cached";
68
+ }
69
+ if (opts?.offline) return "static";
70
+ try {
71
+ const ctrl = new AbortController();
72
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
73
+ const res = await fetch(LITELLM_URL, { signal: ctrl.signal });
74
+ clearTimeout(timer);
75
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
76
+ const prices = mapLitellmPrices(await res.json());
77
+ setLivePrices(prices);
78
+ try {
79
+ await mkdir(dir, { recursive: true });
80
+ await writeFile(file, JSON.stringify({ fetchedAt: Date.now(), prices }));
81
+ } catch {
82
+ }
83
+ return "live";
84
+ } catch {
85
+ if (cached) {
86
+ setLivePrices(cached.prices);
87
+ return "cached";
88
+ }
89
+ return "static";
90
+ }
91
+ }
92
+
16
93
  // src/report.ts
17
94
  import Table from "cli-table3";
18
95
  import pc from "picocolors";
96
+ function renderActivity(rows, unit) {
97
+ const head = unit === "week" ? "Week of" : "Month";
98
+ const table = new Table({ head: [head, "Cost", "Input", "Cache read", "Cache write", "Output", "Turns"] });
99
+ for (const d of rows) {
100
+ table.push([d.date, usd(d.costUsd), tok(d.inputTokens), tok(d.cacheReadTokens), tok(d.cacheCreationTokens), tok(d.outputTokens), String(d.turns)]);
101
+ }
102
+ return `${pc.bold(`Activity by ${unit}`)}
103
+ ${table.toString()}`;
104
+ }
19
105
  function renderReport(report) {
20
106
  const parts = [];
21
107
  const span = report.spanStart && report.spanEnd ? `${report.spanStart} \u2192 ${report.spanEnd}` : `${report.spanDays} days`;
@@ -62,47 +148,110 @@ function renderReport(report) {
62
148
  // src/cli.ts
63
149
  var { version } = createRequire(import.meta.url)("../package.json");
64
150
  var program = new Command();
65
- program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").version(version);
66
- program.command("audit").argument("[project]", "project path (default: current directory)").option("--all", "scan all projects under ~/.claude/projects").option("--json", "output raw JSON report").option("--days <n>", "only include the last N days of activity").action(async (project, opts) => {
151
+ program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").option("--offline", "don't fetch live pricing; use cached/built-in rates").option("--timezone <zone>", 'group days in this timezone: "utc" (default), "local", or an IANA name').version(version);
152
+ function applyGlobals() {
153
+ const opts = program.opts();
154
+ setTimezone(opts.timezone);
155
+ return { offline: opts.offline };
156
+ }
157
+ async function parseAll(projectPath, events) {
158
+ const transcripts = await findTranscripts(projectPath);
159
+ const seenMessageIds = /* @__PURE__ */ new Map();
160
+ const seenToolIds = /* @__PURE__ */ new Set();
161
+ const sessions = await Promise.all(
162
+ transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds, events))
163
+ );
164
+ return { transcripts, sessions };
165
+ }
166
+ program.command("audit").argument("[project]", "project path (default: current directory)").option("--all", "scan all projects under ~/.claude/projects").option("--json", "output raw JSON report").option("--days <n>", "only include the last N days of activity").option("--since <date>", "start date, YYYY-MM-DD or YYYYMMDD (inclusive)").option("--until <date>", "end date, YYYY-MM-DD or YYYYMMDD (inclusive)").option("--weekly", "append activity grouped by week").option("--monthly", "append activity grouped by month").action(async (project, opts) => {
167
+ const globals = applyGlobals();
67
168
  const projectPath = project ?? process.cwd();
68
- const transcripts = await findTranscripts(opts.all ? void 0 : projectPath);
169
+ const [{ transcripts, sessions }] = await Promise.all([
170
+ parseAll(opts.all ? void 0 : projectPath),
171
+ initPricing(globals)
172
+ ]);
69
173
  if (transcripts.length === 0) {
70
174
  console.error(`No Claude Code transcripts found for ${opts.all ? "any project" : projectPath}.`);
71
175
  process.exitCode = 1;
72
176
  return;
73
177
  }
74
- const seenMessageIds = /* @__PURE__ */ new Map();
75
- const seenToolIds = /* @__PURE__ */ new Set();
76
- const sessions = await Promise.all(
77
- transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
78
- );
79
178
  const servers = opts.all ? [] : await findMcpServers(projectPath);
80
179
  const days = opts.days ? Number.parseInt(opts.days, 10) : void 0;
81
180
  const isoDay = (offset) => new Date(Date.now() - offset * 864e5).toISOString().slice(0, 10);
82
- const range = days && days > 0 ? { from: isoDay(days - 1), to: isoDay(0) } : void 0;
181
+ const since = parseDateArg(opts.since);
182
+ const until = parseDateArg(opts.until);
183
+ let range;
184
+ if (since || until) range = { from: since ?? "0000-01-01", to: until ?? "9999-12-31" };
185
+ else if (days && days > 0) range = { from: isoDay(days - 1), to: isoDay(0) };
83
186
  const report = buildReport(sessions, servers, range);
84
- console.log(opts.json ? JSON.stringify(report, null, 2) : renderReport(report));
187
+ if (opts.json) {
188
+ console.log(JSON.stringify(report, null, 2));
189
+ return;
190
+ }
191
+ const parts = [renderReport(report)];
192
+ const grouping = opts.monthly ? "month" : opts.weekly ? "week" : void 0;
193
+ if (grouping) parts.push(renderActivity(groupDaily(report.daily, grouping), grouping));
194
+ console.log(parts.join("\n\n"));
195
+ });
196
+ program.command("blocks").description("Claude usage grouped into rolling 5-hour billing windows").option("--json", "output raw JSON").option("--active", "show only the currently active block").option("--recent", "only blocks from the last 3 days").option("--token-limit <n>", 'warn near this many tokens per block ("max" = highest past block)').option("--session-length <hours>", "block length in hours (default 5)").action(async (opts) => {
197
+ const globals = applyGlobals();
198
+ const events = [];
199
+ await Promise.all([parseAll(void 0, events), initPricing(globals)]);
200
+ const hours = opts.sessionLength ? Number.parseFloat(opts.sessionLength) : 5;
201
+ let blocks = buildBlocks(events, { sessionLengthMs: hours * 36e5 });
202
+ if (opts.recent) blocks = blocks.filter((b) => b.lastTs >= Date.now() - 3 * 864e5);
203
+ if (opts.active) blocks = blocks.filter((b) => b.active);
204
+ const tokenLimit = opts.tokenLimit === "max" ? "max" : opts.tokenLimit ? Number.parseInt(opts.tokenLimit, 10) : void 0;
205
+ if (opts.json) {
206
+ console.log(JSON.stringify(blocks, null, 2));
207
+ return;
208
+ }
209
+ console.log(renderBlocksReport(blocks, { tokenLimit }));
210
+ });
211
+ program.command("statusline").description("compact usage line for Claude Code's statusLine hook (reads hook JSON on stdin)").argument("[action]", '"enable" writes the hook into ~/.claude/settings.json, "disable" removes it').option("--cost-source <mode>", "session cost source: auto | cc | calc | both", "auto").action(async (action, opts) => {
212
+ const globals = applyGlobals();
213
+ const sl = await import("./statusline-EXFMQYFF.js");
214
+ if (action === "enable" || action === "disable") {
215
+ try {
216
+ console.log(action === "enable" ? await sl.enableStatusline() : await sl.disableStatusline());
217
+ } catch (err) {
218
+ console.error(`Could not update settings: ${err instanceof Error ? err.message : err}`);
219
+ process.exitCode = 1;
220
+ }
221
+ return;
222
+ }
223
+ if (action !== void 0) {
224
+ console.error(`Unknown action "${action}" \u2014 use "enable" or "disable".`);
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+ await initPricing({ ...globals, offline: true });
229
+ let input = {};
230
+ try {
231
+ input = JSON.parse(await sl.readStdin());
232
+ } catch {
233
+ }
234
+ const valid = ["auto", "cc", "calc", "both"];
235
+ const costSource = valid.includes(opts.costSource ?? "auto") ? opts.costSource ?? "auto" : "auto";
236
+ console.log(await sl.statusline(input, Date.now(), void 0, { costSource }));
85
237
  });
86
238
  program.action(async () => {
239
+ const globals = applyGlobals();
240
+ await initPricing(globals);
87
241
  if (!process.stdout.isTTY) {
88
- const transcripts = await findTranscripts(void 0);
242
+ const { transcripts, sessions } = await parseAll(void 0);
89
243
  if (transcripts.length === 0) {
90
244
  console.error("No Claude Code transcripts found.");
91
245
  process.exitCode = 1;
92
246
  return;
93
247
  }
94
- const seenMessageIds = /* @__PURE__ */ new Map();
95
- const seenToolIds = /* @__PURE__ */ new Set();
96
- const sessions = await Promise.all(
97
- transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
98
- );
99
248
  console.log(renderReport(buildReport(sessions, [])));
100
249
  return;
101
250
  }
102
251
  const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
103
252
  import("ink"),
104
253
  import("react"),
105
- import("./Root-6SZ5FQDN.js"),
254
+ import("./Root-TUEV5MJB.js"),
106
255
  import("./Fullscreen-GK2ZYXHV.js")
107
256
  ]);
108
257
  render(React.createElement(Fullscreen, null, React.createElement(Root)));