claude-rpc 0.3.10 → 0.3.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-rpc",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Discord Rich Presence for Claude Code — live model, project, tokens, and lifetime stats driven by Claude Code's hook system.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/daemon.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, watch, appendFileS
3
3
  import { Client } from '@xhayper/discord-rpc';
4
4
  import { readState } from './state.js';
5
5
  import { buildVars, fillTemplate, framePasses, applyIdle } from './format.js';
6
- import { scan, readAggregate, findLiveSessions } from './scanner.js';
6
+ import { scan, readAggregate, findLiveSessions, readSessionTokens } from './scanner.js';
7
7
  import { detectGithubUrl } from './git.js';
8
8
  import { CONFIG_PATH, STATE_PATH, PID_PATH, LOG_PATH, STATE_DIR, AGGREGATE_PATH } from './paths.js';
9
9
 
@@ -82,6 +82,23 @@ function buildActivity(opts = {}) {
82
82
  // see ongoing transcript activity, not just this daemon's hook state.
83
83
  state.liveSessions = opts.liveSessions || liveSessions;
84
84
  state = applyIdle(state, config);
85
+
86
+ // Pull live session tokens from the transcript file. Claude Code's hook
87
+ // payloads don't include usage data, so state.tokens from PostToolUse
88
+ // events is always {0,0,0,0}. The transcript is the only running source
89
+ // of truth — readSessionTokens is mtime-cached, so this is cheap unless
90
+ // the session is actively writing.
91
+ if (state.cwd && state.status !== 'stale') {
92
+ const cwdLower = state.cwd.toLowerCase();
93
+ const match = (state.liveSessions || []).find(s =>
94
+ (s.cwd || '').toLowerCase() === cwdLower
95
+ );
96
+ if (match) {
97
+ const t = readSessionTokens(match.path);
98
+ if (t) state.tokens = t;
99
+ }
100
+ }
101
+
85
102
  const vars = buildVars(state, config, opts.aggregate || aggregate);
86
103
  const p = config.presence || {};
87
104
 
package/src/scanner.js CHANGED
@@ -350,6 +350,47 @@ function readTranscriptCwd(path, mtimeMs) {
350
350
  return cwd;
351
351
  }
352
352
 
353
+ // Per-transcript token cache. Reading a multi-MB .jsonl on every push tick
354
+ // (4s) would be wasteful, so we only re-parse when the file's mtime has
355
+ // advanced since the last read.
356
+ const sessionTokenCache = new Map(); // path → { mtime, tokens }
357
+
358
+ // Sum input/output/cache tokens from a single transcript JSONL.
359
+ //
360
+ // We need this because Claude Code's hook payloads don't carry usage data —
361
+ // tokens are an assistant-message field, not a tool-call field, so PostToolUse
362
+ // hooks fire with no `usage` block to capture. The live transcript is the
363
+ // only source of truth for the current session's running token count.
364
+ //
365
+ // Returns null when the file can't be read; { input, output, cacheRead,
366
+ // cacheWrite } otherwise. Cached by mtime — repeat calls with no file
367
+ // activity are O(1).
368
+ export function readSessionTokens(path) {
369
+ let st;
370
+ try { st = statSync(path); } catch { return null; }
371
+ const cached = sessionTokenCache.get(path);
372
+ if (cached && cached.mtime === st.mtimeMs) return cached.tokens;
373
+
374
+ const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
375
+ try {
376
+ const raw = readFileSync(path, 'utf8');
377
+ for (const line of raw.split('\n')) {
378
+ if (!line) continue;
379
+ const r = safeJson(line);
380
+ if (!r || r.type !== 'assistant') continue;
381
+ const u = r.message?.usage;
382
+ if (!u) continue;
383
+ tokens.input += u.input_tokens || 0;
384
+ tokens.output += u.output_tokens || 0;
385
+ tokens.cacheRead += u.cache_read_input_tokens || 0;
386
+ tokens.cacheWrite += u.cache_creation_input_tokens || 0;
387
+ }
388
+ } catch { return null; }
389
+
390
+ sessionTokenCache.set(path, { mtime: st.mtimeMs, tokens });
391
+ return tokens;
392
+ }
393
+
353
394
  // Detect live sessions by transcript mtime. Returns array of { path, project, cwd, mtime, ageSec }.
354
395
  // A session is "live" if its .jsonl was modified within thresholdMs.
355
396
  export function findLiveSessions({ projectsDir = CLAUDE_PROJECTS, thresholdMs = 90_000 } = {}) {