clauddy 1.28.2 → 1.28.3

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 (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/usage.js +57 -26
package/README.md CHANGED
@@ -4,7 +4,7 @@ A cute pixel-art desktop pet for macOS that tracks your Claude Code usage — mi
4
4
 
5
5
  <p align="center">
6
6
 
7
- https://github.com/user-attachments/assets/dbe00d9a-b49c-48ea-941c-76c517dec358
7
+ https://github.com/user-attachments/assets/be8d339c-ac69-4980-96a7-8e286f8b080f
8
8
 
9
9
  <em>A little terracotta creature that lives in the corner of your screen, eats your tokens, and naps when you're idle.</em>
10
10
  </p>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.28.2",
4
+ "version": "1.28.3",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {
package/usage.js CHANGED
@@ -162,39 +162,70 @@ function walkJsonl(dir, out, cutoffMs) {
162
162
  }
163
163
  }
164
164
 
165
+ function ingestLine(entries, line) {
166
+ // most lines are tool results and prompts: skip them without a parse
167
+ if (!line.includes('"usage"')) return
168
+ let obj
169
+ try {
170
+ obj = JSON.parse(line)
171
+ } catch {
172
+ return
173
+ }
174
+ if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) return
175
+ if (obj.message.model === '<synthetic>') return // Claude Code internal messages
176
+ const ts = Date.parse(obj.timestamp || obj.message?.timestamp || 0)
177
+ if (!ts) return
178
+ entries.push({
179
+ ts,
180
+ model: obj.message.model,
181
+ tokens: tokensOf({ usage: obj.message.usage }),
182
+ key: `${obj.message.id || ''}:${obj.requestId || obj.uuid || ''}`,
183
+ })
184
+ }
185
+
186
+ const CHUNK = 8 * 1024 * 1024
187
+
188
+ // Session logs only ever grow, and a long one passes 500 MB: a poll reads just
189
+ // the bytes appended since the last one, in chunks, so the main process never
190
+ // stalls on a full re-read. A line still being written waits in `rest`.
165
191
  function parseFile(full, st) {
166
- const cached = fileCache.get(full)
167
- if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
168
- return cached.entries
192
+ let cached = fileCache.get(full)
193
+ // shrunk or replaced: start over
194
+ if (!cached || st.size < cached.offset) {
195
+ cached = { offset: 0, rest: Buffer.alloc(0), entries: [] }
196
+ fileCache.set(full, cached)
169
197
  }
170
- const entries = []
171
- let raw
198
+ if (st.size <= cached.offset) return cached.entries
199
+ let fd
172
200
  try {
173
- raw = fs.readFileSync(full, 'utf8')
201
+ fd = fs.openSync(full, 'r')
174
202
  } catch {
175
- return entries
203
+ return cached.entries
176
204
  }
177
- for (const line of raw.split('\n')) {
178
- if (!line) continue
179
- let obj
180
- try {
181
- obj = JSON.parse(line)
182
- } catch {
183
- continue
205
+ try {
206
+ while (cached.offset < st.size) {
207
+ const chunk = Buffer.alloc(Math.min(CHUNK, st.size - cached.offset))
208
+ const n = fs.readSync(fd, chunk, 0, chunk.length, cached.offset)
209
+ if (n <= 0) break
210
+ cached.offset += n
211
+ const buf = Buffer.concat([cached.rest, chunk.subarray(0, n)])
212
+ // cut at the last newline in bytes, so a split UTF-8 character survives
213
+ const end = buf.lastIndexOf(0x0a)
214
+ if (end < 0) {
215
+ cached.rest = buf
216
+ continue
217
+ }
218
+ cached.rest = Buffer.from(buf.subarray(end + 1))
219
+ for (const line of buf.toString('utf8', 0, end).split('\n')) {
220
+ if (line) ingestLine(cached.entries, line)
221
+ }
184
222
  }
185
- if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) continue
186
- if (obj.message.model === '<synthetic>') continue // Claude Code internal messages
187
- const ts = Date.parse(obj.timestamp || obj.message?.timestamp || 0)
188
- if (!ts) continue
189
- entries.push({
190
- ts,
191
- model: obj.message.model,
192
- tokens: tokensOf({ usage: obj.message.usage }),
193
- key: `${obj.message.id || ''}:${obj.requestId || obj.uuid || ''}`,
194
- })
223
+ } catch {
224
+ // unreadable mid-way: keep what we have, retry from the offset next poll
225
+ } finally {
226
+ fs.closeSync(fd)
195
227
  }
196
- fileCache.set(full, { mtimeMs: st.mtimeMs, size: st.size, entries })
197
- return entries
228
+ return cached.entries
198
229
  }
199
230
 
200
231
  const DAYS = 30