claude-rpc 0.6.1 → 0.6.2

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.6.1",
3
+ "version": "0.6.2",
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/format.js CHANGED
@@ -140,6 +140,37 @@ function fmtHour(h) {
140
140
  return `${hh}:00`;
141
141
  }
142
142
 
143
+ // Detect a cwd that would leak the user's OS username if rendered on
144
+ // Discord. Examples:
145
+ // /home/lucas → basename matches $USER → leak
146
+ // C:\Users\lucas → equals $USERPROFILE → leak
147
+ // /Users/lucas/projects/x → basename "x" ≠ user → fine
148
+ // On a real privacy-sensitive cwd (the home dir itself, with no project
149
+ // scoping), buildVars falls back to `appName` so the card reads
150
+ // "Idle in Claude Code" instead of "Idle in lucas".
151
+ function looksLikeUsernameLeak(cwd) {
152
+ if (!cwd) return false;
153
+ // Check both POSIX and Windows env vars unconditionally — a test or
154
+ // edge case might have one without the other, and over-suppressing
155
+ // the leak side is the safe direction.
156
+ const homes = [process.env.HOME, process.env.USERPROFILE].filter(Boolean);
157
+ const users = [process.env.USER, process.env.USERNAME].filter(Boolean);
158
+ // Normalize path separators so Windows-style cwds work on POSIX
159
+ // basename (which doesn't split on '\').
160
+ const norm = (p) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
161
+ const cwdN = norm(cwd);
162
+ for (const home of homes) {
163
+ if (cwdN === norm(home)) return true;
164
+ }
165
+ if (users.length) {
166
+ const base = cwdN.split('/').pop() || '';
167
+ for (const u of users) {
168
+ if (base === u.toLowerCase()) return true;
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+
143
174
  // Trim "C:\repo\src\app\page.tsx" → "src/app/page.tsx" (3 trailing segments).
144
175
  function prettyFilePath(p) {
145
176
  if (!p) return '';
@@ -156,7 +187,14 @@ export function buildVars(state, config, aggregate) {
156
187
  // {tokens} / {tokensFmt} now means the grand total (in + out + cache).
157
188
  const sessionTokens = sessionReal + sessionCacheRead + sessionCacheWrite;
158
189
  const duration = state.sessionStart ? Date.now() - state.sessionStart : 0;
159
- const projectPretty = humanProject(state.cwd) || 'Claude Code';
190
+ // Privacy: when cwd is the user's home dir (or its basename matches the
191
+ // OS username), don't render it. "Idle in lucas" on Discord is a username
192
+ // leak to anyone viewing the card. Fall back to the configured app name.
193
+ const cwdIsLeaky = looksLikeUsernameLeak(state.cwd);
194
+ const safeCwd = cwdIsLeaky ? '' : (state.cwd || '');
195
+ const projectPretty = cwdIsLeaky
196
+ ? (config?.appName || 'Claude Code')
197
+ : (humanProject(state.cwd) || 'Claude Code');
160
198
  const currentToolPretty = humanTool(state.currentTool);
161
199
  const modelPretty = humanModel(state.model);
162
200
 
@@ -298,7 +336,7 @@ export function buildVars(state, config, aggregate) {
298
336
  statusIcon: config?.statusIcons?.[state.status] || state.status || 'idle',
299
337
  project: projectPretty,
300
338
  projectPretty,
301
- cwd: state.cwd || '',
339
+ cwd: safeCwd,
302
340
  model: state.model || 'claude',
303
341
  modelPretty,
304
342
  messages,
@@ -527,6 +565,27 @@ export function fillTemplate(tpl, vars) {
527
565
  return tpl.replace(/\{(\w+)\}/g, (_, key) => (key in vars ? String(vars[key]) : `{${key}}`));
528
566
  }
529
567
 
568
+ // Helper used by every "go stale" branch in applyIdle. Wipes the current-
569
+ // activity slots so rotation frames can't render yesterday's project /
570
+ // file / tool names, and zeroes the session counters that are tied to
571
+ // the now-dead session.
572
+ function staleWipe(state) {
573
+ return {
574
+ ...state,
575
+ status: 'stale',
576
+ currentTool: null,
577
+ currentFile: null,
578
+ sessionStart: null,
579
+ cwd: '',
580
+ messages: 0,
581
+ tools: 0,
582
+ filesOpened: [],
583
+ filesEdited: [],
584
+ filesRead: [],
585
+ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
586
+ };
587
+ }
588
+
530
589
  // Apply idle/stale transitions based on lastActivity age. Used by both daemon
531
590
  // and the `preview` CLI command so they agree.
532
591
  //
@@ -546,22 +605,7 @@ export function applyIdle(state, cfg = {}) {
546
605
  // Authoritative close signal from the SessionEnd hook — trust it instead
547
606
  // of waiting on staleSessionMin. Any other hook clears the flag, so a
548
607
  // sibling session staying alive will reset us out of this branch.
549
- if (state.claudeClosed) {
550
- return {
551
- ...state,
552
- status: 'stale',
553
- currentTool: null,
554
- currentFile: null,
555
- sessionStart: null,
556
- cwd: '',
557
- messages: 0,
558
- tools: 0,
559
- filesOpened: [],
560
- filesEdited: [],
561
- filesRead: [],
562
- tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
563
- };
564
- }
608
+ if (state.claudeClosed) return staleWipe(state);
565
609
 
566
610
  // Notification is a brief status — hold it for ~8s after the hook fires,
567
611
  // then fall through to normal idle/stale processing.
@@ -578,22 +622,7 @@ export function applyIdle(state, cfg = {}) {
578
622
  const liveAgeMs = mostRecentLiveMs ? now - mostRecentLiveMs : Infinity;
579
623
 
580
624
  // Truly dormant: no live transcripts AND local state is old → stale.
581
- if (ageMs > staleMs && liveAgeMs > staleMs) {
582
- return {
583
- ...state,
584
- status: 'stale',
585
- currentTool: null,
586
- currentFile: null,
587
- sessionStart: null,
588
- cwd: '',
589
- messages: 0,
590
- tools: 0,
591
- filesOpened: [],
592
- filesEdited: [],
593
- filesRead: [],
594
- tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
595
- };
596
- }
625
+ if (ageMs > staleMs && liveAgeMs > staleMs) return staleWipe(state);
597
626
 
598
627
  // Local state is stale but a live transcript exists somewhere on disk.
599
628
  // Borrow the most-recent live session as our "active" context, since the
@@ -619,11 +648,25 @@ export function applyIdle(state, cfg = {}) {
619
648
  }
620
649
 
621
650
  // Local state is fresh.
622
- if (state.status === 'idle') return state;
651
+ if (state.status === 'idle') {
652
+ // Fast-path stale: if there are NO transcripts being written anywhere
653
+ // on disk, Claude Code isn't running. SessionEnd may not have fired
654
+ // (force-quit, OS sleep, crash). Going stale here clears Discord
655
+ // within ~90-120s of close instead of waiting the full staleMs (5min)
656
+ // — keeps the user's cwd off the card when they're away from their
657
+ // machine. The 5min legacy fallback below still catches the case
658
+ // where transcript mtime is fresh but the hook channel is silent.
659
+ if (liveSessions.length === 0) return staleWipe(state);
660
+ return state;
661
+ }
623
662
  if (ageMs > idleMs) {
624
663
  // Hook channel is quiet, but a live transcript was modified recently?
625
664
  // Keep "working" instead of dropping to "idle".
626
665
  if (liveAgeMs <= idleMs) return state;
666
+ // Hooks quiet AND no live transcripts → Claude is closed, not paused.
667
+ // Skip idle, go straight to stale. Same privacy reasoning as the
668
+ // idle-state fast-path above.
669
+ if (liveSessions.length === 0) return staleWipe(state);
627
670
  // Going idle — wipe "current activity" indicators so rotation frames
628
671
  // gated on filesEdited / currentFile / currentTool stop showing stale
629
672
  // active-session data. Keep the session counters (messages/tools/tokens)
package/src/version.js CHANGED
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { ROOT } from './paths.js';
13
13
 
14
- const BAKED = '0.6.1';
14
+ const BAKED = '0.6.2';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {