claude-rpc 1.2.0 → 1.3.0

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": "1.2.0",
3
+ "version": "1.3.0",
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/cli.js CHANGED
@@ -1673,11 +1673,25 @@ async function doLink(argv) {
1673
1673
  return fail(`link failed: ${r.json?.error || r.status}`,
1674
1674
  { hint: `get a fresh code: run \`claude-rpc link\` on your main machine, or ${LINK_PAGE}`, code: EX_SYS_ERROR });
1675
1675
  }
1676
- // Mirror the verified identity locally so `profile status` agrees.
1676
+ // Mirror the verified identity locally so `profile status` agrees — and
1677
+ // adopt the server's handle + enable publishing when the local config has
1678
+ // neither (a brand-new machine via `setup --link`): without these the
1679
+ // daemon's 30-min flush never uploads this machine's slice. An explicit
1680
+ // local handle or an explicit enabled:false is always respected.
1677
1681
  const userCfg = readJson(CONFIG_PATH, {});
1678
- userCfg.profile = { ...(userCfg.profile || {}), githubUser: r.json.githubUser, verified: true };
1682
+ const prevProfile = userCfg.profile || {};
1683
+ userCfg.profile = {
1684
+ ...prevProfile,
1685
+ githubUser: r.json.githubUser,
1686
+ verified: true,
1687
+ ...(prevProfile.handle ? {} : { handle: r.json.handle }),
1688
+ ...(prevProfile.enabled === undefined ? { enabled: true } : {}),
1689
+ };
1679
1690
  writeUserConfig(userCfg);
1680
1691
  console.log(` ${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser`);
1692
+ if (r.json.created) {
1693
+ console.log(` ${c.green}✓${c.reset} board profile created — handle ${c.cyan}@${r.json.handle}${c.reset} ${c.dim}(rename any time: claude-rpc profile set --handle <you>)${c.reset}`);
1694
+ }
1681
1695
  if (r.json.merged) {
1682
1696
  // This machine joined an existing identity: its stats now roll up under the
1683
1697
  // canonical handle, one board row across all your machines.
@@ -2106,8 +2120,9 @@ async function doWrappedPublish(argv) {
2106
2120
  pair('ships', fmtNum(w.ships)),
2107
2121
  pair('est. cost', fmtCost(w.costUsd), c.yellow),
2108
2122
  pair('projects', w.topProjects.map((p) => p.name).join(' · ') || c.dim + '(none public)' + c.reset, ''),
2109
- pair('languages', w.topLanguages.join(' · ') || '—', ''),
2123
+ pair('languages', w.topLanguages.map((l) => l.name).join(' · ') || '—', ''),
2110
2124
  pair('models', w.topModels.map((m) => `${m.name} ${m.pct}%`).join(' · ') || '—', ''),
2125
+ pair('hotspot', w.hotspot ? `${w.hotspot.name} (${fmtNum(w.hotspot.count)} edits)` : '—', ''),
2111
2126
  ], 72);
2112
2127
  console.log(` ${c.dim}full payload: the fields above plus lines/cache%/peaks — nothing else.${c.reset}`);
2113
2128
  console.log(` ${c.dim}private-listed and pattern-matched project names are already excluded.${c.reset}\n`);
@@ -2261,6 +2276,7 @@ function overview() {
2261
2276
  function help() {
2262
2277
  const cmds = [
2263
2278
  ['setup', 'Install Claude Code hooks + Windows startup entry (~/.claude/settings.json)'],
2279
+ ['setup --link <code>', 'One-liner onboarding: install + verify via a claude-rpc.com code (+ --wrapped publishes your year)'],
2264
2280
  ['uninstall', 'Remove Claude Code hooks + Windows startup entry'],
2265
2281
  ['upgrade-config', 'Re-run idempotent migrations on an existing config.json'],
2266
2282
  ['start', 'Start the Discord RPC daemon (detached)'],
@@ -2410,6 +2426,33 @@ process.on('unhandledRejection', (e) => {
2410
2426
  console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
2411
2427
  console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
2412
2428
  }
2429
+ // One-liner onboarding: `setup --link <code> [--wrapped]` — the wrapped
2430
+ // page mints the code after a GitHub login, so a single paste installs,
2431
+ // verifies the machine as that login (the worker creates the profile on
2432
+ // first contact), runs the first scan, publishes real totals, and —
2433
+ // with --wrapped — puts the year-in-review on the web. Each step is
2434
+ // best-effort-loud: a failure explains itself but never rolls back the
2435
+ // completed install above.
2436
+ {
2437
+ const argv = process.argv.slice(3);
2438
+ const linkAt = argv.indexOf('--link');
2439
+ const linkCode = linkAt !== -1 ? (argv[linkAt + 1] || '').trim() : null;
2440
+ if (linkCode) {
2441
+ console.log('');
2442
+ await doLink([linkCode]);
2443
+ if (process.exitCode) break; // link failed — its own message + hint already printed
2444
+ console.log(` ${c.dim}first scan — counting your history…${c.reset}`);
2445
+ doScan(false);
2446
+ const { flushProfile } = await import('./community.js');
2447
+ const flushed = await flushProfile(loadConfig());
2448
+ if (flushed.ok) {
2449
+ console.log(` ${c.green}✓${c.reset} profile published — ${c.cyan}${fmtNum(flushed.totals.tokens)}${c.reset} tokens on the board`);
2450
+ }
2451
+ if (argv.includes('--wrapped')) await doWrappedPublish(['--yes']);
2452
+ } else if (argv.includes('--wrapped')) {
2453
+ console.log(` ${c.yellow}!${c.reset} --wrapped needs a profile — pair it with --link <code>, or run \`claude-rpc wrapped --publish\` after setup`);
2454
+ }
2455
+ }
2413
2456
  setupOutro(target, changed);
2414
2457
  break;
2415
2458
  }
package/src/community.js CHANGED
@@ -26,6 +26,7 @@ import { VERSION } from './version.js';
26
26
  import { profileIsPublishable } from './leaderboard.js';
27
27
  import { projectNameIsPrivate } from './privacy.js';
28
28
  import { cleanProjectName } from './scanner.js';
29
+ import { humanModel } from './format.js';
29
30
 
30
31
  const CURSOR_PATH = join(STATE_DIR, 'community-cursor.json');
31
32
 
@@ -157,25 +158,50 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
157
158
  .map(([name, activeMs]) => ({ name, activeMs }));
158
159
 
159
160
  // Year-scoped model mix from the per-day byModel buckets (scan cache v7).
160
- const modelTok = {};
161
+ // Share is by SPEND, matching the local wrapped's "your models, by spend"
162
+ // slide; token share is the fallback when everything costed to zero. Names
163
+ // are humanized here so the web story renders "Opus 4.8", same as local.
164
+ const modelAgg = {};
161
165
  for (const [, d] of days) {
162
166
  for (const [m, v] of Object.entries(d.byModel || {})) {
163
- modelTok[m] = (modelTok[m] || 0) + (v.tokens || 0);
167
+ const t = (modelAgg[m] ||= { tokens: 0, cost: 0 });
168
+ t.tokens += v.tokens || 0;
169
+ t.cost += v.cost || 0;
164
170
  }
165
171
  }
166
- const modelTotal = Object.values(modelTok).reduce((a, b) => a + b, 0);
167
- const topModels = Object.entries(modelTok).sort((a, b) => b[1] - a[1]).slice(0, 4)
168
- .map(([name, t]) => ({ name, pct: modelTotal ? Math.round((t / modelTotal) * 100) : 0 }));
172
+ const costTotal = Object.values(modelAgg).reduce((a, b) => a + b.cost, 0);
173
+ const tokTotal = Object.values(modelAgg).reduce((a, b) => a + b.tokens, 0);
174
+ const share = (v) => costTotal > 0 ? v.cost / costTotal : (tokTotal > 0 ? v.tokens / tokTotal : 0);
175
+ const topModels = Object.entries(modelAgg).sort((a, b) => share(b[1]) - share(a[1])).slice(0, 4)
176
+ .map(([name, v]) => ({ name: humanModel(name) || name, pct: Math.round(share(v) * 100) }));
169
177
 
170
178
  // Lifetime dimensions (no yearly slice exists for these).
171
179
  const toolTotal = Object.values(agg.toolBreakdown || {}).reduce((a, b) => a + b, 0);
172
180
  const toolMix = Object.entries(agg.toolBreakdown || {}).sort((a, b) => b[1] - a[1]).slice(0, 3)
173
181
  .map(([name, n]) => ({ name, pct: toolTotal ? Math.round((n / toolTotal) * 100) : 0 }));
174
182
  const topLanguages = Object.entries(agg.languages || {})
175
- .sort((a, b) => (b[1].edits || 0) - (a[1].edits || 0)).slice(0, 5).map(([n]) => n);
183
+ .sort((a, b) => (b[1].edits || 0) - (a[1].edits || 0)).slice(0, 5)
184
+ .map(([name, v]) => ({ name, edits: v.edits || 0 }));
176
185
  const sl = agg.sessionLengths || {};
177
186
  const marathonPct = sl.count ? Math.round((((sl.buckets?.h2to4 || 0) + (sl.buckets?.gt4h || 0)) / sl.count) * 100) : 0;
178
187
 
188
+ // The hotspot file (basename only, never a path) and peak weekday — the two
189
+ // remaining slides of the local story. Both lifetime, like the local page.
190
+ const top = (agg.topEditedFiles || [])[0] || null;
191
+ const hotName = top ? String(top.path || '').split(/[\\/]/).pop() : null;
192
+ const hotspot = top && hotName && !projectNameIsPrivate(hotName, config, cleanProjectName)
193
+ ? { name: hotName, count: top.count || 0, ...(top.daysSinceLastEdit != null ? { daysSinceLastEdit: top.daysSinceLastEdit } : {}) }
194
+ : null;
195
+ let peakWeekday = null;
196
+ {
197
+ const WD = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
198
+ for (const [k, v] of Object.entries(agg.byWeekday || {})) {
199
+ if ((v.activeMs || 0) > 0 && (!peakWeekday || v.activeMs > peakWeekday.activeMs)) {
200
+ peakWeekday = { name: WD[Number(k)] || '', activeMs: v.activeMs };
201
+ }
202
+ }
203
+ }
204
+
179
205
  const compactions = Object.entries(agg.compactionsByDay || {})
180
206
  .filter(([k]) => k.startsWith(`${y}-`)).reduce((acc, [, n]) => acc + n, 0);
181
207
 
@@ -188,6 +214,8 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
188
214
  tokens,
189
215
  prompts: sum('userMessages'),
190
216
  streakBest: agg.longestStreak || 0,
217
+ streak: agg.streak || 0,
218
+ daysSinceFirst: agg.daysSinceFirst || 0,
191
219
  daysActive: days.filter(([, d]) => (d.activeMs || 0) > 0 || (d.userMessages || 0) > 0).length,
192
220
  linesAdded: sum('linesAdded'),
193
221
  linesRemoved: sum('linesRemoved'),
@@ -204,6 +232,8 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
204
232
  topLanguages,
205
233
  topModels,
206
234
  toolMix,
235
+ ...(hotspot ? { hotspot } : {}),
236
+ ...(peakWeekday ? { peakWeekday } : {}),
207
237
  },
208
238
  };
209
239
  }
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 = '1.2.0';
14
+ const BAKED = '1.3.0';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {