@v1nvn/tokens 0.25.0 → 0.27.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.
package/README.md CHANGED
@@ -20,17 +20,25 @@ claude plugin install tokens@agentic
20
20
  In a terminal, bare:
21
21
 
22
22
  ```sh
23
- npx -y @v1nvn/tokens@0.25.0
23
+ npx -y @v1nvn/tokens@0.27.0
24
24
  ```
25
25
 
26
26
  ## Usage
27
27
 
28
28
  | Invocation | Does |
29
29
  |---|---|
30
- | `npx -y @v1nvn/tokens@0.25.0` | per-model table: input/output/cache-write/cache-read tokens, cache hit rate, 24 h window + 7-day daily totals |
30
+ | `npx -y @v1nvn/tokens@0.27.0` | per-model table: input/output/cache-write/cache-read tokens, cache hit rate, 24 h window + 7-day daily totals |
31
31
 
32
32
  Works for every profile writing to `~/.claude/projects` — default `claude`,
33
- `claudez`, headless `claude -p` runs alike.
33
+ `claudez`, headless `claude -p` runs alike. Files older than the 7-day window
34
+ are skipped by mtime, keeping the scan under a second even with a large
35
+ transcript history.
36
+
37
+ **Semantics:** `input_tokens` is the *uncached* input only; the modeled context
38
+ is `input + cacheRead + cacheCreation`. Hit rate =
39
+ `cacheRead / (input + cacheRead + cacheCreation)`. On the GLM Coding Plan,
40
+ cached tokens count fully against quota, so a high hit rate saves latency, not
41
+ quota.
34
42
 
35
43
  ## Develop
36
44
 
@@ -51,4 +59,5 @@ yarn lint && yarn typecheck # from the repo root
51
59
  ## Contracts
52
60
 
53
61
  - Reads transcripts only; writes nothing.
54
- - No flags — the report shape is the contract the plugin's hook prints too.
62
+ - One flag: `--hook` emits the UserPromptExpansion block the plugin's hook
63
+ prints; bare invocation prints the same report.
package/dist/index.js CHANGED
@@ -4,30 +4,33 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { Command } from "commander";
6
6
  //#region ../core/dist/index.js
7
- /**
8
- * Read the Claude Code hook event JSON from stdin; empty or unparseable input
9
- * yields an empty event, never a thrown error — a hook must always answer.
10
- */
11
- function readHookEvent(stream = process.stdin) {
12
- return new Promise((resolve) => {
7
+ function readAll(stream) {
8
+ return new Promise((resolve, reject) => {
13
9
  let data = "";
14
10
  stream.setEncoding("utf8");
15
11
  stream.on("data", (chunk) => {
16
12
  data += chunk;
17
13
  });
18
14
  stream.on("end", () => {
19
- try {
20
- resolve(JSON.parse(data));
21
- } catch {
22
- resolve({});
23
- }
24
- });
25
- stream.on("error", () => {
26
- resolve({});
15
+ resolve(data);
27
16
  });
17
+ stream.on("error", reject);
28
18
  });
29
19
  }
30
20
  /**
21
+ * Read the Claude Code hook event JSON from stdin; empty or unparseable input
22
+ * yields an empty event, never a thrown error — a hook must always answer.
23
+ */
24
+ function readHookEvent(stream = process.stdin) {
25
+ return readAll(stream).then((data) => {
26
+ try {
27
+ return JSON.parse(data);
28
+ } catch {
29
+ return {};
30
+ }
31
+ }, () => ({}));
32
+ }
33
+ /**
31
34
  * Print the UserPromptExpansion decision for a zero-token command: `block`
32
35
  * keeps the command from reaching the model, with the output as the `reason`.
33
36
  */
@@ -37,15 +40,15 @@ function emitHookBlock(reason) {
37
40
  reason
38
41
  })}\n`);
39
42
  }
40
- function parseQuietly(program, args) {
43
+ function parseQuietly(program, args, recover) {
41
44
  try {
42
45
  program.allowExcessArguments(false).exitOverride().configureOutput({
43
46
  writeOut: () => void 0,
44
47
  writeErr: () => void 0
45
48
  }).parse([...args], { from: "user" });
46
49
  return program;
47
- } catch {
48
- return;
50
+ } catch (err) {
51
+ return recover?.(err);
49
52
  }
50
53
  }
51
54
  function printUsageAndExit(program) {
@@ -70,6 +73,11 @@ async function hookOrPrint(hook, failure, run, stdin = process.stdin) {
70
73
  process.exit(1);
71
74
  }
72
75
  }
76
+ function claudeProjectsDir() {
77
+ const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), ".claude");
78
+ return join(claudeDir, "projects");
79
+ }
80
+ Number.MAX_SAFE_INTEGER;
73
81
  /**
74
82
  * Plain-text rendering primitives shared by the zai and tokens report
75
83
  * renderers. Output targets a monospace terminal / hook-block `reason`, so
@@ -100,6 +108,16 @@ var EIGHTHS = [
100
108
  "▊",
101
109
  "▉"
102
110
  ];
111
+ function rule() {
112
+ return "─".repeat(68);
113
+ }
114
+ function pad2(n) {
115
+ return String(n).padStart(2, "0");
116
+ }
117
+ /** Local-time `YYYY-MM-DD` key. */
118
+ function ymd(date) {
119
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
120
+ }
103
121
  function fmtTokens(n) {
104
122
  if (n == null || Number.isNaN(n)) return "—";
105
123
  if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
@@ -141,7 +159,6 @@ function barField(v, max, width) {
141
159
  * markdown. Alignment comes from fixed-width columns and unicode block glyphs.
142
160
  * Input is the aggregate JSON produced by scan.ts.
143
161
  */
144
- var W = 68;
145
162
  /** cacheRead / modeled context; input_tokens is uncached input only. */
146
163
  function hitRate({ input = 0, cacheRead = 0, cacheCreation = 0 } = {}) {
147
164
  const denom = input + cacheRead + cacheCreation;
@@ -156,17 +173,10 @@ function dayLabel(day) {
156
173
  return m ? `${MONTHS[+m[2] - 1]} ${m[3]}` : day;
157
174
  }
158
175
  function fmtClock(date) {
159
- return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
160
- }
161
- /** Local-time 'YYYY-MM-DD' for a Date, without depending on toLocaleString. */
162
- function localKey(date) {
163
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
176
+ return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
164
177
  }
165
178
  function render(scanResult, { now = /* @__PURE__ */ new Date() } = {}) {
166
179
  const out = [];
167
- function rule() {
168
- return "─".repeat(W);
169
- }
170
180
  const rows = scanResult.last24.filter((r) => totalTokens(r) > 0);
171
181
  const sum = rows.reduce((a, r) => {
172
182
  a.input += r.input;
@@ -185,7 +195,7 @@ function render(scanResult, { now = /* @__PURE__ */ new Date() } = {}) {
185
195
  const pctHit = Math.round(hitRate(sum));
186
196
  const left = " Token usage · transcripts";
187
197
  const winStart = /* @__PURE__ */ new Date(now.getTime() - 864e5);
188
- const win = `${dayLabel(localKey(winStart))} ${fmtClock(winStart)} → ${dayLabel(localKey(now))} ${fmtClock(now)} · 24h`;
198
+ const win = `${dayLabel(ymd(winStart))} ${fmtClock(winStart)} → ${dayLabel(ymd(now))} ${fmtClock(now)} · 24h`;
189
199
  out.push(rule());
190
200
  out.push(left + padL(win, 42));
191
201
  out.push(rule());
@@ -199,7 +209,7 @@ function render(scanResult, { now = /* @__PURE__ */ new Date() } = {}) {
199
209
  out.push(` ${padR(r.model, 14)}${padL(fmtTokens(r.input), 8)} in · ${padL(fmtTokens(r.output), 8)} out · ${padL(fmtTokens(r.cacheRead), 8)} read · ${padL(fmtTokens(r.cacheCreation), 8)} created ${padL(`${pct}%`, 4)} ${barField(pct, 100, 14)}`);
200
210
  }
201
211
  let days = scanResult.days;
202
- const firstDay = localKey(/* @__PURE__ */ new Date(now.getTime() - 6048e5));
212
+ const firstDay = ymd(/* @__PURE__ */ new Date(now.getTime() - 6048e5));
203
213
  if (days.length && days[0].day === firstDay && firstDay !== days[days.length - 1].day) days = days.slice(1);
204
214
  out.push("");
205
215
  out.push(" Daily · last 7 days " + "─".repeat(Math.max(0, 47)));
@@ -220,8 +230,9 @@ function render(scanResult, { now = /* @__PURE__ */ new Date() } = {}) {
220
230
  * Transcript token scanner.
221
231
  *
222
232
  * Claude Code persists every assistant message's `usage` block to session
223
- * transcripts at ~/.claude/projects/<project-dir>/<session>.jsonl — for every
224
- * profile (default claude, claudez, …), interactive and headless alike. This
233
+ * transcripts at $CLAUDE_DIR/projects/<project-dir>/<session>.jsonl
234
+ * (default ~/.claude) — for every profile (default claude, claudez, …),
235
+ * interactive and headless alike. This
225
236
  * scans those files and aggregates token usage per model and per local day:
226
237
  * input (uncached), output, cacheRead, cacheCreation, call count.
227
238
  */
@@ -260,7 +271,7 @@ function toModelRows(m) {
260
271
  })).sort(byTotalDesc);
261
272
  }
262
273
  function scan({ projectsDir, now = /* @__PURE__ */ new Date() } = {}) {
263
- const dir = projectsDir ?? join(homedir(), ".claude", "projects");
274
+ const dir = projectsDir ?? claudeProjectsDir();
264
275
  if (!existsSync(dir)) throw new Error(`no transcripts directory at ${dir}`);
265
276
  const windowStart = now.getTime() - 6048e5;
266
277
  const last24Start = now.getTime() - 864e5;
@@ -285,8 +296,7 @@ function scan({ projectsDir, now = /* @__PURE__ */ new Date() } = {}) {
285
296
  if (!u || !model || !j.timestamp) continue;
286
297
  const ts = new Date(j.timestamp).getTime();
287
298
  if (!(ts >= windowStart)) continue;
288
- const local = new Date(ts);
289
- add(bucket(days, `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, "0")}-${String(local.getDate()).padStart(2, "0")}`), u);
299
+ add(bucket(days, ymd(new Date(ts))), u);
290
300
  add(bucket(models, model), u);
291
301
  if (ts >= last24Start) add(bucket(last24, model), u);
292
302
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../core/dist/index.js","../src/format.ts","../src/scan.ts","../src/index.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n//#region src/hook.ts\n/**\n* Read the Claude Code hook event JSON from stdin; empty or unparseable input\n* yields an empty event, never a thrown error — a hook must always answer.\n*/\nfunction readHookEvent(stream = process.stdin) {\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tstream.on(\"end\", () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(data));\n\t\t\t} catch {\n\t\t\t\tresolve({});\n\t\t\t}\n\t\t});\n\t\tstream.on(\"error\", () => {\n\t\t\tresolve({});\n\t\t});\n\t});\n}\n/**\n* Which transcript lastReply should read, per the hook event: transcript_path\n* when it names a real file, else session_id, else nothing (lastReply then\n* falls back to the newest session for the current project).\n*/\nfunction replyTarget(event) {\n\tif (event.transcript_path !== void 0 && isFile$1(event.transcript_path)) return event.transcript_path;\n\treturn event.session_id || void 0;\n}\nfunction isFile$1(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n/**\n* Print the UserPromptExpansion decision for a zero-token command: `block`\n* keeps the command from reaching the model, with the output as the `reason`.\n*/\nfunction emitHookBlock(reason) {\n\tprocess.stdout.write(`${JSON.stringify({\n\t\tdecision: \"block\",\n\t\treason\n\t})}\\n`);\n}\n//#endregion\n//#region src/cli.ts\nfunction parseQuietly(program, args) {\n\ttry {\n\t\tprogram.allowExcessArguments(false).exitOverride().configureOutput({\n\t\t\twriteOut: () => void 0,\n\t\t\twriteErr: () => void 0\n\t\t}).parse([...args], { from: \"user\" });\n\t\treturn program;\n\t} catch {\n\t\treturn;\n\t}\n}\nfunction printUsageAndExit(program) {\n\tconsole.error(program.helpInformation());\n\tprocess.exit(1);\n}\nasync function hookOrPrint(hook, failure, run, stdin = process.stdin) {\n\tif (hook) {\n\t\tlet reason;\n\t\ttry {\n\t\t\treason = await run(await readHookEvent(stdin));\n\t\t} catch (e) {\n\t\t\treason = `${failure}: ${e.message}`;\n\t\t}\n\t\temitHookBlock(reason);\n\t\treturn;\n\t}\n\ttry {\n\t\tconsole.log(await run(void 0));\n\t} catch (e) {\n\t\tconsole.error(e.message);\n\t\tprocess.exit(1);\n\t}\n}\n//#endregion\n//#region src/last-reply.ts\nfunction isFile(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\nfunction textBlocks(entry) {\n\tconst content = entry.message?.content;\n\tif (entry.type !== \"assistant\" || !Array.isArray(content)) return;\n\tconst blocks = content.filter((b) => typeof b === \"object\" && b !== null && b.type === \"text\");\n\treturn blocks.length > 0 ? blocks : void 0;\n}\n/**\n* The last assistant text reply from a Claude Code session, reproducing Claude\n* Code's `/copy` byte-for-byte: the last assistant entry that contains a\n* `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n* joined with a blank line, and no trailing newline.\n*\n* @param arg a transcript file, a session UUID under the project dir, or\n* nothing to use the newest session for the current project.\n*/\nfunction lastReply(arg) {\n\tconst claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), \".claude\");\n\tconst proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\"/\", \"-\");\n\tconst projDir = join(claudeDir, \"projects\", proj);\n\tlet file;\n\tif (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n\telse if (existsSync(projDir)) {\n\t\tconst newest = readdirSync(projDir).filter((f) => f.endsWith(\".jsonl\")).map((f) => ({\n\t\t\tf,\n\t\t\tmtimeMs: statSync(join(projDir, f)).mtimeMs\n\t\t})).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);\n\t\tif (newest) file = join(projDir, newest.f);\n\t}\n\tif (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);\n\tlet last;\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.includes(\"\\\"assistant\\\"\")) continue;\n\t\tlet entry;\n\t\ttry {\n\t\t\tentry = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst blocks = textBlocks(entry);\n\t\tif (blocks) last = blocks;\n\t}\n\treturn (last ?? []).map((b) => b.text ?? \"\").join(\"\\n\\n\");\n}\n//#endregion\n//#region src/text-format.ts\n/**\n* Plain-text rendering primitives shared by the zai and tokens report\n* renderers. Output targets a monospace terminal / hook-block `reason`, so\n* everything here is fixed-width: padding, block-glyph bars, and compact\n* number formatting.\n*/\nvar MONTHS = [\n\t\"Jan\",\n\t\"Feb\",\n\t\"Mar\",\n\t\"Apr\",\n\t\"May\",\n\t\"Jun\",\n\t\"Jul\",\n\t\"Aug\",\n\t\"Sep\",\n\t\"Oct\",\n\t\"Nov\",\n\t\"Dec\"\n];\nvar EIGHTHS = [\n\t\"\",\n\t\"▏\",\n\t\"▎\",\n\t\"▍\",\n\t\"▌\",\n\t\"▋\",\n\t\"▊\",\n\t\"▉\"\n];\nfunction fmtTokens(n) {\n\tif (n == null || Number.isNaN(n)) return \"—\";\n\tif (n >= 1e9) return (n / 1e9).toFixed(1) + \"B\";\n\tif (n >= 1e6) return (n / 1e6).toFixed(1) + \"M\";\n\tif (n >= 1e3) return (n / 1e3).toFixed(1) + \"K\";\n\treturn String(n);\n}\nfunction fmtNum(n) {\n\treturn (n || 0).toLocaleString(\"en-US\");\n}\nfunction padR(s, n) {\n\treturn s.length >= n ? s : s + \" \".repeat(n - s.length);\n}\nfunction padL(s, n) {\n\treturn s.length >= n ? s : \" \".repeat(n - s.length) + s;\n}\n/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */\nfunction barField(v, max, width) {\n\tif (!v || v <= 0 || max <= 0) return \" \".repeat(width);\n\tconst scaled = v / max * width;\n\tlet full = Math.floor(scaled);\n\tlet fi = Math.round((scaled - full) * 8);\n\tif (fi === 8) {\n\t\tfull += 1;\n\t\tfi = 0;\n\t}\n\tif (full === 0 && fi === 0) fi = 1;\n\tlet s = \"█\".repeat(Math.min(full, width));\n\tif (full < width && fi > 0) s += EIGHTHS[fi];\n\tif (s.length < width) s += \" \".repeat(width - s.length);\n\treturn s.slice(0, width);\n}\n/** Filled/empty meter: █ for used, ░ for remaining. */\nfunction meter(pct, width) {\n\tlet filled = Math.round((pct || 0) / 100 * width);\n\tfilled = Math.max(0, Math.min(width, filled));\n\treturn \"█\".repeat(filled) + \"░\".repeat(width - filled);\n}\n//#endregion\nexport { MONTHS, barField, emitHookBlock, fmtNum, fmtTokens, hookOrPrint, lastReply, meter, padL, padR, parseQuietly, printUsageAndExit, readHookEvent, replyTarget };\n\n//# sourceMappingURL=index.js.map","/**\n * Plain-text token-usage renderer.\n *\n * Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on\n * markdown. Alignment comes from fixed-width columns and unicode block glyphs.\n * Input is the aggregate JSON produced by scan.ts.\n */\n\nimport {\n barField,\n fmtNum,\n fmtTokens,\n MONTHS,\n padL,\n padR,\n} from '@v1nvn/agentic-core';\n\nimport type { DayRow, ModelRow, ScanResult, UsageAcc } from './scan.js';\n\nconst W = 68; // overall rule width\n\n/** cacheRead / modeled context; input_tokens is uncached input only. */\nexport function hitRate({\n input = 0,\n cacheRead = 0,\n cacheCreation = 0,\n}: Partial<UsageAcc> = {}): number {\n const denom = input + cacheRead + cacheCreation;\n return denom > 0 ? (cacheRead / denom) * 100 : 0;\n}\n\nfunction totalTokens(a: UsageAcc): number {\n return (\n (a.input || 0) +\n (a.output || 0) +\n (a.cacheRead || 0) +\n (a.cacheCreation || 0)\n );\n}\n\n/** '2026-08-15' → 'Aug 15'. */\nfunction dayLabel(day: string): string {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(day);\n return m ? `${MONTHS[+m[2] - 1]} ${m[3]}` : day;\n}\n\nfunction fmtClock(date: Date): string {\n return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;\n}\n\n/** Local-time 'YYYY-MM-DD' for a Date, without depending on toLocaleString. */\nfunction localKey(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n}\n\nexport function render(\n scanResult: ScanResult,\n { now = new Date() } = {},\n): string {\n const out: string[] = [];\n function rule(): string {\n return '─'.repeat(W);\n }\n\n const allRows: ModelRow[] = scanResult.last24;\n const rows = allRows.filter(r => totalTokens(r) > 0); // <synthetic> etc. carry no tokens\n const sum = rows.reduce<UsageAcc>(\n (a, r) => {\n a.input += r.input;\n a.output += r.output;\n a.cacheRead += r.cacheRead;\n a.cacheCreation += r.cacheCreation;\n a.calls += r.calls;\n return a;\n },\n { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, calls: 0 },\n );\n const pctHit = Math.round(hitRate(sum));\n\n // ---- header ----\n const left = ' Token usage · transcripts';\n const winStart = new Date(now.getTime() - 24 * 3600 * 1000);\n const win = `${dayLabel(localKey(winStart))} ${fmtClock(winStart)} → ${dayLabel(localKey(now))} ${fmtClock(now)} · 24h`;\n out.push(rule());\n out.push(left + padL(win, W - left.length));\n out.push(rule());\n\n // ---- lead ----\n out.push('');\n out.push(\n ` ${fmtTokens(totalTokens(sum))} tokens across ${fmtNum(sum.calls)} model calls — ${pctHit}% cache hit rate.`,\n );\n\n // ---- model mix · last 24h ----\n out.push('');\n out.push(' Model mix · last 24h ' + '─'.repeat(Math.max(0, W - 22)));\n if (rows.length === 0) {\n out.push(' (no usage recorded in the last 24 hours)');\n }\n for (const r of rows) {\n const pct = Math.round(hitRate(r));\n out.push(\n ` ${padR(r.model, 14)}${padL(fmtTokens(r.input), 8)} in · ${padL(fmtTokens(r.output), 8)} out ·` +\n ` ${padL(fmtTokens(r.cacheRead), 8)} read · ${padL(fmtTokens(r.cacheCreation), 8)} created ${padL(`${pct}%`, 4)} ${barField(pct, 100, 14)}`,\n );\n }\n\n // ---- daily · last 7 days ----\n // The bucket holding the window-start day covers only part of that calendar\n // day — drop it (unless the window began at midnight, which scan would have\n // bucketed as a full day).\n let days: DayRow[] = scanResult.days;\n const firstDay = localKey(new Date(now.getTime() - 7 * 24 * 3600 * 1000));\n if (\n days.length &&\n days[0].day === firstDay &&\n firstDay !== days[days.length - 1].day\n ) {\n days = days.slice(1);\n }\n out.push('');\n out.push(' Daily · last 7 days ' + '─'.repeat(Math.max(0, W - 21)));\n const maxDay = Math.max(0, ...days.map(totalTokens));\n for (const d of [...days].reverse()) {\n const pct = Math.round(hitRate(d));\n out.push(\n ` ${dayLabel(d.day)} ${padL(fmtTokens(totalTokens(d)), 8)} ${barField(totalTokens(d), maxDay, 24)} ${padL(`${pct}%`, 4)}`,\n );\n }\n if (days.length === 0) {\n out.push(' (no usage recorded in the last 7 days)');\n }\n\n out.push('');\n out.push(\n ` Covers every profile writing to ~/.claude/projects — hit rate = read / (in + read + created).`,\n );\n out.push(rule());\n return out.join('\\n');\n}\n","/**\n * Transcript token scanner.\n *\n * Claude Code persists every assistant message's `usage` block to session\n * transcripts at ~/.claude/projects/<project-dir>/<session>.jsonl — for every\n * profile (default claude, claudez, …), interactive and headless alike. This\n * scans those files and aggregates token usage per model and per local day:\n * input (uncached), output, cacheRead, cacheCreation, call count.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nconst DAYS = 7;\n\nexport interface UsageAcc {\n cacheCreation: number;\n cacheRead: number;\n calls: number;\n input: number;\n output: number;\n}\n\nexport interface DayRow extends UsageAcc {\n day: string;\n}\n\nexport interface ModelRow extends UsageAcc {\n model: string;\n}\n\nexport interface ScanResult {\n days: DayRow[];\n last24: ModelRow[];\n models: ModelRow[];\n now: string;\n}\n\ninterface UsageBlock {\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n}\n\ninterface TranscriptEntry {\n message?: { model?: string; usage?: UsageBlock };\n timestamp?: string;\n}\n\nfunction zero(): UsageAcc {\n return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, calls: 0 };\n}\n\nfunction add(acc: UsageAcc, u: UsageBlock, n = 1): void {\n acc.input += n * (u.input_tokens ?? 0);\n acc.output += n * (u.output_tokens ?? 0);\n acc.cacheRead += n * (u.cache_read_input_tokens ?? 0);\n acc.cacheCreation += n * (u.cache_creation_input_tokens ?? 0);\n acc.calls += n;\n}\n\nfunction byTotalDesc(a: UsageAcc, b: UsageAcc): number {\n return (\n b.input +\n b.output +\n b.cacheRead +\n b.cacheCreation -\n (a.input + a.output + a.cacheRead + a.cacheCreation)\n );\n}\n\n/** Get-or-create the map entry, so callers never hold a missing accumulator. */\nfunction bucket<K>(map: Map<K, UsageAcc>, key: K): UsageAcc {\n let acc = map.get(key);\n if (acc === undefined) {\n acc = zero();\n map.set(key, acc);\n }\n return acc;\n}\n\nfunction toModelRows(m: Map<string, UsageAcc>): ModelRow[] {\n return [...m.entries()]\n .map(([model, acc]) => ({ model, ...acc }))\n .sort(byTotalDesc);\n}\n\nexport function scan({\n projectsDir,\n now = new Date(),\n}: { now?: Date; projectsDir?: string } = {}): ScanResult {\n const dir = projectsDir ?? join(homedir(), '.claude', 'projects');\n if (!existsSync(dir)) {\n throw new Error(`no transcripts directory at ${dir}`);\n }\n\n const windowStart = now.getTime() - DAYS * 24 * 3600 * 1000;\n const last24Start = now.getTime() - 24 * 3600 * 1000;\n\n const days = new Map<string, UsageAcc>(); // 'YYYY-MM-DD' → acc\n const last24 = new Map<string, UsageAcc>(); // model → acc\n const models = new Map<string, UsageAcc>(); // model → acc (whole 7d window, for the model mix)\n\n const projectDirs = readdirSync(dir)\n .map(d => join(dir, d))\n .filter(d => statSync(d).isDirectory());\n\n for (const pdir of projectDirs) {\n for (const f of readdirSync(pdir)) {\n if (!f.endsWith('.jsonl')) {\n continue;\n }\n const fp = join(pdir, f);\n if (statSync(fp).mtimeMs < windowStart) {\n continue;\n }\n\n for (const line of readFileSync(fp, 'utf8').split('\\n')) {\n if (!line.includes('\"usage\"')) {\n continue;\n }\n let j: TranscriptEntry;\n try {\n j = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const u = j.message?.usage;\n const model = j.message?.model;\n if (!u || !model || !j.timestamp) {\n continue;\n }\n\n const ts = new Date(j.timestamp).getTime();\n if (!(ts >= windowStart)) {\n continue;\n }\n\n const local = new Date(ts);\n const dayKey = `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, '0')}-${String(local.getDate()).padStart(2, '0')}`;\n add(bucket(days, dayKey), u);\n add(bucket(models, model), u);\n if (ts >= last24Start) {\n add(bucket(last24, model), u);\n }\n }\n }\n }\n\n return {\n now: now.toISOString(),\n days: [...days.entries()]\n .sort(([a], [b]) => (a < b ? -1 : 1))\n .map(([day, acc]) => ({ day, ...acc })),\n models: toModelRows(models),\n last24: toModelRows(last24),\n };\n}\n","import {\n hookOrPrint,\n parseQuietly,\n printUsageAndExit,\n} from '@v1nvn/agentic-core';\nimport { Command } from 'commander';\n\nimport { render } from './format.js';\nimport { scan } from './scan.js';\n\nconst program = new Command()\n .name('tokens-report')\n .description(\n 'Per-model token usage and cache hit rate from local transcripts',\n )\n .option('--hook', 'emit a UserPromptExpansion block instead of printing');\n\nconst parsed =\n parseQuietly(program, process.argv.slice(2)) ?? printUsageAndExit(program);\nconst { hook } = parsed.opts<{ hook: boolean | undefined }>();\n\nfunction report(): string {\n return render(scan());\n}\n\nawait hookOrPrint(hook ?? false, 'query failed', report);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../core/dist/index.js","../src/format.ts","../src/scan.ts","../src/index.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n//#region src/input.ts\nfunction readAll(stream) {\n\treturn new Promise((resolve, reject) => {\n\t\tlet data = \"\";\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tstream.on(\"end\", () => {\n\t\t\tresolve(data);\n\t\t});\n\t\tstream.on(\"error\", reject);\n\t});\n}\nfunction readStdin(stream = process.stdin) {\n\treturn readAll(stream);\n}\n/** A markdown file's contents, with the CLI's no-such-file error. */\nfunction readMarkdownFile(path) {\n\tif (!statSync(path, { throwIfNoEntry: false })?.isFile()) throw new Error(`no such file: ${path}`);\n\treturn readFileSync(path, \"utf8\");\n}\n//#endregion\n//#region src/hook.ts\n/**\n* Read the Claude Code hook event JSON from stdin; empty or unparseable input\n* yields an empty event, never a thrown error — a hook must always answer.\n*/\nfunction readHookEvent(stream = process.stdin) {\n\treturn readAll(stream).then((data) => {\n\t\ttry {\n\t\t\treturn JSON.parse(data);\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}, () => ({}));\n}\n/**\n* Which transcript lastReply should read, per the hook event: transcript_path\n* when it names a real file, else session_id, else nothing (lastReply then\n* falls back to the newest session for the current project).\n*/\nfunction replyTarget(event) {\n\tif (event.transcript_path !== void 0 && isFile(event.transcript_path)) return event.transcript_path;\n\treturn event.session_id || void 0;\n}\nfunction isFile(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n/**\n* Print the UserPromptExpansion decision for a zero-token command: `block`\n* keeps the command from reaching the model, with the output as the `reason`.\n*/\nfunction emitHookBlock(reason) {\n\tprocess.stdout.write(`${JSON.stringify({\n\t\tdecision: \"block\",\n\t\treason\n\t})}\\n`);\n}\n//#endregion\n//#region src/cli.ts\nfunction parseQuietly(program, args, recover) {\n\ttry {\n\t\tprogram.allowExcessArguments(false).exitOverride().configureOutput({\n\t\t\twriteOut: () => void 0,\n\t\t\twriteErr: () => void 0\n\t\t}).parse([...args], { from: \"user\" });\n\t\treturn program;\n\t} catch (err) {\n\t\treturn recover?.(err);\n\t}\n}\nfunction printUsageAndExit(program) {\n\tconsole.error(program.helpInformation());\n\tprocess.exit(1);\n}\nasync function hookOrPrint(hook, failure, run, stdin = process.stdin) {\n\tif (hook) {\n\t\tlet reason;\n\t\ttry {\n\t\t\treason = await run(await readHookEvent(stdin));\n\t\t} catch (e) {\n\t\t\treason = `${failure}: ${e.message}`;\n\t\t}\n\t\temitHookBlock(reason);\n\t\treturn;\n\t}\n\ttry {\n\t\tconsole.log(await run(void 0));\n\t} catch (e) {\n\t\tconsole.error(e.message);\n\t\tprocess.exit(1);\n\t}\n}\n//#endregion\n//#region src/dev.ts\nvar RELOAD_DEBOUNCE_MS = 150;\nvar IGNORED_SEGMENTS = [\n\t\"/node_modules/\",\n\t\"/dist/\",\n\t\"/.git/\",\n\t\"/coverage/\"\n];\nfunction shouldReload(file) {\n\treturn IGNORED_SEGMENTS.every((seg) => !file.includes(seg));\n}\n/**\n* Debounced file-change reloads, serialized via a promise chain: concurrent\n* saves never overlap, and a mid-reload save still produces a follow-up.\n*/\nfunction watchWithReload(vite, reload) {\n\tlet chain = Promise.resolve();\n\tlet timer;\n\tvite.watcher.on(\"change\", (file) => {\n\t\tif (!shouldReload(file)) return;\n\t\tif (timer) clearTimeout(timer);\n\t\ttimer = setTimeout(() => {\n\t\t\ttimer = void 0;\n\t\t\tchain = chain.then(reload);\n\t\t}, RELOAD_DEBOUNCE_MS);\n\t});\n}\n//#endregion\n//#region src/last-reply.ts\nfunction textBlocks(entry) {\n\tconst content = entry.message?.content;\n\tif (entry.type !== \"assistant\" || !Array.isArray(content)) return;\n\tconst blocks = content.filter((b) => typeof b === \"object\" && b !== null && b.type === \"text\");\n\treturn blocks.length > 0 ? blocks : void 0;\n}\nfunction claudeProjectsDir() {\n\tconst claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), \".claude\");\n\treturn join(claudeDir, \"projects\");\n}\n/**\n* The last assistant text reply from a Claude Code session, reproducing Claude\n* Code's `/copy` byte-for-byte: the last assistant entry that contains a\n* `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n* joined with a blank line, and no trailing newline.\n*\n* @param arg a transcript file, a session UUID under the project dir, or\n* nothing to use the newest session for the current project.\n*/\nfunction lastReply(arg) {\n\tconst proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\"/\", \"-\");\n\tconst projDir = join(claudeProjectsDir(), proj);\n\tlet file;\n\tif (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n\telse if (existsSync(projDir)) {\n\t\tconst newest = readdirSync(projDir).filter((f) => f.endsWith(\".jsonl\")).map((f) => ({\n\t\t\tf,\n\t\t\tmtimeMs: statSync(join(projDir, f)).mtimeMs\n\t\t})).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);\n\t\tif (newest) file = join(projDir, newest.f);\n\t}\n\tif (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);\n\tlet last;\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.includes(\"\\\"assistant\\\"\")) continue;\n\t\tlet entry;\n\t\ttry {\n\t\t\tentry = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst blocks = textBlocks(entry);\n\t\tif (blocks) last = blocks;\n\t}\n\treturn (last ?? []).map((b) => b.text ?? \"\").join(\"\\n\\n\");\n}\n//#endregion\n//#region src/logger.ts\nvar LEVEL_RANK = {\n\tdebug: 10,\n\tinfo: 20,\n\twarn: 30,\n\terror: 40,\n\tsilent: Number.MAX_SAFE_INTEGER\n};\nfunction levelEnabled(active, wanted) {\n\treturn LEVEL_RANK[wanted] >= LEVEL_RANK[active];\n}\nvar LEVEL_LABEL = {\n\tdebug: \"DEBUG\",\n\terror: \"ERROR\",\n\tinfo: \"INFO\",\n\twarn: \"WARN\"\n};\nvar Logger = class {\n\tactiveLevel;\n\tstderr;\n\tconstructor(activeLevel, stderr = (line) => process.stderr.write(`${line}\\n`)) {\n\t\tthis.activeLevel = activeLevel;\n\t\tthis.stderr = stderr;\n\t}\n\tdebug(message) {\n\t\tthis.log(\"debug\", message);\n\t}\n\terror(message) {\n\t\tthis.log(\"error\", message);\n\t}\n\tinfo(message) {\n\t\tthis.log(\"info\", message);\n\t}\n\twarn(message) {\n\t\tthis.log(\"warn\", message);\n\t}\n\tlog(level, message) {\n\t\tif (levelEnabled(this.activeLevel, level)) this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n\t}\n};\n//#endregion\n//#region src/shutdown.ts\n/** Close each closable in order (best-effort), then force-exit. */\nfunction shutdownOnSignals(closables, signals = [\"SIGINT\", \"SIGTERM\"]) {\n\tconst shutdown = () => {\n\t\tclosables.reduce((acc, closable) => acc.then(async () => {\n\t\t\ttry {\n\t\t\t\tawait closable.close();\n\t\t\t} catch {}\n\t\t}), Promise.resolve()).finally(() => {\n\t\t\tprocess.exit(0);\n\t\t});\n\t};\n\tfor (const signal of signals) process.on(signal, shutdown);\n}\n//#endregion\n//#region src/text-format.ts\n/**\n* Plain-text rendering primitives shared by the zai and tokens report\n* renderers. Output targets a monospace terminal / hook-block `reason`, so\n* everything here is fixed-width: padding, block-glyph bars, and compact\n* number formatting.\n*/\nvar MONTHS = [\n\t\"Jan\",\n\t\"Feb\",\n\t\"Mar\",\n\t\"Apr\",\n\t\"May\",\n\t\"Jun\",\n\t\"Jul\",\n\t\"Aug\",\n\t\"Sep\",\n\t\"Oct\",\n\t\"Nov\",\n\t\"Dec\"\n];\nvar EIGHTHS = [\n\t\"\",\n\t\"▏\",\n\t\"▎\",\n\t\"▍\",\n\t\"▌\",\n\t\"▋\",\n\t\"▊\",\n\t\"▉\"\n];\nvar RULE_WIDTH = 68;\nfunction rule() {\n\treturn \"─\".repeat(68);\n}\nfunction pad2(n) {\n\treturn String(n).padStart(2, \"0\");\n}\n/** Local-time `YYYY-MM-DD` key. */\nfunction ymd(date) {\n\treturn `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n}\nfunction fmtTokens(n) {\n\tif (n == null || Number.isNaN(n)) return \"—\";\n\tif (n >= 1e9) return (n / 1e9).toFixed(1) + \"B\";\n\tif (n >= 1e6) return (n / 1e6).toFixed(1) + \"M\";\n\tif (n >= 1e3) return (n / 1e3).toFixed(1) + \"K\";\n\treturn String(n);\n}\nfunction fmtNum(n) {\n\treturn (n || 0).toLocaleString(\"en-US\");\n}\nfunction padR(s, n) {\n\treturn s.length >= n ? s : s + \" \".repeat(n - s.length);\n}\nfunction padL(s, n) {\n\treturn s.length >= n ? s : \" \".repeat(n - s.length) + s;\n}\n/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */\nfunction barField(v, max, width) {\n\tif (!v || v <= 0 || max <= 0) return \" \".repeat(width);\n\tconst scaled = v / max * width;\n\tlet full = Math.floor(scaled);\n\tlet fi = Math.round((scaled - full) * 8);\n\tif (fi === 8) {\n\t\tfull += 1;\n\t\tfi = 0;\n\t}\n\tif (full === 0 && fi === 0) fi = 1;\n\tlet s = \"█\".repeat(Math.min(full, width));\n\tif (full < width && fi > 0) s += EIGHTHS[fi];\n\tif (s.length < width) s += \" \".repeat(width - s.length);\n\treturn s.slice(0, width);\n}\n/** Filled/empty meter: █ for used, ░ for remaining. */\nfunction meter(pct, width) {\n\tlet filled = Math.round((pct || 0) / 100 * width);\n\tfilled = Math.max(0, Math.min(width, filled));\n\treturn \"█\".repeat(filled) + \"░\".repeat(width - filled);\n}\n//#endregion\nexport { Logger, MONTHS, RULE_WIDTH, barField, claudeProjectsDir, emitHookBlock, fmtNum, fmtTokens, hookOrPrint, isFile, lastReply, levelEnabled, meter, pad2, padL, padR, parseQuietly, printUsageAndExit, readAll, readHookEvent, readMarkdownFile, readStdin, replyTarget, rule, shutdownOnSignals, watchWithReload, ymd };\n\n//# sourceMappingURL=index.js.map","/**\n * Plain-text token-usage renderer.\n *\n * Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on\n * markdown. Alignment comes from fixed-width columns and unicode block glyphs.\n * Input is the aggregate JSON produced by scan.ts.\n */\n\nimport {\n barField,\n fmtNum,\n fmtTokens,\n MONTHS,\n pad2,\n padL,\n padR,\n rule,\n RULE_WIDTH,\n ymd,\n} from '@v1nvn/agentic-core';\n\nimport type { DayRow, ModelRow, ScanResult, UsageAcc } from './scan.js';\n\n/** cacheRead / modeled context; input_tokens is uncached input only. */\nexport function hitRate({\n input = 0,\n cacheRead = 0,\n cacheCreation = 0,\n}: Partial<UsageAcc> = {}): number {\n const denom = input + cacheRead + cacheCreation;\n return denom > 0 ? (cacheRead / denom) * 100 : 0;\n}\n\nfunction totalTokens(a: UsageAcc): number {\n return (\n (a.input || 0) +\n (a.output || 0) +\n (a.cacheRead || 0) +\n (a.cacheCreation || 0)\n );\n}\n\n/** '2026-08-15' → 'Aug 15'. */\nfunction dayLabel(day: string): string {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(day);\n return m ? `${MONTHS[+m[2] - 1]} ${m[3]}` : day;\n}\n\nfunction fmtClock(date: Date): string {\n return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;\n}\n\nexport function render(\n scanResult: ScanResult,\n { now = new Date() } = {},\n): string {\n const out: string[] = [];\n\n const allRows: ModelRow[] = scanResult.last24;\n const rows = allRows.filter(r => totalTokens(r) > 0); // <synthetic> etc. carry no tokens\n const sum = rows.reduce<UsageAcc>(\n (a, r) => {\n a.input += r.input;\n a.output += r.output;\n a.cacheRead += r.cacheRead;\n a.cacheCreation += r.cacheCreation;\n a.calls += r.calls;\n return a;\n },\n { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, calls: 0 },\n );\n const pctHit = Math.round(hitRate(sum));\n\n const left = ' Token usage · transcripts';\n const winStart = new Date(now.getTime() - 24 * 3600 * 1000);\n const win = `${dayLabel(ymd(winStart))} ${fmtClock(winStart)} → ${dayLabel(ymd(now))} ${fmtClock(now)} · 24h`;\n out.push(rule());\n out.push(left + padL(win, RULE_WIDTH - left.length));\n out.push(rule());\n\n out.push('');\n out.push(\n ` ${fmtTokens(totalTokens(sum))} tokens across ${fmtNum(sum.calls)} model calls — ${pctHit}% cache hit rate.`,\n );\n\n out.push('');\n out.push(' Model mix · last 24h ' + '─'.repeat(Math.max(0, RULE_WIDTH - 22)));\n if (rows.length === 0) {\n out.push(' (no usage recorded in the last 24 hours)');\n }\n for (const r of rows) {\n const pct = Math.round(hitRate(r));\n out.push(\n ` ${padR(r.model, 14)}${padL(fmtTokens(r.input), 8)} in · ${padL(fmtTokens(r.output), 8)} out ·` +\n ` ${padL(fmtTokens(r.cacheRead), 8)} read · ${padL(fmtTokens(r.cacheCreation), 8)} created ${padL(`${pct}%`, 4)} ${barField(pct, 100, 14)}`,\n );\n }\n\n // The bucket holding the window-start day covers only part of that calendar\n // day — drop it (unless the window began at midnight, which scan would have\n // bucketed as a full day).\n let days: DayRow[] = scanResult.days;\n const firstDay = ymd(new Date(now.getTime() - 7 * 24 * 3600 * 1000));\n if (\n days.length &&\n days[0].day === firstDay &&\n firstDay !== days[days.length - 1].day\n ) {\n days = days.slice(1);\n }\n out.push('');\n out.push(' Daily · last 7 days ' + '─'.repeat(Math.max(0, RULE_WIDTH - 21)));\n const maxDay = Math.max(0, ...days.map(totalTokens));\n for (const d of [...days].reverse()) {\n const pct = Math.round(hitRate(d));\n out.push(\n ` ${dayLabel(d.day)} ${padL(fmtTokens(totalTokens(d)), 8)} ${barField(totalTokens(d), maxDay, 24)} ${padL(`${pct}%`, 4)}`,\n );\n }\n if (days.length === 0) {\n out.push(' (no usage recorded in the last 7 days)');\n }\n\n out.push('');\n out.push(\n ` Covers every profile writing to ~/.claude/projects — hit rate = read / (in + read + created).`,\n );\n out.push(rule());\n return out.join('\\n');\n}\n","/**\n * Transcript token scanner.\n *\n * Claude Code persists every assistant message's `usage` block to session\n * transcripts at $CLAUDE_DIR/projects/<project-dir>/<session>.jsonl\n * (default ~/.claude) — for every profile (default claude, claudez, …),\n * interactive and headless alike. This\n * scans those files and aggregates token usage per model and per local day:\n * input (uncached), output, cacheRead, cacheCreation, call count.\n */\n\nimport { claudeProjectsDir, ymd } from '@v1nvn/agentic-core';\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst DAYS = 7;\n\nexport interface UsageAcc {\n cacheCreation: number;\n cacheRead: number;\n calls: number;\n input: number;\n output: number;\n}\n\nexport interface DayRow extends UsageAcc {\n day: string;\n}\n\nexport interface ModelRow extends UsageAcc {\n model: string;\n}\n\nexport interface ScanResult {\n days: DayRow[];\n last24: ModelRow[];\n models: ModelRow[];\n now: string;\n}\n\ninterface UsageBlock {\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n}\n\ninterface TranscriptEntry {\n message?: { model?: string; usage?: UsageBlock };\n timestamp?: string;\n}\n\nfunction zero(): UsageAcc {\n return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, calls: 0 };\n}\n\nfunction add(acc: UsageAcc, u: UsageBlock, n = 1): void {\n acc.input += n * (u.input_tokens ?? 0);\n acc.output += n * (u.output_tokens ?? 0);\n acc.cacheRead += n * (u.cache_read_input_tokens ?? 0);\n acc.cacheCreation += n * (u.cache_creation_input_tokens ?? 0);\n acc.calls += n;\n}\n\nfunction byTotalDesc(a: UsageAcc, b: UsageAcc): number {\n return (\n b.input +\n b.output +\n b.cacheRead +\n b.cacheCreation -\n (a.input + a.output + a.cacheRead + a.cacheCreation)\n );\n}\n\n/** Get-or-create the map entry, so callers never hold a missing accumulator. */\nfunction bucket<K>(map: Map<K, UsageAcc>, key: K): UsageAcc {\n let acc = map.get(key);\n if (acc === undefined) {\n acc = zero();\n map.set(key, acc);\n }\n return acc;\n}\n\nfunction toModelRows(m: Map<string, UsageAcc>): ModelRow[] {\n return [...m.entries()]\n .map(([model, acc]) => ({ model, ...acc }))\n .sort(byTotalDesc);\n}\n\nexport function scan({\n projectsDir,\n now = new Date(),\n}: { now?: Date; projectsDir?: string } = {}): ScanResult {\n const dir = projectsDir ?? claudeProjectsDir();\n if (!existsSync(dir)) {\n throw new Error(`no transcripts directory at ${dir}`);\n }\n\n const windowStart = now.getTime() - DAYS * 24 * 3600 * 1000;\n const last24Start = now.getTime() - 24 * 3600 * 1000;\n\n const days = new Map<string, UsageAcc>(); // 'YYYY-MM-DD' → acc\n const last24 = new Map<string, UsageAcc>(); // model → acc\n const models = new Map<string, UsageAcc>(); // model → acc (whole 7d window, for the model mix)\n\n const projectDirs = readdirSync(dir)\n .map(d => join(dir, d))\n .filter(d => statSync(d).isDirectory());\n\n for (const pdir of projectDirs) {\n for (const f of readdirSync(pdir)) {\n if (!f.endsWith('.jsonl')) {\n continue;\n }\n const fp = join(pdir, f);\n if (statSync(fp).mtimeMs < windowStart) {\n continue;\n }\n\n for (const line of readFileSync(fp, 'utf8').split('\\n')) {\n if (!line.includes('\"usage\"')) {\n continue;\n }\n let j: TranscriptEntry;\n try {\n j = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const u = j.message?.usage;\n const model = j.message?.model;\n if (!u || !model || !j.timestamp) {\n continue;\n }\n\n const ts = new Date(j.timestamp).getTime();\n if (!(ts >= windowStart)) {\n continue;\n }\n\n const local = new Date(ts);\n const dayKey = ymd(local);\n add(bucket(days, dayKey), u);\n add(bucket(models, model), u);\n if (ts >= last24Start) {\n add(bucket(last24, model), u);\n }\n }\n }\n }\n\n return {\n now: now.toISOString(),\n days: [...days.entries()]\n .sort(([a], [b]) => (a < b ? -1 : 1))\n .map(([day, acc]) => ({ day, ...acc })),\n models: toModelRows(models),\n last24: toModelRows(last24),\n };\n}\n","import {\n hookOrPrint,\n parseQuietly,\n printUsageAndExit,\n} from '@v1nvn/agentic-core';\nimport { Command } from 'commander';\n\nimport { render } from './format.js';\nimport { scan } from './scan.js';\n\nconst program = new Command()\n .name('tokens-report')\n .description(\n 'Per-model token usage and cache hit rate from local transcripts',\n )\n .option('--hook', 'emit a UserPromptExpansion block instead of printing');\n\nconst parsed =\n parseQuietly(program, process.argv.slice(2)) ?? printUsageAndExit(program);\nconst { hook } = parsed.opts<{ hook: boolean | undefined }>();\n\nfunction report(): string {\n return render(scan());\n}\n\nawait hookOrPrint(hook ?? false, 'query failed', report);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v1nvn/tokens",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Per-model token usage and cache hit rate from local Claude Code transcripts — the tokens-report CLI behind the tokens plugin.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,10 @@
17
17
  "cache",
18
18
  "transcripts"
19
19
  ],
20
- "author": "v1nvn",
20
+ "author": {
21
+ "name": "v1nvn",
22
+ "email": "v1n@outlook.com"
23
+ },
21
24
  "license": "MIT",
22
25
  "repository": {
23
26
  "type": "git",
@@ -34,7 +37,7 @@
34
37
  "node": ">=22"
35
38
  },
36
39
  "dependencies": {
37
- "@v1nvn/agentic-core": "^0.25.0",
40
+ "@v1nvn/agentic-core": "^0.27.0",
38
41
  "commander": "^15.0.0"
39
42
  },
40
43
  "devDependencies": {