claude-rpc 0.15.0 → 0.15.1

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.0",
3
+ "version": "0.15.1",
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
@@ -15,7 +15,7 @@ import { readState } from './state.js';
15
15
  import { buildVars, fillTemplate, humanProject, humanTool, applyIdle, framePasses, fmtNum } from './format.js';
16
16
  import { scan, readAggregate, findLiveSessions, dayKey, weekKey } from './scanner.js';
17
17
  import { runHookCli } from './hook.js';
18
- import { install as runInstall, uninstall as runUninstall, isInstalled, migrateConfig, installHooks, ensureCanonicalExe, installMcp, uninstallMcp, mcpServerCommand } from './install.js';
18
+ import { install as runInstall, uninstall as runUninstall, isInstalled, migrateConfig, installHooks, ensureCanonicalExe, installMcp, uninstallMcp, mcpServerCommand, setupOutro } from './install.js';
19
19
  import { startTui } from './tui.js';
20
20
  import { generateInsights } from './insights.js';
21
21
  import { maybeNudge } from './nudge.js';
@@ -68,7 +68,7 @@ function daemonPid() {
68
68
  function startDaemon({ quiet = false } = {}) {
69
69
  const pid = daemonPid();
70
70
  if (pid) {
71
- if (!quiet) console.log(`${c.yellow}!${c.reset} Daemon already running (pid ${pid}). Run 'stop' first to restart.`);
71
+ if (!quiet) console.log(` ${c.yellow}!${c.reset} ${'daemon running'.padEnd(16)}${c.dim}already up (pid ${pid}) · bounce it with ${c.reset}${c.cyan}claude-rpc restart${c.reset}`);
72
72
  return false;
73
73
  }
74
74
  // In packaged mode the "daemon script" is the exe itself with a subcommand;
@@ -79,19 +79,19 @@ function startDaemon({ quiet = false } = {}) {
79
79
  const args = IS_PACKAGED ? ['daemon'] : [DAEMON_SCRIPT];
80
80
  const child = spawn(exe, args, { detached: true, stdio: 'ignore', windowsHide: true });
81
81
  child.unref();
82
- if (!quiet) console.log(`${c.green}✓${c.reset} Daemon launched (pid ${c.cyan}${child.pid}${c.reset}) ${c.dim}logs: ${LOG_PATH}${c.reset}`);
82
+ if (!quiet) console.log(` ${c.green}✓${c.reset} ${'daemon launched'.padEnd(16)}${c.dim}pid ${c.reset}${c.cyan}${child.pid}${c.reset}${c.dim} · log ${shortPath(LOG_PATH)}${c.reset}`);
83
83
  return true;
84
84
  }
85
85
 
86
86
  function stopDaemon({ quiet = false } = {}) {
87
87
  const pid = daemonPid();
88
- if (!pid) { if (!quiet) console.log('Daemon not running.'); return false; }
88
+ if (!pid) { if (!quiet) console.log(` ${c.cyan}·${c.reset} daemon not running`); return false; }
89
89
  try {
90
90
  process.kill(pid, 'SIGTERM');
91
- if (!quiet) console.log(`${c.green}✓${c.reset} Sent SIGTERM to pid ${c.cyan}${pid}${c.reset}`);
91
+ if (!quiet) console.log(` ${c.green}✓${c.reset} ${'daemon stopping'.padEnd(16)}${c.dim}sent SIGTERM to pid ${pid}${c.reset}`);
92
92
  return true;
93
93
  } catch (e) {
94
- if (!quiet) console.log(`${c.red}✗${c.reset} Failed to stop: ${e.message}`);
94
+ if (!quiet) console.log(` ${c.red}✗${c.reset} ${'stop failed'.padEnd(16)}${c.dim}${e.message}${c.reset}`);
95
95
  return false;
96
96
  }
97
97
  }
@@ -581,24 +581,24 @@ function dumpVars() {
581
581
  }
582
582
 
583
583
  function doScan(force = false) {
584
- console.log(`${c.dim}Scanning ~/.claude/projects${c.reset}`, force ? '(force re-parse)' : '(incremental)');
584
+ console.log(` ${c.dim}scanning ~/.claude/projects ${force ? '(force re-parse)' : '(incremental)'}…${c.reset}`);
585
585
  const t0 = Date.now();
586
586
  let lastReport = 0;
587
587
  const result = scan({
588
588
  force,
589
589
  onProgress: ({ scanned, total }) => {
590
590
  if (Date.now() - lastReport > 500) {
591
- process.stdout.write(`\r parsed ${scanned}/${total}…`);
591
+ process.stdout.write(`\r ${c.dim}parsed ${scanned}/${total}…${c.reset}`);
592
592
  lastReport = Date.now();
593
593
  }
594
594
  },
595
595
  });
596
596
  process.stdout.write('\n');
597
- console.log(`${c.green}✓${c.reset} Done in ${Date.now() - t0}ms — ${c.cyan}${result.scanned}${c.reset} parsed · ${result.skipped} cached · ${result.removed} removed (${result.total} total)`);
597
+ console.log(` ${c.green}✓${c.reset} scan complete ${c.dim}${Date.now() - t0}ms — ${c.reset}${c.cyan}${result.scanned}${c.reset}${c.dim} parsed · ${result.skipped} cached · ${result.removed} removed (${result.total} total)${c.reset}`);
598
598
  if (result.dirs && result.dirs.length > 1) {
599
- console.log(`${c.dim}Scanned roots:${c.reset} ${result.dirs.join(', ')}`);
599
+ console.log(` ${c.dim}roots: ${result.dirs.join(', ')}${c.reset}`);
600
600
  }
601
- console.log(`${c.dim}Aggregate written to ${AGGREGATE_PATH}${c.reset}`);
601
+ console.log(` ${c.dim}aggregate ${AGGREGATE_PATH}${c.reset}`);
602
602
  }
603
603
 
604
604
  // Backfill from any folder that has .jsonl transcripts. Useful for:
@@ -617,9 +617,9 @@ function doBackfill(argv) {
617
617
  }
618
618
  if (!existsSync(path)) {
619
619
  fail(`path doesn't exist: ${path}`,
620
- { hint: 'check the spelling, or run `claude-rpc doctor` to see where transcripts live' });
620
+ { hint: 'check the spelling relative paths resolve from the current directory' });
621
621
  }
622
- console.log(`${c.dim}Backfilling from${c.reset} ${c.cyan}${path}${c.reset}…`);
622
+ console.log(` ${c.dim}backfilling from ${c.reset}${c.cyan}${path}${c.reset}${c.dim}…${c.reset}`);
623
623
  const t0 = Date.now();
624
624
  let lastReport = 0;
625
625
  // Pass `extraDirs` rather than `projectsDirs` — this way the default
@@ -630,16 +630,16 @@ function doBackfill(argv) {
630
630
  extraDirs: [path],
631
631
  onProgress: ({ scanned, total }) => {
632
632
  if (Date.now() - lastReport > 500) {
633
- process.stdout.write(`\r parsed ${scanned}/${total}…`);
633
+ process.stdout.write(`\r ${c.dim}parsed ${scanned}/${total}…${c.reset}`);
634
634
  lastReport = Date.now();
635
635
  }
636
636
  },
637
637
  });
638
638
  process.stdout.write('\n');
639
- console.log(`${c.green}✓${c.reset} Done in ${Date.now() - t0}ms — ${c.cyan}${result.scanned}${c.reset} new/changed · ${result.skipped} cached`);
640
- console.log(`${c.dim}Scanned roots:${c.reset} ${result.dirs.join(', ')}`);
639
+ console.log(` ${c.green}✓${c.reset} backfill complete ${c.dim}${Date.now() - t0}ms — ${c.reset}${c.cyan}${result.scanned}${c.reset}${c.dim} new/changed · ${result.skipped} cached${c.reset}`);
640
+ console.log(` ${c.dim}roots: ${result.dirs.join(', ')}${c.reset}`);
641
641
  const hours = ((result.aggregate.activeMs || 0) / 3_600_000).toFixed(1);
642
- console.log(`${c.dim}Aggregate now:${c.reset} ${result.aggregate.sessions} sessions · ${hours}h · ${result.aggregate.userMessages} prompts`);
642
+ console.log(` ${c.dim}aggregate now: ${result.aggregate.sessions} sessions · ${hours}h · ${result.aggregate.userMessages} prompts${c.reset}`);
643
643
  }
644
644
 
645
645
  function showInsights() {
@@ -678,7 +678,7 @@ async function doBadge(argv) {
678
678
  }
679
679
  if (opts.out) {
680
680
  writeFileSync(opts.out, svg);
681
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${opts.out}${c.reset} (${svg.length} bytes)`);
681
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${opts.out}${c.reset} ${c.dim}(${svg.length} bytes)${c.reset}`);
682
682
  } else {
683
683
  process.stdout.write(svg);
684
684
  }
@@ -715,11 +715,11 @@ async function publishBadgeToGist(svg, opts) {
715
715
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
716
716
  const wasUpdate = !!stored.id;
717
717
  console.log('');
718
- console.log(` ${c.green}✓${c.reset} ${wasUpdate ? 'Updated' : 'Created'} gist ${c.cyan}${result.id}${c.reset}`);
719
- console.log(` ${c.dim}raw:${c.reset} ${c.cyan}${result.rawUrl}${c.reset}`);
720
- if (result.htmlUrl) console.log(` ${c.dim}gist:${c.reset} ${c.dim}${result.htmlUrl}${c.reset}`);
718
+ console.log(` ${c.green}✓${c.reset} ${wasUpdate ? 'updated' : 'created'} gist ${c.cyan}${result.id}${c.reset}`);
719
+ console.log(` ${c.dim}raw: ${c.reset}${c.cyan}${result.rawUrl}${c.reset}`);
720
+ if (result.htmlUrl) console.log(` ${c.dim}gist: ${result.htmlUrl}${c.reset}`);
721
721
  console.log('');
722
- console.log(` ${c.dim}Paste into your README:${c.reset}`);
722
+ console.log(` ${c.dim}paste into your README:${c.reset}`);
723
723
  console.log(` ${gistMarkdown({ owner: result.owner, id: result.id, filename, label: 'Claude' })}`);
724
724
  console.log('');
725
725
  } catch (e) {
@@ -753,8 +753,8 @@ async function doCard(argv) {
753
753
  const svg = renderCard(aggregate, { range: opts.range });
754
754
  if (opts.out) {
755
755
  writeFileSync(opts.out, svg);
756
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${opts.out}${c.reset} (${svg.length} bytes)`);
757
- console.log(`${c.dim}Tip: open in a browser, right-click Save as PNG. Or drop straight into a Discord message it'll render inline.${c.reset}`);
756
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${opts.out}${c.reset} ${c.dim}(${svg.length} bytes)${c.reset}`);
757
+ console.log(` ${c.dim}tip: open in a browser and save as PNG or drop it straight into a Discord message; it renders inline${c.reset}`);
758
758
  } else {
759
759
  process.stdout.write(svg);
760
760
  }
@@ -788,8 +788,8 @@ async function doGithubStat(argv) {
788
788
  }
789
789
  if (opts.out) {
790
790
  writeFileSync(opts.out, svg);
791
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${opts.out}${c.reset} (${svg.length} bytes)`);
792
- console.log(`${c.dim}Embed in your README: <img src="${opts.out}" alt="Claude Code stats" width="500" />${c.reset}`);
791
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${opts.out}${c.reset} ${c.dim}(${svg.length} bytes)${c.reset}`);
792
+ console.log(` ${c.dim}embed in your README: <img src="${opts.out}" alt="Claude Code stats" width="500" />${c.reset}`);
793
793
  } else {
794
794
  process.stdout.write(svg);
795
795
  }
@@ -825,13 +825,13 @@ async function doCalendar(argv) {
825
825
  else if (argv[i] === '--gist') opts.gist = true;
826
826
  }
827
827
  const aggregate = readAggregate();
828
- if (!aggregate) fail('no aggregate yet — run `claude-rpc scan` first', { code: EX_BAD_STATE });
828
+ if (!aggregate) fail('no aggregate yet — nothing to render', { hint: 'run `claude-rpc scan` first', code: EX_BAD_STATE });
829
829
  const { renderCalendar } = await import('./calendar.js');
830
830
  const svg = renderCalendar(aggregate, {});
831
831
  if (opts.gist) return publishBadgeToGist(svg, { metric: 'calendar', range: 'year' });
832
832
  if (opts.out) {
833
833
  writeFileSync(opts.out, svg);
834
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${opts.out}${c.reset} (${svg.length} bytes)`);
834
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${opts.out}${c.reset} ${c.dim}(${svg.length} bytes)${c.reset}`);
835
835
  } else process.stdout.write(svg);
836
836
  }
837
837
 
@@ -846,7 +846,7 @@ async function doSessionCard(argv) {
846
846
  const svg = renderSessionCard(vars, {});
847
847
  if (opts.out) {
848
848
  writeFileSync(opts.out, svg);
849
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${opts.out}${c.reset} (${svg.length} bytes)`);
849
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${opts.out}${c.reset} ${c.dim}(${svg.length} bytes)${c.reset}`);
850
850
  } else process.stdout.write(svg);
851
851
  }
852
852
 
@@ -864,8 +864,8 @@ function doMcpInstall(argv) {
864
864
  const manual = (r) => `claude mcp add claude-rpc --scope ${scope} -- ${r.command} ${r.args.join(' ')}`;
865
865
  if (res.ok) {
866
866
  console.log('');
867
- console.log(` ${c.green}✓${c.reset} Registered the ${c.cyan}claude-rpc${c.reset} MCP server with Claude Code (scope: ${scope}).`);
868
- console.log(` ${c.dim}Restart Claude Code (or run /mcp), then ask: "how long have I coded today?"${c.reset}`);
867
+ console.log(` ${c.green}✓${c.reset} registered the ${c.cyan}claude-rpc${c.reset} MCP server with Claude Code ${c.dim}(scope: ${scope})${c.reset}`);
868
+ console.log(` ${c.dim}restart Claude Code (or run /mcp), then ask: "how long have I coded today?"${c.reset}`);
869
869
  console.log('');
870
870
  } else if (res.reason === 'no-claude') {
871
871
  fail('the `claude` CLI was not found on your PATH', {
@@ -880,7 +880,7 @@ function doMcpInstall(argv) {
880
880
  function doMcpUninstall(argv) {
881
881
  const scope = argv.includes('--project') ? 'project' : argv.includes('--local') ? 'local' : 'user';
882
882
  const res = uninstallMcp({ scope });
883
- if (res.ok) console.log(`${c.green}✓${c.reset} Removed the claude-rpc MCP server (scope: ${scope}).`);
883
+ if (res.ok) console.log(` ${c.green}✓${c.reset} removed the claude-rpc MCP server ${c.dim}(scope: ${scope})${c.reset}`);
884
884
  else if (res.reason === 'no-claude') fail('the `claude` CLI was not found on your PATH', { code: EX_USER_ERROR });
885
885
  else fail('could not remove the MCP server', { hint: 'claude mcp remove claude-rpc', code: EX_USER_ERROR });
886
886
  }
@@ -897,8 +897,8 @@ function doMcpUninstall(argv) {
897
897
  function doPrivate() {
898
898
  const cwd = process.cwd();
899
899
  const list = addPrivateCwd(cwd);
900
- console.log(`${c.green}✓${c.reset} ${c.cyan}${cwd}${c.reset} marked private`);
901
- console.log(`${c.dim} ${list.length} ${list.length === 1 ? 'path' : 'paths'} in the private list. Daemon picks it up within ~5 min (cache TTL) or restart.${c.reset}`);
900
+ console.log(` ${c.green}✓${c.reset} ${c.cyan}${cwd}${c.reset} marked private`);
901
+ console.log(` ${c.dim}${list.length} ${list.length === 1 ? 'path' : 'paths'} in the private list the daemon picks it up within ~5 min, or ${c.reset}${c.cyan}claude-rpc restart${c.reset}`);
902
902
  }
903
903
 
904
904
  function doPublic() {
@@ -906,9 +906,9 @@ function doPublic() {
906
906
  const before = listPrivateCwds().length;
907
907
  const list = removePrivateCwd(cwd);
908
908
  if (list.length === before) {
909
- console.log(`${c.yellow}!${c.reset} ${c.cyan}${cwd}${c.reset} wasn't in the private list`);
909
+ console.log(` ${c.yellow}!${c.reset} ${c.cyan}${cwd}${c.reset} wasn't in the private list`);
910
910
  } else {
911
- console.log(`${c.green}✓${c.reset} ${c.cyan}${cwd}${c.reset} removed from the private list`);
911
+ console.log(` ${c.green}✓${c.reset} ${c.cyan}${cwd}${c.reset} removed from the private list`);
912
912
  }
913
913
  }
914
914
 
@@ -950,8 +950,8 @@ function doPause(argv) {
950
950
  if (arg === 'off' || arg === 'resume') return doResume();
951
951
  if (arg === 'status') {
952
952
  const until = pauseUntil();
953
- if (until) console.log(`${c.yellow}●${c.reset} paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
954
- else console.log(`${c.green}○${c.reset} not paused`);
953
+ if (until) console.log(` ${c.yellow}●${c.reset} paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
954
+ else console.log(` ${c.green}○${c.reset} not paused`);
955
955
  return;
956
956
  }
957
957
  const ms = parseDuration(argv[0]);
@@ -960,17 +960,17 @@ function doPause(argv) {
960
960
  { hint: 'use 30m, 2h, 1h30m, or a bare number of minutes (default: 1h)', code: EX_USER_ERROR });
961
961
  }
962
962
  const until = setPause(ms);
963
- console.log(`${c.green}✓${c.reset} presence paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
964
- console.log(`${c.dim} the daemon clears the card within a few seconds. Resume early with ${c.reset}${c.cyan}claude-rpc resume${c.reset}`);
963
+ console.log(` ${c.green}✓${c.reset} presence paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
964
+ console.log(` ${c.dim}the daemon clears the card within seconds resume early with ${c.reset}${c.cyan}claude-rpc resume${c.reset}`);
965
965
  }
966
966
 
967
967
  function doResume() {
968
968
  const was = pauseUntil();
969
969
  clearPause();
970
970
  if (was) {
971
- console.log(`${c.green}✓${c.reset} presence resumed ${c.dim}(was paused until ${fmtClock(was)})${c.reset}`);
971
+ console.log(` ${c.green}✓${c.reset} presence resumed ${c.dim}(was paused until ${fmtClock(was)})${c.reset}`);
972
972
  } else {
973
- console.log(`${c.dim}presence wasn't paused.${c.reset}`);
973
+ console.log(` ${c.cyan}·${c.reset} presence wasn't paused`);
974
974
  }
975
975
  }
976
976
 
@@ -999,7 +999,7 @@ async function doExport(argv) {
999
999
  }
1000
1000
  if (out) {
1001
1001
  writeFileSync(out, payload);
1002
- console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${out}${c.reset} (${payload.length} bytes, ${csv ? 'CSV' : 'JSON'})`);
1002
+ console.log(` ${c.green}✓${c.reset} wrote ${c.cyan}${out}${c.reset} ${c.dim}(${payload.length} bytes, ${csv ? 'CSV' : 'JSON'})${c.reset}`);
1003
1003
  } else {
1004
1004
  process.stdout.write(payload);
1005
1005
  }
@@ -1018,7 +1018,12 @@ function squadAuth() {
1018
1018
  const cfg = loadConfig();
1019
1019
  const endpoint = (cfg.community?.endpoint || '').replace(/\/+$/, '');
1020
1020
  const instanceId = cfg.community?.instanceId;
1021
- if (!endpoint) fail('no community endpoint configured', { code: EX_BAD_STATE });
1021
+ if (!endpoint) {
1022
+ fail('no community endpoint configured', {
1023
+ hint: 'config.json is missing community.endpoint — re-run `claude-rpc setup` to restore the default',
1024
+ code: EX_BAD_STATE,
1025
+ });
1026
+ }
1022
1027
  if (!instanceId) {
1023
1028
  fail('squads need an identity', {
1024
1029
  hint: 'run `claude-rpc profile set --handle <name> && claude-rpc profile on` first',
@@ -1044,9 +1049,9 @@ function squadPageUrl(id) { return `https://claude-rpc.vercel.app/squad/${id}`;
1044
1049
 
1045
1050
  function printSquadInvite(squad) {
1046
1051
  console.log('');
1047
- console.log(` ${c.green}✓${c.reset} squad ${c.bold}${squad.name}${c.reset}`);
1048
- console.log(` ${c.dim}invite code:${c.reset} ${c.cyan}${squad.code}${c.reset}`);
1049
- console.log(` ${c.dim}standings: ${c.reset} ${c.cyan}${squadPageUrl(squad.id)}${c.reset}`);
1052
+ console.log(` ${c.green}✓${c.reset} squad ${c.bold}${squad.name}${c.reset}`);
1053
+ console.log(` ${c.dim}invite code:${c.reset} ${c.cyan}${squad.code}${c.reset}`);
1054
+ console.log(` ${c.dim}standings: ${c.reset} ${c.cyan}${squadPageUrl(squad.id)}${c.reset}`);
1050
1055
  console.log('');
1051
1056
  console.log(` ${c.dim}send your crew this:${c.reset}`);
1052
1057
  console.log(` join my Claude Code squad "${squad.name}" — npx claude-rpc@latest setup, then:`);
@@ -1083,19 +1088,28 @@ async function doSquadCmd(argv) {
1083
1088
  if (sub === 'status' || sub === '') return squadStatus(ctx);
1084
1089
  if (sub === 'create') {
1085
1090
  const name = argv.slice(1).join(' ').trim();
1086
- if (!name) return fail('usage: claude-rpc squad create <name>', { code: EX_USER_ERROR });
1091
+ if (!name) {
1092
+ return fail('usage: claude-rpc squad create <name>',
1093
+ { hint: 'example: claude-rpc squad create "the night shift"', code: EX_USER_ERROR });
1094
+ }
1087
1095
  const r = await ctx.post('/squad/create', { name });
1088
1096
  if (r.status !== 200) return fail(`create failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
1089
1097
  return printSquadInvite(r.json.squad);
1090
1098
  }
1091
1099
  if (sub === 'join') {
1092
1100
  const code = (argv[1] || '').trim();
1093
- if (!code) return fail('usage: claude-rpc squad join SQ-XXXXXX', { code: EX_USER_ERROR });
1101
+ if (!code) {
1102
+ return fail('usage: claude-rpc squad join SQ-XXXXXX',
1103
+ { hint: 'the invite code comes from whoever created the squad (`claude-rpc squad create`)', code: EX_USER_ERROR });
1104
+ }
1094
1105
  const r = await ctx.post('/squad/join', { code });
1095
- if (r.status !== 200) return fail(`join failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
1106
+ if (r.status !== 200) {
1107
+ return fail(`join failed: ${r.json?.error || r.status}`,
1108
+ { hint: 'double-check the invite code with whoever created the squad', code: EX_SYS_ERROR });
1109
+ }
1096
1110
  const s = r.json.squad;
1097
- console.log(`${c.green}✓${c.reset} ${s.alreadyMember ? 'already in' : 'joined'} ${c.bold}${s.name}${c.reset} (${s.members} member${s.members === 1 ? '' : 's'})`);
1098
- console.log(` ${c.dim}standings: ${squadPageUrl(s.id)} · claude-rpc squad${c.reset}`);
1111
+ console.log(` ${c.green}✓${c.reset} ${s.alreadyMember ? 'already in' : 'joined'} ${c.bold}${s.name}${c.reset} ${c.dim}(${s.members} member${s.members === 1 ? '' : 's'})${c.reset}`);
1112
+ console.log(` ${c.dim}standings: ${squadPageUrl(s.id)} — or run ${c.reset}${c.cyan}claude-rpc squad${c.reset}`);
1099
1113
  return;
1100
1114
  }
1101
1115
  if (sub === 'leave') {
@@ -1114,7 +1128,7 @@ async function doSquadCmd(argv) {
1114
1128
  }
1115
1129
  const r = await ctx.post('/squad/leave', { squadId: target.id });
1116
1130
  if (r.status !== 200) return fail(`leave failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
1117
- console.log(`${c.green}✓${c.reset} left ${c.bold}${target.name}${c.reset}${r.json.dissolved ? ` ${c.dim}(last member — squad dissolved)${c.reset}` : ''}`);
1131
+ console.log(` ${c.green}✓${c.reset} left ${c.bold}${target.name}${c.reset}${r.json.dissolved ? ` ${c.dim}(last member — squad dissolved)${c.reset}` : ''}`);
1118
1132
  return;
1119
1133
  }
1120
1134
  fail(`unknown squad subcommand: ${sub}`, {
@@ -1147,14 +1161,15 @@ async function doLink(argv) {
1147
1161
  }
1148
1162
  const r = await ctx.post('/pair/claim', { code });
1149
1163
  if (r.status !== 200) {
1150
- return fail(`link failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
1164
+ return fail(`link failed: ${r.json?.error || r.status}`,
1165
+ { hint: 'grab a fresh code from https://claude-rpc.vercel.app/squads and try again', code: EX_SYS_ERROR });
1151
1166
  }
1152
1167
  // Mirror the verified identity locally so `profile status` agrees.
1153
1168
  const userCfg = readJson(CONFIG_PATH, {});
1154
1169
  userCfg.profile = { ...(userCfg.profile || {}), githubUser: r.json.githubUser, verified: true };
1155
1170
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1156
- console.log(`${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser.`);
1157
- console.log(` ${c.dim}head back to https://claude-rpc.vercel.app/squads — it picks the link up automatically.${c.reset}`);
1171
+ console.log(` ${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser`);
1172
+ console.log(` ${c.dim}head back to https://claude-rpc.vercel.app/squads — it picks the link up automatically${c.reset}`);
1158
1173
  }
1159
1174
 
1160
1175
  // ── Community totals ─────────────────────────────────────────────────────
@@ -1201,7 +1216,7 @@ async function communityOn() {
1201
1216
  const cfg = loadConfig();
1202
1217
  const community = cfg.community || {};
1203
1218
  if (community.enabled) {
1204
- console.log(`${c.green}✓${c.reset} community totals are already enabled`);
1219
+ console.log(` ${c.green}✓${c.reset} community totals are already enabled`);
1205
1220
  return;
1206
1221
  }
1207
1222
  console.log('');
@@ -1246,12 +1261,12 @@ async function communityOn() {
1246
1261
  function communityOff() {
1247
1262
  const userCfg = readJson(CONFIG_PATH, {});
1248
1263
  if (!userCfg.community?.enabled) {
1249
- console.log(`${c.dim}community totals are already off.${c.reset}`);
1264
+ console.log(` ${c.cyan}·${c.reset} community totals are already off`);
1250
1265
  return;
1251
1266
  }
1252
1267
  userCfg.community = { ...userCfg.community, enabled: false };
1253
1268
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1254
- console.log(`${c.green}✓${c.reset} community totals disabled. instanceId retained for re-enable continuity.`);
1269
+ console.log(` ${c.green}✓${c.reset} community totals disabled ${c.dim}(instanceId retained for re-enable continuity)${c.reset}`);
1255
1270
  }
1256
1271
 
1257
1272
  async function communityReport() {
@@ -1263,11 +1278,11 @@ async function communityReport() {
1263
1278
  const result = await flushCommunity(cfg);
1264
1279
  console.log('');
1265
1280
  if (result.ok && result.delta) {
1266
- console.log(` ${c.green}✓${c.reset} reported ${c.cyan}+${result.delta.sessions} sessions${c.reset} ${c.cyan}+${result.delta.tokens} tokens${c.reset}`);
1281
+ console.log(` ${c.green}✓${c.reset} reported ${c.cyan}+${result.delta.sessions} sessions${c.reset} ${c.cyan}+${result.delta.tokens} tokens${c.reset}`);
1267
1282
  } else if (result.ok) {
1268
- console.log(` ${c.dim}${result.reason}${c.reset}`);
1283
+ console.log(` ${c.cyan}·${c.reset} ${c.dim}${result.reason}${c.reset}`);
1269
1284
  } else {
1270
- console.log(` ${c.yellow}↳${c.reset} flush did not complete ${c.dim}(${result.reason}${result.error ? ': ' + result.error : ''})${c.reset}`);
1285
+ console.log(` ${c.yellow}!${c.reset} flush did not complete ${c.dim}(${result.reason}${result.error ? ': ' + result.error : ''})${c.reset}`);
1271
1286
  }
1272
1287
  console.log('');
1273
1288
  }
@@ -1283,6 +1298,32 @@ function readFlag(argv, name) {
1283
1298
  return eq ? eq.slice(name.length + 3) : undefined;
1284
1299
  }
1285
1300
 
1301
+ // Single source of truth for the profile checklist. `profile`/`profile status`
1302
+ // renders all of it; mutations point at just the first unfinished step.
1303
+ // Verification is `done` whichever way it happened — web pairing (doLink sets
1304
+ // profile.verified) or the gist fallback (profileVerify).
1305
+ function profileSteps(p) {
1306
+ return [
1307
+ { key: 'handle', done: lb.isValidHandle(p.handle), label: 'set a handle', cmd: 'claude-rpc profile set --handle <name>', note: p.handle },
1308
+ { key: 'publish', done: !!p.enabled, label: 'enable publishing', cmd: 'claude-rpc profile on', note: 'daemon republishes automatically' },
1309
+ { key: 'verify', done: !!p.verified, label: 'verify via GitHub', cmd: 'claude-rpc link <code>', note: p.githubUser ? `@${p.githubUser}` : '' },
1310
+ ];
1311
+ }
1312
+
1313
+ // One dim pointer at the next unfinished step — what mutations print instead
1314
+ // of re-rendering the whole dashboard.
1315
+ function profileNextStep() {
1316
+ const p = loadConfig().profile || {};
1317
+ const next = profileSteps(p).find((s) => !s.done);
1318
+ if (!next) {
1319
+ console.log(` ${c.dim}→ all set — you're live at${c.reset} ${c.cyan}https://claude-rpc.vercel.app/u/${encodeURIComponent(p.handle)}${c.reset}`);
1320
+ } else if (next.key === 'verify') {
1321
+ console.log(` ${c.dim}→ next: log in at${c.reset} ${c.cyan}https://claude-rpc.vercel.app/squads${c.reset}${c.dim}, then${c.reset} ${c.cyan}claude-rpc link <code>${c.reset}`);
1322
+ } else {
1323
+ console.log(` ${c.dim}→ next:${c.reset} ${c.cyan}${next.cmd}${c.reset} ${c.dim}(${next.label})${c.reset}`);
1324
+ }
1325
+ }
1326
+
1286
1327
  function profileStatus() {
1287
1328
  const p = (loadConfig().profile) || {};
1288
1329
  const handleOk = lb.isValidHandle(p.handle);
@@ -1307,23 +1348,27 @@ function profileStatus() {
1307
1348
  // Setup checklist — same shape every time, so the user always sees where
1308
1349
  // they are and the exact next command. This is the screen the daemon's
1309
1350
  // breadcrumbs point back to.
1310
- const steps = [
1311
- { done: handleOk, label: 'set a handle', cmd: 'claude-rpc profile set --handle <name>', note: p.handle },
1312
- { done: !!p.enabled, label: 'enable publishing', cmd: 'claude-rpc profile on', note: 'daemon republishes automatically' },
1313
- { done: !!p.verified, label: 'verify on GitHub', cmd: 'claude-rpc profile verify', note: p.githubUser ? `@${p.githubUser}` : '' },
1314
- ];
1351
+ const steps = profileSteps(p);
1315
1352
  if (steps.every((s) => s.done)) {
1316
- console.log(` ${c.green}✓${c.reset} all set — you're live at ${c.cyan}${boardUrl}${c.reset}`);
1353
+ console.log(` ${c.green}✓${c.reset} all set — you're live at ${c.cyan}${boardUrl}${c.reset}`);
1317
1354
  } else {
1318
1355
  const nextIdx = steps.findIndex((s) => !s.done);
1319
- box('next steps', steps.map((s, i) => {
1356
+ const lines = steps.map((s, i) => {
1320
1357
  const mark = s.done ? `${c.green}✓${c.reset}` : (i === nextIdx ? `${c.yellow}○${c.reset}` : `${c.dim}○${c.reset}`);
1321
1358
  const label = s.done ? `${c.dim}${s.label}${c.reset}` : `${c.bold}${s.label}${c.reset}`;
1322
1359
  const tail = s.done
1323
1360
  ? `${c.dim}${s.note || 'done'}${c.reset}`
1324
1361
  : `${c.cyan}${s.cmd}${c.reset}${i === nextIdx ? ` ${c.dim}← next${c.reset}` : ''}`;
1325
1362
  return `${mark} ${i + 1}. ${label}${' '.repeat(Math.max(1, 20 - s.label.length))}${tail}`;
1326
- }));
1363
+ });
1364
+ // Web pairing is the primary verify path; the gist dance stays available
1365
+ // for terminals with no browser nearby.
1366
+ if (!steps[2].done) {
1367
+ lines.push('');
1368
+ lines.push(`${c.dim}the code comes from${c.reset} ${c.cyan}https://claude-rpc.vercel.app/squads${c.reset} ${c.dim}(log in with GitHub)${c.reset}`);
1369
+ lines.push(`${c.dim}no browser? fall back to${c.reset} ${c.cyan}claude-rpc profile verify${c.reset}`);
1370
+ }
1371
+ box('next steps', lines);
1327
1372
  }
1328
1373
  console.log('');
1329
1374
  }
@@ -1356,8 +1401,14 @@ function profileSet(argv) {
1356
1401
 
1357
1402
  userCfg.profile = next;
1358
1403
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1359
- console.log(`${c.green}✓${c.reset} profile saved`);
1360
- profileStatus();
1404
+ // One-line confirmation + a pointer at the next step. The full dashboard
1405
+ // stays behind `claude-rpc profile` — mutations shouldn't re-render it.
1406
+ const saved = [];
1407
+ if (rawHandle !== undefined) saved.push(`handle ${next.handle}`);
1408
+ if (rawName !== undefined) saved.push(`name ${next.displayName || '—'}`);
1409
+ if (rawGh !== undefined) saved.push(`github ${next.githubUser || '—'}`);
1410
+ console.log(` ${c.green}✓${c.reset} profile saved${saved.length ? ` ${c.dim}${saved.join(' · ')}${c.reset}` : ''}`);
1411
+ profileNextStep();
1361
1412
  }
1362
1413
 
1363
1414
  function profileEnable(on) {
@@ -1382,10 +1433,11 @@ function profileEnable(on) {
1382
1433
  }
1383
1434
  userCfg.profile = next;
1384
1435
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1385
- console.log(`${c.green}✓${c.reset} leaderboard publishing ${on ? 'enabled' : 'disabled'}`);
1386
1436
  if (on) {
1387
- console.log(` ${c.dim}publish now with ${c.reset}${c.cyan}claude-rpc profile publish${c.reset}${c.dim} (or wait for the next daemon flush).${c.reset}`);
1388
- profileStatus();
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}`);
1438
+ profileNextStep();
1439
+ } else {
1440
+ console.log(` ${c.green}✓${c.reset} leaderboard publishing disabled`);
1389
1441
  }
1390
1442
  }
1391
1443
 
@@ -1399,12 +1451,12 @@ async function profilePublish() {
1399
1451
  });
1400
1452
  }
1401
1453
  const { flushProfile } = await import('./community.js');
1402
- console.log(`${c.dim}publishing @${cfg.profile.handle} to the board…${c.reset}`);
1454
+ console.log(` ${c.dim}publishing @${cfg.profile.handle} to the board…${c.reset}`);
1403
1455
  const r = await flushProfile(cfg);
1404
1456
  if (r.ok) {
1405
- console.log(`${c.green}✓${c.reset} published — see it at ${c.cyan}https://claude-rpc.vercel.app/u/${encodeURIComponent(cfg.profile.handle)}${c.reset}`);
1457
+ console.log(` ${c.green}✓${c.reset} published — see it at ${c.cyan}https://claude-rpc.vercel.app/u/${encodeURIComponent(cfg.profile.handle)}${c.reset}`);
1406
1458
  } else if (r.reason === 'rate-limited') {
1407
- console.log(`${c.yellow}!${c.reset} rate-limited — already published in the last minute; the board has you.`);
1459
+ console.log(` ${c.yellow}!${c.reset} rate-limited — already published in the last minute; the board has you`);
1408
1460
  } else {
1409
1461
  return fail(`publish failed: ${r.reason}${r.error ? ' (' + r.error + ')' : ''}`, { code: EX_SYS_ERROR });
1410
1462
  }
@@ -1421,13 +1473,18 @@ async function profileVerify() {
1421
1473
  // and publishing that gist already requires gh auth, so the account is
1422
1474
  // known by the time it matters.
1423
1475
  if (!profile.githubUser) {
1424
- console.log(`${c.dim}no --github set — your verified identity will be the account that owns the proof gist.${c.reset}`);
1476
+ console.log(` ${c.dim}no --github set — your verified identity will be the account that owns the proof gist${c.reset}`);
1425
1477
  }
1426
1478
  if (!community.instanceId) {
1427
1479
  return fail('enable the profile first', { hint: 'claude-rpc profile on', code: EX_BAD_STATE });
1428
1480
  }
1429
1481
  const endpoint = (community.endpoint || '').replace(/\/+$/, '');
1430
- if (!endpoint) return fail('no community endpoint configured', { code: EX_BAD_STATE });
1482
+ if (!endpoint) {
1483
+ return fail('no community endpoint configured', {
1484
+ hint: 'config.json is missing community.endpoint — re-run `claude-rpc setup` to restore the default',
1485
+ code: EX_BAD_STATE,
1486
+ });
1487
+ }
1431
1488
 
1432
1489
  const post = async (path, body) => {
1433
1490
  const res = await fetch(endpoint + path, {
@@ -1445,13 +1502,13 @@ async function profileVerify() {
1445
1502
  const { flushProfile } = await import('./community.js');
1446
1503
  await flushProfile(cfg);
1447
1504
  }
1448
- console.log(`${c.dim}requesting a verification token…${c.reset}`);
1505
+ console.log(` ${c.dim}requesting a verification token…${c.reset}`);
1449
1506
  const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser || null });
1450
1507
  if (!start.json?.token) return fail(`verify/start failed: ${start.json?.error || start.status}`, { code: EX_SYS_ERROR });
1451
1508
  const token = start.json.token;
1452
1509
 
1453
1510
  const { publishGistFile } = await import('./gist.js');
1454
- console.log(`${c.dim}publishing a public proof gist…${c.reset}`);
1511
+ console.log(` ${c.dim}publishing a public proof gist…${c.reset}`);
1455
1512
  const gist = await publishGistFile({
1456
1513
  svg: `claude-rpc leaderboard verification\n${token}\n`,
1457
1514
  filename: 'claude-rpc-verify.txt',
@@ -1462,7 +1519,7 @@ async function profileVerify() {
1462
1519
  // Hand the worker the gist ID so it fetches that gist directly (no
1463
1520
  // gist-list lag) and reads the real owner — instant, and the owner becomes
1464
1521
  // the verified identity regardless of what --github was set to.
1465
- console.log(`${c.dim}confirming with the server…${c.reset}`);
1522
+ console.log(` ${c.dim}confirming with the server…${c.reset}`);
1466
1523
  const check = await post('/verify/check', { instanceId: community.instanceId, gistId: gist.id });
1467
1524
  if (check.json?.verified) {
1468
1525
  const who = check.json.githubUser || gist.owner || profile.githubUser;
@@ -1471,13 +1528,13 @@ async function profileVerify() {
1471
1528
  const userCfg = readJson(CONFIG_PATH, {});
1472
1529
  userCfg.profile = { ...(userCfg.profile || {}), ...(who ? { githubUser: who } : {}), verified: true };
1473
1530
  writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
1474
- console.log(`${c.green}✓${c.reset} verified as @${who} — you'll show the ✓ on the board.`);
1531
+ console.log(` ${c.green}✓${c.reset} verified as ${c.cyan}@${who}${c.reset} — you'll show the ✓ on the board`);
1475
1532
  if (who && profile.githubUser && who.toLowerCase() !== profile.githubUser.toLowerCase()) {
1476
- console.log(` ${c.dim}(your gist is owned by @${who}, so the profile now uses that account.)${c.reset}`);
1533
+ console.log(` ${c.dim}(your gist is owned by @${who}, so the profile now uses that account)${c.reset}`);
1477
1534
  }
1478
1535
  } else {
1479
- console.log(`${c.yellow}!${c.reset} not confirmed: ${check.json?.error || check.status}`);
1480
- console.log(` ${c.dim}make sure the gist is public, then re-run ${c.reset}${c.cyan}claude-rpc profile verify${c.reset}${c.dim}.${c.reset}`);
1536
+ console.log(` ${c.yellow}!${c.reset} not confirmed: ${check.json?.error || check.status}`);
1537
+ console.log(` ${c.dim}make sure the gist is public, then re-run ${c.reset}${c.cyan}claude-rpc profile verify${c.reset}`);
1481
1538
  }
1482
1539
  } catch (e) {
1483
1540
  return fail(`verification failed: ${e.message}`, {
@@ -1514,7 +1571,8 @@ async function doCommunity(argv) {
1514
1571
 
1515
1572
  function tailLog() {
1516
1573
  if (!existsSync(LOG_PATH)) {
1517
- console.log(`${c.yellow}No log yet at ${LOG_PATH}${c.reset}`);
1574
+ console.log(` ${c.yellow}!${c.reset} no log yet ${c.dim}${LOG_PATH}${c.reset}`);
1575
+ console.log(` ${c.gray}↳ the daemon creates it on first start: ${c.reset}${c.cyan}claude-rpc start${c.reset}`);
1518
1576
  return;
1519
1577
  }
1520
1578
  // Print the last ~30 lines, then follow.
@@ -1646,18 +1704,19 @@ function help() {
1646
1704
  ['tail', 'Tail the daemon log file'],
1647
1705
  ['daemon', 'Run daemon in foreground (debug)'],
1648
1706
  ];
1707
+ const colW = cmds.reduce((m, [name]) => Math.max(m, name.length), 0);
1649
1708
  console.log('');
1650
1709
  console.log(` ${c.bold}${c.magenta}◆ claude-rpc${c.reset} ${c.dim}— Discord Rich Presence for Claude Code${c.reset}`);
1651
1710
  console.log('');
1652
1711
  console.log(` ${c.dim}Commands:${c.reset}`);
1653
1712
  for (const [name, desc] of cmds) {
1654
- console.log(` ${c.cyan}${name.padEnd(10)}${c.reset} ${desc}`);
1713
+ console.log(` ${c.cyan}${name.padEnd(colW)}${c.reset} ${desc}`);
1655
1714
  }
1656
1715
  console.log('');
1657
1716
  console.log(` ${c.dim}First-time setup:${c.reset}`);
1658
- console.log(` 1. Set ${c.cyan}clientId${c.reset} in ${c.cyan}config.json${c.reset} to your Discord app id.`);
1659
- console.log(` 2. (Optional) Upload art under Rich Presence Art Assets: ${c.cyan}claude${c.reset}, ${c.cyan}working${c.reset}, ${c.cyan}idle${c.reset}, ${c.cyan}thinking${c.reset}.`);
1660
- console.log(` 3. ${c.cyan}npm install${c.reset} && ${c.cyan}claude-rpc setup${c.reset} && ${c.cyan}claude-rpc start${c.reset}.`);
1717
+ console.log(` 1. ${c.cyan}claude-rpc setup${c.reset} wires hooks, seeds config, starts the daemon.`);
1718
+ console.log(` 2. Open Claude Code and send a prompt the card appears in Discord.`);
1719
+ console.log(` 3. ${c.dim}(optional)${c.reset} Use your own Discord app: set ${c.cyan}clientId${c.reset} in ${c.cyan}config.json${c.reset} and upload art under Rich Presence → Art Assets: ${c.cyan}claude${c.reset}, ${c.cyan}working${c.reset}, ${c.cyan}idle${c.reset}, ${c.cyan}thinking${c.reset}.`);
1661
1720
  console.log('');
1662
1721
  console.log(` ${c.dim}Tip: ${c.reset}edit ${c.cyan}config.json${c.reset} to customize rotation frames. Run ${c.cyan}claude-rpc preview${c.reset} to see the result without Discord.`);
1663
1722
  console.log('');
@@ -1686,8 +1745,10 @@ const packagedDefault = IS_PACKAGED && !cmd;
1686
1745
  // startup, install = with) but in practice users expect one command
1687
1746
  // to do everything. Non-Windows: addStartupEntry is a no-op + warning.
1688
1747
  case 'setup':
1689
- case 'install':
1690
- await runInstall({ exePath: EXE_PATH || process.execPath });
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 });
1691
1752
  // Slimmer first run: bring the daemon up now so the card appears
1692
1753
  // immediately, instead of making the user run a separate `start`.
1693
1754
  // Best-effort — a start hiccup must never make `setup` look failed.
@@ -1696,20 +1757,23 @@ const packagedDefault = IS_PACKAGED && !cmd;
1696
1757
  // Our own tree is npm's throwaway _npx cache; launch from the global
1697
1758
  // install setup just promoted to, via the PATH-resolved bin.
1698
1759
  if (!daemonPid()) {
1699
- spawn('claude-rpc', ['daemon'], {
1760
+ const child = spawn('claude-rpc', ['daemon'], {
1700
1761
  detached: true, stdio: 'ignore', windowsHide: true,
1701
1762
  shell: process.platform === 'win32',
1702
- }).unref();
1703
- console.log(`${c.green}✓${c.reset} Daemon launched ${c.dim}logs: ${LOG_PATH}${c.reset}`);
1763
+ });
1764
+ child.unref();
1765
+ console.log(` ${c.green}✓${c.reset} ${'daemon launched'.padEnd(16)}${c.dim}log ${shortPath(LOG_PATH)}${c.reset}`);
1704
1766
  }
1705
1767
  } else {
1706
1768
  startDaemon();
1707
1769
  }
1708
1770
  } catch (e) {
1709
- console.log(`${c.yellow}!${c.reset} Couldn't auto-start the daemon: ${e.message}`);
1710
- console.log(` ${c.dim}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
1771
+ console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
1772
+ console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
1711
1773
  }
1774
+ setupOutro(target);
1712
1775
  break;
1776
+ }
1713
1777
  case 'uninstall': await runUninstall(); break;
1714
1778
  case 'upgrade-config': migrateConfig(); break;
1715
1779
  case 'start': startDaemon(); break;
@@ -1776,19 +1840,19 @@ const packagedDefault = IS_PACKAGED && !cmd;
1776
1840
  try {
1777
1841
  if (kind === 'setup') {
1778
1842
  await runInstall({ exePath: EXE_PATH || process.execPath });
1779
- console.log(` ${c.green}✓${c.reset} config + hooks repaired`);
1843
+ console.log(` ${c.green}✓${c.reset} config + hooks repaired`);
1780
1844
  } else if (kind === 'rescan') {
1781
1845
  doScan(true);
1782
- console.log(` ${c.green}✓${c.reset} aggregate rebuilt from transcripts`);
1846
+ console.log(` ${c.green}✓${c.reset} aggregate rebuilt from transcripts`);
1783
1847
  } else if (kind === 'daemon') {
1784
1848
  restartDaemon();
1785
1849
  restarted = true;
1786
- console.log(` ${c.green}✓${c.reset} daemon (re)starting`);
1850
+ console.log(` ${c.green}✓${c.reset} daemon (re)starting`);
1787
1851
  } else if (kind === 'discord') {
1788
- console.log(` ${c.yellow}!${c.reset} discord IPC is down — open the Discord ${c.bold}desktop${c.reset} app (RPC isn't exposed by the browser client). Not auto-fixable.`);
1852
+ console.log(` ${c.yellow}!${c.reset} discord IPC is down — open the Discord ${c.bold}desktop${c.reset} app (RPC isn't exposed by the browser client). Not auto-fixable.`);
1789
1853
  }
1790
1854
  } catch (e) {
1791
- console.log(` ${c.red}✗${c.reset} ${kind} step failed: ${e.message}`);
1855
+ console.log(` ${c.red}✗${c.reset} ${kind} step failed: ${e.message}`);
1792
1856
  }
1793
1857
  }
1794
1858
  // A 'setup' rewire only takes effect once the daemon reloads, so ensure a
@@ -1805,8 +1869,9 @@ const packagedDefault = IS_PACKAGED && !cmd;
1805
1869
  default: {
1806
1870
  if (packagedDefault) {
1807
1871
  if (!isInstalled()) {
1808
- await runInstall({ exePath: EXE_PATH || process.execPath });
1872
+ const target = await runInstall({ exePath: EXE_PATH || process.execPath });
1809
1873
  startDaemon();
1874
+ setupOutro(target);
1810
1875
  } else {
1811
1876
  // Self-heal an existing install. Two real failure modes this fixes:
1812
1877
  //
@@ -1824,13 +1889,14 @@ const packagedDefault = IS_PACKAGED && !cmd;
1824
1889
  // Refresh hooks against the canonical exe, migrate config blocks,
1825
1890
  // wipe state, restart daemon. Anything the user has customized in
1826
1891
  // config.json is preserved (migrateConfig is non-destructive).
1827
- console.log('Claude RPC is installed. Refreshing…');
1892
+ console.log('');
1893
+ console.log(` ${c.bold}${c.magenta}◆ claude-rpc${c.reset} ${c.dim}— already installed; refreshing hooks + config${c.reset}`);
1828
1894
  try {
1829
1895
  const target = ensureCanonicalExe(process.execPath);
1830
1896
  migrateConfig();
1831
1897
  installHooks(target);
1832
1898
  } catch (e) {
1833
- console.warn(`refresh skipped: ${e.message}`);
1899
+ console.warn(` ${c.yellow}!${c.reset} ${'refresh skipped'.padEnd(16)}${c.dim}${e.message}${c.reset}`);
1834
1900
  }
1835
1901
  const wasRunning = stopDaemon({ quiet: true });
1836
1902
  try { if (existsSync(STATE_PATH)) unlinkSync(STATE_PATH); } catch { /* state.json locked or already gone — next hook will recreate it */ }
@@ -1847,11 +1913,16 @@ const packagedDefault = IS_PACKAGED && !cmd;
1847
1913
  // click with no args" install-and-start flow.
1848
1914
  overview();
1849
1915
  } else {
1850
- // Version in the error line is deliberate: the #1 cause of "unknown
1916
+ // Version in the hint is deliberate: the #1 cause of "unknown
1851
1917
  // command" in the wild is a stale global install resolving instead of
1852
1918
  // the version the user read the docs for. Make the skew visible.
1853
- fail(`unknown command: ${cmd} (claude-rpc v${VERSION})`,
1854
- { hint: 'run `claude-rpc --help` for the full list — if this command should exist, update first: npm install -g claude-rpc@latest', code: EX_USER_ERROR });
1919
+ fail(`unknown command: ${cmd}`, {
1920
+ hint: [
1921
+ 'run `claude-rpc --help` for the full command list',
1922
+ `this install is v${VERSION} — if the docs mention \`${cmd}\`, update first: npm install -g claude-rpc@latest`,
1923
+ ],
1924
+ code: EX_USER_ERROR,
1925
+ });
1855
1926
  }
1856
1927
  }
1857
1928
  }
package/src/doctor.js CHANGED
@@ -50,7 +50,7 @@ export function fixPlan() {
50
50
  }
51
51
 
52
52
  function section(title) {
53
- console.log(`\n${c.bold}${title}${c.reset}`);
53
+ console.log(`\n ${c.bold}${title}${c.reset}`);
54
54
  }
55
55
 
56
56
  // ── individual checks ────────────────────────────────────────────────────
@@ -363,7 +363,8 @@ function checkDataDir() {
363
363
  export function runDoctor() {
364
364
  counters.pass = 0; counters.fail = 0; counters.warn = 0;
365
365
  findings = [];
366
- console.log(`${c.bold}${c.cyan}claude-rpc doctor${c.reset} ${c.dim}— diagnostic checklist${c.reset}`);
366
+ console.log('');
367
+ console.log(` ${c.bold}${c.magenta}◆ doctor${c.reset} ${c.dim}— diagnostic checklist${c.reset}`);
367
368
 
368
369
  section('Runtime');
369
370
  checkNodeVersion();
package/src/install.js CHANGED
@@ -17,10 +17,23 @@ import {
17
17
  } from './paths.js';
18
18
  import { DEFAULT_CONFIG } from './default-config.js';
19
19
  import { VERSION } from './version.js';
20
+ import { c, SYM_OK, SYM_WARN, SYM_FAIL, SYM_INFO, hintLine } from './ui.js';
20
21
 
21
22
  const STARTUP_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
22
23
  const STARTUP_VALUE = 'ClaudeRPC';
23
24
 
25
+ // Setup output is a phased checklist: every row is `sym label detail`, with
26
+ // the label column fixed-width so the detail column lines up across phases.
27
+ // The same rows print standalone (doctor --fix, packaged refresh) and still
28
+ // read fine outside the phased layout.
29
+ const LABEL_W = 16;
30
+ function step(sym, label, detail = '', log = console.log) {
31
+ log(` ${sym} ${label.padEnd(LABEL_W)}${detail ? `${c.dim}${detail}${c.reset}` : ''}`);
32
+ }
33
+ function phase(title) {
34
+ console.log(`\n ${c.bold}${title}${c.reset}`);
35
+ }
36
+
24
37
  const EVENTS = [
25
38
  'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse',
26
39
  'Stop', 'SubagentStop', 'Notification', 'SessionEnd',
@@ -70,7 +83,7 @@ export function installHooks(exePath) {
70
83
  }
71
84
  }
72
85
  writeJson(CLAUDE_SETTINGS, settings);
73
- console.log(` hooks → ${CLAUDE_SETTINGS}`);
86
+ step(SYM_OK, 'hooks wired', `${EVENTS.length} events → ${CLAUDE_SETTINGS}`);
74
87
  }
75
88
 
76
89
  export function uninstallHooks() {
@@ -85,7 +98,7 @@ export function uninstallHooks() {
85
98
  if (settings.hooks[event].length === 0) delete settings.hooks[event];
86
99
  }
87
100
  writeJson(CLAUDE_SETTINGS, settings);
88
- console.log(` hooks removed from ${CLAUDE_SETTINGS}`);
101
+ step(SYM_OK, 'hooks removed', CLAUDE_SETTINGS);
89
102
  }
90
103
 
91
104
  function regCommand(args) {
@@ -106,13 +119,13 @@ export async function addStartupEntry(exePath) {
106
119
  '/d', `"${exePath}" daemon`,
107
120
  '/f',
108
121
  ]);
109
- console.log(` startup HKCU\\...\\Run\\${STARTUP_VALUE}`);
122
+ step(SYM_OK, 'startup entry', `HKCU\\…\\Run\\${STARTUP_VALUE} — daemon starts at login`);
110
123
  }
111
124
 
112
125
  export async function removeStartupEntry() {
113
126
  try {
114
127
  await regCommand(['delete', STARTUP_KEY, '/v', STARTUP_VALUE, '/f']);
115
- console.log(` startup entry removed`);
128
+ step(SYM_OK, 'startup entry', 'removed');
116
129
  } catch {
117
130
  // Already absent — fine.
118
131
  }
@@ -159,7 +172,7 @@ export function ensureCanonicalExe(currentExe) {
159
172
  const src = statSync(currentExe);
160
173
  const dst = statSync(CANONICAL_EXE);
161
174
  if (src.size === dst.size && Math.abs(src.mtimeMs - dst.mtimeMs) < 2000) {
162
- console.log(` exe already installed → ${CANONICAL_EXE}`);
175
+ step(SYM_OK, 'exe installed', `${CANONICAL_EXE} (unchanged)`);
163
176
  return CANONICAL_EXE;
164
177
  }
165
178
  } catch { /* stat failed — fall through to copy attempt */ }
@@ -176,14 +189,13 @@ export function ensureCanonicalExe(currentExe) {
176
189
  }
177
190
  copyFileSync(currentExe, CANONICAL_EXE);
178
191
  if (process.platform !== 'win32') chmodSync(CANONICAL_EXE, 0o755);
179
- console.log(` exe installed → ${CANONICAL_EXE}`);
180
- console.log(` (the copy at ${currentExe} can be safely deleted)`);
192
+ step(SYM_OK, 'exe installed', CANONICAL_EXE);
193
+ step(SYM_INFO, 'original copy', `${currentExe} safe to delete`);
181
194
  sweepStaleCanonicalBackups();
182
195
  return CANONICAL_EXE;
183
196
  } catch (e) {
184
- console.warn(` ! failed to copy exe to ${CANONICAL_EXE}: ${e.message}`);
185
- console.warn(` falling back to ${currentExe} — manual updates that change`);
186
- console.warn(` the exe path may require running 'setup' again.`);
197
+ step(SYM_WARN, 'exe copy failed', `${CANONICAL_EXE}: ${e.message}`, console.warn);
198
+ hintLine(`falling back to ${currentExe} — if that file moves, run \`claude-rpc setup\` again`, process.stderr);
187
199
  return currentExe;
188
200
  }
189
201
  }
@@ -199,17 +211,17 @@ export function seedConfig() {
199
211
  if (!existsSync(CONFIG_PATH) && existsSync(legacyPath)) {
200
212
  mkdirSync(USER_CONFIG_DIR, { recursive: true });
201
213
  copyFileSync(legacyPath, CONFIG_PATH);
202
- console.log(` config migrated → ${CONFIG_PATH}`);
203
- console.log(` (was: ${legacyPath} — safe to delete on next 'npm update')`);
214
+ step(SYM_OK, 'config migrated', CONFIG_PATH);
215
+ step(SYM_INFO, 'legacy copy', `${legacyPath} — safe to delete on the next npm update`);
204
216
  return false;
205
217
  }
206
218
  } catch (e) {
207
- console.warn(` ! legacy-config migration skipped: ${e.message}`);
219
+ step(SYM_WARN, 'config legacy', `migration skipped: ${e.message}`, console.warn);
208
220
  }
209
221
  }
210
222
 
211
223
  if (existsSync(CONFIG_PATH)) {
212
- console.log(` config exists → ${CONFIG_PATH}`);
224
+ step(SYM_OK, 'config found', CONFIG_PATH);
213
225
  return false;
214
226
  }
215
227
  mkdirSync(USER_CONFIG_DIR, { recursive: true });
@@ -221,9 +233,9 @@ export function seedConfig() {
221
233
  seeded.community.instanceId = randomUUID();
222
234
  }
223
235
  writeFileSync(CONFIG_PATH, JSON.stringify(seeded, null, 2));
224
- console.log(` config seeded → ${CONFIG_PATH}`);
236
+ step(SYM_OK, 'config seeded', CONFIG_PATH);
225
237
  if (seeded.community?.enabled && seeded.community.instanceId) {
226
- console.log(` community totals on by default opt out with \`claude-rpc community off\``);
238
+ step(SYM_INFO, 'community', `anonymous totals on by default · opt out: ${c.reset}${c.cyan}claude-rpc community off`);
227
239
  }
228
240
  return true;
229
241
  }
@@ -270,7 +282,7 @@ export function migrateConfig({ silent = false } = {}) {
270
282
  let cfg;
271
283
  try { cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); }
272
284
  catch (e) {
273
- if (!silent) console.warn(` ! could not read config for migration: ${e.message}`);
285
+ if (!silent) step(SYM_WARN, 'config migration', `could not read config: ${e.message}`, console.warn);
274
286
  return false;
275
287
  }
276
288
  if (!cfg || typeof cfg !== 'object') return false;
@@ -359,14 +371,11 @@ export function migrateConfig({ silent = false } = {}) {
359
371
  }
360
372
 
361
373
  if (added.length === 0) {
362
- if (!silent) console.log(` config up to date ${CONFIG_PATH}`);
374
+ if (!silent) step(SYM_OK, 'config current', 'no new defaults to merge');
363
375
  return false;
364
376
  }
365
377
  writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
366
- if (!silent) {
367
- console.log(` config migrated → ${CONFIG_PATH}`);
368
- console.log(` added: ${added.join(', ')}`);
369
- }
378
+ if (!silent) step(SYM_OK, 'config migrated', `added: ${added.join(', ')}`);
370
379
  return true;
371
380
  }
372
381
 
@@ -417,7 +426,7 @@ function verifyHookPipe(exePath) {
417
426
  // Best-effort + loud: a failed -g (perms, offline) returns false so the caller
418
427
  // can stop with the manual command rather than wire a dead hook.
419
428
  function promoteNpxToGlobal() {
420
- console.log(' npx is one-off — installing claude-rpc globally so hooks survive…');
429
+ step(SYM_INFO, 'npx detected', 'one-off cache — installing globally so hooks survive…');
421
430
  const r = spawnSync('npm', ['install', '-g', `claude-rpc@${VERSION}`], {
422
431
  stdio: 'inherit',
423
432
  shell: process.platform === 'win32', // npm is npm.cmd on Windows
@@ -442,72 +451,90 @@ function warnIfStale() {
442
451
  const [l, v] = [num(latest), num(VERSION)];
443
452
  const newer = l[0] !== v[0] ? l[0] > v[0] : l[1] !== v[1] ? l[1] > v[1] : l[2] > v[2];
444
453
  if (newer) {
445
- console.warn(` ! you're running v${VERSION} but v${latest} is published — npx may have served a stale cache.`);
446
- console.warn(` for the newest version, stop here and re-run: npx claude-rpc@latest setup`);
454
+ step(SYM_WARN, 'newer version', `v${latest} is published but this is v${VERSION} — npx may have served a stale cache`, console.warn);
455
+ hintLine('for the newest version, stop here and re-run: npx claude-rpc@latest setup', process.stderr);
447
456
  }
448
457
  } catch { /* offline or npm missing — a version check must never block setup */ }
449
458
  }
450
459
 
451
460
  export async function install({ exePath, withStartup = true } = {}) {
461
+ console.log('');
462
+ console.log(` ${c.bold}${c.magenta}◆ claude-rpc setup${c.reset} ${c.dim}v${VERSION}${c.reset}`);
452
463
  warnIfStale();
453
464
  if (IS_NPX) {
454
465
  if (!promoteNpxToGlobal()) {
455
- console.error('\n ✗ Global install failed. Run this once, then you\'re set:');
456
- console.error(' npm install -g claude-rpc && claude-rpc setup');
466
+ console.error('');
467
+ step(SYM_FAIL, 'global install', 'failed', console.error);
468
+ hintLine('run this once, then you\'re set: npm install -g claude-rpc && claude-rpc setup', process.stderr);
457
469
  const err = new Error('npx self-install failed');
458
470
  err.code = 3; // system error (see exit-code contract)
459
471
  throw err;
460
472
  }
461
- console.log(' claude-rpc installed globally\n');
462
- }
463
- if (process.platform !== 'win32' && withStartup) {
464
- console.warn('Note: startup registration only works on Windows; other steps still run.');
473
+ step(SYM_OK, 'global install', `claude-rpc@${VERSION}`);
465
474
  }
466
475
  const incoming = exePath || process.execPath;
467
476
  // Canonicalize first so hook + startup entries point at a stable location,
468
477
  // not at the temp/Downloads path the user happened to launch from.
478
+ if (IS_PACKAGED) phase('binary');
469
479
  const target = ensureCanonicalExe(incoming);
470
- console.log('Installing Claude RPC…');
480
+
481
+ phase('config');
471
482
  // Order matters: seed creates the file if missing, then migrate fills in
472
483
  // any blocks new exe versions added (e.g. presence.byStatus from v0.3.6).
473
484
  seedConfig();
474
485
  migrateConfig();
475
- installHooks(target);
476
- if (withStartup && process.platform === 'win32') {
477
- try { await addStartupEntry(target); }
478
- catch (e) { console.warn(` startup entry failed: ${e.message}`); }
479
- }
480
486
 
487
+ phase('claude code');
488
+ installHooks(target);
481
489
  // Proof the hook pipe actually fires. A setup that returns success
482
490
  // without verification is a lie — we caught broken-hook-path bugs
483
491
  // twice during v0.3.x because no one ran a real event after install.
484
492
  const probe = verifyHookPipe(target);
485
493
  if (probe.ok) {
486
- console.log(` hook pipe ✓ ${probe.detail}`);
494
+ step(SYM_OK, 'hook verified', probe.detail);
487
495
  } else {
488
- console.warn(` hook pipe ✗ ${probe.detail}`);
489
- console.warn(` ↳ run \`claude-rpc doctor\` for a full diagnostic`);
496
+ step(SYM_FAIL, 'hook verify', probe.detail, console.warn);
497
+ hintLine('run `claude-rpc doctor` for a full diagnostic', process.stderr);
490
498
  }
491
499
 
492
- console.log('\nDone.');
493
- console.log(`Config: ${CONFIG_PATH}`);
494
- console.log(` (a working Discord app is bundled — edit clientId only to use your own)`);
495
- // setup auto-starts the daemon (see cli.js), so we point at management +
496
- // verification rather than a start command. The packaged exe manages the
497
- // daemon via its own subcommands; the npm/dev bin uses `claude-rpc …`.
498
- if (IS_PACKAGED) {
499
- console.log(`\nManage the daemon: "${target}" daemon (start) · check with claude-rpc doctor`);
500
- } else {
501
- console.log(`\nManage the daemon: claude-rpc start | stop | status`);
502
- console.log(`Verify wiring: claude-rpc doctor`);
500
+ // The CLI's setup case launches the daemon right after this returns, so its
501
+ // row lands under this heading; setupOutro() then closes the screen.
502
+ phase('daemon');
503
+ if (withStartup) {
504
+ if (process.platform === 'win32') {
505
+ try { await addStartupEntry(target); }
506
+ catch (e) { step(SYM_WARN, 'startup entry', `failed: ${e.message}`, console.warn); }
507
+ } else {
508
+ step(SYM_INFO, 'startup entry', 'skipped — login autostart is Windows-only');
509
+ }
503
510
  }
511
+ return target;
512
+ }
513
+
514
+ // The single closing block of `claude-rpc setup` — what to do now, where the
515
+ // levers are. Printed by the CLI after the daemon launch so it always lands
516
+ // last; doctor --fix re-runs install() without it.
517
+ export function setupOutro(target) {
518
+ const point = (label, value, note = '') =>
519
+ 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
+ console.log('');
521
+ console.log(` ${SYM_OK} ${c.bold}setup complete${c.reset} — open Claude Code and send a prompt; your card goes live in Discord.`);
522
+ point('verify wiring', 'claude-rpc doctor');
523
+ if (IS_PACKAGED) point('start daemon', `"${target}" daemon`, 'also runs automatically at login');
524
+ else point('manage daemon', 'claude-rpc start · stop · status');
525
+ point('config', CONFIG_PATH, 'a working Discord app is bundled — set clientId only to use your own');
526
+ console.log('');
504
527
  }
505
528
 
506
529
  export async function uninstall() {
507
- console.log('Uninstalling Claude RPC…');
530
+ console.log('');
531
+ console.log(` ${c.bold}${c.magenta}◆ claude-rpc uninstall${c.reset}`);
532
+ console.log('');
508
533
  uninstallHooks();
509
534
  if (process.platform === 'win32') await removeStartupEntry();
510
- console.log('\nDone. (Config at %APPDATA%\\claude-rpc\\ left intact — delete manually if you want.)');
535
+ console.log('');
536
+ console.log(` ${SYM_OK} ${c.bold}uninstalled${c.reset} — config at ${c.cyan}${USER_CONFIG_DIR}${c.reset} ${c.dim}left intact; delete it manually if you want.${c.reset}`);
537
+ console.log('');
511
538
  }
512
539
 
513
540
  export function isInstalled() {
package/src/ui.js CHANGED
@@ -43,12 +43,21 @@ export const EX_USER_ERROR = 1;
43
43
  export const EX_SYS_ERROR = 2;
44
44
  export const EX_BAD_STATE = 3;
45
45
 
46
- // Print a one-line message plus an indented dim hint on the next line.
47
- // Hint is the tired-user safety net: every failure surface tells you
48
- // what to type next. Empty hint omits the second line.
46
+ // Hint lines sit directly under the message they belong to, aligned with the
47
+ // label (the symbol column differs between TTY glyphs and [fail]-style tags).
48
+ const HINT_INDENT = ' '.repeat(TTY ? 5 : 10);
49
+
50
+ export function hintLine(text, stream = process.stdout) {
51
+ stream.write(`${HINT_INDENT}${c.gray}↳ ${text}${c.reset}\n`);
52
+ }
53
+
54
+ // Print a one-line message plus aligned dim hint line(s) below it. A hint is
55
+ // the tired-user safety net: it tells you what to type next. Accepts a single
56
+ // string or an array (one ↳ line each); empty omits them.
49
57
  function withHint(sym, label, hint, stream = process.stdout) {
50
58
  stream.write(` ${sym} ${label}\n`);
51
- if (hint) stream.write(` ${c.gray}↳ ${hint}${c.reset}\n`);
59
+ const hints = Array.isArray(hint) ? hint : (hint ? [hint] : []);
60
+ for (const h of hints) hintLine(h, stream);
52
61
  }
53
62
 
54
63
  export function ok(label, detail = '') {
@@ -63,11 +72,11 @@ export function warn(label, hint = '') {
63
72
  withHint(SYM_WARN, label, hint);
64
73
  }
65
74
 
66
- // Print a failure with a hint and exit with the given code. The hint is
67
- // the difference between a frustrated user and a fixed user always
68
- // supply one when you can, and default it to `claude-rpc doctor` when
69
- // you have no better idea.
70
- export function fail(label, { hint = 'run `claude-rpc doctor` for a full diagnostic', code = EX_USER_ERROR } = {}) {
75
+ // Print a failure with an optional hint and exit with the given code. Hints
76
+ // must be contextual: point at `claude-rpc doctor` only for local wiring or
77
+ // state problems it actually diagnoses for usage errors, remote rejections,
78
+ // and network failures, give a directly useful hint or none at all.
79
+ export function fail(label, { hint = '', code = EX_USER_ERROR } = {}) {
71
80
  withHint(SYM_FAIL, label, hint, process.stderr);
72
81
  process.exit(code);
73
82
  }
@@ -93,7 +102,5 @@ export function check(label, status, detail = '', hint = '') {
93
102
  else sym = SYM_INFO;
94
103
  const tail = detail ? ` ${c.dim}${detail}${c.reset}` : '';
95
104
  process.stdout.write(` ${sym} ${label}${tail}\n`);
96
- if (hint && status !== 'pass') {
97
- process.stdout.write(` ${c.gray}↳ ${hint}${c.reset}\n`);
98
- }
105
+ if (hint && status !== 'pass') hintLine(hint);
99
106
  }
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.0';
14
+ const BAKED = '0.15.1';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {