claude-rpc 0.14.0 → 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/SECURITY.md +19 -1
- package/package.json +2 -2
- package/src/cli.js +157 -1
- 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.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';
|
|
@@ -1005,6 +1005,158 @@ async function doExport(argv) {
|
|
|
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) 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
|
+
|
|
1008
1160
|
// ── Community totals ─────────────────────────────────────────────────────
|
|
1009
1161
|
//
|
|
1010
1162
|
// `claude-rpc community` → show current state + endpoint
|
|
@@ -1488,6 +1640,8 @@ function help() {
|
|
|
1488
1640
|
['privacy', 'Show resolved visibility for the current directory'],
|
|
1489
1641
|
['community', 'Opt in/out of anonymous community totals (on|off|status|report)'],
|
|
1490
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)'],
|
|
1491
1645
|
['doctor', 'Run a diagnostic checklist — common-failure triage (--fix to auto-repair)'],
|
|
1492
1646
|
['tail', 'Tail the daemon log file'],
|
|
1493
1647
|
['daemon', 'Run daemon in foreground (debug)'],
|
|
@@ -1600,6 +1754,8 @@ const packagedDefault = IS_PACKAGED && !cmd;
|
|
|
1600
1754
|
case 'privacy': doPrivacy(); break;
|
|
1601
1755
|
case 'community': await doCommunity(process.argv.slice(3)); break;
|
|
1602
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;
|
|
1603
1759
|
case 'doctor': {
|
|
1604
1760
|
const { runDoctor, fixPlan } = await import('./doctor.js');
|
|
1605
1761
|
const fix = process.argv.includes('--fix');
|