claude-rpc 0.6.0 → 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/README.md CHANGED
@@ -8,36 +8,39 @@
8
8
  # claude-rpc
9
9
 
10
10
  **Discord Rich Presence for [Claude Code](https://claude.com/claude-code).**
11
- Your model, project, current tool, tokens, and lifetime stats — live in your Discord profile. Driven by Claude Code's hooks. Zero polling, zero overhead between sessions.
11
+ Your live model, project, current tool, tokens, and lifetime stats — in your Discord profile. Driven by the hooks Claude Code already fires. Zero polling between sessions.
12
12
 
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
14
14
  [![Node 18+](https://img.shields.io/badge/node-%3E%3D18-43853d.svg?logo=node.js&logoColor=white)](https://nodejs.org)
15
15
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-hooks-d97757.svg)](https://claude.com/claude-code)
16
16
  [![Discord RPC](https://img.shields.io/badge/Discord-RPC-5865F2.svg?logo=discord&logoColor=white)](https://discord.com/developers/docs/topics/rpc)
17
+ [![Release](https://img.shields.io/github/v/release/rar-file/claude-rpc?color=4c1)](https://github.com/rar-file/claude-rpc/releases/latest)
17
18
 
18
19
  </div>
19
20
 
20
21
  ---
21
22
 
22
23
  <div align="center">
23
- <img src="docs/demo.gif" width="560" alt="Discord Rich Presence card: Claude Code, working in claude-rpc on Opus 4.7" />
24
+ <img src="docs/demo.gif" width="560" alt="Discord Rich Presence card showing Claude Code working in claude-rpc on Opus 4.7" />
24
25
  </div>
25
26
 
26
- ## Install
27
+ A small Node daemon that takes the lifecycle events Claude Code already fires and pipes them into the Discord rich-presence card on your profile. Your friends see what you're building; your future self gets lifetime stats. Built solo, on weekends.
27
28
 
28
- **Windows** (no Node required) — [grab the latest portable exe](https://github.com/rar-file/claude-rpc/releases/latest):
29
+ ## install
30
+
31
+ **Windows (no Node required)** — [grab the portable exe from the latest release](https://github.com/rar-file/claude-rpc/releases/latest):
29
32
 
30
33
  ```sh
31
34
  claude-rpc setup
32
35
  claude-rpc start
33
36
  ```
34
37
 
35
- That's it. Open Claude Code in any project — the daemon picks it up within a second.
38
+ That's the whole pitch. Open Claude Code in any project — the daemon picks it up within a second. Something looks wrong? `claude-rpc doctor`.
36
39
 
37
- The Discord *desktop* app must be running (the browser client doesn't expose the local IPC). Something looks wrong? `claude-rpc doctor`.
40
+ The Discord *desktop* app must be running. The browser client doesn't expose the local IPC bridge that Rich Presence uses.
38
41
 
39
42
  <details>
40
- <summary><b>Other platforms / from source</b></summary>
43
+ <summary><b>other platforms / from source</b></summary>
41
44
 
42
45
  ```sh
43
46
  git clone https://github.com/rar-file/claude-rpc.git
@@ -47,99 +50,77 @@ node ./src/cli.js setup
47
50
  node ./src/cli.js start
48
51
  ```
49
52
 
50
- Or `npm install -g claude-rpc` for the global bin.
53
+ Or `npm install -g claude-rpc` for the global bin. Both modes survive `npm update` without losing your `clientId` — user config lives under the per-OS config dir, not inside `node_modules`.
51
54
  </details>
52
55
 
53
56
  <details>
54
- <summary><b>Use your own Discord app</b></summary>
57
+ <summary><b>use your own Discord app</b></summary>
55
58
 
56
- A working Discord application is bundled into the default config — you don't need to register your own to get started. To use a different app name on the card, create one in the [Discord Developer Portal](https://discord.com/developers/applications), copy the Application ID, and put it into `clientId` in your config:
59
+ A working public Discord application is bundled into the default config — you don't need to register your own to get started. If you want a different app name on the card, create one in the [Discord Developer Portal](https://discord.com/developers/applications), copy the Application ID, and drop it into your config:
57
60
 
58
61
  ```sh
59
- # Linux / macOS
62
+ # Linux
60
63
  echo '{ "clientId": "YOUR_ID" }' > ~/.config/claude-rpc/config.json
61
- # Windows
62
- echo { "clientId": "YOUR_ID" } > %APPDATA%\claude-rpc\config.json
64
+ # macOS
65
+ echo '{ "clientId": "YOUR_ID" }' > ~/Library/Application\ Support/claude-rpc/config.json
66
+ # Windows (PowerShell)
67
+ '{ "clientId": "YOUR_ID" }' | Set-Content $env:APPDATA\claude-rpc\config.json
63
68
  ```
64
69
 
65
- Run `claude-rpc upgrade-config` afterwards if you carry forward a v0.3-era config.
70
+ `claude-rpc upgrade-config` if you're carrying forward a v0.3-era file.
66
71
  </details>
67
72
 
68
- ## Features
73
+ ## what claude-rpc does
69
74
 
70
- **In Discord**
75
+ ### on discord
71
76
 
72
- | | |
73
- | :--- | :--- |
74
- | 🔴 **Live status** | Model, project, current tool/file, and token counts update as you work |
75
- | 🎞️ **Status art** | Large image swaps between *working*, *thinking*, *idle*, *stale*, *notification* |
76
- | 🔁 **Rotation frames** | Cycle through today's stats, streak, top file, lifetime totals, anything you template |
77
- | 🐙 **Auto GitHub button** | When your cwd is a git repo with a github origin, a *View on GitHub* button appears |
78
- | 🔒 **Privacy mode** | Per-project `.claude-rpc.json`, runtime `claude-rpc private` toggle, glob-pattern matchers, and auto-detection of GitHub private repos via `gh` |
77
+ A card that updates as you work. The large image swaps between five states (working / thinking / idle / stale / notification — those gifs at the top of this README). The two lines of text rotate through frames you template — current file, today's hours, lifetime totals, top hotspot, code churn, cost — and the daemon skips frames whose required template variables are empty. The `SessionEnd` hook clears the card instantly when you close Claude Code; no "is it still running?" timeout.
79
78
 
80
- **Beyond Discord**
79
+ A *View on GitHub →* button appears automatically when your cwd is a git repo with a github origin. The daemon checks `.git/config` directly — no shell-out, no surprise GH API call.
81
80
 
82
- | | |
83
- | :--- | :--- |
84
- | 📊 **All-time aggregates** | Hours, prompts, tokens, streaks, hotspots, lines changed, languages, cost, bash usage, web domains, subagent runs — incremental scanner over `~/.claude/projects/*.jsonl` |
85
- | 💰 **Cost estimate** | Per-model spend (Opus/Sonnet/Haiku) using public list prices — editable in `src/pricing.js` |
86
- | 🧠 **Insights** | `claude-rpc insights` generates 3–5 contextual lines: weekly trend, peak weekday, hotspot file, cost pace, streak progress |
87
- | 🖥️ **CLI dashboard** | `claude-rpc status` — heatmap, hour histogram, top tools / files / projects / languages / bash commands / cost |
88
- | 🌐 **Web dashboard** | `claude-rpc serve` — range selector (7d / 30d / 90d / 1y / All), live SSE updates, project drilldown, day-detail modal, theme toggle |
89
- | 🪪 **Badges & cards** | `claude-rpc badge --metric hours --range 7d` (Shields-style SVG) and `claude-rpc card --range year` (poster-style summary) |
90
- | ⚙️ **Config GUI** | Electron app — six tabs: Presence, Discord, Assets, Timing, Daemon, Stats |
81
+ ### on your machine
91
82
 
92
- ## Screens
83
+ Three local surfaces, all reading the same `~/.claude-rpc/aggregate.json`:
93
84
 
94
85
  <table>
95
86
  <tr>
96
- <td align="center" width="50%"><b>Web dashboard</b><br/><sub><code>claude-rpc serve</code></sub><br/><br/><img src="docs/dashboard.png" alt="Web dashboard with range selector, activity chart, heatmap, cost panel, languages stack, and leaderboards" /></td>
97
- <td align="center" width="50%"><b>Settings GUI</b><br/><sub><code>npm run dashboard</code></sub><br/><br/><img src="docs/electron.png" alt="Electron config editor with Presence / Discord / Assets / Timing / Daemon / Stats tabs" /></td>
87
+ <td align="center" width="50%"><b>web dashboard</b><br/><sub><code>claude-rpc serve</code> · port 47474</sub><br/><br/><img src="docs/dashboard.png" alt="Web dashboard with range selector, activity chart, heatmap, cost panel, languages stack, and leaderboards" /></td>
88
+ <td align="center" width="50%"><b>settings gui</b><br/><sub><code>npm run dashboard</code> · Electron</sub><br/><br/><img src="docs/electron.png" alt="Electron config editor with Presence / Discord / Assets / Timing / Daemon / Stats tabs" /></td>
98
89
  </tr>
99
90
  </table>
100
91
 
101
- ## Commands
102
-
103
- | Command | Description |
104
- | ---------------- | -------------------------------------------------------- |
105
- | `setup` | Install Claude Code hooks (`~/.claude/settings.json`) |
106
- | `uninstall` | Remove Claude Code hooks |
107
- | `upgrade-config` | Re-run idempotent migrations on `config.json` |
108
- | `start` | Start the daemon (detached) |
109
- | `stop` | Stop the daemon |
110
- | `restart` | Stop then start |
111
- | `status` | Current session + all-time stats (interactive TUI or `--dump`) |
112
- | `today` | Today's stats + 24h histogram |
113
- | `week` | This week's stats + daily breakdown |
114
- | `serve` | Open the local web dashboard (port 47474) |
115
- | `preview` | Show how each rotation frame renders right now |
116
- | `scan` / `rescan`| Incremental / forced re-parse of `~/.claude/projects` |
117
- | `backfill <dir>` | Import transcripts from any folder (backup, other machine) |
118
- | `insights` | Print 3–5 auto-generated insight lines |
119
- | `badge` | Render a Shields-style SVG (`--metric` `--range` `--out`)|
120
- | `card` | Poster-style SVG summary (`--range year\|month\|week\|all`) |
121
- | `private` / `public` / `privacy` | Per-cwd visibility toggles + status |
122
- | `doctor` | Diagnostic checklist — common-failure triage |
123
- | `tail` / `logs` | Tail the daemon log |
124
- | `daemon` | Run the daemon in the foreground (debug) |
92
+ ```text
93
+ claude-rpc status (TUI — heatmap, hour histogram, leaderboards)
94
+ claude-rpc today (today's stats, focused)
95
+ claude-rpc week (weekday breakdown)
96
+ claude-rpc preview (every rotation frame rendered with real data)
97
+ claude-rpc insights (3–5 auto-generated lines: trend, peak, hotspot)
98
+ ```
125
99
 
126
- Exit codes: `0` ok · `1` user error · `2` system error · `3` wrong state. `--version` and `--help` work as expected.
100
+ The web dashboard pushes updates via SSE; the TUI refreshes on a 3-second tick.
101
+
102
+ ### beyond your machine
127
103
 
128
- ## Config GUI
104
+ Shields-style badges and a poster-style summary card you can paste into a README or a Discord message:
129
105
 
130
106
  ```sh
131
- cd dashboard
132
- npm install
133
- npm start # dev mode
134
- npm run dist:mac # → .dmg
135
- npm run dist:win # → portable .exe
107
+ claude-rpc badge --metric hours --range 7d --out claude-hours.svg
108
+ claude-rpc badge --metric streak --out claude-streak.svg
109
+ claude-rpc card --range year --out year-on-claude.svg
136
110
  ```
137
111
 
138
- The Electron app reads and writes `config.json` directly. The daemon hot-reloads.
112
+ <div align="center">
113
+ <img src="site/examples/year-on-claude.svg" width="560" alt="Year-on-claude card — hours, prompts, tokens, lines, cost, daily activity strip" />
114
+ </div>
139
115
 
140
- ## How it works
116
+ Live equivalents when the daemon is up:
141
117
 
142
- Three cooperating pieces, glued by JSON files on disk.
118
+ - `http://127.0.0.1:47474/api/badge.svg?metric=hours&range=7d`
119
+ - `http://127.0.0.1:47474/api/card.svg?range=year`
120
+
121
+ Cost numbers come from `src/pricing.js`, seeded with **approximate** public list prices. Your actual Claude Code subscription bill is unrelated.
122
+
123
+ ## three pieces, glued by json files
143
124
 
144
125
  ```
145
126
  Claude Code Discord desktop
@@ -157,104 +138,124 @@ Three cooperating pieces, glued by JSON files on disk.
157
138
  └────────────┘
158
139
  ```
159
140
 
160
- 1. **Hook** (`src/hook.js`) Claude Code spawns it on every lifecycle event. Parses the JSON event from stdin and mutates the shared state file.
161
- 2. **Daemon** (`src/daemon.js`) — Long-running. Connects to Discord's local IPC, watches the state file plus periodic transcript scans, pushes presence frames every few seconds. Exponential backoff with jitter on reconnect.
162
- 3. **Scanner** (`src/scanner.js`) — Walks `~/.claude/projects/**/*.jsonl` transcripts for all-time aggregates. Cached at `~/.claude-rpc/aggregate.json` for incremental updates.
141
+ No database, no message bus, no background polling when Claude Code isn't running. State on disk you can `cat` and `jq`. The single runtime dependency is `@xhayper/discord-rpc`.
163
142
 
164
- Persistent state:
143
+ 1. **hook** ([`src/hook.js`](src/hook.js)) — Claude Code spawns it on every lifecycle event. Parses the JSON from stdin and mutates the shared state file. Runs in ~20ms.
144
+ 2. **daemon** ([`src/daemon.js`](src/daemon.js)) — long-running. Connects to Discord's local IPC, watches the state file, pushes presence frames every few seconds. Exponential backoff with jitter on reconnect; `daemon.log` rotates at 5 MB.
145
+ 3. **scanner** ([`src/scanner.js`](src/scanner.js)) — walks `~/.claude/projects/**/*.jsonl` for all-time aggregates (active time, prompts, tools, tokens, streaks, hotspots, lines, languages, cost, bash, web, subagents). Incremental — re-parses only changed files.
146
+
147
+ Persistent state, all human-readable JSON:
165
148
 
166
149
  | Path | What |
167
150
  | ---- | ---- |
168
151
  | `$TMPDIR/claude-rpc/state.json` | Current session, volatile |
169
152
  | `~/.claude-rpc/aggregate.json` | All-time aggregates |
170
153
  | `~/.claude-rpc/scan-cache.json` | Per-transcript scan cache |
154
+ | `~/.claude-rpc/private-list.json` | Runtime privacy toggles |
171
155
  | `~/.claude/settings.json` | Hook registrations (managed by `setup`) |
172
156
 
173
- User config lives at `%APPDATA%\claude-rpc\config.json` (Windows), `~/Library/Application Support/claude-rpc/config.json` (macOS), or `$XDG_CONFIG_HOME/claude-rpc/config.json` (Linux). It only needs to hold *overrides* — defaults are baked into the binary.
157
+ User config lives at `%APPDATA%\claude-rpc\config.json` (Windows), `~/Library/Application Support/claude-rpc/config.json` (macOS), or `$XDG_CONFIG_HOME/claude-rpc/config.json` (Linux). It only needs to hold *overrides* — every key has a baked default. `{ "clientId": "..." }` is a complete config file. Defaults live in [`src/default-config.js`](src/default-config.js); the loader deep-merges over them.
174
158
 
175
- <details>
176
- <summary><b>Configuration reference</b></summary>
177
-
178
- Every key is optional. The shipped defaults work out of the box. Override what you want:
179
-
180
- | Key | Default | Notes |
181
- | ------------------------- | ------- | ------------------------------------------------------------------- |
182
- | `clientId` | bundled | Discord application ID (a working public app ships by default) |
183
- | `updateIntervalMs` | `4000` | How often the daemon pushes to Discord |
184
- | `rotationIntervalMs` | `12000` | How fast rotation frames cycle |
185
- | `rescanIntervalSec` | `300` | How often transcripts are re-aggregated |
186
- | `idleThresholdSec` | `60` | No activity for this long → status `idle` |
187
- | `staleSessionMin` | `5` | No activity (minutes) → status `stale`; presence cleared |
188
- | `notificationWindowSec` | `8` | How long the `notification` status sticks |
189
- | `showElapsed` | `true` | Include the elapsed timer |
190
- | `activityType` | `0` | `0` Playing, `2` Listening, `3` Watching, `5` Competing |
191
- | `statusAssets` | gifs | Image per status (working / thinking / idle / stale / notification) |
192
- | `presence.byStatus` | full | Per-status template block (preferred over `rotation`) |
193
- | `presence.rotation` | — | Legacy: flat array of `{ details, state, requires? }` |
194
- | `presence.buttons` | one | Up to 2 `{ label, url }` buttons |
195
- | `presence.largeImageKey` | gif | Fallback large image when no `statusAssets` match |
196
- | `presence.largeImageText` | tpl | Tooltip on hover |
197
- | `privacy.patterns` | `[]` | Glob list of cwd basenames to treat as private |
198
- | `privacy.mode` | hidden | What `patterns` does — `hidden` / `name-only` / `public` |
199
-
200
- Image precedence: `statusAssets[status]` → `modelAssets[opus|sonnet|haiku]` → `presence.largeImageKey`.
159
+ ## privacy
201
160
 
202
- </details>
161
+ Per-project, runtime, or auto-detected — whichever fits how you work.
203
162
 
204
- <details>
205
- <summary><b>Template variables</b></summary>
206
-
207
- Both `details` and `state` (and button labels and URLs) support `{name}` substitution.
208
-
209
- | Variable | Sample |
210
- | ----------------------- | ------------------ |
211
- | `{statusVerbose}` | `Working` |
212
- | `{project}` | `claude-rpc` |
213
- | `{modelPretty}` | `Opus 4.7` |
214
- | `{currentToolPretty}` | `Edit` |
215
- | `{currentFilePretty}` | `src/app/page.tsx` |
216
- | `{tokensFmt}` | `2.3k` |
217
- | `{messagesLabel}` | `8 prompts` |
218
- | `{todayHours}` | `56m` |
219
- | `{weekHours}` | `3.1h` |
220
- | `{streakLabel}` | `7-day streak` |
221
- | `{allHours}` | `52h` |
222
- | `{allTokensFmt}` | `2.82B` |
223
- | `{peakHour}` | `22:00` |
224
- | `{topEditedFile}` | `index.html` |
225
- | `{linesAddedFmt}` | `24k` |
226
- | `{topLanguage}` | `TypeScript` |
227
- | `{todayCostFmt}` | `$1.23` |
228
- | `{allCostFmt}` | `$89.42` |
229
- | `{gitBranch}` | `main` |
230
-
231
- Run `claude-rpc preview` to see every frame rendered with your real data, including which ones would be hidden by their `requires`. Run `claude-rpc vars` for the full machine-readable list.
163
+ ```jsonc
164
+ // drop at your project root: <project>/.claude-rpc.json
165
+ { "private": true } // shortcut for visibility: "hidden"
166
+ { "visibility": "name-only" } // project name only, no file/tool detail
167
+ { "projectName": "redacted" } // show this name on Discord instead
168
+ ```
232
169
 
233
- </details>
170
+ Or from the command line, in any project:
234
171
 
235
- ## Badges
172
+ ```sh
173
+ claude-rpc private # add cwd to ~/.claude-rpc/private-list.json
174
+ claude-rpc public # remove cwd
175
+ claude-rpc privacy # show the resolved visibility for the current dir
176
+ ```
177
+
178
+ Or globally, in `config.json`:
179
+
180
+ ```json
181
+ { "privacy": { "patterns": ["client-*", "secret-stuff"], "mode": "hidden" } }
182
+ ```
183
+
184
+ If [`gh`](https://cli.github.com/) is installed and authenticated, GitHub-private repos auto-hide (`privacy.githubPrivateMode`, default `hidden` — opt out with `privacy.autoDetectGithubPrivate: false`). 5-minute cache, 1.5s timeout, silent skip when `gh` isn't there.
185
+
186
+ Aggregates and local dashboards are never affected. Privacy is a one-way valve between local state and Discord.
187
+
188
+ ## customizing the card
236
189
 
237
190
  ```sh
238
- claude-rpc badge --metric hours --range 7d --out claude-hours.svg
239
- claude-rpc badge --metric streak --out claude-streak.svg
240
- claude-rpc badge --metric cost --range 30d --out claude-cost.svg
241
- claude-rpc badge --metric lines --range all --out claude-lines.svg
191
+ claude-rpc preview # render every rotation frame with your real data
192
+ claude-rpc vars # dump the full template-variable list as JSON
242
193
  ```
243
194
 
244
- Live via the dashboard too: `http://127.0.0.1:47474/api/badge.svg?metric=hours&range=7d`.
195
+ Frames have a `requires` field; the daemon skips a frame when any of its required vars resolve empty / zero. Write seven frames knowing only the relevant ones render.
196
+
197
+ ```jsonc
198
+ "idle": {
199
+ "details": "Idle in {project}",
200
+ "state": "{modelPretty} · {todayHours} today",
201
+ "rotation": [
202
+ { "details": "This week · {weekHours}", "state": "{weekPromptsLabel} · {weekTokensFmt} tokens",
203
+ "requires": ["weekActiveMs"] },
204
+ { "details": "Code churn · {linesAddedFmt} added",
205
+ "state": "{linesNetFmt} net · {topLanguage}",
206
+ "requires": ["topLanguage"] }
207
+ ]
208
+ }
209
+ ```
245
210
 
246
- Cost numbers come from `src/pricing.js`, seeded with **approximate** public list prices. Your actual Claude Code subscription bill is unrelated.
211
+ The full default config is in [`src/default-config.js`](src/default-config.js) that's the canonical list of every key. ~140 template variables are available; `claude-rpc vars` is the source of truth.
212
+
213
+ ## commands
214
+
215
+ | Command | What it does |
216
+ | ---------------- | ------------ |
217
+ | `setup` | Install Claude Code hooks (test-fires one synthetic SessionStart to prove the pipe works) |
218
+ | `uninstall` | Remove Claude Code hooks |
219
+ | `upgrade-config` | Re-run idempotent migrations on `config.json` |
220
+ | `start` / `stop` / `restart` | Lifecycle for the detached daemon |
221
+ | `status` | Interactive TUI — heatmap, hour histogram, leaderboards (`--dump` for plain output) |
222
+ | `today` / `week` | Focused views (today's stats, weekday breakdown) |
223
+ | `serve` | Open the local web dashboard (port 47474) |
224
+ | `preview` | Render every rotation frame against real state |
225
+ | `scan` / `rescan`| Incremental / forced re-parse of `~/.claude/projects` |
226
+ | `backfill <dir>` | Import transcripts from any folder (backup, other machine) |
227
+ | `insights` | Print 3–5 auto-generated lines about your week |
228
+ | `badge` | Shields-style SVG (`--metric` `--range` `--out`) |
229
+ | `card` | Poster-style SVG (`--range year\|month\|week\|all`) |
230
+ | `private` / `public` / `privacy` | Per-cwd visibility toggles + status |
231
+ | `doctor` | Diagnostic checklist with one-line fix hints |
232
+ | `tail` / `logs` | Tail the daemon log |
233
+ | `daemon` | Run the daemon in the foreground (debugging) |
234
+ | `vars` | Dump the full template-var list as JSON |
247
235
 
248
- ## Troubleshooting
236
+ Exit codes: `0` ok · `1` user error · `2` system error · `3` wrong state. `--version` and `--help` work as expected.
249
237
 
250
- **First step is always `claude-rpc doctor`.** It checks Node version, hook registration, daemon liveness, Discord connection, aggregate freshness, and privacy resolution — with a one-line fix hint per failure.
238
+ ## troubleshooting
251
239
 
252
- **Discord doesn't pick up presence.** The Discord *desktop* app must be running. The browser client doesn't expose the local IPC. Run `claude-rpc tail` to watch the daemon log live.
240
+ **First step is always `claude-rpc doctor`.** It checks Node version, hook registration, daemon liveness, Discord IPC connection, aggregate freshness, and privacy resolution with a one-line fix hint per failure.
253
241
 
254
- **Hooks don't fire.** Run `claude-rpc setup` and check the `hooks` section of `~/.claude/settings.json`. Restart Claude Code so it re-reads hook config. `setup` now test-fires a SessionStart through the same launcher Claude Code will use, so a broken hook command should be caught at install time.
242
+ - **Discord doesn't show anything.** Discord *desktop* must be running. The browser client doesn't expose the local IPC bridge. `claude-rpc tail` shows what the daemon is actually doing.
243
+ - **Hooks don't fire.** `claude-rpc setup` re-registers them and now test-fires a synthetic `SessionStart` end-to-end, so a broken hook command surfaces immediately. Restart Claude Code afterwards so it re-reads its hook config.
244
+ - **Config error.** Bad JSON in `config.json` no longer crashes anything — the daemon logs one line and falls back to baked defaults. `claude-rpc tail` shows the parse error verbatim.
245
+ - **Old binary path baked into hooks.** Common after manual exe replacement. `claude-rpc setup` rewrites hook entries to point at the canonical install location.
246
+
247
+ ## development
248
+
249
+ ```sh
250
+ npm test # 134 tests, ~1.7s
251
+ npm run start # run daemon in foreground
252
+ npm run serve # web dashboard against your real data
253
+ npm run dashboard # Electron settings GUI (dev mode)
254
+ npm run build:exe # SEA single-file binary for the current OS
255
+ ```
255
256
 
256
- **Config error.** A bad `config.json` won't brick the daemon any more it logs one line and falls back to baked-in defaults. Check the daemon log via `claude-rpc tail` to see the parse error.
257
+ Tests are `node --test` with zero deps. The CI pipeline ([release.yml](.github/workflows/release.yml)) gates the matrix build and the npm publish behind the test job. Every public export of `src/*.js` is exercised at least once.
257
258
 
258
- ## License
259
+ ## license
259
260
 
260
261
  [MIT](LICENSE) © Archer Simmons
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-rpc",
3
- "version": "0.6.0",
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/install.js CHANGED
@@ -282,6 +282,12 @@ function verifyHookPipe(exePath) {
282
282
  const args = IS_PACKAGED || IS_NPM_INSTALL
283
283
  ? ['hook', 'SessionStart']
284
284
  : [HOOK_SCRIPT, 'SessionStart'];
285
+ // Windows + npm-install: the global bin is `claude-rpc.cmd` (a batch shim),
286
+ // and Node's spawn doesn't apply PATHEXT — calling `claude-rpc` raw fails
287
+ // with ENOENT. shell:true makes cmd.exe do the resolution, mirroring how
288
+ // Claude Code actually invokes the hook string at runtime. Args are static
289
+ // and trusted; no injection surface.
290
+ const useShell = IS_NPM_INSTALL && process.platform === 'win32';
285
291
  let result;
286
292
  try {
287
293
  result = spawnSync(cmd, args, {
@@ -289,6 +295,7 @@ function verifyHookPipe(exePath) {
289
295
  encoding: 'utf8',
290
296
  timeout: 3000,
291
297
  windowsHide: true,
298
+ shell: useShell,
292
299
  });
293
300
  } catch (e) {
294
301
  return { ok: false, detail: `spawn failed: ${e.message}` };
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.0';
14
+ const BAKED = '0.6.2';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {