claude-rpc 0.15.0 → 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 +1 -1
- package/src/cli.js +187 -111
- package/src/doctor.js +3 -2
- package/src/install.js +133 -59
- package/src/ui.js +19 -12
- package/src/version.js +1 -1
package/package.json
CHANGED
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
599
|
+
console.log(` ${c.dim}roots: ${result.dirs.join(', ')}${c.reset}`);
|
|
600
600
|
}
|
|
601
|
-
console.log(
|
|
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
|
|
620
|
+
{ hint: 'check the spelling — relative paths resolve from the current directory' });
|
|
621
621
|
}
|
|
622
|
-
console.log(
|
|
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(
|
|
640
|
-
console.log(
|
|
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(
|
|
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(
|
|
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}
|
|
719
|
-
console.log(`
|
|
720
|
-
if (result.htmlUrl) 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: ${result.htmlUrl}${c.reset}`);
|
|
721
721
|
console.log('');
|
|
722
|
-
console.log(` ${c.dim}
|
|
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(
|
|
757
|
-
console.log(
|
|
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(
|
|
792
|
-
console.log(
|
|
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',
|
|
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(
|
|
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(
|
|
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}
|
|
868
|
-
console.log(`
|
|
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(
|
|
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(
|
|
901
|
-
console.log(
|
|
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(
|
|
909
|
+
console.log(` ${c.yellow}!${c.reset} ${c.cyan}${cwd}${c.reset} wasn't in the private list`);
|
|
910
910
|
} else {
|
|
911
|
-
console.log(
|
|
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(
|
|
954
|
-
else console.log(
|
|
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(
|
|
964
|
-
console.log(
|
|
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(
|
|
971
|
+
console.log(` ${c.green}✓${c.reset} presence resumed ${c.dim}(was paused until ${fmtClock(was)})${c.reset}`);
|
|
972
972
|
} else {
|
|
973
|
-
console.log(
|
|
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(
|
|
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)
|
|
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}
|
|
1048
|
-
console.log(`
|
|
1049
|
-
console.log(`
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
1098
|
-
console.log(`
|
|
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(
|
|
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}`,
|
|
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(
|
|
1157
|
-
console.log(`
|
|
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(
|
|
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(
|
|
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(
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
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.
|
|
1388
|
-
|
|
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
|
+
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(`
|
|
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(
|
|
1480
|
-
console.log(`
|
|
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(
|
|
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(
|
|
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.
|
|
1659
|
-
console.log(` 2.
|
|
1660
|
-
console.log(` 3. ${c.cyan}
|
|
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,11 @@ 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
|
-
|
|
1748
|
+
case 'install': {
|
|
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 });
|
|
1691
1753
|
// Slimmer first run: bring the daemon up now so the card appears
|
|
1692
1754
|
// immediately, instead of making the user run a separate `start`.
|
|
1693
1755
|
// Best-effort — a start hiccup must never make `setup` look failed.
|
|
@@ -1696,22 +1758,29 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1696
1758
|
// Our own tree is npm's throwaway _npx cache; launch from the global
|
|
1697
1759
|
// install setup just promoted to, via the PATH-resolved bin.
|
|
1698
1760
|
if (!daemonPid()) {
|
|
1699
|
-
spawn('claude-rpc', ['daemon'], {
|
|
1761
|
+
const child = spawn('claude-rpc', ['daemon'], {
|
|
1700
1762
|
detached: true, stdio: 'ignore', windowsHide: true,
|
|
1701
1763
|
shell: process.platform === 'win32',
|
|
1702
|
-
})
|
|
1703
|
-
|
|
1764
|
+
});
|
|
1765
|
+
child.unref();
|
|
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}`);
|
|
1704
1769
|
}
|
|
1705
1770
|
} else {
|
|
1706
1771
|
startDaemon();
|
|
1707
1772
|
}
|
|
1708
1773
|
} catch (e) {
|
|
1709
|
-
console.log(
|
|
1710
|
-
console.log(`
|
|
1774
|
+
console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
|
|
1775
|
+
console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
|
|
1711
1776
|
}
|
|
1777
|
+
setupOutro(target, changed);
|
|
1712
1778
|
break;
|
|
1779
|
+
}
|
|
1713
1780
|
case 'uninstall': await runUninstall(); break;
|
|
1714
|
-
case 'upgrade-config':
|
|
1781
|
+
case 'upgrade-config':
|
|
1782
|
+
if (!migrateConfig()) console.log(` ${c.green}✓${c.reset} config already current — nothing to migrate`);
|
|
1783
|
+
break;
|
|
1715
1784
|
case 'start': startDaemon(); break;
|
|
1716
1785
|
case 'stop': stopDaemon(); break;
|
|
1717
1786
|
case 'restart': restartDaemon(); break;
|
|
@@ -1776,19 +1845,19 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1776
1845
|
try {
|
|
1777
1846
|
if (kind === 'setup') {
|
|
1778
1847
|
await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1779
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1848
|
+
console.log(` ${c.green}✓${c.reset} config + hooks repaired`);
|
|
1780
1849
|
} else if (kind === 'rescan') {
|
|
1781
1850
|
doScan(true);
|
|
1782
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1851
|
+
console.log(` ${c.green}✓${c.reset} aggregate rebuilt from transcripts`);
|
|
1783
1852
|
} else if (kind === 'daemon') {
|
|
1784
1853
|
restartDaemon();
|
|
1785
1854
|
restarted = true;
|
|
1786
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1855
|
+
console.log(` ${c.green}✓${c.reset} daemon (re)starting`);
|
|
1787
1856
|
} else if (kind === 'discord') {
|
|
1788
|
-
console.log(` ${c.yellow}!${c.reset}
|
|
1857
|
+
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
1858
|
}
|
|
1790
1859
|
} catch (e) {
|
|
1791
|
-
console.log(` ${c.red}✗${c.reset}
|
|
1860
|
+
console.log(` ${c.red}✗${c.reset} ${kind} step failed: ${e.message}`);
|
|
1792
1861
|
}
|
|
1793
1862
|
}
|
|
1794
1863
|
// A 'setup' rewire only takes effect once the daemon reloads, so ensure a
|
|
@@ -1805,8 +1874,9 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1805
1874
|
default: {
|
|
1806
1875
|
if (packagedDefault) {
|
|
1807
1876
|
if (!isInstalled()) {
|
|
1808
|
-
await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1877
|
+
const { target } = await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1809
1878
|
startDaemon();
|
|
1879
|
+
setupOutro(target);
|
|
1810
1880
|
} else {
|
|
1811
1881
|
// Self-heal an existing install. Two real failure modes this fixes:
|
|
1812
1882
|
//
|
|
@@ -1824,13 +1894,14 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1824
1894
|
// Refresh hooks against the canonical exe, migrate config blocks,
|
|
1825
1895
|
// wipe state, restart daemon. Anything the user has customized in
|
|
1826
1896
|
// config.json is preserved (migrateConfig is non-destructive).
|
|
1827
|
-
console.log('
|
|
1897
|
+
console.log('');
|
|
1898
|
+
console.log(` ${c.bold}${c.magenta}◆ claude-rpc${c.reset} ${c.dim}— already installed; refreshing hooks + config${c.reset}`);
|
|
1828
1899
|
try {
|
|
1829
1900
|
const target = ensureCanonicalExe(process.execPath);
|
|
1830
1901
|
migrateConfig();
|
|
1831
1902
|
installHooks(target);
|
|
1832
1903
|
} catch (e) {
|
|
1833
|
-
console.warn(`refresh skipped
|
|
1904
|
+
console.warn(` ${c.yellow}!${c.reset} ${'refresh skipped'.padEnd(16)}${c.dim}${e.message}${c.reset}`);
|
|
1834
1905
|
}
|
|
1835
1906
|
const wasRunning = stopDaemon({ quiet: true });
|
|
1836
1907
|
try { if (existsSync(STATE_PATH)) unlinkSync(STATE_PATH); } catch { /* state.json locked or already gone — next hook will recreate it */ }
|
|
@@ -1847,11 +1918,16 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1847
1918
|
// click with no args" install-and-start flow.
|
|
1848
1919
|
overview();
|
|
1849
1920
|
} else {
|
|
1850
|
-
// Version in the
|
|
1921
|
+
// Version in the hint is deliberate: the #1 cause of "unknown
|
|
1851
1922
|
// command" in the wild is a stale global install resolving instead of
|
|
1852
1923
|
// the version the user read the docs for. Make the skew visible.
|
|
1853
|
-
fail(`unknown command: ${cmd}
|
|
1854
|
-
|
|
1924
|
+
fail(`unknown command: ${cmd}`, {
|
|
1925
|
+
hint: [
|
|
1926
|
+
'run `claude-rpc --help` for the full command list',
|
|
1927
|
+
`this install is v${VERSION} — if the docs mention \`${cmd}\`, update first: npm install -g claude-rpc@latest`,
|
|
1928
|
+
],
|
|
1929
|
+
code: EX_USER_ERROR,
|
|
1930
|
+
});
|
|
1855
1931
|
}
|
|
1856
1932
|
}
|
|
1857
1933
|
}
|
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(
|
|
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,41 @@ 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
|
+
//
|
|
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.
|
|
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; }
|
|
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
|
+
}
|
|
47
|
+
log(` ${sym} ${label.padEnd(LABEL_W)}${detail ? `${c.dim}${detail}${c.reset}` : ''}`);
|
|
48
|
+
}
|
|
49
|
+
function dirtyStep(sym, label, detail = '', log = console.log) {
|
|
50
|
+
runDirty = true;
|
|
51
|
+
step(sym, label, detail, log);
|
|
52
|
+
}
|
|
53
|
+
function noop(fact) { noopFacts.push(fact); }
|
|
54
|
+
|
|
24
55
|
const EVENTS = [
|
|
25
56
|
'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse',
|
|
26
57
|
'Stop', 'SubagentStop', 'Notification', 'SessionEnd',
|
|
@@ -43,6 +74,7 @@ function isOurHookCommand(cmd) {
|
|
|
43
74
|
|
|
44
75
|
export function installHooks(exePath) {
|
|
45
76
|
const settings = readJson(CLAUDE_SETTINGS, {});
|
|
77
|
+
const before = JSON.stringify(settings.hooks || {});
|
|
46
78
|
settings.hooks = settings.hooks || {};
|
|
47
79
|
// Three modes, three shapes:
|
|
48
80
|
// packaged → `"<exe>" hook <event>` (canonical exe, no node)
|
|
@@ -69,8 +101,13 @@ export function installHooks(exePath) {
|
|
|
69
101
|
bucket.push({ matcher: '', hooks: [{ type: 'command', command: wanted }] });
|
|
70
102
|
}
|
|
71
103
|
}
|
|
104
|
+
if (JSON.stringify(settings.hooks) === before) {
|
|
105
|
+
noop(`hooks wired (${EVENTS.length} events)`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
72
108
|
writeJson(CLAUDE_SETTINGS, settings);
|
|
73
|
-
|
|
109
|
+
dirtyStep(SYM_OK, 'hooks wired', `${EVENTS.length} events → ${CLAUDE_SETTINGS}`);
|
|
110
|
+
return true;
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
export function uninstallHooks() {
|
|
@@ -85,7 +122,7 @@ export function uninstallHooks() {
|
|
|
85
122
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
86
123
|
}
|
|
87
124
|
writeJson(CLAUDE_SETTINGS, settings);
|
|
88
|
-
|
|
125
|
+
step(SYM_OK, 'hooks removed', CLAUDE_SETTINGS);
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
function regCommand(args) {
|
|
@@ -106,13 +143,14 @@ export async function addStartupEntry(exePath) {
|
|
|
106
143
|
'/d', `"${exePath}" daemon`,
|
|
107
144
|
'/f',
|
|
108
145
|
]);
|
|
109
|
-
|
|
146
|
+
if (runDirty) step(SYM_OK, 'startup entry', `HKCU\\…\\Run\\${STARTUP_VALUE} — daemon starts at login`);
|
|
147
|
+
else noop('startup entry present');
|
|
110
148
|
}
|
|
111
149
|
|
|
112
150
|
export async function removeStartupEntry() {
|
|
113
151
|
try {
|
|
114
152
|
await regCommand(['delete', STARTUP_KEY, '/v', STARTUP_VALUE, '/f']);
|
|
115
|
-
|
|
153
|
+
step(SYM_OK, 'startup entry', 'removed');
|
|
116
154
|
} catch {
|
|
117
155
|
// Already absent — fine.
|
|
118
156
|
}
|
|
@@ -159,7 +197,7 @@ export function ensureCanonicalExe(currentExe) {
|
|
|
159
197
|
const src = statSync(currentExe);
|
|
160
198
|
const dst = statSync(CANONICAL_EXE);
|
|
161
199
|
if (src.size === dst.size && Math.abs(src.mtimeMs - dst.mtimeMs) < 2000) {
|
|
162
|
-
|
|
200
|
+
noop('exe current');
|
|
163
201
|
return CANONICAL_EXE;
|
|
164
202
|
}
|
|
165
203
|
} catch { /* stat failed — fall through to copy attempt */ }
|
|
@@ -176,14 +214,13 @@ export function ensureCanonicalExe(currentExe) {
|
|
|
176
214
|
}
|
|
177
215
|
copyFileSync(currentExe, CANONICAL_EXE);
|
|
178
216
|
if (process.platform !== 'win32') chmodSync(CANONICAL_EXE, 0o755);
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
dirtyStep(SYM_OK, 'exe installed', CANONICAL_EXE);
|
|
218
|
+
step(SYM_INFO, 'original copy', `${currentExe} — safe to delete`);
|
|
181
219
|
sweepStaleCanonicalBackups();
|
|
182
220
|
return CANONICAL_EXE;
|
|
183
221
|
} catch (e) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.warn(` the exe path may require running 'setup' again.`);
|
|
222
|
+
step(SYM_WARN, 'exe copy failed', `${CANONICAL_EXE}: ${e.message}`, console.warn);
|
|
223
|
+
hintLine(`falling back to ${currentExe} — if that file moves, run \`claude-rpc setup\` again`, process.stderr);
|
|
187
224
|
return currentExe;
|
|
188
225
|
}
|
|
189
226
|
}
|
|
@@ -199,17 +236,17 @@ export function seedConfig() {
|
|
|
199
236
|
if (!existsSync(CONFIG_PATH) && existsSync(legacyPath)) {
|
|
200
237
|
mkdirSync(USER_CONFIG_DIR, { recursive: true });
|
|
201
238
|
copyFileSync(legacyPath, CONFIG_PATH);
|
|
202
|
-
|
|
203
|
-
|
|
239
|
+
dirtyStep(SYM_OK, 'config migrated', CONFIG_PATH);
|
|
240
|
+
step(SYM_INFO, 'legacy copy', `${legacyPath} — safe to delete on the next npm update`);
|
|
204
241
|
return false;
|
|
205
242
|
}
|
|
206
243
|
} catch (e) {
|
|
207
|
-
|
|
244
|
+
step(SYM_WARN, 'config legacy', `migration skipped: ${e.message}`, console.warn);
|
|
208
245
|
}
|
|
209
246
|
}
|
|
210
247
|
|
|
211
248
|
if (existsSync(CONFIG_PATH)) {
|
|
212
|
-
|
|
249
|
+
noop('config current');
|
|
213
250
|
return false;
|
|
214
251
|
}
|
|
215
252
|
mkdirSync(USER_CONFIG_DIR, { recursive: true });
|
|
@@ -221,9 +258,9 @@ export function seedConfig() {
|
|
|
221
258
|
seeded.community.instanceId = randomUUID();
|
|
222
259
|
}
|
|
223
260
|
writeFileSync(CONFIG_PATH, JSON.stringify(seeded, null, 2));
|
|
224
|
-
|
|
261
|
+
dirtyStep(SYM_OK, 'config seeded', CONFIG_PATH);
|
|
225
262
|
if (seeded.community?.enabled && seeded.community.instanceId) {
|
|
226
|
-
|
|
263
|
+
step(SYM_INFO, 'community', `anonymous totals on by default · opt out: ${c.reset}${c.cyan}claude-rpc community off`);
|
|
227
264
|
}
|
|
228
265
|
return true;
|
|
229
266
|
}
|
|
@@ -270,7 +307,7 @@ export function migrateConfig({ silent = false } = {}) {
|
|
|
270
307
|
let cfg;
|
|
271
308
|
try { cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); }
|
|
272
309
|
catch (e) {
|
|
273
|
-
if (!silent)
|
|
310
|
+
if (!silent) step(SYM_WARN, 'config migration', `could not read config: ${e.message}`, console.warn);
|
|
274
311
|
return false;
|
|
275
312
|
}
|
|
276
313
|
if (!cfg || typeof cfg !== 'object') return false;
|
|
@@ -358,15 +395,9 @@ export function migrateConfig({ silent = false } = {}) {
|
|
|
358
395
|
if (changed) added.push('presence.buttons[] → CTA');
|
|
359
396
|
}
|
|
360
397
|
|
|
361
|
-
if (added.length === 0)
|
|
362
|
-
if (!silent) console.log(` config up to date → ${CONFIG_PATH}`);
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
398
|
+
if (added.length === 0) return false;
|
|
365
399
|
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
|
-
}
|
|
400
|
+
if (!silent) dirtyStep(SYM_OK, 'config migrated', `added: ${added.join(', ')}`);
|
|
370
401
|
return true;
|
|
371
402
|
}
|
|
372
403
|
|
|
@@ -417,12 +448,29 @@ function verifyHookPipe(exePath) {
|
|
|
417
448
|
// Best-effort + loud: a failed -g (perms, offline) returns false so the caller
|
|
418
449
|
// can stop with the manual command rather than wire a dead hook.
|
|
419
450
|
function promoteNpxToGlobal() {
|
|
420
|
-
|
|
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 */ }
|
|
421
462
|
const r = spawnSync('npm', ['install', '-g', `claude-rpc@${VERSION}`], {
|
|
422
|
-
|
|
463
|
+
encoding: 'utf8',
|
|
423
464
|
shell: process.platform === 'win32', // npm is npm.cmd on Windows
|
|
424
465
|
});
|
|
425
|
-
|
|
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;
|
|
426
474
|
}
|
|
427
475
|
|
|
428
476
|
// Best-effort registry check. npx serves stale cached copies without
|
|
@@ -442,72 +490,98 @@ function warnIfStale() {
|
|
|
442
490
|
const [l, v] = [num(latest), num(VERSION)];
|
|
443
491
|
const newer = l[0] !== v[0] ? l[0] > v[0] : l[1] !== v[1] ? l[1] > v[1] : l[2] > v[2];
|
|
444
492
|
if (newer) {
|
|
445
|
-
|
|
446
|
-
|
|
493
|
+
step(SYM_WARN, 'newer version', `v${latest} is published but this is v${VERSION} — npx may have served a stale cache`, console.warn);
|
|
494
|
+
hintLine('for the newest version, stop here and re-run: npx claude-rpc@latest setup', process.stderr);
|
|
447
495
|
}
|
|
448
496
|
} catch { /* offline or npm missing — a version check must never block setup */ }
|
|
449
497
|
}
|
|
450
498
|
|
|
451
499
|
export async function install({ exePath, withStartup = true } = {}) {
|
|
500
|
+
resetRun();
|
|
501
|
+
console.log('');
|
|
502
|
+
console.log(` ${c.bold}${c.magenta}◆ claude-rpc setup${c.reset} ${c.dim}v${VERSION}${c.reset}`);
|
|
452
503
|
warnIfStale();
|
|
453
504
|
if (IS_NPX) {
|
|
454
505
|
if (!promoteNpxToGlobal()) {
|
|
455
|
-
console.error('
|
|
456
|
-
|
|
506
|
+
console.error('');
|
|
507
|
+
step(SYM_FAIL, 'global install', 'failed', console.error);
|
|
508
|
+
hintLine('run this once, then you\'re set: npm install -g claude-rpc && claude-rpc setup', process.stderr);
|
|
457
509
|
const err = new Error('npx self-install failed');
|
|
458
510
|
err.code = 3; // system error (see exit-code contract)
|
|
459
511
|
throw err;
|
|
460
512
|
}
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
if (process.platform !== 'win32' && withStartup) {
|
|
464
|
-
console.warn('Note: startup registration only works on Windows; other steps still run.');
|
|
513
|
+
step(SYM_OK, 'global install', `claude-rpc@${VERSION}`);
|
|
465
514
|
}
|
|
466
515
|
const incoming = exePath || process.execPath;
|
|
467
516
|
// Canonicalize first so hook + startup entries point at a stable location,
|
|
468
517
|
// not at the temp/Downloads path the user happened to launch from.
|
|
518
|
+
if (IS_PACKAGED) phase('binary');
|
|
469
519
|
const target = ensureCanonicalExe(incoming);
|
|
470
|
-
|
|
520
|
+
|
|
521
|
+
phase('config');
|
|
471
522
|
// Order matters: seed creates the file if missing, then migrate fills in
|
|
472
523
|
// any blocks new exe versions added (e.g. presence.byStatus from v0.3.6).
|
|
473
524
|
seedConfig();
|
|
474
525
|
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
526
|
|
|
527
|
+
phase('claude code');
|
|
528
|
+
installHooks(target);
|
|
481
529
|
// Proof the hook pipe actually fires. A setup that returns success
|
|
482
530
|
// without verification is a lie — we caught broken-hook-path bugs
|
|
483
531
|
// twice during v0.3.x because no one ran a real event after install.
|
|
484
532
|
const probe = verifyHookPipe(target);
|
|
485
|
-
if (probe.ok) {
|
|
486
|
-
|
|
533
|
+
if (!probe.ok) {
|
|
534
|
+
step(SYM_FAIL, 'hook verify', probe.detail, console.warn);
|
|
535
|
+
hintLine('run `claude-rpc doctor` for a full diagnostic', process.stderr);
|
|
536
|
+
} else if (runDirty) {
|
|
537
|
+
step(SYM_OK, 'hook verified', probe.detail);
|
|
487
538
|
} else {
|
|
488
|
-
|
|
489
|
-
console.warn(` ↳ run \`claude-rpc doctor\` for a full diagnostic`);
|
|
539
|
+
noop('hook pipe verified');
|
|
490
540
|
}
|
|
491
541
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
console.log(`Verify wiring: claude-rpc doctor`);
|
|
542
|
+
// The CLI's setup case launches the daemon right after this returns, so its
|
|
543
|
+
// row lands under this heading; setupOutro() then closes the screen.
|
|
544
|
+
phase('daemon');
|
|
545
|
+
if (withStartup) {
|
|
546
|
+
if (process.platform === 'win32') {
|
|
547
|
+
try { await addStartupEntry(target); }
|
|
548
|
+
catch (e) { step(SYM_WARN, 'startup entry', `failed: ${e.message}`, console.warn); }
|
|
549
|
+
} else if (runDirty) {
|
|
550
|
+
step(SYM_INFO, 'startup entry', 'skipped — login autostart is Windows-only');
|
|
551
|
+
}
|
|
503
552
|
}
|
|
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 };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// The single closing block of `claude-rpc setup` — what to do now, where the
|
|
561
|
+
// levers are. Printed by the CLI after the daemon launch so it always lands
|
|
562
|
+
// last; doctor --fix re-runs install() without it.
|
|
563
|
+
export function setupOutro(target, changed = true) {
|
|
564
|
+
if (!changed) return;
|
|
565
|
+
const point = (label, value, note = '') =>
|
|
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}` : ''}`);
|
|
567
|
+
console.log('');
|
|
568
|
+
console.log(` ${SYM_OK} ${c.bold}setup complete${c.reset} — open Claude Code and send a prompt; your card goes live in Discord.`);
|
|
569
|
+
point('verify wiring', 'claude-rpc doctor');
|
|
570
|
+
if (IS_PACKAGED) point('start daemon', `"${target}" daemon`, 'also runs automatically at login');
|
|
571
|
+
else point('manage daemon', 'claude-rpc start · stop · status');
|
|
572
|
+
point('config', CONFIG_PATH, 'a working Discord app is bundled — set clientId only to use your own');
|
|
573
|
+
console.log('');
|
|
504
574
|
}
|
|
505
575
|
|
|
506
576
|
export async function uninstall() {
|
|
507
|
-
console.log('
|
|
577
|
+
console.log('');
|
|
578
|
+
console.log(` ${c.bold}${c.magenta}◆ claude-rpc uninstall${c.reset}`);
|
|
579
|
+
console.log('');
|
|
508
580
|
uninstallHooks();
|
|
509
581
|
if (process.platform === 'win32') await removeStartupEntry();
|
|
510
|
-
console.log('
|
|
582
|
+
console.log('');
|
|
583
|
+
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}`);
|
|
584
|
+
console.log('');
|
|
511
585
|
}
|
|
512
586
|
|
|
513
587
|
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
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
export function fail(label, { hint = '
|
|
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
|
}
|