claude-rpc 0.15.1 → 0.15.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.15.1",
3
+ "version": "0.15.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/cli.js CHANGED
@@ -1434,7 +1434,7 @@ function profileEnable(on) {
1434
1434
  userCfg.profile = next;
1435
1435
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1436
1436
  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}`);
1437
+ 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
1438
  profileNextStep();
1439
1439
  } else {
1440
1440
  console.log(` ${c.green}✓${c.reset} leaderboard publishing disabled`);
@@ -1746,9 +1746,10 @@ const packagedDefault = IS_PACKAGED && !cmd;
1746
1746
  // to do everything. Non-Windows: addStartupEntry is a no-op + warning.
1747
1747
  case 'setup':
1748
1748
  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 });
1749
+ // runInstall prints the phased checklist (or a one-line "already set
1750
+ // up" on clean re-runs); the daemon row lands after it, then setupOutro
1751
+ // closes the screen only when something actually changed.
1752
+ const { target, changed } = await runInstall({ exePath: EXE_PATH || process.execPath });
1752
1753
  // Slimmer first run: bring the daemon up now so the card appears
1753
1754
  // immediately, instead of making the user run a separate `start`.
1754
1755
  // Best-effort — a start hiccup must never make `setup` look failed.
@@ -1763,6 +1764,8 @@ const packagedDefault = IS_PACKAGED && !cmd;
1763
1764
  });
1764
1765
  child.unref();
1765
1766
  console.log(` ${c.green}✓${c.reset} ${'daemon launched'.padEnd(16)}${c.dim}log ${shortPath(LOG_PATH)}${c.reset}`);
1767
+ } else {
1768
+ console.log(` ${c.cyan}·${c.reset} ${'daemon running'.padEnd(16)}${c.dim}pid ${daemonPid()}${c.reset}`);
1766
1769
  }
1767
1770
  } else {
1768
1771
  startDaemon();
@@ -1771,11 +1774,13 @@ const packagedDefault = IS_PACKAGED && !cmd;
1771
1774
  console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
1772
1775
  console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
1773
1776
  }
1774
- setupOutro(target);
1777
+ setupOutro(target, changed);
1775
1778
  break;
1776
1779
  }
1777
1780
  case 'uninstall': await runUninstall(); break;
1778
- case 'upgrade-config': migrateConfig(); break;
1781
+ case 'upgrade-config':
1782
+ if (!migrateConfig()) console.log(` ${c.green}✓${c.reset} config already current — nothing to migrate`);
1783
+ break;
1779
1784
  case 'start': startDaemon(); break;
1780
1785
  case 'stop': stopDaemon(); break;
1781
1786
  case 'restart': restartDaemon(); break;
@@ -1869,7 +1874,7 @@ const packagedDefault = IS_PACKAGED && !cmd;
1869
1874
  default: {
1870
1875
  if (packagedDefault) {
1871
1876
  if (!isInstalled()) {
1872
- const target = await runInstall({ exePath: EXE_PATH || process.execPath });
1877
+ const { target } = await runInstall({ exePath: EXE_PATH || process.execPath });
1873
1878
  startDaemon();
1874
1879
  setupOutro(target);
1875
1880
  } 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.2';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {