claude-rpc 1.2.0 → 1.3.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/package.json +1 -1
- package/src/cli.js +46 -3
- package/src/community.js +36 -6
- package/src/version.js +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1673,11 +1673,25 @@ async function doLink(argv) {
|
|
|
1673
1673
|
return fail(`link failed: ${r.json?.error || r.status}`,
|
|
1674
1674
|
{ hint: `get a fresh code: run \`claude-rpc link\` on your main machine, or ${LINK_PAGE}`, code: EX_SYS_ERROR });
|
|
1675
1675
|
}
|
|
1676
|
-
// Mirror the verified identity locally so `profile status` agrees
|
|
1676
|
+
// Mirror the verified identity locally so `profile status` agrees — and
|
|
1677
|
+
// adopt the server's handle + enable publishing when the local config has
|
|
1678
|
+
// neither (a brand-new machine via `setup --link`): without these the
|
|
1679
|
+
// daemon's 30-min flush never uploads this machine's slice. An explicit
|
|
1680
|
+
// local handle or an explicit enabled:false is always respected.
|
|
1677
1681
|
const userCfg = readJson(CONFIG_PATH, {});
|
|
1678
|
-
|
|
1682
|
+
const prevProfile = userCfg.profile || {};
|
|
1683
|
+
userCfg.profile = {
|
|
1684
|
+
...prevProfile,
|
|
1685
|
+
githubUser: r.json.githubUser,
|
|
1686
|
+
verified: true,
|
|
1687
|
+
...(prevProfile.handle ? {} : { handle: r.json.handle }),
|
|
1688
|
+
...(prevProfile.enabled === undefined ? { enabled: true } : {}),
|
|
1689
|
+
};
|
|
1679
1690
|
writeUserConfig(userCfg);
|
|
1680
1691
|
console.log(` ${c.green}✓${c.reset} linked as ${c.cyan}@${r.json.githubUser}${c.reset} — profile verified, squads unlocked in the browser`);
|
|
1692
|
+
if (r.json.created) {
|
|
1693
|
+
console.log(` ${c.green}✓${c.reset} board profile created — handle ${c.cyan}@${r.json.handle}${c.reset} ${c.dim}(rename any time: claude-rpc profile set --handle <you>)${c.reset}`);
|
|
1694
|
+
}
|
|
1681
1695
|
if (r.json.merged) {
|
|
1682
1696
|
// This machine joined an existing identity: its stats now roll up under the
|
|
1683
1697
|
// canonical handle, one board row across all your machines.
|
|
@@ -2106,8 +2120,9 @@ async function doWrappedPublish(argv) {
|
|
|
2106
2120
|
pair('ships', fmtNum(w.ships)),
|
|
2107
2121
|
pair('est. cost', fmtCost(w.costUsd), c.yellow),
|
|
2108
2122
|
pair('projects', w.topProjects.map((p) => p.name).join(' · ') || c.dim + '(none public)' + c.reset, ''),
|
|
2109
|
-
pair('languages', w.topLanguages.join(' · ') || '—', ''),
|
|
2123
|
+
pair('languages', w.topLanguages.map((l) => l.name).join(' · ') || '—', ''),
|
|
2110
2124
|
pair('models', w.topModels.map((m) => `${m.name} ${m.pct}%`).join(' · ') || '—', ''),
|
|
2125
|
+
pair('hotspot', w.hotspot ? `${w.hotspot.name} (${fmtNum(w.hotspot.count)} edits)` : '—', ''),
|
|
2111
2126
|
], 72);
|
|
2112
2127
|
console.log(` ${c.dim}full payload: the fields above plus lines/cache%/peaks — nothing else.${c.reset}`);
|
|
2113
2128
|
console.log(` ${c.dim}private-listed and pattern-matched project names are already excluded.${c.reset}\n`);
|
|
@@ -2261,6 +2276,7 @@ function overview() {
|
|
|
2261
2276
|
function help() {
|
|
2262
2277
|
const cmds = [
|
|
2263
2278
|
['setup', 'Install Claude Code hooks + Windows startup entry (~/.claude/settings.json)'],
|
|
2279
|
+
['setup --link <code>', 'One-liner onboarding: install + verify via a claude-rpc.com code (+ --wrapped publishes your year)'],
|
|
2264
2280
|
['uninstall', 'Remove Claude Code hooks + Windows startup entry'],
|
|
2265
2281
|
['upgrade-config', 'Re-run idempotent migrations on an existing config.json'],
|
|
2266
2282
|
['start', 'Start the Discord RPC daemon (detached)'],
|
|
@@ -2410,6 +2426,33 @@ process.on('unhandledRejection', (e) => {
|
|
|
2410
2426
|
console.log(` ${c.yellow}!${c.reset} ${'daemon start'.padEnd(16)}${c.dim}couldn't auto-start: ${e.message}${c.reset}`);
|
|
2411
2427
|
console.log(` ${c.gray}↳ run \`claude-rpc start\` when you're ready${c.reset}`);
|
|
2412
2428
|
}
|
|
2429
|
+
// One-liner onboarding: `setup --link <code> [--wrapped]` — the wrapped
|
|
2430
|
+
// page mints the code after a GitHub login, so a single paste installs,
|
|
2431
|
+
// verifies the machine as that login (the worker creates the profile on
|
|
2432
|
+
// first contact), runs the first scan, publishes real totals, and —
|
|
2433
|
+
// with --wrapped — puts the year-in-review on the web. Each step is
|
|
2434
|
+
// best-effort-loud: a failure explains itself but never rolls back the
|
|
2435
|
+
// completed install above.
|
|
2436
|
+
{
|
|
2437
|
+
const argv = process.argv.slice(3);
|
|
2438
|
+
const linkAt = argv.indexOf('--link');
|
|
2439
|
+
const linkCode = linkAt !== -1 ? (argv[linkAt + 1] || '').trim() : null;
|
|
2440
|
+
if (linkCode) {
|
|
2441
|
+
console.log('');
|
|
2442
|
+
await doLink([linkCode]);
|
|
2443
|
+
if (process.exitCode) break; // link failed — its own message + hint already printed
|
|
2444
|
+
console.log(` ${c.dim}first scan — counting your history…${c.reset}`);
|
|
2445
|
+
doScan(false);
|
|
2446
|
+
const { flushProfile } = await import('./community.js');
|
|
2447
|
+
const flushed = await flushProfile(loadConfig());
|
|
2448
|
+
if (flushed.ok) {
|
|
2449
|
+
console.log(` ${c.green}✓${c.reset} profile published — ${c.cyan}${fmtNum(flushed.totals.tokens)}${c.reset} tokens on the board`);
|
|
2450
|
+
}
|
|
2451
|
+
if (argv.includes('--wrapped')) await doWrappedPublish(['--yes']);
|
|
2452
|
+
} else if (argv.includes('--wrapped')) {
|
|
2453
|
+
console.log(` ${c.yellow}!${c.reset} --wrapped needs a profile — pair it with --link <code>, or run \`claude-rpc wrapped --publish\` after setup`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2413
2456
|
setupOutro(target, changed);
|
|
2414
2457
|
break;
|
|
2415
2458
|
}
|
package/src/community.js
CHANGED
|
@@ -26,6 +26,7 @@ import { VERSION } from './version.js';
|
|
|
26
26
|
import { profileIsPublishable } from './leaderboard.js';
|
|
27
27
|
import { projectNameIsPrivate } from './privacy.js';
|
|
28
28
|
import { cleanProjectName } from './scanner.js';
|
|
29
|
+
import { humanModel } from './format.js';
|
|
29
30
|
|
|
30
31
|
const CURSOR_PATH = join(STATE_DIR, 'community-cursor.json');
|
|
31
32
|
|
|
@@ -157,25 +158,50 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
|
|
|
157
158
|
.map(([name, activeMs]) => ({ name, activeMs }));
|
|
158
159
|
|
|
159
160
|
// Year-scoped model mix from the per-day byModel buckets (scan cache v7).
|
|
160
|
-
|
|
161
|
+
// Share is by SPEND, matching the local wrapped's "your models, by spend"
|
|
162
|
+
// slide; token share is the fallback when everything costed to zero. Names
|
|
163
|
+
// are humanized here so the web story renders "Opus 4.8", same as local.
|
|
164
|
+
const modelAgg = {};
|
|
161
165
|
for (const [, d] of days) {
|
|
162
166
|
for (const [m, v] of Object.entries(d.byModel || {})) {
|
|
163
|
-
|
|
167
|
+
const t = (modelAgg[m] ||= { tokens: 0, cost: 0 });
|
|
168
|
+
t.tokens += v.tokens || 0;
|
|
169
|
+
t.cost += v.cost || 0;
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
172
|
+
const costTotal = Object.values(modelAgg).reduce((a, b) => a + b.cost, 0);
|
|
173
|
+
const tokTotal = Object.values(modelAgg).reduce((a, b) => a + b.tokens, 0);
|
|
174
|
+
const share = (v) => costTotal > 0 ? v.cost / costTotal : (tokTotal > 0 ? v.tokens / tokTotal : 0);
|
|
175
|
+
const topModels = Object.entries(modelAgg).sort((a, b) => share(b[1]) - share(a[1])).slice(0, 4)
|
|
176
|
+
.map(([name, v]) => ({ name: humanModel(name) || name, pct: Math.round(share(v) * 100) }));
|
|
169
177
|
|
|
170
178
|
// Lifetime dimensions (no yearly slice exists for these).
|
|
171
179
|
const toolTotal = Object.values(agg.toolBreakdown || {}).reduce((a, b) => a + b, 0);
|
|
172
180
|
const toolMix = Object.entries(agg.toolBreakdown || {}).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
173
181
|
.map(([name, n]) => ({ name, pct: toolTotal ? Math.round((n / toolTotal) * 100) : 0 }));
|
|
174
182
|
const topLanguages = Object.entries(agg.languages || {})
|
|
175
|
-
.sort((a, b) => (b[1].edits || 0) - (a[1].edits || 0)).slice(0, 5)
|
|
183
|
+
.sort((a, b) => (b[1].edits || 0) - (a[1].edits || 0)).slice(0, 5)
|
|
184
|
+
.map(([name, v]) => ({ name, edits: v.edits || 0 }));
|
|
176
185
|
const sl = agg.sessionLengths || {};
|
|
177
186
|
const marathonPct = sl.count ? Math.round((((sl.buckets?.h2to4 || 0) + (sl.buckets?.gt4h || 0)) / sl.count) * 100) : 0;
|
|
178
187
|
|
|
188
|
+
// The hotspot file (basename only, never a path) and peak weekday — the two
|
|
189
|
+
// remaining slides of the local story. Both lifetime, like the local page.
|
|
190
|
+
const top = (agg.topEditedFiles || [])[0] || null;
|
|
191
|
+
const hotName = top ? String(top.path || '').split(/[\\/]/).pop() : null;
|
|
192
|
+
const hotspot = top && hotName && !projectNameIsPrivate(hotName, config, cleanProjectName)
|
|
193
|
+
? { name: hotName, count: top.count || 0, ...(top.daysSinceLastEdit != null ? { daysSinceLastEdit: top.daysSinceLastEdit } : {}) }
|
|
194
|
+
: null;
|
|
195
|
+
let peakWeekday = null;
|
|
196
|
+
{
|
|
197
|
+
const WD = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
198
|
+
for (const [k, v] of Object.entries(agg.byWeekday || {})) {
|
|
199
|
+
if ((v.activeMs || 0) > 0 && (!peakWeekday || v.activeMs > peakWeekday.activeMs)) {
|
|
200
|
+
peakWeekday = { name: WD[Number(k)] || '', activeMs: v.activeMs };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
179
205
|
const compactions = Object.entries(agg.compactionsByDay || {})
|
|
180
206
|
.filter(([k]) => k.startsWith(`${y}-`)).reduce((acc, [, n]) => acc + n, 0);
|
|
181
207
|
|
|
@@ -188,6 +214,8 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
|
|
|
188
214
|
tokens,
|
|
189
215
|
prompts: sum('userMessages'),
|
|
190
216
|
streakBest: agg.longestStreak || 0,
|
|
217
|
+
streak: agg.streak || 0,
|
|
218
|
+
daysSinceFirst: agg.daysSinceFirst || 0,
|
|
191
219
|
daysActive: days.filter(([, d]) => (d.activeMs || 0) > 0 || (d.userMessages || 0) > 0).length,
|
|
192
220
|
linesAdded: sum('linesAdded'),
|
|
193
221
|
linesRemoved: sum('linesRemoved'),
|
|
@@ -204,6 +232,8 @@ export function buildWrappedPayload(aggregate, config, { instanceId, year, now =
|
|
|
204
232
|
topLanguages,
|
|
205
233
|
topModels,
|
|
206
234
|
toolMix,
|
|
235
|
+
...(hotspot ? { hotspot } : {}),
|
|
236
|
+
...(peakWeekday ? { peakWeekday } : {}),
|
|
207
237
|
},
|
|
208
238
|
};
|
|
209
239
|
}
|