claude-rpc 0.13.7 → 0.15.0
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/README.md +6 -2
- package/SECURITY.md +19 -1
- package/package.json +2 -2
- package/src/cli.js +297 -26
- package/src/daemon.js +60 -58
- package/src/discord-ipc.js +10 -0
- package/src/format.js +17 -13
- package/src/git.js +39 -7
- package/src/hook.js +10 -1
- package/src/install.js +24 -0
- package/src/paths.js +3 -0
- package/src/pause.js +56 -0
- package/src/pricing.js +14 -7
- package/src/scanner.js +142 -37
- package/src/server/index.js +8 -2
- package/src/server/sse.js +26 -12
- package/src/ui.js +10 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# claude-rpc
|
|
9
9
|
|
|
10
|
-
**Discord Rich Presence for [Claude Code](https://claude.com/claude-code).**
|
|
10
|
+
**Discord Rich Presence (RPC) for [Claude Code](https://claude.com/claude-code).**
|
|
11
11
|
Your live model, project, current tool, tokens, and lifetime stats — in your Discord profile. Driven by the hooks Claude Code already fires. Zero polling between sessions.
|
|
12
12
|
|
|
13
13
|
**→ [claude-rpc.vercel.app](https://claude-rpc.vercel.app)** — what it looks like, in one page.
|
|
@@ -38,9 +38,11 @@ A small Node daemon that takes the lifecycle events Claude Code already fires an
|
|
|
38
38
|
**macOS / Linux / any Node 18+** — one command:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
npx claude-rpc setup
|
|
41
|
+
npx claude-rpc@latest setup
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
(The `@latest` matters — bare `npx claude-rpc` will happily reuse a stale cached copy.)
|
|
45
|
+
|
|
44
46
|
That installs `claude-rpc` globally, wires the hooks into Claude Code, and starts the daemon — no separate `start` step. Open Claude Code in any project and the card appears within a second. Something looks wrong? `claude-rpc doctor` (or `claude-rpc doctor --fix` to auto-repair).
|
|
45
47
|
|
|
46
48
|
**Prefer a one-liner that figures it out for you?**
|
|
@@ -312,6 +314,8 @@ npm run build:exe # SEA single-file binary for the current OS
|
|
|
312
314
|
|
|
313
315
|
Tests are `node --test` with zero deps. The CI pipeline ([release.yml](.github/workflows/release.yml)) runs the suite (plus the Cloudflare Worker's own tests) across Node 18/20/22 and gates the matrix build and the npm publish behind it. Every pure/logic module in `src/*.js` is unit-tested, including the MCP transport and the SVG renderers; the long-running daemon, the TUI, and the CLI dispatcher are covered by integration and manual smoke testing rather than unit tests.
|
|
314
316
|
|
|
317
|
+
Where the project is headed (and what it will deliberately never do) lives in [`ROADMAP.md`](ROADMAP.md).
|
|
318
|
+
|
|
315
319
|
## license
|
|
316
320
|
|
|
317
321
|
[MIT](LICENSE) © Archer Simmons
|
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.0",
|
|
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,7 +12,7 @@ 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
18
|
import { install as runInstall, uninstall as runUninstall, isInstalled, migrateConfig, installHooks, ensureCanonicalExe, installMcp, uninstallMcp, mcpServerCommand } from './install.js';
|
|
@@ -22,10 +22,11 @@ import { maybeNudge } from './nudge.js';
|
|
|
22
22
|
import { badgeSvg } from './badge.js';
|
|
23
23
|
import { fmtCost } from './pricing.js';
|
|
24
24
|
import { addPrivateCwd, removePrivateCwd, listPrivateCwds, resolveVisibility } from './privacy.js';
|
|
25
|
+
import { parseDuration, setPause, clearPause, pauseUntil } from './pause.js';
|
|
25
26
|
import { loadConfig, hasUserConfig } from './config.js';
|
|
26
27
|
import * as lb from './leaderboard.js';
|
|
27
28
|
import { VERSION } from './version.js';
|
|
28
|
-
import { fail, EX_USER_ERROR, EX_BAD_STATE, EX_SYS_ERROR } from './ui.js';
|
|
29
|
+
import { fail, tailLines, EX_USER_ERROR, EX_BAD_STATE, EX_SYS_ERROR } from './ui.js';
|
|
29
30
|
import { randomUUID } from 'node:crypto';
|
|
30
31
|
import { createInterface } from 'node:readline';
|
|
31
32
|
import { basename } from 'node:path';
|
|
@@ -110,7 +111,7 @@ function box(title, lines, minWidth = 64) {
|
|
|
110
111
|
const termWidth = process.stdout.columns || 100;
|
|
111
112
|
const maxAllowed = Math.max(40, termWidth - 2);
|
|
112
113
|
const width = Math.min(maxAllowed, Math.max(minWidth, longest + 4, title.length + 8));
|
|
113
|
-
const top = `${c.gray}┌─ ${c.reset}${c.bold}${title}${c.reset} ${c.gray}${'─'.repeat(Math.max(0, width -
|
|
114
|
+
const top = `${c.gray}┌─ ${c.reset}${c.bold}${title}${c.reset} ${c.gray}${'─'.repeat(Math.max(0, width - 5 - title.length))}┐${c.reset}`;
|
|
114
115
|
const bottom = `${c.gray}└${'─'.repeat(width - 2)}┘${c.reset}`;
|
|
115
116
|
console.log(top);
|
|
116
117
|
for (const raw of lines) {
|
|
@@ -932,6 +933,230 @@ function doPrivacy() {
|
|
|
932
933
|
console.log('');
|
|
933
934
|
}
|
|
934
935
|
|
|
936
|
+
// ── Pause / resume ───────────────────────────────────────────────────────
|
|
937
|
+
//
|
|
938
|
+
// `claude-rpc pause [30m|2h|1h30m|90]` → snooze the Discord card globally
|
|
939
|
+
// (default 1h). `claude-rpc resume` (or `pause off`) lifts it early; expiry
|
|
940
|
+
// lifts it automatically. Privacy controls are per-cwd — this is the
|
|
941
|
+
// "I'm screen-sharing" switch that hides everything regardless of project.
|
|
942
|
+
|
|
943
|
+
function fmtClock(ts) {
|
|
944
|
+
const d = new Date(ts);
|
|
945
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function doPause(argv) {
|
|
949
|
+
const arg = (argv[0] || '').toLowerCase();
|
|
950
|
+
if (arg === 'off' || arg === 'resume') return doResume();
|
|
951
|
+
if (arg === 'status') {
|
|
952
|
+
const until = pauseUntil();
|
|
953
|
+
if (until) console.log(`${c.yellow}●${c.reset} paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
|
|
954
|
+
else console.log(`${c.green}○${c.reset} not paused`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const ms = parseDuration(argv[0]);
|
|
958
|
+
if (ms === null) {
|
|
959
|
+
fail(`could not parse duration: ${argv[0]}`,
|
|
960
|
+
{ hint: 'use 30m, 2h, 1h30m, or a bare number of minutes (default: 1h)', code: EX_USER_ERROR });
|
|
961
|
+
}
|
|
962
|
+
const until = setPause(ms);
|
|
963
|
+
console.log(`${c.green}✓${c.reset} presence paused until ${c.cyan}${fmtClock(until)}${c.reset}`);
|
|
964
|
+
console.log(`${c.dim} the daemon clears the card within a few seconds. Resume early with ${c.reset}${c.cyan}claude-rpc resume${c.reset}`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function doResume() {
|
|
968
|
+
const was = pauseUntil();
|
|
969
|
+
clearPause();
|
|
970
|
+
if (was) {
|
|
971
|
+
console.log(`${c.green}✓${c.reset} presence resumed ${c.dim}(was paused until ${fmtClock(was)})${c.reset}`);
|
|
972
|
+
} else {
|
|
973
|
+
console.log(`${c.dim}presence wasn't paused.${c.reset}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
978
|
+
//
|
|
979
|
+
// `claude-rpc export [--csv] [--out <file>]` — the full aggregate as JSON, or
|
|
980
|
+
// the per-day breakdown as CSV (same shape the dashboard's /api/export routes
|
|
981
|
+
// serve, without needing the server up). Writes to stdout unless --out, so it
|
|
982
|
+
// pipes cleanly into jq / a spreadsheet import.
|
|
983
|
+
|
|
984
|
+
async function doExport(argv) {
|
|
985
|
+
const csv = argv.includes('--csv');
|
|
986
|
+
let out = '';
|
|
987
|
+
const i = argv.findIndex((a) => a === '--out' || a === '-o');
|
|
988
|
+
if (i !== -1) out = argv[i + 1] || '';
|
|
989
|
+
const aggregate = readAggregate();
|
|
990
|
+
if (!aggregate) {
|
|
991
|
+
fail('no aggregate yet — nothing to export', { hint: 'run `claude-rpc scan` first', code: EX_BAD_STATE });
|
|
992
|
+
}
|
|
993
|
+
let payload;
|
|
994
|
+
if (csv) {
|
|
995
|
+
const { aggregateToCsv } = await import('./server/api.js');
|
|
996
|
+
payload = aggregateToCsv(aggregate);
|
|
997
|
+
} else {
|
|
998
|
+
payload = JSON.stringify(aggregate, null, 2) + '\n';
|
|
999
|
+
}
|
|
1000
|
+
if (out) {
|
|
1001
|
+
writeFileSync(out, payload);
|
|
1002
|
+
console.log(`${c.green}✓${c.reset} Wrote ${c.cyan}${out}${c.reset} (${payload.length} bytes, ${csv ? 'CSV' : 'JSON'})`);
|
|
1003
|
+
} else {
|
|
1004
|
+
process.stdout.write(payload);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
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) fail('no community endpoint configured', { code: EX_BAD_STATE });
|
|
1022
|
+
if (!instanceId) {
|
|
1023
|
+
fail('squads need an identity', {
|
|
1024
|
+
hint: 'run `claude-rpc profile set --handle <name> && claude-rpc profile on` first',
|
|
1025
|
+
code: EX_BAD_STATE,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
const post = async (path, body) => {
|
|
1029
|
+
const res = await fetch(endpoint + path, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1032
|
+
body: JSON.stringify({ instanceId, ...body }),
|
|
1033
|
+
});
|
|
1034
|
+
return { status: res.status, json: await res.json().catch(() => ({})) };
|
|
1035
|
+
};
|
|
1036
|
+
const get = async (path) => {
|
|
1037
|
+
const res = await fetch(endpoint + path);
|
|
1038
|
+
return { status: res.status, json: await res.json().catch(() => ({})) };
|
|
1039
|
+
};
|
|
1040
|
+
return { cfg, endpoint, instanceId, post, get };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function squadPageUrl(id) { return `https://claude-rpc.vercel.app/squad/${id}`; }
|
|
1044
|
+
|
|
1045
|
+
function printSquadInvite(squad) {
|
|
1046
|
+
console.log('');
|
|
1047
|
+
console.log(` ${c.green}✓${c.reset} squad ${c.bold}${squad.name}${c.reset}`);
|
|
1048
|
+
console.log(` ${c.dim}invite code:${c.reset} ${c.cyan}${squad.code}${c.reset}`);
|
|
1049
|
+
console.log(` ${c.dim}standings: ${c.reset} ${c.cyan}${squadPageUrl(squad.id)}${c.reset}`);
|
|
1050
|
+
console.log('');
|
|
1051
|
+
console.log(` ${c.dim}send your crew this:${c.reset}`);
|
|
1052
|
+
console.log(` join my Claude Code squad "${squad.name}" — npx claude-rpc@latest setup, then:`);
|
|
1053
|
+
console.log(` ${c.cyan}claude-rpc squad join ${squad.code}${c.reset} ${c.dim}(or join in the browser: ${squadPageUrl(squad.id)})${c.reset}`);
|
|
1054
|
+
console.log('');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function squadStatus({ post, get }) {
|
|
1058
|
+
const mine = await post('/squads/mine', {});
|
|
1059
|
+
if (!mine.json?.squads) return fail(`could not load squads: ${mine.json?.error || mine.status}`, { code: EX_SYS_ERROR });
|
|
1060
|
+
if (!mine.json.squads.length) {
|
|
1061
|
+
console.log('');
|
|
1062
|
+
console.log(` ${c.dim}no squads yet — start one:${c.reset} ${c.cyan}claude-rpc squad create "the night shift"${c.reset}`);
|
|
1063
|
+
console.log('');
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
for (const s of mine.json.squads) {
|
|
1067
|
+
const r = await get(`/squad?id=${encodeURIComponent(s.id)}`);
|
|
1068
|
+
const standings = r.json?.standings || [];
|
|
1069
|
+
const lines = standings.map((row) => {
|
|
1070
|
+
const who = `${row.displayName || '@' + row.handle}${row.verified ? ' ✓' : ''}${row.owner ? ` ${c.dim}(owner)${c.reset}` : ''}`;
|
|
1071
|
+
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}`;
|
|
1072
|
+
});
|
|
1073
|
+
lines.push('');
|
|
1074
|
+
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}`);
|
|
1075
|
+
box(`${s.name} (${s.members})`, lines, 70);
|
|
1076
|
+
console.log('');
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function doSquadCmd(argv) {
|
|
1081
|
+
const sub = (argv[0] || 'status').toLowerCase();
|
|
1082
|
+
const ctx = squadAuth();
|
|
1083
|
+
if (sub === 'status' || sub === '') return squadStatus(ctx);
|
|
1084
|
+
if (sub === 'create') {
|
|
1085
|
+
const name = argv.slice(1).join(' ').trim();
|
|
1086
|
+
if (!name) return fail('usage: claude-rpc squad create <name>', { code: EX_USER_ERROR });
|
|
1087
|
+
const r = await ctx.post('/squad/create', { name });
|
|
1088
|
+
if (r.status !== 200) return fail(`create failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
1089
|
+
return printSquadInvite(r.json.squad);
|
|
1090
|
+
}
|
|
1091
|
+
if (sub === 'join') {
|
|
1092
|
+
const code = (argv[1] || '').trim();
|
|
1093
|
+
if (!code) return fail('usage: claude-rpc squad join SQ-XXXXXX', { code: EX_USER_ERROR });
|
|
1094
|
+
const r = await ctx.post('/squad/join', { code });
|
|
1095
|
+
if (r.status !== 200) return fail(`join failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
1096
|
+
const s = r.json.squad;
|
|
1097
|
+
console.log(`${c.green}✓${c.reset} ${s.alreadyMember ? 'already in' : 'joined'} ${c.bold}${s.name}${c.reset} (${s.members} member${s.members === 1 ? '' : 's'})`);
|
|
1098
|
+
console.log(` ${c.dim}standings: ${squadPageUrl(s.id)} · claude-rpc squad${c.reset}`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (sub === 'leave') {
|
|
1102
|
+
const mine = await ctx.post('/squads/mine', {});
|
|
1103
|
+
const squads = mine.json?.squads || [];
|
|
1104
|
+
if (!squads.length) return fail('you are not in any squads', { code: EX_BAD_STATE });
|
|
1105
|
+
let target = null;
|
|
1106
|
+
const wanted = (argv[1] || '').toLowerCase();
|
|
1107
|
+
if (wanted) target = squads.find((s) => s.id === wanted || s.name.toLowerCase() === wanted);
|
|
1108
|
+
else if (squads.length === 1) target = squads[0];
|
|
1109
|
+
if (!target) {
|
|
1110
|
+
return fail(squads.length > 1 && !wanted ? 'you are in several squads — name one' : `no squad matching "${argv[1]}"`, {
|
|
1111
|
+
hint: `claude-rpc squad leave <id|name> — yours: ${squads.map((s) => `${s.name} (${s.id})`).join(', ')}`,
|
|
1112
|
+
code: EX_USER_ERROR,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const r = await ctx.post('/squad/leave', { squadId: target.id });
|
|
1116
|
+
if (r.status !== 200) return fail(`leave failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
1117
|
+
console.log(`${c.green}✓${c.reset} left ${c.bold}${target.name}${c.reset}${r.json.dissolved ? ` ${c.dim}(last member — squad dissolved)${c.reset}` : ''}`);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
fail(`unknown squad subcommand: ${sub}`, {
|
|
1121
|
+
hint: 'try: squad [status|create <name>|join <code>|leave [id]]',
|
|
1122
|
+
code: EX_USER_ERROR,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// ── Link (CLI ↔ web pairing) ─────────────────────────────────────────────
|
|
1127
|
+
//
|
|
1128
|
+
// `claude-rpc link <code>` — the code comes from claude-rpc.vercel.app/squads
|
|
1129
|
+
// while logged in with GitHub. Claims it against this install's instanceId,
|
|
1130
|
+
// which verifies the profile (✓) and unlocks managing squads from the
|
|
1131
|
+
// browser. Replaces the gist dance for anyone who uses the website.
|
|
1132
|
+
|
|
1133
|
+
async function doLink(argv) {
|
|
1134
|
+
const code = (argv[0] || '').trim();
|
|
1135
|
+
if (!code) {
|
|
1136
|
+
fail('usage: claude-rpc link <code>', {
|
|
1137
|
+
hint: 'log in at https://claude-rpc.vercel.app/squads — it shows you the code',
|
|
1138
|
+
code: EX_USER_ERROR,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
const ctx = squadAuth();
|
|
1142
|
+
// Make sure the profile row exists server-side before claiming — same
|
|
1143
|
+
// pre-publish profileVerify does, so link works on a fresh `profile on`.
|
|
1144
|
+
if (lb.profileIsPublishable(ctx.cfg.profile || {})) {
|
|
1145
|
+
const { flushProfile } = await import('./community.js');
|
|
1146
|
+
await flushProfile(ctx.cfg);
|
|
1147
|
+
}
|
|
1148
|
+
const r = await ctx.post('/pair/claim', { code });
|
|
1149
|
+
if (r.status !== 200) {
|
|
1150
|
+
return fail(`link failed: ${r.json?.error || r.status}`, { code: EX_SYS_ERROR });
|
|
1151
|
+
}
|
|
1152
|
+
// Mirror the verified identity locally so `profile status` agrees.
|
|
1153
|
+
const userCfg = readJson(CONFIG_PATH, {});
|
|
1154
|
+
userCfg.profile = { ...(userCfg.profile || {}), githubUser: r.json.githubUser, verified: true };
|
|
1155
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1156
|
+
console.log(`${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser.`);
|
|
1157
|
+
console.log(` ${c.dim}head back to https://claude-rpc.vercel.app/squads — it picks the link up automatically.${c.reset}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
935
1160
|
// ── Community totals ─────────────────────────────────────────────────────
|
|
936
1161
|
//
|
|
937
1162
|
// `claude-rpc community` → show current state + endpoint
|
|
@@ -1060,16 +1285,46 @@ function readFlag(argv, name) {
|
|
|
1060
1285
|
|
|
1061
1286
|
function profileStatus() {
|
|
1062
1287
|
const p = (loadConfig().profile) || {};
|
|
1288
|
+
const handleOk = lb.isValidHandle(p.handle);
|
|
1289
|
+
const boardUrl = handleOk ? `https://claude-rpc.vercel.app/u/${encodeURIComponent(p.handle)}` : '';
|
|
1290
|
+
|
|
1063
1291
|
console.log('');
|
|
1064
|
-
console.log(` ${c.bold}
|
|
1292
|
+
console.log(` ${c.bold}${c.magenta}◆ profile${c.reset} ${c.dim}— public leaderboard identity${c.reset}`);
|
|
1065
1293
|
console.log('');
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1294
|
+
|
|
1295
|
+
const githubLine = p.githubUser
|
|
1296
|
+
? `${p.githubUser}${p.verified ? ` ${c.green}✓ verified${c.reset}` : ` ${c.dim}(unverified)${c.reset}`}`
|
|
1297
|
+
: `${c.dim}—${c.reset}`;
|
|
1298
|
+
box('profile', [
|
|
1299
|
+
pair('state', p.enabled ? `${c.green}● publishing${c.reset}` : `${c.dim}○ off${c.reset}`, ''),
|
|
1300
|
+
pair('handle', handleOk ? `${c.cyan}${p.handle}${c.reset}` : `${c.dim}(unset)${c.reset}`, ''),
|
|
1301
|
+
pair('name', p.displayName || `${c.dim}—${c.reset}`, ''),
|
|
1302
|
+
pair('github', githubLine, ''),
|
|
1303
|
+
...(p.enabled && handleOk ? [pair('board', boardUrl, c.cyan)] : []),
|
|
1304
|
+
]);
|
|
1070
1305
|
console.log('');
|
|
1071
|
-
|
|
1072
|
-
|
|
1306
|
+
|
|
1307
|
+
// Setup checklist — same shape every time, so the user always sees where
|
|
1308
|
+
// they are and the exact next command. This is the screen the daemon's
|
|
1309
|
+
// 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
|
+
];
|
|
1315
|
+
if (steps.every((s) => s.done)) {
|
|
1316
|
+
console.log(` ${c.green}✓${c.reset} all set — you're live at ${c.cyan}${boardUrl}${c.reset}`);
|
|
1317
|
+
} else {
|
|
1318
|
+
const nextIdx = steps.findIndex((s) => !s.done);
|
|
1319
|
+
box('next steps', steps.map((s, i) => {
|
|
1320
|
+
const mark = s.done ? `${c.green}✓${c.reset}` : (i === nextIdx ? `${c.yellow}○${c.reset}` : `${c.dim}○${c.reset}`);
|
|
1321
|
+
const label = s.done ? `${c.dim}${s.label}${c.reset}` : `${c.bold}${s.label}${c.reset}`;
|
|
1322
|
+
const tail = s.done
|
|
1323
|
+
? `${c.dim}${s.note || 'done'}${c.reset}`
|
|
1324
|
+
: `${c.cyan}${s.cmd}${c.reset}${i === nextIdx ? ` ${c.dim}← next${c.reset}` : ''}`;
|
|
1325
|
+
return `${mark} ${i + 1}. ${label}${' '.repeat(Math.max(1, 20 - s.label.length))}${tail}`;
|
|
1326
|
+
}));
|
|
1327
|
+
}
|
|
1073
1328
|
console.log('');
|
|
1074
1329
|
}
|
|
1075
1330
|
|
|
@@ -1094,6 +1349,9 @@ function profileSet(argv) {
|
|
|
1094
1349
|
if (!u) return fail(`invalid GitHub username: ${rawGh}`, { code: EX_USER_ERROR });
|
|
1095
1350
|
next.githubUser = u;
|
|
1096
1351
|
}
|
|
1352
|
+
// The ✓ belongs to the account that was verified — switching accounts
|
|
1353
|
+
// means re-verifying.
|
|
1354
|
+
if (next.githubUser !== (userCfg.profile || {}).githubUser) delete next.verified;
|
|
1097
1355
|
}
|
|
1098
1356
|
|
|
1099
1357
|
userCfg.profile = next;
|
|
@@ -1127,7 +1385,7 @@ function profileEnable(on) {
|
|
|
1127
1385
|
console.log(`${c.green}✓${c.reset} leaderboard publishing ${on ? 'enabled' : 'disabled'}`);
|
|
1128
1386
|
if (on) {
|
|
1129
1387
|
console.log(` ${c.dim}publish now with ${c.reset}${c.cyan}claude-rpc profile publish${c.reset}${c.dim} (or wait for the next daemon flush).${c.reset}`);
|
|
1130
|
-
|
|
1388
|
+
profileStatus();
|
|
1131
1389
|
}
|
|
1132
1390
|
}
|
|
1133
1391
|
|
|
@@ -1158,10 +1416,12 @@ async function profileVerify() {
|
|
|
1158
1416
|
const cfg = loadConfig();
|
|
1159
1417
|
const profile = cfg.profile || {};
|
|
1160
1418
|
const community = cfg.community || {};
|
|
1419
|
+
// No --github required up front: the worker treats it as a hint only and
|
|
1420
|
+
// takes the authoritative identity from whoever owns the proof gist —
|
|
1421
|
+
// and publishing that gist already requires gh auth, so the account is
|
|
1422
|
+
// known by the time it matters.
|
|
1161
1423
|
if (!profile.githubUser) {
|
|
1162
|
-
|
|
1163
|
-
hint: 'claude-rpc profile set --github <user>', code: EX_BAD_STATE,
|
|
1164
|
-
});
|
|
1424
|
+
console.log(`${c.dim}no --github set — your verified identity will be the account that owns the proof gist.${c.reset}`);
|
|
1165
1425
|
}
|
|
1166
1426
|
if (!community.instanceId) {
|
|
1167
1427
|
return fail('enable the profile first', { hint: 'claude-rpc profile on', code: EX_BAD_STATE });
|
|
@@ -1186,7 +1446,7 @@ async function profileVerify() {
|
|
|
1186
1446
|
await flushProfile(cfg);
|
|
1187
1447
|
}
|
|
1188
1448
|
console.log(`${c.dim}requesting a verification token…${c.reset}`);
|
|
1189
|
-
const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser });
|
|
1449
|
+
const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser || null });
|
|
1190
1450
|
if (!start.json?.token) return fail(`verify/start failed: ${start.json?.error || start.status}`, { code: EX_SYS_ERROR });
|
|
1191
1451
|
const token = start.json.token;
|
|
1192
1452
|
|
|
@@ -1206,13 +1466,11 @@ async function profileVerify() {
|
|
|
1206
1466
|
const check = await post('/verify/check', { instanceId: community.instanceId, gistId: gist.id });
|
|
1207
1467
|
if (check.json?.verified) {
|
|
1208
1468
|
const who = check.json.githubUser || gist.owner || profile.githubUser;
|
|
1209
|
-
// Persist the authoritative owner
|
|
1210
|
-
// match what got verified.
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1215
|
-
}
|
|
1469
|
+
// Persist the authoritative owner + a local verified marker so the
|
|
1470
|
+
// profile checklist and future publishes match what got verified.
|
|
1471
|
+
const userCfg = readJson(CONFIG_PATH, {});
|
|
1472
|
+
userCfg.profile = { ...(userCfg.profile || {}), ...(who ? { githubUser: who } : {}), verified: true };
|
|
1473
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1216
1474
|
console.log(`${c.green}✓${c.reset} verified as @${who} — you'll show the ✓ on the board.`);
|
|
1217
1475
|
if (who && profile.githubUser && who.toLowerCase() !== profile.githubUser.toLowerCase()) {
|
|
1218
1476
|
console.log(` ${c.dim}(your gist is owned by @${who}, so the profile now uses that account.)${c.reset}`);
|
|
@@ -1260,8 +1518,7 @@ function tailLog() {
|
|
|
1260
1518
|
return;
|
|
1261
1519
|
}
|
|
1262
1520
|
// Print the last ~30 lines, then follow.
|
|
1263
|
-
const
|
|
1264
|
-
const tail = raw.slice(-31, -1);
|
|
1521
|
+
const tail = tailLines(readFileSync(LOG_PATH, 'utf8'));
|
|
1265
1522
|
for (const line of tail) process.stdout.write(line + '\n');
|
|
1266
1523
|
let lastSize = readFileSync(LOG_PATH).length;
|
|
1267
1524
|
console.log(`${c.dim}-- tailing ${LOG_PATH} (Ctrl-C to stop) --${c.reset}`);
|
|
@@ -1375,11 +1632,16 @@ function help() {
|
|
|
1375
1632
|
['mcp install', 'Wire the stats MCP server into Claude Code (one command)'],
|
|
1376
1633
|
['mcp', 'Run the MCP server (stdio) — exposes your stats to Claude'],
|
|
1377
1634
|
['wrapped', 'Open your animated year-in-review (Claude Wrapped)'],
|
|
1635
|
+
['pause', 'Snooze the Discord card globally (pause [30m|2h], default 1h)'],
|
|
1636
|
+
['resume', 'Lift a pause early'],
|
|
1637
|
+
['export', 'Dump the aggregate as JSON, or daily rows as CSV (--csv --out)'],
|
|
1378
1638
|
['private', 'Mark the current directory as private (hide from Discord)'],
|
|
1379
1639
|
['public', 'Un-mark the current directory'],
|
|
1380
1640
|
['privacy', 'Show resolved visibility for the current directory'],
|
|
1381
1641
|
['community', 'Opt in/out of anonymous community totals (on|off|status|report)'],
|
|
1382
1642
|
['profile', 'Public leaderboard identity (status|set|on|off|publish|verify)'],
|
|
1643
|
+
['squad', 'Private mini-leaderboards with friends (create|join|leave|status)'],
|
|
1644
|
+
['link', 'Pair this install with your web login (code from /squads page)'],
|
|
1383
1645
|
['doctor', 'Run a diagnostic checklist — common-failure triage (--fix to auto-repair)'],
|
|
1384
1646
|
['tail', 'Tail the daemon log file'],
|
|
1385
1647
|
['daemon', 'Run daemon in foreground (debug)'],
|
|
@@ -1483,11 +1745,17 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1483
1745
|
break;
|
|
1484
1746
|
}
|
|
1485
1747
|
case 'wrapped': process.env.CLAUDE_RPC_OPEN_PATH = '/wrapped'; await import('./server/index.js'); break;
|
|
1748
|
+
case 'pause': doPause(process.argv.slice(3)); break;
|
|
1749
|
+
case 'resume':
|
|
1750
|
+
case 'unpause': doResume(); break;
|
|
1751
|
+
case 'export': await doExport(process.argv.slice(3)); break;
|
|
1486
1752
|
case 'private': doPrivate(); break;
|
|
1487
1753
|
case 'public': doPublic(); break;
|
|
1488
1754
|
case 'privacy': doPrivacy(); break;
|
|
1489
1755
|
case 'community': await doCommunity(process.argv.slice(3)); break;
|
|
1490
1756
|
case 'profile': await doProfile(process.argv.slice(3)); break;
|
|
1757
|
+
case 'squad': await doSquadCmd(process.argv.slice(3)); break;
|
|
1758
|
+
case 'link': await doLink(process.argv.slice(3)); break;
|
|
1491
1759
|
case 'doctor': {
|
|
1492
1760
|
const { runDoctor, fixPlan } = await import('./doctor.js');
|
|
1493
1761
|
const fix = process.argv.includes('--fix');
|
|
@@ -1579,8 +1847,11 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1579
1847
|
// click with no args" install-and-start flow.
|
|
1580
1848
|
overview();
|
|
1581
1849
|
} else {
|
|
1582
|
-
|
|
1583
|
-
|
|
1850
|
+
// Version in the error line is deliberate: the #1 cause of "unknown
|
|
1851
|
+
// command" in the wild is a stale global install resolving instead of
|
|
1852
|
+
// the version the user read the docs for. Make the skew visible.
|
|
1853
|
+
fail(`unknown command: ${cmd} (claude-rpc v${VERSION})`,
|
|
1854
|
+
{ hint: 'run `claude-rpc --help` for the full list — if this command should exist, update first: npm install -g claude-rpc@latest', code: EX_USER_ERROR });
|
|
1584
1855
|
}
|
|
1585
1856
|
}
|
|
1586
1857
|
}
|