claude-rpc 0.7.1 → 0.7.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.
package/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
  **Discord Rich Presence for [Claude Code](https://claude.com/claude-code).**
11
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
+ **→ [claude-rpc.vercel.app](https://claude-rpc.vercel.app)** — what it looks like, in one page.
14
+
13
15
  [![community · sessions](https://claude-rpc-totals.claude-rpc.workers.dev/sessions.svg)](#community-totals)   [![community · tokens](https://claude-rpc-totals.claude-rpc.workers.dev/tokens.svg)](#community-totals)
14
16
 
15
17
  <sub>live — on by default for fresh installs, opt out any time. see [community totals](#community-totals)</sub>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-rpc",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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/privacy.js CHANGED
@@ -25,7 +25,10 @@ import { execFileSync } from 'node:child_process';
25
25
  import { join, basename, dirname, resolve as resolvePath } from 'node:path';
26
26
  import { DATA_DIR } from './paths.js';
27
27
 
28
- const PRIVATE_LIST_PATH = join(DATA_DIR, 'private-list.json');
28
+ // CLAUDE_RPC_PRIVATE_LIST lets tests point the runtime store at a temp file
29
+ // instead of the user's real ~/.claude-rpc/private-list.json. Unset in normal
30
+ // operation.
31
+ const PRIVATE_LIST_PATH = process.env.CLAUDE_RPC_PRIVATE_LIST || join(DATA_DIR, 'private-list.json');
29
32
  const TTL_MS = 5 * 60 * 1000;
30
33
 
31
34
  const projectFileCache = new Map(); // cwd → { ts, value | null }
@@ -82,7 +85,14 @@ function readPrivateList() {
82
85
  if (!existsSync(PRIVATE_LIST_PATH)) return { paths: [] };
83
86
  try {
84
87
  const v = JSON.parse(readFileSync(PRIVATE_LIST_PATH, 'utf8'));
85
- if (Array.isArray(v?.paths)) return v;
88
+ if (v && typeof v === 'object') {
89
+ // `paths` is the legacy binary list (presence ≡ hidden). `visibility`
90
+ // is the richer GUI-managed map: { "<abs cwd>": "hidden|name-only|public" }.
91
+ return {
92
+ paths: Array.isArray(v.paths) ? v.paths : [],
93
+ visibility: (v.visibility && typeof v.visibility === 'object') ? v.visibility : undefined,
94
+ };
95
+ }
86
96
  } catch { /* broken JSON ≡ no list (treat as empty rather than crash) */ }
87
97
  return { paths: [] };
88
98
  }
@@ -122,6 +132,50 @@ function isInPrivateList(cwd) {
122
132
  );
123
133
  }
124
134
 
135
+ // ── Central visibility map (GUI-managed) ────────────────────────────────
136
+ // A richer alternative to the binary `paths` list: maps an absolute cwd to
137
+ // an explicit visibility level. Highest-priority runtime layer — an explicit
138
+ // 'public' here even overrides config-glob / gh auto-detect, because the user
139
+ // deliberately marked it. Subdirectories inherit a marked parent.
140
+
141
+ const VIS_LEVELS = ['public', 'name-only', 'hidden'];
142
+ function normLevel(v) { return VIS_LEVELS.includes(v) ? v : null; }
143
+
144
+ function visibilityForCwd(cwd) {
145
+ if (!cwd) return null;
146
+ const map = readPrivateList().visibility;
147
+ if (!map) return null;
148
+ const abs = resolvePath(cwd);
149
+ if (map[abs]) return normLevel(map[abs]);
150
+ for (const [p, v] of Object.entries(map)) {
151
+ if (abs === p || abs.startsWith(p + '/') || abs.startsWith(p + '\\')) return normLevel(v);
152
+ }
153
+ return null;
154
+ }
155
+
156
+ // Set (or clear) the explicit visibility for a cwd. `level` of null/'default'
157
+ // removes the override so resolution falls back through the lower layers.
158
+ // Returns the updated visibility map.
159
+ export function setCwdVisibility(cwd, level) {
160
+ const abs = resolvePath(cwd);
161
+ const list = readPrivateList();
162
+ const map = (list.visibility && typeof list.visibility === 'object') ? list.visibility : {};
163
+ const norm = normLevel(level);
164
+ if (norm) map[abs] = norm;
165
+ else delete map[abs];
166
+ // A path managed by the map shouldn't also linger in the legacy binary
167
+ // list, where it would always read as hidden regardless of the map value.
168
+ list.paths = (list.paths || []).filter((p) => p !== abs);
169
+ list.visibility = map;
170
+ writePrivateList(list);
171
+ return map;
172
+ }
173
+
174
+ // Read the current explicit-visibility map (abs cwd → level).
175
+ export function listVisibility() {
176
+ return readPrivateList().visibility || {};
177
+ }
178
+
125
179
  // ── GitHub-private detection (best-effort, gh CLI) ──────────────────────
126
180
 
127
181
  function detectGithubPrivate(cwd) {
@@ -151,6 +205,12 @@ export function resolveVisibility(cwd, config = {}) {
151
205
  if (proj?.visibility) {
152
206
  return { visibility: proj.visibility, projectName: proj.projectName, reason: '.claude-rpc.json' };
153
207
  }
208
+ // Central GUI-managed map (incl. explicit 'public' as an opt-out of
209
+ // auto-hide). Ranks above the legacy binary list and config/gh layers.
210
+ const mapped = visibilityForCwd(cwd);
211
+ if (mapped) {
212
+ return { visibility: mapped, projectName: proj?.projectName ?? null, reason: 'private-list (visibility)' };
213
+ }
154
214
  if (isInPrivateList(cwd)) {
155
215
  return { visibility: 'hidden', projectName: proj?.projectName ?? null, reason: 'private-list' };
156
216
  }
package/src/server/api.js CHANGED
@@ -173,3 +173,38 @@ export function dayDetail(dayKeyStr) {
173
173
  if (!day) return null;
174
174
  return { day: dayKeyStr, ...day };
175
175
  }
176
+
177
+ // Flatten the aggregate's byDay map into a daily-rows CSV for spreadsheet /
178
+ // pandas analysis. One row per day, sorted ascending. Date keys are
179
+ // YYYY-MM-DD and all other columns are numeric, so nothing needs quoting.
180
+ export const CSV_COLUMNS = [
181
+ 'date', 'activeMs', 'activeHours', 'sessions', 'userMessages', 'toolCalls',
182
+ 'linesAdded', 'linesRemoved', 'cost', 'inputTokens', 'outputTokens',
183
+ 'cacheReadTokens', 'cacheWriteTokens', 'notifications',
184
+ ];
185
+
186
+ export function aggregateToCsv(agg) {
187
+ const byDay = (agg && agg.byDay) || {};
188
+ const rows = [CSV_COLUMNS.join(',')];
189
+ for (const date of Object.keys(byDay).sort()) {
190
+ const d = byDay[date] || {};
191
+ const activeMs = d.activeMs || 0;
192
+ rows.push([
193
+ date,
194
+ activeMs,
195
+ (activeMs / 3_600_000).toFixed(3),
196
+ d.sessions || 0,
197
+ d.userMessages || 0,
198
+ d.toolCalls || 0,
199
+ d.linesAdded || 0,
200
+ d.linesRemoved || 0,
201
+ (d.cost || 0).toFixed(4),
202
+ d.inputTokens || 0,
203
+ d.outputTokens || 0,
204
+ d.cacheReadTokens || 0,
205
+ d.cacheWriteTokens || 0,
206
+ d.notifications || 0,
207
+ ].join(','));
208
+ }
209
+ return rows.join('\n') + '\n';
210
+ }