claude-rpc 0.15.1 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-rpc",
3
- "version": "0.15.1",
3
+ "version": "0.15.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/cli.js CHANGED
@@ -1169,6 +1169,11 @@ async function doLink(argv) {
1169
1169
  userCfg.profile = { ...(userCfg.profile || {}), githubUser: r.json.githubUser, verified: true };
1170
1170
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1171
1171
  console.log(` ${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser`);
1172
+ if (r.json.merged) {
1173
+ // This machine joined an existing identity: its stats now roll up under the
1174
+ // canonical handle, one board row across all your machines.
1175
+ console.log(` ${c.green}✓${c.reset} this machine now merges into ${c.cyan}@${r.json.handle}${c.reset} ${c.dim}— stats from all your machines count as one${c.reset}`);
1176
+ }
1172
1177
  console.log(` ${c.dim}head back to https://claude-rpc.vercel.app/squads — it picks the link up automatically${c.reset}`);
1173
1178
  }
1174
1179
 
@@ -1434,7 +1439,7 @@ function profileEnable(on) {
1434
1439
  userCfg.profile = next;
1435
1440
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1436
1441
  if (on) {
1437
- console.log(` ${c.green}✓${c.reset} leaderboard publishing enabled ${c.dim}(live on the next daemon flush or now: ${c.reset}${c.cyan}claude-rpc profile publish${c.reset}${c.dim})${c.reset}`);
1442
+ console.log(` ${c.green}✓${c.reset} publishing enabled ${c.dim} board syncs on the next flush, or now: ${c.reset}${c.cyan}claude-rpc profile publish${c.reset}`);
1438
1443
  profileNextStep();
1439
1444
  } else {
1440
1445
  console.log(` ${c.green}✓${c.reset} leaderboard publishing disabled`);
@@ -1746,9 +1751,10 @@ const packagedDefault = IS_PACKAGED && !cmd;
1746
1751
  // to do everything. Non-Windows: addStartupEntry is a no-op + warning.
1747
1752
  case 'setup':
1748
1753
  case 'install': {
1749
- // runInstall prints the phased checklist and leaves the `daemon` phase
1750
- // open; the launch row lands there, then setupOutro closes the screen.
1751
- const target = await runInstall({ exePath: EXE_PATH || process.execPath });
1754
+ // runInstall prints the phased checklist (or a one-line "already set
1755
+ // up" on clean re-runs); the daemon row lands after it, then setupOutro
1756
+ // closes the screen only when something actually changed.
1757
+ const { target, changed } = await runInstall({ exePath: EXE_PATH || process.execPath });
1752
1758
  // Slimmer first run: bring the daemon up now so the card appears
1753
1759
  // immediately, instead of making the user run a separate `start`.
1754
1760
  // Best-effort — a start hiccup must never make `setup` look failed.
@@ -1763,6 +1769,8 @@ const packagedDefault = IS_PACKAGED && !cmd;
1763
1769
  });
1764
1770
  child.unref();
1765
1771
  console.log(` ${c.green}✓${c.reset} ${'daemon launched'.padEnd(16)}${c.dim}log ${shortPath(LOG_PATH)}${c.reset}`);
1772
+ } else {
1773
+ console.log(` ${c.cyan}·${c.reset} ${'daemon running'.padEnd(16)}${c.dim}pid ${daemonPid()}${c.reset}`);
1766
1774
  }
1767
1775
  } else {
1768
1776
  startDaemon();
@@ -1771,11 +1779,13 @@ const packagedDefault = IS_PACKAGED && !cmd;
1771
1779
  console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
1772
1780
  console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
1773
1781
  }
1774
- setupOutro(target);
1782
+ setupOutro(target, changed);
1775
1783
  break;
1776
1784
  }
1777
1785
  case 'uninstall': await runUninstall(); break;
1778
- case 'upgrade-config': migrateConfig(); break;
1786
+ case 'upgrade-config':
1787
+ if (!migrateConfig()) console.log(` ${c.green}✓${c.reset} config already current — nothing to migrate`);
1788
+ break;
1779
1789
  case 'start': startDaemon(); break;
1780
1790
  case 'stop': stopDaemon(); break;
1781
1791
  case 'restart': restartDaemon(); break;
@@ -1869,7 +1879,7 @@ const packagedDefault = IS_PACKAGED && !cmd;
1869
1879
  default: {
1870
1880
  if (packagedDefault) {
1871
1881
  if (!isInstalled()) {
1872
- const target = await runInstall({ exePath: EXE_PATH || process.execPath });
1882
+ const { target } = await runInstall({ exePath: EXE_PATH || process.execPath });
1873
1883
  startDaemon();
1874
1884
  setupOutro(target);
1875
1885
  } else {
package/src/install.js CHANGED
@@ -26,13 +26,31 @@ const STARTUP_VALUE = 'ClaudeRPC';
26
26
  // the label column fixed-width so the detail column lines up across phases.
27
27
  // The same rows print standalone (doctor --fix, packaged refresh) and still
28
28
  // read fine outside the phased layout.
29
+ //
30
+ // Loud when something changes, near-silent when nothing does: a re-run where
31
+ // everything is already in place collapses to ONE summary line instead of
32
+ // re-printing the checklist. State-changing steps print rows (flushing their
33
+ // pending phase header) and mark the run dirty; confirmations record a
34
+ // `noop()` fact for the summary. Failures always print.
29
35
  const LABEL_W = 16;
36
+ let pendingPhase = null;
37
+ let runDirty = false;
38
+ let noopFacts = [];
39
+
40
+ function resetRun() { pendingPhase = null; runDirty = false; noopFacts = []; }
41
+ function phase(title) { pendingPhase = title; }
30
42
  function step(sym, label, detail = '', log = console.log) {
43
+ if (pendingPhase) {
44
+ console.log(`\n ${c.bold}${pendingPhase}${c.reset}`);
45
+ pendingPhase = null;
46
+ }
31
47
  log(` ${sym} ${label.padEnd(LABEL_W)}${detail ? `${c.dim}${detail}${c.reset}` : ''}`);
32
48
  }
33
- function phase(title) {
34
- console.log(`\n ${c.bold}${title}${c.reset}`);
49
+ function dirtyStep(sym, label, detail = '', log = console.log) {
50
+ runDirty = true;
51
+ step(sym, label, detail, log);
35
52
  }
53
+ function noop(fact) { noopFacts.push(fact); }
36
54
 
37
55
  const EVENTS = [
38
56
  'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse',
@@ -56,6 +74,7 @@ function isOurHookCommand(cmd) {
56
74
 
57
75
  export function installHooks(exePath) {
58
76
  const settings = readJson(CLAUDE_SETTINGS, {});
77
+ const before = JSON.stringify(settings.hooks || {});
59
78
  settings.hooks = settings.hooks || {};
60
79
  // Three modes, three shapes:
61
80
  // packaged → `"<exe>" hook <event>` (canonical exe, no node)
@@ -82,8 +101,13 @@ export function installHooks(exePath) {
82
101
  bucket.push({ matcher: '', hooks: [{ type: 'command', command: wanted }] });
83
102
  }
84
103
  }
104
+ if (JSON.stringify(settings.hooks) === before) {
105
+ noop(`hooks wired (${EVENTS.length} events)`);
106
+ return false;
107
+ }
85
108
  writeJson(CLAUDE_SETTINGS, settings);
86
- step(SYM_OK, 'hooks wired', `${EVENTS.length} events → ${CLAUDE_SETTINGS}`);
109
+ dirtyStep(SYM_OK, 'hooks wired', `${EVENTS.length} events → ${CLAUDE_SETTINGS}`);
110
+ return true;
87
111
  }
88
112
 
89
113
  export function uninstallHooks() {
@@ -119,7 +143,8 @@ export async function addStartupEntry(exePath) {
119
143
  '/d', `"${exePath}" daemon`,
120
144
  '/f',
121
145
  ]);
122
- step(SYM_OK, 'startup entry', `HKCU\\…\\Run\\${STARTUP_VALUE} — daemon starts at login`);
146
+ if (runDirty) step(SYM_OK, 'startup entry', `HKCU\\…\\Run\\${STARTUP_VALUE} — daemon starts at login`);
147
+ else noop('startup entry present');
123
148
  }
124
149
 
125
150
  export async function removeStartupEntry() {
@@ -172,7 +197,7 @@ export function ensureCanonicalExe(currentExe) {
172
197
  const src = statSync(currentExe);
173
198
  const dst = statSync(CANONICAL_EXE);
174
199
  if (src.size === dst.size && Math.abs(src.mtimeMs - dst.mtimeMs) < 2000) {
175
- step(SYM_OK, 'exe installed', `${CANONICAL_EXE} (unchanged)`);
200
+ noop('exe current');
176
201
  return CANONICAL_EXE;
177
202
  }
178
203
  } catch { /* stat failed — fall through to copy attempt */ }
@@ -189,7 +214,7 @@ export function ensureCanonicalExe(currentExe) {
189
214
  }
190
215
  copyFileSync(currentExe, CANONICAL_EXE);
191
216
  if (process.platform !== 'win32') chmodSync(CANONICAL_EXE, 0o755);
192
- step(SYM_OK, 'exe installed', CANONICAL_EXE);
217
+ dirtyStep(SYM_OK, 'exe installed', CANONICAL_EXE);
193
218
  step(SYM_INFO, 'original copy', `${currentExe} — safe to delete`);
194
219
  sweepStaleCanonicalBackups();
195
220
  return CANONICAL_EXE;
@@ -211,7 +236,7 @@ export function seedConfig() {
211
236
  if (!existsSync(CONFIG_PATH) && existsSync(legacyPath)) {
212
237
  mkdirSync(USER_CONFIG_DIR, { recursive: true });
213
238
  copyFileSync(legacyPath, CONFIG_PATH);
214
- step(SYM_OK, 'config migrated', CONFIG_PATH);
239
+ dirtyStep(SYM_OK, 'config migrated', CONFIG_PATH);
215
240
  step(SYM_INFO, 'legacy copy', `${legacyPath} — safe to delete on the next npm update`);
216
241
  return false;
217
242
  }
@@ -221,7 +246,7 @@ export function seedConfig() {
221
246
  }
222
247
 
223
248
  if (existsSync(CONFIG_PATH)) {
224
- step(SYM_OK, 'config found', CONFIG_PATH);
249
+ noop('config current');
225
250
  return false;
226
251
  }
227
252
  mkdirSync(USER_CONFIG_DIR, { recursive: true });
@@ -233,7 +258,7 @@ export function seedConfig() {
233
258
  seeded.community.instanceId = randomUUID();
234
259
  }
235
260
  writeFileSync(CONFIG_PATH, JSON.stringify(seeded, null, 2));
236
- step(SYM_OK, 'config seeded', CONFIG_PATH);
261
+ dirtyStep(SYM_OK, 'config seeded', CONFIG_PATH);
237
262
  if (seeded.community?.enabled && seeded.community.instanceId) {
238
263
  step(SYM_INFO, 'community', `anonymous totals on by default · opt out: ${c.reset}${c.cyan}claude-rpc community off`);
239
264
  }
@@ -370,12 +395,9 @@ export function migrateConfig({ silent = false } = {}) {
370
395
  if (changed) added.push('presence.buttons[] → CTA');
371
396
  }
372
397
 
373
- if (added.length === 0) {
374
- if (!silent) step(SYM_OK, 'config current', 'no new defaults to merge');
375
- return false;
376
- }
398
+ if (added.length === 0) return false;
377
399
  writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
378
- if (!silent) step(SYM_OK, 'config migrated', `added: ${added.join(', ')}`);
400
+ if (!silent) dirtyStep(SYM_OK, 'config migrated', `added: ${added.join(', ')}`);
379
401
  return true;
380
402
  }
381
403
 
@@ -426,12 +448,29 @@ function verifyHookPipe(exePath) {
426
448
  // Best-effort + loud: a failed -g (perms, offline) returns false so the caller
427
449
  // can stop with the manual command rather than wire a dead hook.
428
450
  function promoteNpxToGlobal() {
429
- step(SYM_INFO, 'npx detected', 'one-off cache installing globally so hooks survive…');
451
+ // Already promoted on a previous run? The PATH-resolved bin answers fast.
452
+ try {
453
+ const v = spawnSync('claude-rpc', ['--version'], {
454
+ encoding: 'utf8', timeout: 4000, windowsHide: true,
455
+ shell: process.platform === 'win32',
456
+ });
457
+ if ((v.stdout || '').trim() === `claude-rpc ${VERSION}`) {
458
+ noop('global install current');
459
+ return true;
460
+ }
461
+ } catch { /* not installed yet — promote below */ }
430
462
  const r = spawnSync('npm', ['install', '-g', `claude-rpc@${VERSION}`], {
431
- stdio: 'inherit',
463
+ encoding: 'utf8',
432
464
  shell: process.platform === 'win32', // npm is npm.cmd on Windows
433
465
  });
434
- return !r.error && r.status === 0;
466
+ if (r.error || r.status !== 0) {
467
+ // The piped npm chatter only matters when it failed.
468
+ if (r.stdout) process.stderr.write(r.stdout);
469
+ if (r.stderr) process.stderr.write(r.stderr);
470
+ return false;
471
+ }
472
+ dirtyStep(SYM_OK, 'installed globally', `claude-rpc@${VERSION} — hooks survive npx's throwaway cache`);
473
+ return true;
435
474
  }
436
475
 
437
476
  // Best-effort registry check. npx serves stale cached copies without
@@ -458,6 +497,7 @@ function warnIfStale() {
458
497
  }
459
498
 
460
499
  export async function install({ exePath, withStartup = true } = {}) {
500
+ resetRun();
461
501
  console.log('');
462
502
  console.log(` ${c.bold}${c.magenta}◆ claude-rpc setup${c.reset} ${c.dim}v${VERSION}${c.reset}`);
463
503
  warnIfStale();
@@ -490,11 +530,13 @@ export async function install({ exePath, withStartup = true } = {}) {
490
530
  // without verification is a lie — we caught broken-hook-path bugs
491
531
  // twice during v0.3.x because no one ran a real event after install.
492
532
  const probe = verifyHookPipe(target);
493
- if (probe.ok) {
494
- step(SYM_OK, 'hook verified', probe.detail);
495
- } else {
533
+ if (!probe.ok) {
496
534
  step(SYM_FAIL, 'hook verify', probe.detail, console.warn);
497
535
  hintLine('run `claude-rpc doctor` for a full diagnostic', process.stderr);
536
+ } else if (runDirty) {
537
+ step(SYM_OK, 'hook verified', probe.detail);
538
+ } else {
539
+ noop('hook pipe verified');
498
540
  }
499
541
 
500
542
  // The CLI's setup case launches the daemon right after this returns, so its
@@ -504,17 +546,22 @@ export async function install({ exePath, withStartup = true } = {}) {
504
546
  if (process.platform === 'win32') {
505
547
  try { await addStartupEntry(target); }
506
548
  catch (e) { step(SYM_WARN, 'startup entry', `failed: ${e.message}`, console.warn); }
507
- } else {
549
+ } else if (runDirty) {
508
550
  step(SYM_INFO, 'startup entry', 'skipped — login autostart is Windows-only');
509
551
  }
510
552
  }
511
- return target;
553
+ // Nothing changed: the checklist above stayed silent, so say so in one line.
554
+ if (!runDirty && probe.ok) {
555
+ console.log(` ${SYM_OK} ${c.bold}already set up${c.reset} ${c.dim}${noopFacts.join(' · ')}${c.reset}`);
556
+ }
557
+ return { target, changed: runDirty };
512
558
  }
513
559
 
514
560
  // The single closing block of `claude-rpc setup` — what to do now, where the
515
561
  // levers are. Printed by the CLI after the daemon launch so it always lands
516
562
  // last; doctor --fix re-runs install() without it.
517
- export function setupOutro(target) {
563
+ export function setupOutro(target, changed = true) {
564
+ if (!changed) return;
518
565
  const point = (label, value, note = '') =>
519
566
  console.log(` ${c.dim}→${c.reset} ${c.dim}${label.padEnd(14)}${c.reset} ${c.cyan}${value}${c.reset}${note ? ` ${c.dim}${note}${c.reset}` : ''}`);
520
567
  console.log('');
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.15.1';
14
+ const BAKED = '0.15.3';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {