claude-rpc 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +56 -13
- package/src/community.js +11 -29
- package/src/daemon.js +2 -2
- package/src/version.js +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1125,7 +1125,31 @@ function profileEnable(on) {
|
|
|
1125
1125
|
userCfg.profile = next;
|
|
1126
1126
|
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1127
1127
|
console.log(`${c.green}✓${c.reset} leaderboard publishing ${on ? 'enabled' : 'disabled'}`);
|
|
1128
|
-
if (on)
|
|
1128
|
+
if (on) {
|
|
1129
|
+
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
|
+
console.log(` ${c.dim}earn the ✓ with ${c.reset}${c.cyan}claude-rpc profile verify${c.reset}${c.dim}.${c.reset}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// One-shot publish so you appear on the board immediately, instead of waiting
|
|
1135
|
+
// for the daemon's next flush.
|
|
1136
|
+
async function profilePublish() {
|
|
1137
|
+
const cfg = loadConfig();
|
|
1138
|
+
if (!lb.profileIsPublishable(cfg.profile || {})) {
|
|
1139
|
+
return fail('enable the profile first', {
|
|
1140
|
+
hint: 'claude-rpc profile set --handle <name> && claude-rpc profile on', code: EX_BAD_STATE,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const { flushProfile } = await import('./community.js');
|
|
1144
|
+
console.log(`${c.dim}publishing @${cfg.profile.handle} to the board…${c.reset}`);
|
|
1145
|
+
const r = await flushProfile(cfg);
|
|
1146
|
+
if (r.ok) {
|
|
1147
|
+
console.log(`${c.green}✓${c.reset} published — see it at ${c.cyan}https://claude-rpc.vercel.app/u/${encodeURIComponent(cfg.profile.handle)}${c.reset}`);
|
|
1148
|
+
} else if (r.reason === 'rate-limited') {
|
|
1149
|
+
console.log(`${c.yellow}!${c.reset} rate-limited — already published in the last minute; the board has you.`);
|
|
1150
|
+
} else {
|
|
1151
|
+
return fail(`publish failed: ${r.reason}${r.error ? ' (' + r.error + ')' : ''}`, { code: EX_SYS_ERROR });
|
|
1152
|
+
}
|
|
1129
1153
|
}
|
|
1130
1154
|
|
|
1131
1155
|
// GitHub verification: ask the worker for a one-time token, publish it in a
|
|
@@ -1155,29 +1179,47 @@ async function profileVerify() {
|
|
|
1155
1179
|
};
|
|
1156
1180
|
|
|
1157
1181
|
try {
|
|
1182
|
+
// Make sure the profile row exists server-side before we verify it, so
|
|
1183
|
+
// verification works regardless of whether `profile publish` was run first.
|
|
1184
|
+
if (lb.profileIsPublishable(profile)) {
|
|
1185
|
+
const { flushProfile } = await import('./community.js');
|
|
1186
|
+
await flushProfile(cfg);
|
|
1187
|
+
}
|
|
1158
1188
|
console.log(`${c.dim}requesting a verification token…${c.reset}`);
|
|
1159
1189
|
const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser });
|
|
1160
1190
|
if (!start.json?.token) return fail(`verify/start failed: ${start.json?.error || start.status}`, { code: EX_SYS_ERROR });
|
|
1161
1191
|
const token = start.json.token;
|
|
1162
1192
|
|
|
1163
1193
|
const { publishGistFile } = await import('./gist.js');
|
|
1164
|
-
console.log(`${c.dim}publishing a proof gist
|
|
1165
|
-
await publishGistFile({
|
|
1166
|
-
svg: `claude-rpc leaderboard verification
|
|
1194
|
+
console.log(`${c.dim}publishing a public proof gist…${c.reset}`);
|
|
1195
|
+
const gist = await publishGistFile({
|
|
1196
|
+
svg: `claude-rpc leaderboard verification\n${token}\n`,
|
|
1167
1197
|
filename: 'claude-rpc-verify.txt',
|
|
1168
1198
|
description: 'claude-rpc profile verification',
|
|
1169
1199
|
isPublic: true,
|
|
1170
1200
|
});
|
|
1171
1201
|
|
|
1172
|
-
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1202
|
+
// Hand the worker the gist ID so it fetches that gist directly (no
|
|
1203
|
+
// gist-list lag) and reads the real owner — instant, and the owner becomes
|
|
1204
|
+
// the verified identity regardless of what --github was set to.
|
|
1205
|
+
console.log(`${c.dim}confirming with the server…${c.reset}`);
|
|
1206
|
+
const check = await post('/verify/check', { instanceId: community.instanceId, gistId: gist.id });
|
|
1176
1207
|
if (check.json?.verified) {
|
|
1177
|
-
|
|
1208
|
+
const who = check.json.githubUser || gist.owner || profile.githubUser;
|
|
1209
|
+
// Persist the authoritative owner so the local profile + future publishes
|
|
1210
|
+
// match what got verified.
|
|
1211
|
+
if (who && who !== profile.githubUser) {
|
|
1212
|
+
const userCfg = readJson(CONFIG_PATH, {});
|
|
1213
|
+
userCfg.profile = { ...(userCfg.profile || {}), githubUser: who };
|
|
1214
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(userCfg, null, 2));
|
|
1215
|
+
}
|
|
1216
|
+
console.log(`${c.green}✓${c.reset} verified as @${who} — you'll show the ✓ on the board.`);
|
|
1217
|
+
if (who && profile.githubUser && who.toLowerCase() !== profile.githubUser.toLowerCase()) {
|
|
1218
|
+
console.log(` ${c.dim}(your gist is owned by @${who}, so the profile now uses that account.)${c.reset}`);
|
|
1219
|
+
}
|
|
1178
1220
|
} else {
|
|
1179
|
-
console.log(`${c.yellow}!${c.reset} not confirmed
|
|
1180
|
-
console.log(` ${c.dim}the gist
|
|
1221
|
+
console.log(`${c.yellow}!${c.reset} not confirmed: ${check.json?.error || check.status}`);
|
|
1222
|
+
console.log(` ${c.dim}make sure the gist is public, then re-run ${c.reset}${c.cyan}claude-rpc profile verify${c.reset}${c.dim}.${c.reset}`);
|
|
1181
1223
|
}
|
|
1182
1224
|
} catch (e) {
|
|
1183
1225
|
return fail(`verification failed: ${e.message}`, {
|
|
@@ -1193,8 +1235,9 @@ async function doProfile(argv) {
|
|
|
1193
1235
|
if (sub === 'on') return profileEnable(true);
|
|
1194
1236
|
if (sub === 'off') return profileEnable(false);
|
|
1195
1237
|
if (sub === 'verify') return profileVerify();
|
|
1238
|
+
if (sub === 'publish') return profilePublish();
|
|
1196
1239
|
fail(`unknown profile subcommand: ${sub}`, {
|
|
1197
|
-
hint: 'try: profile [status|set|on|off|verify]',
|
|
1240
|
+
hint: 'try: profile [status|set|on|off|verify|publish]',
|
|
1198
1241
|
code: EX_USER_ERROR,
|
|
1199
1242
|
});
|
|
1200
1243
|
}
|
|
@@ -1336,7 +1379,7 @@ function help() {
|
|
|
1336
1379
|
['public', 'Un-mark the current directory'],
|
|
1337
1380
|
['privacy', 'Show resolved visibility for the current directory'],
|
|
1338
1381
|
['community', 'Opt in/out of anonymous community totals (on|off|status|report)'],
|
|
1339
|
-
['profile', 'Public leaderboard identity
|
|
1382
|
+
['profile', 'Public leaderboard identity (status|set|on|off|publish|verify)'],
|
|
1340
1383
|
['doctor', 'Run a diagnostic checklist — common-failure triage (--fix to auto-repair)'],
|
|
1341
1384
|
['tail', 'Tail the daemon log file'],
|
|
1342
1385
|
['daemon', 'Run daemon in foreground (debug)'],
|
package/src/community.js
CHANGED
|
@@ -26,7 +26,6 @@ import { VERSION } from './version.js';
|
|
|
26
26
|
import { profileIsPublishable } from './leaderboard.js';
|
|
27
27
|
|
|
28
28
|
const CURSOR_PATH = join(STATE_DIR, 'community-cursor.json');
|
|
29
|
-
const PROFILE_CURSOR_PATH = join(STATE_DIR, 'profile-cursor.json');
|
|
30
29
|
|
|
31
30
|
export function readCursor(path = CURSOR_PATH) {
|
|
32
31
|
if (!existsSync(path)) return { sessions: 0, tokens: 0, ts: 0 };
|
|
@@ -87,25 +86,20 @@ function totalTokens(aggregate) {
|
|
|
87
86
|
+ (aggregate?.cacheWriteTokens || 0);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function buildProfilePayload(aggregate, profileCfg, cursor, { instanceId, now = Date.now() }) {
|
|
98
|
-
const sessions = aggregate?.sessions || 0;
|
|
99
|
-
const tokens = totalTokens(aggregate);
|
|
100
|
-
const activeMs = aggregate?.activeMs || 0;
|
|
89
|
+
// A profile reports ABSOLUTE lifetime totals (not deltas). It's per-user and
|
|
90
|
+
// keyed by the instanceId, so the server just stores the latest value — no
|
|
91
|
+
// cursor, no double-count risk, and the board matches your real aggregate
|
|
92
|
+
// exactly. (Deltas were wrong here: the first publish carried the entire
|
|
93
|
+
// lifetime total, which blew past the per-report caps for any established user.)
|
|
94
|
+
export function buildProfilePayload(aggregate, profileCfg, { instanceId, now = Date.now() }) {
|
|
101
95
|
return {
|
|
102
96
|
instanceId,
|
|
103
97
|
handle: profileCfg.handle,
|
|
104
98
|
displayName: profileCfg.displayName || null,
|
|
105
99
|
githubUser: profileCfg.githubUser || null,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
tokens: totalTokens(aggregate),
|
|
101
|
+
sessions: aggregate?.sessions || 0,
|
|
102
|
+
activeMs: aggregate?.activeMs || 0,
|
|
109
103
|
streak: aggregate?.streak || 0,
|
|
110
104
|
version: VERSION,
|
|
111
105
|
osFamily: osFamily(),
|
|
@@ -115,7 +109,6 @@ export function buildProfilePayload(aggregate, profileCfg, cursor, { instanceId,
|
|
|
115
109
|
|
|
116
110
|
export async function flushProfile(cfg, {
|
|
117
111
|
aggregatePath = AGGREGATE_PATH,
|
|
118
|
-
cursorPath = PROFILE_CURSOR_PATH,
|
|
119
112
|
fetchImpl = globalThis.fetch,
|
|
120
113
|
} = {}) {
|
|
121
114
|
const profile = cfg?.profile || {};
|
|
@@ -130,9 +123,7 @@ export async function flushProfile(cfg, {
|
|
|
130
123
|
try { aggregate = JSON.parse(readFileSync(aggregatePath, 'utf8')); }
|
|
131
124
|
catch { return { ok: false, reason: 'unreadable-aggregate' }; }
|
|
132
125
|
|
|
133
|
-
const
|
|
134
|
-
const payload = buildProfilePayload(aggregate, profile, cursor, { instanceId });
|
|
135
|
-
|
|
126
|
+
const payload = buildProfilePayload(aggregate, profile, { instanceId });
|
|
136
127
|
const url = community.endpoint.replace(/\/+$/, '') + '/profile';
|
|
137
128
|
let res;
|
|
138
129
|
try {
|
|
@@ -148,16 +139,7 @@ export async function flushProfile(cfg, {
|
|
|
148
139
|
if (res.status === 429) return { ok: false, reason: 'rate-limited' };
|
|
149
140
|
return { ok: false, reason: `http-${res.status}` };
|
|
150
141
|
}
|
|
151
|
-
|
|
152
|
-
// Advance the cursor only on acceptance (same reasoning as flushCommunity).
|
|
153
|
-
writeCursor({
|
|
154
|
-
sessions: (cursor.sessions || 0) + payload.sessionsDelta,
|
|
155
|
-
tokens: (cursor.tokens || 0) + payload.tokensDelta,
|
|
156
|
-
activeMs: (cursor.activeMs || 0) + payload.activeMsDelta,
|
|
157
|
-
ts: payload.ts,
|
|
158
|
-
}, cursorPath);
|
|
159
|
-
|
|
160
|
-
return { ok: true, delta: { sessions: payload.sessionsDelta, tokens: payload.tokensDelta, activeMs: payload.activeMsDelta } };
|
|
142
|
+
return { ok: true, totals: { tokens: payload.tokens, sessions: payload.sessions, activeMs: payload.activeMs } };
|
|
161
143
|
}
|
|
162
144
|
|
|
163
145
|
// Single best-effort flush. Returns { ok, reason, delta? } — never throws.
|
package/src/daemon.js
CHANGED
|
@@ -627,8 +627,8 @@ async function runCommunityFlush() {
|
|
|
627
627
|
}
|
|
628
628
|
if (config.profile?.enabled) {
|
|
629
629
|
const pr = await flushProfile(config);
|
|
630
|
-
if (pr.ok && pr.
|
|
631
|
-
log(`profile: published @${config.profile.handle} (
|
|
630
|
+
if (pr.ok && pr.totals) {
|
|
631
|
+
log(`profile: published @${config.profile.handle} (${pr.totals.tokens} tokens)`);
|
|
632
632
|
} else if (!pr.ok && pr.reason !== 'rate-limited' && pr.reason !== 'disabled') {
|
|
633
633
|
log(`profile: ${pr.reason}${pr.error ? ' (' + pr.error + ')' : ''}`);
|
|
634
634
|
}
|