claude-rpc 1.0.1 → 1.0.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/README.md +4 -4
- package/package.json +2 -2
- package/src/calendar.js +1 -1
- package/src/card.js +1 -1
- package/src/cli.js +8 -8
- package/src/daemon.js +30 -2
- package/src/default-config.js +12 -1
- package/src/gist.js +4 -2
- package/src/install.js +5 -3
- package/src/presence.js +28 -2
- package/src/privacy.js +6 -1
- package/src/profile.js +1 -1
- package/src/server/assets/wrapped.client.js +2 -2
- package/src/server/index.js +2 -1
- package/src/session-card.js +1 -1
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
**Discord Rich Presence for [Claude Code](https://claude.com/claude-code)** — your live model, project, tokens, and lifetime stats, driven by the hooks Claude Code already fires.
|
|
17
17
|
|
|
18
|
-
**[claude-rpc.
|
|
18
|
+
**[claude-rpc.com →](https://claude-rpc.com)** — see it in one page.
|
|
19
19
|
|
|
20
20
|
[](#community-totals) [](#community-totals)
|
|
21
21
|
|
|
@@ -53,7 +53,7 @@ That installs `claude-rpc` globally, wires the hooks into Claude Code, and start
|
|
|
53
53
|
**Prefer a one-liner that figures it out for you?**
|
|
54
54
|
|
|
55
55
|
```sh
|
|
56
|
-
curl -fsSL https://claude-rpc.
|
|
56
|
+
curl -fsSL https://claude-rpc.com/install | sh
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Detects Node (installs the npm package) or falls back to the prebuilt Apple-Silicon binary, then runs `setup` for you.
|
|
@@ -149,9 +149,9 @@ claude-rpc readme --raw | pbcopy # straight to your clipboard
|
|
|
149
149
|
**Live card + badges, paste once.** With a public profile (`claude-rpc profile set --handle <you> && claude-rpc profile on`), your stats are served as an always-current card (and badges) from the community worker — no `gh`, no gist, nothing to re-run:
|
|
150
150
|
|
|
151
151
|
```md
|
|
152
|
-
[](https://claude-rpc.
|
|
152
|
+
[](https://claude-rpc.com/u/<you>)
|
|
153
153
|
|
|
154
|
-
[](https://claude-rpc.
|
|
154
|
+
[](https://claude-rpc.com/?ref=badge)
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
The card shows tokens, sessions, active hours and streak; badge `metric=` is one of `tokens · sessions · hours · streak` (optional `&label=` to retitle). Both refresh themselves as the daemon flushes your profile (~every 30 min). Your profile page at `/u/<you>` has a one-click "copy" for the whole block.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-rpc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
|
7
7
|
"author": "Archer Simmons",
|
|
8
|
-
"homepage": "https://
|
|
8
|
+
"homepage": "https://claude-rpc.com",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/rar-file/claude-rpc.git"
|
package/src/calendar.js
CHANGED
|
@@ -86,7 +86,7 @@ export function renderCalendar(aggregate, { weeks = 53, generatedAt = new Date()
|
|
|
86
86
|
<text x="${W - 170}" y="${H - 10}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="9" fill="${PALETTE.inkFaint}">less</text>
|
|
87
87
|
${legend}
|
|
88
88
|
<text x="${W - 28}" y="${H - 10}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="9" fill="${PALETTE.inkFaint}">more</text>
|
|
89
|
-
<text x="${padX}" y="${H - 8}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="9" fill="${PALETTE.inkFaint}">claude-rpc.
|
|
89
|
+
<text x="${padX}" y="${H - 8}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="9" fill="${PALETTE.inkFaint}">claude-rpc.com · v${VERSION}</text>
|
|
90
90
|
</svg>`;
|
|
91
91
|
}
|
|
92
92
|
|
package/src/card.js
CHANGED
|
@@ -322,7 +322,7 @@ export function renderCard(aggregate, { range = 'year', generatedAt = new Date()
|
|
|
322
322
|
<!-- ── credits ── -->
|
|
323
323
|
<g transform="translate(${W - 60} ${H - 24})" text-anchor="end">
|
|
324
324
|
<text font-family="JetBrains Mono, ui-monospace, monospace" font-size="10"
|
|
325
|
-
fill="${PALETTE.inkFaint}">${escapeXml(allTimeHours)}h all-time · claude-rpc.
|
|
325
|
+
fill="${PALETTE.inkFaint}">${escapeXml(allTimeHours)}h all-time · claude-rpc.com</text>
|
|
326
326
|
</g>
|
|
327
327
|
</svg>`;
|
|
328
328
|
}
|
package/src/cli.js
CHANGED
|
@@ -1066,7 +1066,7 @@ function doReadme(argv) {
|
|
|
1066
1066
|
const raw = argv.includes('--raw') || argv.includes('--plain');
|
|
1067
1067
|
const cfg = loadConfig();
|
|
1068
1068
|
const endpoint = (cfg.community?.endpoint || 'https://claude-rpc-totals.claude-rpc.workers.dev').replace(/\/+$/, '');
|
|
1069
|
-
const site = 'https://claude-rpc.
|
|
1069
|
+
const site = 'https://claude-rpc.com';
|
|
1070
1070
|
const profile = cfg.profile || {};
|
|
1071
1071
|
const handle = profile.handle;
|
|
1072
1072
|
const live = !!profile.enabled && lb.isValidHandle(handle);
|
|
@@ -1287,7 +1287,7 @@ async function doExport(argv) {
|
|
|
1287
1287
|
// `claude-rpc squad join <code>`
|
|
1288
1288
|
// `claude-rpc squad` / `squad status` → standings for every squad you're in
|
|
1289
1289
|
// `claude-rpc squad leave [id]`
|
|
1290
|
-
// The web flow (claude-rpc.
|
|
1290
|
+
// The web flow (claude-rpc.com + GitHub login) drives the same worker
|
|
1291
1291
|
// endpoints; the CLI authenticates with the community instanceId it already has.
|
|
1292
1292
|
|
|
1293
1293
|
function squadAuth() {
|
|
@@ -1332,7 +1332,7 @@ function squadAuth() {
|
|
|
1332
1332
|
return { cfg, endpoint, instanceId, post, get };
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
|
-
function squadPageUrl(id) { return `https://claude-rpc.
|
|
1335
|
+
function squadPageUrl(id) { return `https://claude-rpc.com/squad/${id}`; }
|
|
1336
1336
|
|
|
1337
1337
|
function printSquadInvite(squad) {
|
|
1338
1338
|
console.log('');
|
|
@@ -1431,11 +1431,11 @@ async function doSquadCmd(argv) {
|
|
|
1431
1431
|
// one-time code, no browser needed
|
|
1432
1432
|
// claude-rpc link <code> on the NEW machine → claims it, merging this
|
|
1433
1433
|
// install into the same leaderboard identity
|
|
1434
|
-
// The browser fallback lives at claude-rpc.
|
|
1434
|
+
// The browser fallback lives at claude-rpc.com/link (log in with
|
|
1435
1435
|
// GitHub → same code) for when the other machine isn't handy. Claiming
|
|
1436
1436
|
// verifies the profile (✓) and unlocks managing squads from the browser.
|
|
1437
1437
|
|
|
1438
|
-
const LINK_PAGE = 'https://claude-rpc.
|
|
1438
|
+
const LINK_PAGE = 'https://claude-rpc.com/link';
|
|
1439
1439
|
|
|
1440
1440
|
// Mint side: this machine asks the worker for a code. The worker only obliges
|
|
1441
1441
|
// when this install's canonical profile is verified — the ✓ a claim grants
|
|
@@ -1638,7 +1638,7 @@ function profileNextStep() {
|
|
|
1638
1638
|
const p = loadConfig().profile || {};
|
|
1639
1639
|
const next = profileSteps(p).find((s) => !s.done);
|
|
1640
1640
|
if (!next) {
|
|
1641
|
-
console.log(` ${c.dim}→ all set — you're live at${c.reset} ${c.cyan}https://claude-rpc.
|
|
1641
|
+
console.log(` ${c.dim}→ all set — you're live at${c.reset} ${c.cyan}https://claude-rpc.com/u/${encodeURIComponent(p.handle)}${c.reset}`);
|
|
1642
1642
|
} else if (next.key === 'verify') {
|
|
1643
1643
|
console.log(` ${c.dim}→ next: run${c.reset} ${c.cyan}claude-rpc link${c.reset} ${c.dim}on a machine that's already verified, then${c.reset} ${c.cyan}claude-rpc link <code>${c.reset} ${c.dim}here — first machine? ${LINK_PAGE}${c.reset}`);
|
|
1644
1644
|
} else {
|
|
@@ -1649,7 +1649,7 @@ function profileNextStep() {
|
|
|
1649
1649
|
function profileStatus() {
|
|
1650
1650
|
const p = (loadConfig().profile) || {};
|
|
1651
1651
|
const handleOk = lb.isValidHandle(p.handle);
|
|
1652
|
-
const boardUrl = handleOk ? `https://claude-rpc.
|
|
1652
|
+
const boardUrl = handleOk ? `https://claude-rpc.com/u/${encodeURIComponent(p.handle)}` : '';
|
|
1653
1653
|
|
|
1654
1654
|
console.log('');
|
|
1655
1655
|
console.log(` ${c.bold}${c.magenta}◆ profile${c.reset} ${c.dim}— public leaderboard identity${c.reset}`);
|
|
@@ -1776,7 +1776,7 @@ async function profilePublish() {
|
|
|
1776
1776
|
console.log(` ${c.dim}publishing @${cfg.profile.handle} to the board…${c.reset}`);
|
|
1777
1777
|
const r = await flushProfile(cfg);
|
|
1778
1778
|
if (r.ok) {
|
|
1779
|
-
console.log(` ${c.green}✓${c.reset} published — see it at ${c.cyan}https://claude-rpc.
|
|
1779
|
+
console.log(` ${c.green}✓${c.reset} published — see it at ${c.cyan}https://claude-rpc.com/u/${encodeURIComponent(cfg.profile.handle)}${c.reset}`);
|
|
1780
1780
|
} else if (r.reason === 'rate-limited') {
|
|
1781
1781
|
console.log(` ${c.yellow}!${c.reset} rate-limited — already published in the last minute; the board has you`);
|
|
1782
1782
|
} else {
|
package/src/daemon.js
CHANGED
|
@@ -117,6 +117,15 @@ let lastSentHash = '';
|
|
|
117
117
|
let lastSentAt = 0; // ms of the last write ATTEMPT (success or fail → back off either way)
|
|
118
118
|
let pendingSend = null; // { kind, activity, hash, logMsg } coalesced during the gap; latest wins
|
|
119
119
|
let flushTimer = null;
|
|
120
|
+
// Timestamps (ms) of recent write ATTEMPTS (set + clear both hit Discord), for
|
|
121
|
+
// the sliding-window rate cap below. The per-write gap alone leaves the daemon
|
|
122
|
+
// riding Discord's ~5-per-20s SET_ACTIVITY ceiling — many triggers fire and a
|
|
123
|
+
// 4s gap still permits 6 writes in some 20s windows, which makes Discord EMPTY
|
|
124
|
+
// the presence (card → app name + bare timer). This bounds the COUNT per window
|
|
125
|
+
// regardless of how many triggers fire. Deliberately NOT cleared by
|
|
126
|
+
// resetTransmit (a scan/session/config refresh forces a re-render but must not
|
|
127
|
+
// reset the rate budget, or those refreshes could burst us past the limit).
|
|
128
|
+
let recentSends = [];
|
|
120
129
|
// Last status we acted on for outbound side-effects (webhook / desktop notify).
|
|
121
130
|
// Tracked separately from the render hash so we fire once per transition.
|
|
122
131
|
let lastNotifiedStatus = null;
|
|
@@ -407,12 +416,24 @@ function pushPresence() {
|
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
// Minimum gap between Discord writes, read live so a config reload takes effect.
|
|
410
|
-
// Floored at 2s as a sanity minimum
|
|
411
|
-
//
|
|
419
|
+
// Floored at 2s as a sanity minimum. The gap spaces consecutive writes; the
|
|
420
|
+
// sliding-window cap below is what actually bounds writes-per-window so we stay
|
|
421
|
+
// under Discord's ~5-per-20s SET_ACTIVITY limit (see default-config).
|
|
412
422
|
function activityGapMs() {
|
|
413
423
|
return Math.max(2000, config.minActivityGapMs || 4000);
|
|
414
424
|
}
|
|
415
425
|
|
|
426
|
+
// Sliding-window rate cap parameters, read live like the gap. Defaults to 4
|
|
427
|
+
// writes per 20s — one under Discord's ~5-per-20s SET_ACTIVITY ceiling, so we
|
|
428
|
+
// stay clear of it even if Discord counts its window inclusively or our clock
|
|
429
|
+
// drifts. Floored so a bad config can't disable the protection entirely.
|
|
430
|
+
function activityWindowMs() {
|
|
431
|
+
return Math.max(5000, config.activityWindowMs || 20000);
|
|
432
|
+
}
|
|
433
|
+
function maxActivityWrites() {
|
|
434
|
+
return Math.max(2, config.maxActivityWrites || 4);
|
|
435
|
+
}
|
|
436
|
+
|
|
416
437
|
// Sentinel for a clear — can never collide with a real activity hash, since
|
|
417
438
|
// JSON.stringify of an activity object always begins with '{'.
|
|
418
439
|
const CLEAR_HASH = 'cleared';
|
|
@@ -425,6 +446,7 @@ function transmit(kind, activity, logMsg) {
|
|
|
425
446
|
const d = throttleDecision({
|
|
426
447
|
hash, lastSentHash, lastSentAt,
|
|
427
448
|
now: Date.now(), gapMs: activityGapMs(), flushPending: !!flushTimer,
|
|
449
|
+
recentSends, windowMs: activityWindowMs(), maxPerWindow: maxActivityWrites(),
|
|
428
450
|
});
|
|
429
451
|
if (d.action === 'skip') { pendingSend = null; return; }
|
|
430
452
|
if (d.action === 'send') { doSend({ kind, activity, hash, logMsg }); return; }
|
|
@@ -451,6 +473,12 @@ function flushPendingSend() {
|
|
|
451
473
|
// by the next push rather than being recorded as success and stranded.
|
|
452
474
|
async function doSend({ kind, activity, hash, logMsg }) {
|
|
453
475
|
lastSentAt = Date.now();
|
|
476
|
+
// Record the attempt and drop anything older than the window — this is the
|
|
477
|
+
// ledger throttleDecision reads to enforce the per-window cap. Counts every
|
|
478
|
+
// attempt (set AND clear) since both are SET_ACTIVITY writes to Discord.
|
|
479
|
+
recentSends.push(lastSentAt);
|
|
480
|
+
const cutoff = lastSentAt - activityWindowMs();
|
|
481
|
+
if (recentSends[0] <= cutoff) recentSends = recentSends.filter((t) => t > cutoff);
|
|
454
482
|
try {
|
|
455
483
|
if (kind === 'clear') await client.user.clearActivity();
|
|
456
484
|
else await client.user.setActivity(activity);
|
package/src/default-config.js
CHANGED
|
@@ -28,6 +28,17 @@ export const DEFAULT_CONFIG = {
|
|
|
28
28
|
// collapse to the latest and flush when the gap expires. 4s stays safely
|
|
29
29
|
// under the limit; lower values risk Discord throttling the card.
|
|
30
30
|
minActivityGapMs: 4000,
|
|
31
|
+
// Hard sliding-window cap on Discord writes, on top of minActivityGapMs. The
|
|
32
|
+
// gap alone only spaces *consecutive* writes — with a 4s gap a 20s window can
|
|
33
|
+
// still catch 6 writes (floor(20000/4000)+1) once several triggers (the
|
|
34
|
+
// rotation tick, a scan, a live-session change, a config reload) coincide,
|
|
35
|
+
// and that 6th write is what makes Discord EMPTY the presence (the card
|
|
36
|
+
// collapses to just the app name + elapsed timer, no details or art). This
|
|
37
|
+
// bounds the COUNT per window: no more than maxActivityWrites writes per
|
|
38
|
+
// activityWindowMs, whatever fires. Default 4-per-20s leaves a write of
|
|
39
|
+
// headroom under Discord's ~5-per-20s ceiling.
|
|
40
|
+
maxActivityWrites: 4,
|
|
41
|
+
activityWindowMs: 20000,
|
|
31
42
|
rescanIntervalSec: 300,
|
|
32
43
|
idleThresholdSec: 60,
|
|
33
44
|
// Time (minutes) of no hook activity AND no live transcripts on disk before
|
|
@@ -230,7 +241,7 @@ export const DEFAULT_CONFIG = {
|
|
|
230
241
|
// ?ref=discord lets the landing page attribute installs that originate
|
|
231
242
|
// from a presence card. (When the cwd is a github repo the daemon also
|
|
232
243
|
// prepends a "View on GitHub →" button, so both can show.)
|
|
233
|
-
{ label: "Get claude-rpc →", url: "https://claude-rpc.
|
|
244
|
+
{ label: "Get claude-rpc →", url: "https://claude-rpc.com/?ref=discord" },
|
|
234
245
|
],
|
|
235
246
|
},
|
|
236
247
|
statusIcons: {
|
package/src/gist.js
CHANGED
|
@@ -27,9 +27,11 @@ function ghQuote(a) {
|
|
|
27
27
|
return /[\s"]/.test(a) ? `"${String(a).replace(/"/g, '""')}"` : a;
|
|
28
28
|
}
|
|
29
29
|
function gh(args, opts = {}) {
|
|
30
|
+
// windowsHide so the gh.exe/cmd shim doesn't flash a console window on
|
|
31
|
+
// Windows (shell:true routes through cmd.exe; hide that too). No-op elsewhere.
|
|
30
32
|
return WIN
|
|
31
|
-
? spawnSync('gh', args.map(ghQuote), { ...opts, shell: true })
|
|
32
|
-
: spawnSync('gh', args, opts);
|
|
33
|
+
? spawnSync('gh', args.map(ghQuote), { ...opts, shell: true, windowsHide: true })
|
|
34
|
+
: spawnSync('gh', args, { ...opts, windowsHide: true });
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Bare fetch has no total timeout; a stalled GitHub endpoint would hang the
|
package/src/install.js
CHANGED
|
@@ -447,15 +447,15 @@ export function installMcp({ exePath, scope = 'user' } = {}) {
|
|
|
447
447
|
const { command, args } = mcpServerCommand(exePath);
|
|
448
448
|
const winShell = process.platform === 'win32';
|
|
449
449
|
// Replace any stale entry first so re-running is idempotent (ignore failure).
|
|
450
|
-
spawnSync('claude', ['mcp', 'remove', 'claude-rpc', '--scope', scope], { stdio: 'ignore', shell: winShell });
|
|
451
|
-
const r = spawnSync('claude', ['mcp', 'add', 'claude-rpc', '--scope', scope, '--', command, ...args], { stdio: 'inherit', shell: winShell });
|
|
450
|
+
spawnSync('claude', ['mcp', 'remove', 'claude-rpc', '--scope', scope], { stdio: 'ignore', shell: winShell, windowsHide: true });
|
|
451
|
+
const r = spawnSync('claude', ['mcp', 'add', 'claude-rpc', '--scope', scope, '--', command, ...args], { stdio: 'inherit', shell: winShell, windowsHide: true });
|
|
452
452
|
if (r.error && r.error.code === 'ENOENT') return { ok: false, reason: 'no-claude', command, args };
|
|
453
453
|
if (r.status !== 0) return { ok: false, reason: 'add-failed', code: r.status, command, args };
|
|
454
454
|
return { ok: true, command, args, scope };
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
export function uninstallMcp({ scope = 'user' } = {}) {
|
|
458
|
-
const r = spawnSync('claude', ['mcp', 'remove', 'claude-rpc', '--scope', scope], { stdio: 'inherit', shell: process.platform === 'win32' });
|
|
458
|
+
const r = spawnSync('claude', ['mcp', 'remove', 'claude-rpc', '--scope', scope], { stdio: 'inherit', shell: process.platform === 'win32', windowsHide: true });
|
|
459
459
|
if (r.error && r.error.code === 'ENOENT') return { ok: false, reason: 'no-claude' };
|
|
460
460
|
return { ok: r.status === 0 };
|
|
461
461
|
}
|
|
@@ -666,6 +666,7 @@ function promoteNpxToGlobal() {
|
|
|
666
666
|
const r = spawnSync('npm', ['install', '-g', `claude-rpc@${VERSION}`], {
|
|
667
667
|
encoding: 'utf8',
|
|
668
668
|
shell: process.platform === 'win32', // npm is npm.cmd on Windows
|
|
669
|
+
windowsHide: true, // don't flash a console window
|
|
669
670
|
});
|
|
670
671
|
if (r.error || r.status !== 0) {
|
|
671
672
|
// The piped npm chatter only matters when it failed.
|
|
@@ -687,6 +688,7 @@ function warnIfStale() {
|
|
|
687
688
|
const r = spawnSync('npm', ['view', 'claude-rpc', 'version'], {
|
|
688
689
|
encoding: 'utf8', timeout: 4000,
|
|
689
690
|
shell: process.platform === 'win32', // npm is npm.cmd on Windows
|
|
691
|
+
windowsHide: true, // don't flash a console window
|
|
690
692
|
});
|
|
691
693
|
const latest = (r.stdout || '').trim();
|
|
692
694
|
if (!latest || latest === VERSION) return;
|
package/src/presence.js
CHANGED
|
@@ -113,9 +113,35 @@ export function shouldShowGithubButton(p, state) {
|
|
|
113
113
|
// - 'defer' : inside the gap, or a flush is already queued — coalesce to the
|
|
114
114
|
// LATEST payload and let the caller flush it in `waitMs`, so the
|
|
115
115
|
// final state of every burst still lands, once, under the limit
|
|
116
|
-
|
|
116
|
+
//
|
|
117
|
+
// The per-write `gapMs` only spaces *consecutive* writes; it does NOT bound
|
|
118
|
+
// writes-per-window. With gapMs == windowMs/maxPerWindow (the 4s default sits
|
|
119
|
+
// exactly on Discord's ~5-per-20s ceiling) a 20s window can still catch
|
|
120
|
+
// floor(windowMs/gapMs)+1 == 6 gap-spaced writes — one over the limit — because
|
|
121
|
+
// many independent triggers (the rotation tick re-rendering {toolElapsed}, a
|
|
122
|
+
// background scan, a live-session change, a config reload) each want to write.
|
|
123
|
+
// That extra write is exactly what makes Discord EMPTY the presence (the card
|
|
124
|
+
// collapses to just the app name + elapsed timer). So when `recentSends` (the
|
|
125
|
+
// timestamps of recent writes, set+clear) and `maxPerWindow` are supplied we
|
|
126
|
+
// add a hard sliding-window cap on top of the gap: defer until the oldest write
|
|
127
|
+
// in the window expires, guaranteeing no more than `maxPerWindow` writes in any
|
|
128
|
+
// `windowMs` no matter how many triggers fire. Omitting them keeps the old
|
|
129
|
+
// gap-only behavior (used by tests that predate the cap).
|
|
130
|
+
export function throttleDecision({ hash, lastSentHash, lastSentAt, now, gapMs, flushPending, recentSends, windowMs, maxPerWindow }) {
|
|
117
131
|
if (hash === lastSentHash) return { action: 'skip', waitMs: 0 };
|
|
118
|
-
|
|
132
|
+
let waitMs = Math.max(0, (lastSentAt || 0) + gapMs - now);
|
|
133
|
+
if (maxPerWindow && windowMs && Array.isArray(recentSends)) {
|
|
134
|
+
// Strict window (age < windowMs) so a write exactly windowMs old has already
|
|
135
|
+
// fallen out — matches a "requests in the last 20s" limiter and keeps the
|
|
136
|
+
// boundary case from sneaking a write over the line.
|
|
137
|
+
const inWindow = recentSends.filter((t) => t > now - windowMs);
|
|
138
|
+
if (inWindow.length >= maxPerWindow) {
|
|
139
|
+
// The oldest write we must let expire before another may go out. Once it
|
|
140
|
+
// ages past windowMs the window drops below the cap and we can send.
|
|
141
|
+
const oldest = inWindow[inWindow.length - maxPerWindow];
|
|
142
|
+
waitMs = Math.max(waitMs, oldest + windowMs - now);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
119
145
|
if (waitMs === 0 && !flushPending) return { action: 'send', waitMs: 0 };
|
|
120
146
|
return { action: 'defer', waitMs };
|
|
121
147
|
}
|
package/src/privacy.js
CHANGED
|
@@ -197,7 +197,12 @@ function probeGithubPrivate(cwd) {
|
|
|
197
197
|
execFile(
|
|
198
198
|
'gh',
|
|
199
199
|
['repo', 'view', '--json', 'isPrivate', '-q', '.isPrivate'],
|
|
200
|
-
|
|
200
|
+
// windowsHide: on Windows `gh` is a console app; without this the daemon —
|
|
201
|
+
// which runs with no console of its own — pops a visible terminal window for
|
|
202
|
+
// the life of the probe (~1s) every time this fires (a cwd cache-miss, i.e.
|
|
203
|
+
// entering a project, then once per 5-min TTL). That's the stray terminal
|
|
204
|
+
// flash users see "sometimes" while working. No-op off Windows.
|
|
205
|
+
{ cwd, timeout: 1500, windowsHide: true },
|
|
201
206
|
(err, stdout) => {
|
|
202
207
|
ghProbeInFlight.delete(cwd);
|
|
203
208
|
let value = null;
|
package/src/profile.js
CHANGED
|
@@ -135,7 +135,7 @@ export function renderProfileCard(aggregate, { handle = '', generatedAt = new Da
|
|
|
135
135
|
<!-- footer -->
|
|
136
136
|
<text x="40" y="${H - 18}"
|
|
137
137
|
font-family="JetBrains Mono, ui-monospace, monospace"
|
|
138
|
-
font-size="11" fill="${PALETTE.inkFaint}">${escapeXml(`best streak ${t.longestStreak}d · ≈${fmtCost(t.cost)} · claude-rpc.
|
|
138
|
+
font-size="11" fill="${PALETTE.inkFaint}">${escapeXml(`best streak ${t.longestStreak}d · ≈${fmtCost(t.cost)} · claude-rpc.com`)}</text>
|
|
139
139
|
</svg>`;
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// Landing-page CTA used by the finale share. The dashboard is served on
|
|
21
21
|
// localhost, so sharing `location.href` hands the recipient a dead link —
|
|
22
22
|
// the finale shares this public URL + a stats summary instead.
|
|
23
|
-
const LANDING = 'https://claude-rpc.
|
|
23
|
+
const LANDING = 'https://claude-rpc.com/?ref=wrapped';
|
|
24
24
|
let _shareText = ''; // built in the finale slide, consumed by wireFinale
|
|
25
25
|
|
|
26
26
|
// ── build the story slides from the wrapped payload ──────────
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
${cell('Lines', (d.linesNet >= 0 ? '+' : '−') + fmtNum(Math.abs(d.linesNet)), 'grass')}
|
|
132
132
|
${cell('Hotspot', d.hotspot ? esc(d.hotspot.name) : '—')}
|
|
133
133
|
</div>
|
|
134
|
-
<div class="foot">made with claude-rpc · claude-rpc.
|
|
134
|
+
<div class="foot">made with claude-rpc · claude-rpc.com</div>
|
|
135
135
|
</div>
|
|
136
136
|
<div class="actions">
|
|
137
137
|
<button class="btn primary" id="w-replay">↺ replay</button>
|
package/src/server/index.js
CHANGED
|
@@ -133,7 +133,8 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
133
133
|
const opener = process.platform === 'win32' ? `start "" "${url}"`
|
|
134
134
|
: process.platform === 'darwin' ? `open "${url}"`
|
|
135
135
|
: `xdg-open "${url}"`;
|
|
136
|
-
|
|
136
|
+
// windowsHide so the cmd.exe that runs `start` doesn't flash a console.
|
|
137
|
+
exec(opener, { windowsHide: true }, () => {});
|
|
137
138
|
}
|
|
138
139
|
});
|
|
139
140
|
|
package/src/session-card.js
CHANGED
|
@@ -60,7 +60,7 @@ export function renderSessionCard(vars = {}, { generatedAt = new Date() } = {})
|
|
|
60
60
|
${statCell(C[1], 184, 'Est. cost', String(cost), PALETTE.blurple)}
|
|
61
61
|
${statCell(C[2], 184, 'Reads', String(v.filesRead ?? 0))}
|
|
62
62
|
|
|
63
|
-
<text x="40" y="${H - 16}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="10" fill="${PALETTE.inkFaint}">${escapeXml(`${v.currentFilePretty ? 'last: ' + v.currentFilePretty + ' · ' : ''}claude-rpc.
|
|
63
|
+
<text x="40" y="${H - 16}" font-family="JetBrains Mono, ui-monospace, monospace" font-size="10" fill="${PALETTE.inkFaint}">${escapeXml(`${v.currentFilePretty ? 'last: ' + v.currentFilePretty + ' · ' : ''}claude-rpc.com · v${VERSION}`)}</text>
|
|
64
64
|
</svg>`;
|
|
65
65
|
}
|
|
66
66
|
|