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 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.vercel.app →](https://claude-rpc.vercel.app)** — see it in one page.
18
+ **[claude-rpc.com →](https://claude-rpc.com)** — see it in one page.
19
19
 
20
20
  [![community · sessions](https://claude-rpc-totals.claude-rpc.workers.dev/sessions.svg)](#community-totals)   [![community · tokens](https://claude-rpc-totals.claude-rpc.workers.dev/tokens.svg)](#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.vercel.app/install | sh
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
- [![Claude Code stats](https://claude-rpc-totals.claude-rpc.workers.dev/card/<you>.svg)](https://claude-rpc.vercel.app/u/<you>)
152
+ [![Claude Code stats](https://claude-rpc-totals.claude-rpc.workers.dev/card/<you>.svg)](https://claude-rpc.com/u/<you>)
153
153
 
154
- [![Claude Code hours](https://claude-rpc-totals.claude-rpc.workers.dev/badge/<you>.svg?metric=hours)](https://claude-rpc.vercel.app/?ref=badge)
154
+ [![Claude Code hours](https://claude-rpc-totals.claude-rpc.workers.dev/badge/<you>.svg?metric=hours)](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.1",
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://github.com/rar-file/claude-rpc#readme",
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.vercel.app · v${VERSION}</text>
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.vercel.app</text>
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.vercel.app';
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.vercel.app + GitHub login) drives the same worker
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.vercel.app/squad/${id}`; }
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.vercel.app/link (log in with
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.vercel.app/link';
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.vercel.app/u/${encodeURIComponent(p.handle)}${c.reset}`);
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.vercel.app/u/${encodeURIComponent(p.handle)}` : '';
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.vercel.app/u/${encodeURIComponent(cfg.profile.handle)}${c.reset}`);
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; the 4s default keeps us under Discord's
411
- // ~5-per-20s SET_ACTIVITY limit (see default-config.minActivityGapMs).
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);
@@ -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.vercel.app/?ref=discord" },
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
- export function throttleDecision({ hash, lastSentHash, lastSentAt, now, gapMs, flushPending }) {
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
- const waitMs = Math.max(0, (lastSentAt || 0) + gapMs - now);
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
- { cwd, timeout: 1500 },
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.vercel.app`)}</text>
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.vercel.app/?ref=wrapped';
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.vercel.app</div>
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>
@@ -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
- exec(opener, () => {});
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
 
@@ -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.vercel.app · v${VERSION}`)}</text>
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
 
package/src/version.js CHANGED
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { ROOT } from './paths.js';
13
13
 
14
- const BAKED = '1.0.1';
14
+ const BAKED = '1.0.3';
15
15
 
16
16
  function readPkgVersion() {
17
17
  try {