claude-rpc 0.14.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/SECURITY.md +19 -1
- package/package.json +2 -2
- package/src/cli.js +325 -98
- package/src/doctor.js +3 -2
- package/src/install.js +80 -53
- package/src/ui.js +19 -12
- package/src/version.js +1 -1
package/SECURITY.md
CHANGED
|
@@ -124,7 +124,25 @@ Publishes a badge SVG to *your own* GitHub gist via the `gh` CLI or a
|
|
|
124
124
|
`gist.github.com`. Never runs unattended, never on install, never from the
|
|
125
125
|
daemon.
|
|
126
126
|
|
|
127
|
-
### 3c.
|
|
127
|
+
### 3c. Squads & web login — opt-in, profile-derived
|
|
128
|
+
|
|
129
|
+
**Source:** `worker/src/index.js` (+ `worker/src/auth.js`), `src/cli.js`
|
|
130
|
+
(`squad` command), `site/squad.html`. Squads are private mini-leaderboards
|
|
131
|
+
that regroup stats you **already publish** via the opt-in profile — joining
|
|
132
|
+
one sends nothing new from your machine; the worker derives weekly standings
|
|
133
|
+
from the same clamped lifetime totals the public board uses.
|
|
134
|
+
|
|
135
|
+
"Log in with GitHub" on the website is plain OAuth (no scopes — public
|
|
136
|
+
identity only; we never see email or repos). Sessions are stateless signed
|
|
137
|
+
tokens holding **only your public GitHub login**, stored in your browser's
|
|
138
|
+
localStorage and expiring after 7 days. The browser never receives an
|
|
139
|
+
`instanceId` — that remains the CLI's local credential; the worker resolves
|
|
140
|
+
GitHub login → profile via the link your own gist verification created.
|
|
141
|
+
Worker-side storage adds: `gh:<login>` → profile link, `squad:*` membership
|
|
142
|
+
records, and weekly baseline snapshots (auto-expiring). Leaving your last
|
|
143
|
+
squad deletes its record.
|
|
144
|
+
|
|
145
|
+
### 3d. Presence GIF assets — Discord-side only
|
|
128
146
|
|
|
129
147
|
`default-config.js` references `https://cdn.qualit.ly/clawd-*.gif`. These URLs
|
|
130
148
|
are handed to Discord as image keys; **Discord's** client fetches them to render
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-rpc",
|
|
3
|
-
"version": "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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dist:mac": "npm run build:exe && npm run prep:dashboard && npm --prefix dashboard run dist:mac",
|
|
33
33
|
"dist:win": "npm run build:exe && npm run prep:dashboard && npm --prefix dashboard run dist:win",
|
|
34
34
|
"test": "node --test test/*.test.js",
|
|
35
|
-
"lint": "eslint src test",
|
|
35
|
+
"lint": "eslint src test vscode-extension",
|
|
36
36
|
"format": "prettier --write \"src/**/*.js\" \"test/**/*.js\"",
|
|
37
37
|
"format:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\"",
|
|
38
38
|
"typecheck": "tsc -p jsconfig.json"
|
package/src/cli.js
CHANGED
|
@@ -12,10 +12,10 @@ if (process.platform === 'win32' && process.stdout.isTTY) {
|
|
|
12
12
|
}
|
|
13
13
|
import { DAEMON_SCRIPT, PID_PATH, STATE_PATH, LOG_PATH, AGGREGATE_PATH, CONFIG_PATH, IS_PACKAGED, IS_NPX, EXE_PATH, CANONICAL_EXE } from './paths.js';
|
|
14
14
|
import { readState } from './state.js';
|
|
15
|
-
import { buildVars, fillTemplate, humanProject, humanTool, applyIdle, framePasses } from './format.js';
|
|
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,12 +999,179 @@ 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
|
}
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
+
// ── Squads — private mini-leaderboards (terminal parity for the web UI) ───
|
|
1009
|
+
//
|
|
1010
|
+
// `claude-rpc squad create <name>` → invite code + link
|
|
1011
|
+
// `claude-rpc squad join <code>`
|
|
1012
|
+
// `claude-rpc squad` / `squad status` → standings for every squad you're in
|
|
1013
|
+
// `claude-rpc squad leave [id]`
|
|
1014
|
+
// The web flow (claude-rpc.vercel.app + GitHub login) drives the same worker
|
|
1015
|
+
// endpoints; the CLI authenticates with the community instanceId it already has.
|
|
1016
|
+
|
|
1017
|
+
function squadAuth() {
|
|
1018
|
+
const cfg = loadConfig();
|
|
1019
|
+
const endpoint = (cfg.community?.endpoint || '').replace(/\/+$/, '');
|
|
1020
|
+
const instanceId = cfg.community?.instanceId;
|
|
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
|
+
}
|
|
1027
|
+
if (!instanceId) {
|
|
1028
|
+
fail('squads need an identity', {
|
|
1029
|
+
hint: 'run `claude-rpc profile set --handle <name> && claude-rpc profile on` first',
|
|
1030
|
+
code: EX_BAD_STATE,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const post = async (path, body) => {
|
|
1034
|
+
const res = await fetch(endpoint + path, {
|
|
1035
|
+
method: 'POST',
|
|
1036
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1037
|
+
body: JSON.stringify({ instanceId, ...body }),
|
|
1038
|
+
});
|
|
1039
|
+
return { status: res.status, json: await res.json().catch(() => ({})) };
|
|
1040
|
+
};
|
|
1041
|
+
const get = async (path) => {
|
|
1042
|
+
const res = await fetch(endpoint + path);
|
|
1043
|
+
return { status: res.status, json: await res.json().catch(() => ({})) };
|
|
1044
|
+
};
|
|
1045
|
+
return { cfg, endpoint, instanceId, post, get };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function squadPageUrl(id) { return `https://claude-rpc.vercel.app/squad/${id}`; }
|
|
1049
|
+
|
|
1050
|
+
function printSquadInvite(squad) {
|
|
1051
|
+
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}`);
|
|
1055
|
+
console.log('');
|
|
1056
|
+
console.log(` ${c.dim}send your crew this:${c.reset}`);
|
|
1057
|
+
console.log(` join my Claude Code squad "${squad.name}" — npx claude-rpc@latest setup, then:`);
|
|
1058
|
+
console.log(` ${c.cyan}claude-rpc squad join ${squad.code}${c.reset} ${c.dim}(or join in the browser: ${squadPageUrl(squad.id)})${c.reset}`);
|
|
1059
|
+
console.log('');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function squadStatus({ post, get }) {
|
|
1063
|
+
const mine = await post('/squads/mine', {});
|
|
1064
|
+
if (!mine.json?.squads) return fail(`could not load squads: ${mine.json?.error || mine.status}`, { code: EX_SYS_ERROR });
|
|
1065
|
+
if (!mine.json.squads.length) {
|
|
1066
|
+
console.log('');
|
|
1067
|
+
console.log(` ${c.dim}no squads yet — start one:${c.reset} ${c.cyan}claude-rpc squad create "the night shift"${c.reset}`);
|
|
1068
|
+
console.log('');
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
for (const s of mine.json.squads) {
|
|
1072
|
+
const r = await get(`/squad?id=${encodeURIComponent(s.id)}`);
|
|
1073
|
+
const standings = r.json?.standings || [];
|
|
1074
|
+
const lines = standings.map((row) => {
|
|
1075
|
+
const who = `${row.displayName || '@' + row.handle}${row.verified ? ' ✓' : ''}${row.owner ? ` ${c.dim}(owner)${c.reset}` : ''}`;
|
|
1076
|
+
return `${c.bold}#${row.rank}${c.reset} ${who.padEnd(28)} ${c.cyan}${fmtNum(row.weekTokens)}${c.reset} ${c.dim}this week${c.reset} · ${fmtNum(row.tokens)} ${c.dim}lifetime${c.reset}`;
|
|
1077
|
+
});
|
|
1078
|
+
lines.push('');
|
|
1079
|
+
lines.push(`${c.dim}week ${r.json?.squad?.week || ''} · invite ${c.reset}${c.cyan}${s.code}${c.reset}${c.dim} · ${squadPageUrl(s.id)}${c.reset}`);
|
|
1080
|
+
box(`${s.name} (${s.members})`, lines, 70);
|
|
1081
|
+
console.log('');
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async function doSquadCmd(argv) {
|
|
1086
|
+
const sub = (argv[0] || 'status').toLowerCase();
|
|
1087
|
+
const ctx = squadAuth();
|
|
1088
|
+
if (sub === 'status' || sub === '') return squadStatus(ctx);
|
|
1089
|
+
if (sub === 'create') {
|
|
1090
|
+
const name = argv.slice(1).join(' ').trim();
|
|
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
|
+
}
|
|
1095
|
+
const r = await ctx.post('/squad/create', { name });
|
|
1096
|
+
if (r.status !== 200) return fail(`create failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
1097
|
+
return printSquadInvite(r.json.squad);
|
|
1098
|
+
}
|
|
1099
|
+
if (sub === 'join') {
|
|
1100
|
+
const code = (argv[1] || '').trim();
|
|
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
|
+
}
|
|
1105
|
+
const r = await ctx.post('/squad/join', { code });
|
|
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
|
+
}
|
|
1110
|
+
const s = r.json.squad;
|
|
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}`);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (sub === 'leave') {
|
|
1116
|
+
const mine = await ctx.post('/squads/mine', {});
|
|
1117
|
+
const squads = mine.json?.squads || [];
|
|
1118
|
+
if (!squads.length) return fail('you are not in any squads', { code: EX_BAD_STATE });
|
|
1119
|
+
let target = null;
|
|
1120
|
+
const wanted = (argv[1] || '').toLowerCase();
|
|
1121
|
+
if (wanted) target = squads.find((s) => s.id === wanted || s.name.toLowerCase() === wanted);
|
|
1122
|
+
else if (squads.length === 1) target = squads[0];
|
|
1123
|
+
if (!target) {
|
|
1124
|
+
return fail(squads.length > 1 && !wanted ? 'you are in several squads — name one' : `no squad matching "${argv[1]}"`, {
|
|
1125
|
+
hint: `claude-rpc squad leave <id|name> — yours: ${squads.map((s) => `${s.name} (${s.id})`).join(', ')}`,
|
|
1126
|
+
code: EX_USER_ERROR,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
const r = await ctx.post('/squad/leave', { squadId: target.id });
|
|
1130
|
+
if (r.status !== 200) return fail(`leave failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
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}` : ''}`);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
fail(`unknown squad subcommand: ${sub}`, {
|
|
1135
|
+
hint: 'try: squad [status|create <name>|join <code>|leave [id]]',
|
|
1136
|
+
code: EX_USER_ERROR,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ── Link (CLI ↔ web pairing) ─────────────────────────────────────────────
|
|
1141
|
+
//
|
|
1142
|
+
// `claude-rpc link <code>` — the code comes from claude-rpc.vercel.app/squads
|
|
1143
|
+
// while logged in with GitHub. Claims it against this install's instanceId,
|
|
1144
|
+
// which verifies the profile (✓) and unlocks managing squads from the
|
|
1145
|
+
// browser. Replaces the gist dance for anyone who uses the website.
|
|
1146
|
+
|
|
1147
|
+
async function doLink(argv) {
|
|
1148
|
+
const code = (argv[0] || '').trim();
|
|
1149
|
+
if (!code) {
|
|
1150
|
+
fail('usage: claude-rpc link <code>', {
|
|
1151
|
+
hint: 'log in at https://claude-rpc.vercel.app/squads — it shows you the code',
|
|
1152
|
+
code: EX_USER_ERROR,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
const ctx = squadAuth();
|
|
1156
|
+
// Make sure the profile row exists server-side before claiming — same
|
|
1157
|
+
// pre-publish profileVerify does, so link works on a fresh `profile on`.
|
|
1158
|
+
if (lb.profileIsPublishable(ctx.cfg.profile || {})) {
|
|
1159
|
+
const { flushProfile } = await import('./community.js');
|
|
1160
|
+
await flushProfile(ctx.cfg);
|
|
1161
|
+
}
|
|
1162
|
+
const r = await ctx.post('/pair/claim', { code });
|
|
1163
|
+
if (r.status !== 200) {
|
|
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 });
|
|
1166
|
+
}
|
|
1167
|
+
// Mirror the verified identity locally so `profile status` agrees.
|
|
1168
|
+
const userCfg = readJson(CONFIG_PATH, {});
|
|
1169
|
+
userCfg.profile = { ...(userCfg.profile || {}), githubUser: r.json.githubUser, verified: true };
|
|
1170
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
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}`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1008
1175
|
// ── Community totals ─────────────────────────────────────────────────────
|
|
1009
1176
|
//
|
|
1010
1177
|
// `claude-rpc community` → show current state + endpoint
|
|
@@ -1049,7 +1216,7 @@ async function communityOn() {
|
|
|
1049
1216
|
const cfg = loadConfig();
|
|
1050
1217
|
const community = cfg.community || {};
|
|
1051
1218
|
if (community.enabled) {
|
|
1052
|
-
console.log(
|
|
1219
|
+
console.log(` ${c.green}✓${c.reset} community totals are already enabled`);
|
|
1053
1220
|
return;
|
|
1054
1221
|
}
|
|
1055
1222
|
console.log('');
|
|
@@ -1094,12 +1261,12 @@ async function communityOn() {
|
|
|
1094
1261
|
function communityOff() {
|
|
1095
1262
|
const userCfg = readJson(CONFIG_PATH, {});
|
|
1096
1263
|
if (!userCfg.community?.enabled) {
|
|
1097
|
-
console.log(
|
|
1264
|
+
console.log(` ${c.cyan}·${c.reset} community totals are already off`);
|
|
1098
1265
|
return;
|
|
1099
1266
|
}
|
|
1100
1267
|
userCfg.community = { ...userCfg.community, enabled: false };
|
|
1101
1268
|
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1102
|
-
console.log(
|
|
1269
|
+
console.log(` ${c.green}✓${c.reset} community totals disabled ${c.dim}(instanceId retained for re-enable continuity)${c.reset}`);
|
|
1103
1270
|
}
|
|
1104
1271
|
|
|
1105
1272
|
async function communityReport() {
|
|
@@ -1111,11 +1278,11 @@ async function communityReport() {
|
|
|
1111
1278
|
const result = await flushCommunity(cfg);
|
|
1112
1279
|
console.log('');
|
|
1113
1280
|
if (result.ok && result.delta) {
|
|
1114
|
-
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}`);
|
|
1115
1282
|
} else if (result.ok) {
|
|
1116
|
-
console.log(` ${c.dim}${result.reason}${c.reset}`);
|
|
1283
|
+
console.log(` ${c.cyan}·${c.reset} ${c.dim}${result.reason}${c.reset}`);
|
|
1117
1284
|
} else {
|
|
1118
|
-
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}`);
|
|
1119
1286
|
}
|
|
1120
1287
|
console.log('');
|
|
1121
1288
|
}
|
|
@@ -1131,6 +1298,32 @@ function readFlag(argv, name) {
|
|
|
1131
1298
|
return eq ? eq.slice(name.length + 3) : undefined;
|
|
1132
1299
|
}
|
|
1133
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
|
+
|
|
1134
1327
|
function profileStatus() {
|
|
1135
1328
|
const p = (loadConfig().profile) || {};
|
|
1136
1329
|
const handleOk = lb.isValidHandle(p.handle);
|
|
@@ -1155,23 +1348,27 @@ function profileStatus() {
|
|
|
1155
1348
|
// Setup checklist — same shape every time, so the user always sees where
|
|
1156
1349
|
// they are and the exact next command. This is the screen the daemon's
|
|
1157
1350
|
// breadcrumbs point back to.
|
|
1158
|
-
const steps =
|
|
1159
|
-
{ done: handleOk, label: 'set a handle', cmd: 'claude-rpc profile set --handle <name>', note: p.handle },
|
|
1160
|
-
{ done: !!p.enabled, label: 'enable publishing', cmd: 'claude-rpc profile on', note: 'daemon republishes automatically' },
|
|
1161
|
-
{ done: !!p.verified, label: 'verify on GitHub', cmd: 'claude-rpc profile verify', note: p.githubUser ? `@${p.githubUser}` : '' },
|
|
1162
|
-
];
|
|
1351
|
+
const steps = profileSteps(p);
|
|
1163
1352
|
if (steps.every((s) => s.done)) {
|
|
1164
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1353
|
+
console.log(` ${c.green}✓${c.reset} all set — you're live at ${c.cyan}${boardUrl}${c.reset}`);
|
|
1165
1354
|
} else {
|
|
1166
1355
|
const nextIdx = steps.findIndex((s) => !s.done);
|
|
1167
|
-
|
|
1356
|
+
const lines = steps.map((s, i) => {
|
|
1168
1357
|
const mark = s.done ? `${c.green}✓${c.reset}` : (i === nextIdx ? `${c.yellow}○${c.reset}` : `${c.dim}○${c.reset}`);
|
|
1169
1358
|
const label = s.done ? `${c.dim}${s.label}${c.reset}` : `${c.bold}${s.label}${c.reset}`;
|
|
1170
1359
|
const tail = s.done
|
|
1171
1360
|
? `${c.dim}${s.note || 'done'}${c.reset}`
|
|
1172
1361
|
: `${c.cyan}${s.cmd}${c.reset}${i === nextIdx ? ` ${c.dim}← next${c.reset}` : ''}`;
|
|
1173
1362
|
return `${mark} ${i + 1}. ${label}${' '.repeat(Math.max(1, 20 - s.label.length))}${tail}`;
|
|
1174
|
-
})
|
|
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);
|
|
1175
1372
|
}
|
|
1176
1373
|
console.log('');
|
|
1177
1374
|
}
|
|
@@ -1204,8 +1401,14 @@ function profileSet(argv) {
|
|
|
1204
1401
|
|
|
1205
1402
|
userCfg.profile = next;
|
|
1206
1403
|
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1207
|
-
|
|
1208
|
-
|
|
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();
|
|
1209
1412
|
}
|
|
1210
1413
|
|
|
1211
1414
|
function profileEnable(on) {
|
|
@@ -1230,10 +1433,11 @@ function profileEnable(on) {
|
|
|
1230
1433
|
}
|
|
1231
1434
|
userCfg.profile = next;
|
|
1232
1435
|
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1233
|
-
console.log(`${c.green}✓${c.reset} leaderboard publishing ${on ? 'enabled' : 'disabled'}`);
|
|
1234
1436
|
if (on) {
|
|
1235
|
-
console.log(` ${c.dim}
|
|
1236
|
-
|
|
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`);
|
|
1237
1441
|
}
|
|
1238
1442
|
}
|
|
1239
1443
|
|
|
@@ -1247,12 +1451,12 @@ async function profilePublish() {
|
|
|
1247
1451
|
});
|
|
1248
1452
|
}
|
|
1249
1453
|
const { flushProfile } = await import('./community.js');
|
|
1250
|
-
console.log(
|
|
1454
|
+
console.log(` ${c.dim}publishing @${cfg.profile.handle} to the board…${c.reset}`);
|
|
1251
1455
|
const r = await flushProfile(cfg);
|
|
1252
1456
|
if (r.ok) {
|
|
1253
|
-
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}`);
|
|
1254
1458
|
} else if (r.reason === 'rate-limited') {
|
|
1255
|
-
console.log(
|
|
1459
|
+
console.log(` ${c.yellow}!${c.reset} rate-limited — already published in the last minute; the board has you`);
|
|
1256
1460
|
} else {
|
|
1257
1461
|
return fail(`publish failed: ${r.reason}${r.error ? ' (' + r.error + ')' : ''}`, { code: EX_SYS_ERROR });
|
|
1258
1462
|
}
|
|
@@ -1269,13 +1473,18 @@ async function profileVerify() {
|
|
|
1269
1473
|
// and publishing that gist already requires gh auth, so the account is
|
|
1270
1474
|
// known by the time it matters.
|
|
1271
1475
|
if (!profile.githubUser) {
|
|
1272
|
-
console.log(
|
|
1476
|
+
console.log(` ${c.dim}no --github set — your verified identity will be the account that owns the proof gist${c.reset}`);
|
|
1273
1477
|
}
|
|
1274
1478
|
if (!community.instanceId) {
|
|
1275
1479
|
return fail('enable the profile first', { hint: 'claude-rpc profile on', code: EX_BAD_STATE });
|
|
1276
1480
|
}
|
|
1277
1481
|
const endpoint = (community.endpoint || '').replace(/\/+$/, '');
|
|
1278
|
-
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
|
+
}
|
|
1279
1488
|
|
|
1280
1489
|
const post = async (path, body) => {
|
|
1281
1490
|
const res = await fetch(endpoint + path, {
|
|
@@ -1293,13 +1502,13 @@ async function profileVerify() {
|
|
|
1293
1502
|
const { flushProfile } = await import('./community.js');
|
|
1294
1503
|
await flushProfile(cfg);
|
|
1295
1504
|
}
|
|
1296
|
-
console.log(
|
|
1505
|
+
console.log(` ${c.dim}requesting a verification token…${c.reset}`);
|
|
1297
1506
|
const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser || null });
|
|
1298
1507
|
if (!start.json?.token) return fail(`verify/start failed: ${start.json?.error || start.status}`, { code: EX_SYS_ERROR });
|
|
1299
1508
|
const token = start.json.token;
|
|
1300
1509
|
|
|
1301
1510
|
const { publishGistFile } = await import('./gist.js');
|
|
1302
|
-
console.log(
|
|
1511
|
+
console.log(` ${c.dim}publishing a public proof gist…${c.reset}`);
|
|
1303
1512
|
const gist = await publishGistFile({
|
|
1304
1513
|
svg: `claude-rpc leaderboard verification\n${token}\n`,
|
|
1305
1514
|
filename: 'claude-rpc-verify.txt',
|
|
@@ -1310,7 +1519,7 @@ async function profileVerify() {
|
|
|
1310
1519
|
// Hand the worker the gist ID so it fetches that gist directly (no
|
|
1311
1520
|
// gist-list lag) and reads the real owner — instant, and the owner becomes
|
|
1312
1521
|
// the verified identity regardless of what --github was set to.
|
|
1313
|
-
console.log(
|
|
1522
|
+
console.log(` ${c.dim}confirming with the server…${c.reset}`);
|
|
1314
1523
|
const check = await post('/verify/check', { instanceId: community.instanceId, gistId: gist.id });
|
|
1315
1524
|
if (check.json?.verified) {
|
|
1316
1525
|
const who = check.json.githubUser || gist.owner || profile.githubUser;
|
|
@@ -1319,13 +1528,13 @@ async function profileVerify() {
|
|
|
1319
1528
|
const userCfg = readJson(CONFIG_PATH, {});
|
|
1320
1529
|
userCfg.profile = { ...(userCfg.profile || {}), ...(who ? { githubUser: who } : {}), verified: true };
|
|
1321
1530
|
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1322
|
-
console.log(
|
|
1531
|
+
console.log(` ${c.green}✓${c.reset} verified as ${c.cyan}@${who}${c.reset} — you'll show the ✓ on the board`);
|
|
1323
1532
|
if (who && profile.githubUser && who.toLowerCase() !== profile.githubUser.toLowerCase()) {
|
|
1324
|
-
console.log(`
|
|
1533
|
+
console.log(` ${c.dim}(your gist is owned by @${who}, so the profile now uses that account)${c.reset}`);
|
|
1325
1534
|
}
|
|
1326
1535
|
} else {
|
|
1327
|
-
console.log(
|
|
1328
|
-
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}`);
|
|
1329
1538
|
}
|
|
1330
1539
|
} catch (e) {
|
|
1331
1540
|
return fail(`verification failed: ${e.message}`, {
|
|
@@ -1362,7 +1571,8 @@ async function doCommunity(argv) {
|
|
|
1362
1571
|
|
|
1363
1572
|
function tailLog() {
|
|
1364
1573
|
if (!existsSync(LOG_PATH)) {
|
|
1365
|
-
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}`);
|
|
1366
1576
|
return;
|
|
1367
1577
|
}
|
|
1368
1578
|
// Print the last ~30 lines, then follow.
|
|
@@ -1488,22 +1698,25 @@ function help() {
|
|
|
1488
1698
|
['privacy', 'Show resolved visibility for the current directory'],
|
|
1489
1699
|
['community', 'Opt in/out of anonymous community totals (on|off|status|report)'],
|
|
1490
1700
|
['profile', 'Public leaderboard identity (status|set|on|off|publish|verify)'],
|
|
1701
|
+
['squad', 'Private mini-leaderboards with friends (create|join|leave|status)'],
|
|
1702
|
+
['link', 'Pair this install with your web login (code from /squads page)'],
|
|
1491
1703
|
['doctor', 'Run a diagnostic checklist — common-failure triage (--fix to auto-repair)'],
|
|
1492
1704
|
['tail', 'Tail the daemon log file'],
|
|
1493
1705
|
['daemon', 'Run daemon in foreground (debug)'],
|
|
1494
1706
|
];
|
|
1707
|
+
const colW = cmds.reduce((m, [name]) => Math.max(m, name.length), 0);
|
|
1495
1708
|
console.log('');
|
|
1496
1709
|
console.log(` ${c.bold}${c.magenta}◆ claude-rpc${c.reset} ${c.dim}— Discord Rich Presence for Claude Code${c.reset}`);
|
|
1497
1710
|
console.log('');
|
|
1498
1711
|
console.log(` ${c.dim}Commands:${c.reset}`);
|
|
1499
1712
|
for (const [name, desc] of cmds) {
|
|
1500
|
-
console.log(` ${c.cyan}${name.padEnd(
|
|
1713
|
+
console.log(` ${c.cyan}${name.padEnd(colW)}${c.reset} ${desc}`);
|
|
1501
1714
|
}
|
|
1502
1715
|
console.log('');
|
|
1503
1716
|
console.log(` ${c.dim}First-time setup:${c.reset}`);
|
|
1504
|
-
console.log(` 1.
|
|
1505
|
-
console.log(` 2.
|
|
1506
|
-
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}.`);
|
|
1507
1720
|
console.log('');
|
|
1508
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.`);
|
|
1509
1722
|
console.log('');
|
|
@@ -1532,8 +1745,10 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1532
1745
|
// startup, install = with) but in practice users expect one command
|
|
1533
1746
|
// to do everything. Non-Windows: addStartupEntry is a no-op + warning.
|
|
1534
1747
|
case 'setup':
|
|
1535
|
-
case 'install':
|
|
1536
|
-
|
|
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 });
|
|
1537
1752
|
// Slimmer first run: bring the daemon up now so the card appears
|
|
1538
1753
|
// immediately, instead of making the user run a separate `start`.
|
|
1539
1754
|
// Best-effort — a start hiccup must never make `setup` look failed.
|
|
@@ -1542,20 +1757,23 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1542
1757
|
// Our own tree is npm's throwaway _npx cache; launch from the global
|
|
1543
1758
|
// install setup just promoted to, via the PATH-resolved bin.
|
|
1544
1759
|
if (!daemonPid()) {
|
|
1545
|
-
spawn('claude-rpc', ['daemon'], {
|
|
1760
|
+
const child = spawn('claude-rpc', ['daemon'], {
|
|
1546
1761
|
detached: true, stdio: 'ignore', windowsHide: true,
|
|
1547
1762
|
shell: process.platform === 'win32',
|
|
1548
|
-
})
|
|
1549
|
-
|
|
1763
|
+
});
|
|
1764
|
+
child.unref();
|
|
1765
|
+
console.log(` ${c.green}✓${c.reset} ${'daemon launched'.padEnd(16)}${c.dim}log ${shortPath(LOG_PATH)}${c.reset}`);
|
|
1550
1766
|
}
|
|
1551
1767
|
} else {
|
|
1552
1768
|
startDaemon();
|
|
1553
1769
|
}
|
|
1554
1770
|
} catch (e) {
|
|
1555
|
-
console.log(
|
|
1556
|
-
console.log(`
|
|
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}`);
|
|
1557
1773
|
}
|
|
1774
|
+
setupOutro(target);
|
|
1558
1775
|
break;
|
|
1776
|
+
}
|
|
1559
1777
|
case 'uninstall': await runUninstall(); break;
|
|
1560
1778
|
case 'upgrade-config': migrateConfig(); break;
|
|
1561
1779
|
case 'start': startDaemon(); break;
|
|
@@ -1600,6 +1818,8 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1600
1818
|
case 'privacy': doPrivacy(); break;
|
|
1601
1819
|
case 'community': await doCommunity(process.argv.slice(3)); break;
|
|
1602
1820
|
case 'profile': await doProfile(process.argv.slice(3)); break;
|
|
1821
|
+
case 'squad': await doSquadCmd(process.argv.slice(3)); break;
|
|
1822
|
+
case 'link': await doLink(process.argv.slice(3)); break;
|
|
1603
1823
|
case 'doctor': {
|
|
1604
1824
|
const { runDoctor, fixPlan } = await import('./doctor.js');
|
|
1605
1825
|
const fix = process.argv.includes('--fix');
|
|
@@ -1620,19 +1840,19 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1620
1840
|
try {
|
|
1621
1841
|
if (kind === 'setup') {
|
|
1622
1842
|
await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1623
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1843
|
+
console.log(` ${c.green}✓${c.reset} config + hooks repaired`);
|
|
1624
1844
|
} else if (kind === 'rescan') {
|
|
1625
1845
|
doScan(true);
|
|
1626
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1846
|
+
console.log(` ${c.green}✓${c.reset} aggregate rebuilt from transcripts`);
|
|
1627
1847
|
} else if (kind === 'daemon') {
|
|
1628
1848
|
restartDaemon();
|
|
1629
1849
|
restarted = true;
|
|
1630
|
-
console.log(` ${c.green}✓${c.reset}
|
|
1850
|
+
console.log(` ${c.green}✓${c.reset} daemon (re)starting`);
|
|
1631
1851
|
} else if (kind === 'discord') {
|
|
1632
|
-
console.log(` ${c.yellow}!${c.reset}
|
|
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.`);
|
|
1633
1853
|
}
|
|
1634
1854
|
} catch (e) {
|
|
1635
|
-
console.log(` ${c.red}✗${c.reset}
|
|
1855
|
+
console.log(` ${c.red}✗${c.reset} ${kind} step failed: ${e.message}`);
|
|
1636
1856
|
}
|
|
1637
1857
|
}
|
|
1638
1858
|
// A 'setup' rewire only takes effect once the daemon reloads, so ensure a
|
|
@@ -1649,8 +1869,9 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1649
1869
|
default: {
|
|
1650
1870
|
if (packagedDefault) {
|
|
1651
1871
|
if (!isInstalled()) {
|
|
1652
|
-
await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1872
|
+
const target = await runInstall({ exePath: EXE_PATH || process.execPath });
|
|
1653
1873
|
startDaemon();
|
|
1874
|
+
setupOutro(target);
|
|
1654
1875
|
} else {
|
|
1655
1876
|
// Self-heal an existing install. Two real failure modes this fixes:
|
|
1656
1877
|
//
|
|
@@ -1668,13 +1889,14 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1668
1889
|
// Refresh hooks against the canonical exe, migrate config blocks,
|
|
1669
1890
|
// wipe state, restart daemon. Anything the user has customized in
|
|
1670
1891
|
// config.json is preserved (migrateConfig is non-destructive).
|
|
1671
|
-
console.log('
|
|
1892
|
+
console.log('');
|
|
1893
|
+
console.log(` ${c.bold}${c.magenta}◆ claude-rpc${c.reset} ${c.dim}— already installed; refreshing hooks + config${c.reset}`);
|
|
1672
1894
|
try {
|
|
1673
1895
|
const target = ensureCanonicalExe(process.execPath);
|
|
1674
1896
|
migrateConfig();
|
|
1675
1897
|
installHooks(target);
|
|
1676
1898
|
} catch (e) {
|
|
1677
|
-
console.warn(`refresh skipped
|
|
1899
|
+
console.warn(` ${c.yellow}!${c.reset} ${'refresh skipped'.padEnd(16)}${c.dim}${e.message}${c.reset}`);
|
|
1678
1900
|
}
|
|
1679
1901
|
const wasRunning = stopDaemon({ quiet: true });
|
|
1680
1902
|
try { if (existsSync(STATE_PATH)) unlinkSync(STATE_PATH); } catch { /* state.json locked or already gone — next hook will recreate it */ }
|
|
@@ -1691,11 +1913,16 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1691
1913
|
// click with no args" install-and-start flow.
|
|
1692
1914
|
overview();
|
|
1693
1915
|
} else {
|
|
1694
|
-
// Version in the
|
|
1916
|
+
// Version in the hint is deliberate: the #1 cause of "unknown
|
|
1695
1917
|
// command" in the wild is a stale global install resolving instead of
|
|
1696
1918
|
// the version the user read the docs for. Make the skew visible.
|
|
1697
|
-
fail(`unknown command: ${cmd}
|
|
1698
|
-
|
|
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
|
+
});
|
|
1699
1926
|
}
|
|
1700
1927
|
}
|
|
1701
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
+
step(SYM_OK, 'config seeded', CONFIG_PATH);
|
|
225
237
|
if (seeded.community?.enabled && seeded.community.instanceId) {
|
|
226
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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('
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
+
step(SYM_OK, 'hook verified', probe.detail);
|
|
487
495
|
} else {
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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('
|
|
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('
|
|
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
|
-
//
|
|
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
|
}
|