clanker-stats 0.3.2 → 0.4.1

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.
Files changed (2) hide show
  1. package/cli.js +35 -22
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFile, writeFile } from "node:fs/promises"
4
+ import { createReadStream } from "node:fs"
5
+ import { createInterface } from "node:readline"
4
6
  import { homedir } from "node:os"
5
7
  import { join } from "node:path"
6
8
  import { execFileSync } from "node:child_process"
@@ -41,31 +43,39 @@ async function collectClaude() {
41
43
  async function collectCodex() {
42
44
  const counts = new Map()
43
45
  const dir = join(home, ".codex", "sessions")
44
- for (const path of await fg("*/*/*/*rollout-*.jsonl", { cwd: dir })) {
45
- const text = await readFile(join(dir, path), "utf-8")
46
- let prevCumulative = -1
47
- for (const line of text.split("\n")) {
48
- if (!line.includes('"token_count"')) continue
49
- try {
50
- const obj = JSON.parse(line)
51
- if (obj.payload?.type !== "token_count") continue
52
- const cumulative = obj.payload.info?.total_token_usage?.total_tokens ?? -1
53
- if (cumulative === prevCumulative) continue
54
- prevCumulative = cumulative
55
- const tokens = obj.payload.info?.last_token_usage?.total_tokens
56
- if (tokens > 0 && obj.timestamp) add(counts, isoToDate(obj.timestamp), tokens)
57
- } catch {}
58
- }
46
+ for (const path of await fg("**/*.jsonl", { cwd: dir })) {
47
+ try {
48
+ const rl = createInterface({ input: createReadStream(join(dir, path)), crlfDelay: Infinity })
49
+ let prevCumulative = null
50
+ for await (const line of rl) {
51
+ if (!line.includes('"token_count"')) continue
52
+ try {
53
+ const obj = JSON.parse(line)
54
+ if (obj.payload?.type !== "token_count") continue
55
+ const cumTotal = obj.payload.info?.total_token_usage?.total_tokens
56
+ if (cumTotal != null && cumTotal === prevCumulative) continue
57
+ let tokens = obj.payload.info?.last_token_usage?.total_tokens
58
+ if (!tokens && cumTotal != null && prevCumulative != null) tokens = cumTotal - prevCumulative
59
+ if (cumTotal != null) prevCumulative = cumTotal
60
+ if (tokens > 0 && obj.timestamp) add(counts, isoToDate(obj.timestamp), tokens)
61
+ } catch {}
62
+ }
63
+ } catch {}
59
64
  }
60
65
  return counts
61
66
  }
62
67
 
63
68
  async function collectOpenCode() {
64
69
  const counts = new Map()
65
- const dir = join(home, ".local", "share", "opencode", "storage", "message")
66
- for (const path of await fg("ses_*/msg_*.json", { cwd: dir })) {
70
+ const dirs = [
71
+ join(home, ".local", "share", "opencode", "storage", "message"),
72
+ ...(process.platform === "darwin" ? [join(home, "Library", "Application Support", "opencode", "storage", "message")] : []),
73
+ ]
74
+ for (const dir of dirs)
75
+ for (const path of await fg("ses_*/msg_*.json", { cwd: dir, suppressErrors: true })) {
67
76
  try {
68
77
  const obj = JSON.parse(await readFile(join(dir, path), "utf-8"))
78
+ if (obj.role !== "assistant") continue
69
79
  const t = obj.tokens
70
80
  if (!t) continue
71
81
  const tokens = (t.input || 0) + (t.output || 0) + (t.reasoning || 0) +
@@ -79,12 +89,15 @@ async function collectOpenCode() {
79
89
  async function collectGemini() {
80
90
  const counts = new Map()
81
91
  const dir = join(home, ".gemini", "tmp")
82
- for (const path of await fg("*/chats/session-*.json", { cwd: dir })) {
92
+ for (const path of await fg("*/chats/*.json", { cwd: dir })) {
83
93
  try {
84
94
  const obj = JSON.parse(await readFile(join(dir, path), "utf-8"))
85
95
  if (!obj.messages) continue
86
96
  for (const msg of obj.messages) {
87
- if (msg.tokens?.total && msg.timestamp) add(counts, isoToDate(msg.timestamp), msg.tokens.total)
97
+ if (msg.type !== "gemini") continue
98
+ const t = msg.tokens
99
+ const tk = t?.total || ((t?.input || 0) + (t?.tool || 0) + (t?.output || 0) + (t?.thoughts || 0) + (t?.cached || 0))
100
+ if (tk > 0 && msg.timestamp) add(counts, isoToDate(msg.timestamp), tk)
88
101
  }
89
102
  } catch {}
90
103
  }
@@ -114,14 +127,15 @@ async function collectAmp() {
114
127
  async function collectPi() {
115
128
  const counts = new Map()
116
129
  const dir = join(home, ".pi", "agent", "sessions")
117
- for (const path of await fg("*/*.jsonl", { cwd: dir })) {
130
+ for (const path of await fg("**/*.jsonl", { cwd: dir })) {
118
131
  const text = await readFile(join(dir, path), "utf-8")
119
132
  for (const line of text.split("\n")) {
120
133
  if (!line.includes('"usage"')) continue
121
134
  try {
122
135
  const obj = JSON.parse(line)
123
136
  if (obj.type !== "message") continue
124
- const tokens = obj.message?.usage?.totalTokens
137
+ const u = obj.message?.usage || obj.usage
138
+ const tokens = u?.totalTokens || ((u?.input || 0) + (u?.output || 0) + (u?.reasoning || 0) + (u?.cacheRead || 0) + (u?.cacheWrite || 0))
125
139
  if (tokens > 0 && obj.timestamp) add(counts, isoToDate(obj.timestamp), tokens)
126
140
  } catch {}
127
141
  }
@@ -290,7 +304,6 @@ function openPath(target) {
290
304
  async function main() {
291
305
  const share = process.argv.includes("--share")
292
306
  const results = []
293
-
294
307
  for (const tool of tools) {
295
308
  try {
296
309
  const counts = await tool.collect()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clanker-stats",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "See how many tokens you've burned across AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {