clay-server 2.17.0-beta.1 → 2.17.0-beta.10

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/clay-server)](https://www.npmjs.com/package/clay-server) [![npm downloads](https://img.shields.io/npm/dw/clay-server)](https://www.npmjs.com/package/clay-server) [![GitHub stars](https://img.shields.io/github/stars/chadbyte/clay)](https://github.com/chadbyte/clay) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/chadbyte/clay/blob/main/LICENSE)
10
10
 
11
- Clay gives Claude Code a browser UI that runs on any device. Use it from your phone, run it on macOS, Windows, or Linux. Invite teammates, manage multiple projects from one sidebar, and get push notifications when Claude needs you. Built on the official Claude Agent SDK, not a terminal parser. Your machine is the server. No cloud relay in between, no extra network surface.
11
+ Clay gives Claude Code a browser UI that runs on any device. Use it from your phone, run it on macOS, Windows, or Linux. Invite teammates, manage multiple projects from one sidebar, and get push notifications when Claude needs you. HTTPS and push notifications work out of the box with zero config. Built on the official Claude Agent SDK, not a terminal parser. Your machine is the server. No cloud relay in between, no extra network surface.
12
12
 
13
13
  ---
14
14
 
@@ -98,7 +98,7 @@ Take it further with Ralph Loop, an autonomous coding loop built into Clay. The
98
98
 
99
99
  Your data flows directly from your machine to the Anthropic API, exactly as it does when you use the CLI. Clay adds a browser layer on top, not a middleman.
100
100
 
101
- PIN authentication, per-project/session permissions, and HTTPS are supported by default. For local network use, this is sufficient. For remote access, we recommend a VPN like Tailscale.
101
+ HTTPS is enabled by default with a builtin certificate. PIN authentication and per-project/session permissions are built in. For local network use, this is sufficient. For remote access, we recommend a VPN like Tailscale.
102
102
 
103
103
  ---
104
104
 
@@ -166,20 +166,32 @@ Yes. Create as many as you need. A code reviewer, a writing partner, a project m
166
166
 
167
167
  ---
168
168
 
169
- ## HTTPS for Push
169
+ ## HTTPS
170
170
 
171
- Everything works out of the box. Only push notifications require HTTPS.
171
+ HTTPS is enabled by default using a builtin wildcard certificate for `*.d.clay.studio`. No setup required. Available from `v2.17.0-beta.2`. Your browser connects to a URL like:
172
172
 
173
- Set it up once with [mkcert](https://github.com/FiloSottile/mkcert):
173
+ ```
174
+ https://192-168-1-50.d.clay.studio:2633
175
+ ```
176
+
177
+ The domain resolves to your local IP. All traffic stays on your network. See [clay-dns](clay-dns/) for details on how this works.
178
+
179
+ Push notifications require HTTPS, so they work out of the box with this setup. Install Clay as a PWA on your device to receive them.
180
+
181
+ <details>
182
+ <summary><strong>Alternative: local certificate with mkcert</strong></summary>
183
+
184
+ If you prefer to use a locally generated certificate (e.g. air-gapped environments where DNS is unavailable):
174
185
 
175
186
  ```bash
176
187
  brew install mkcert
177
188
  mkcert -install
189
+ npx clay-server --local-cert
178
190
  ```
179
191
 
180
- Certificates are auto-generated. The setup wizard handles the rest.
192
+ This generates a self-signed certificate trusted by your machine. The setup wizard will guide you through installing the CA on other devices.
181
193
 
182
- If push registration fails: check that your browser trusts the HTTPS certificate and that your phone can reach the server address.
194
+ </details>
183
195
 
184
196
  ---
185
197
 
@@ -192,6 +204,7 @@ npx clay-server --yes # Skip interactive prompts (use defaults)
192
204
  npx clay-server -y --pin 123456
193
205
  # Non-interactive + PIN (for scripts/CI)
194
206
  npx clay-server --no-https # Disable HTTPS
207
+ npx clay-server --local-cert # Use local certificate (mkcert) instead of builtin
195
208
  npx clay-server --no-update # Skip update check
196
209
  npx clay-server --debug # Enable debug panel
197
210
  npx clay-server --add . # Add current directory to running daemon
package/bin/cli.js CHANGED
@@ -53,6 +53,7 @@ var args = process.argv.slice(2);
53
53
  var port = _isDev ? 2635 : 2633;
54
54
  var useHttps = true;
55
55
  var forceMkcert = false;
56
+ var forceBuiltin = false;
56
57
  var skipUpdate = false;
57
58
  var debugMode = false;
58
59
  var autoYes = false;
@@ -84,6 +85,8 @@ for (var i = 0; i < args.length; i++) {
84
85
  useHttps = false;
85
86
  } else if (args[i] === "--local-cert") {
86
87
  forceMkcert = true;
88
+ } else if (args[i] === "--builtin-cert") {
89
+ forceBuiltin = true;
87
90
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
88
91
  skipUpdate = true;
89
92
  } else if (args[i] === "--dev") {
@@ -128,7 +131,8 @@ for (var i = 0; i < args.length; i++) {
128
131
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
129
132
  console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
130
133
  console.log(" --no-https Disable HTTPS (enabled by default)");
131
- console.log(" --local-cert Use local certificate (mkcert) instead of builtin");
134
+ console.log(" --local-cert Use local certificate (mkcert), suppress migration notice");
135
+ console.log(" --builtin-cert Use builtin certificate even if mkcert is installed");
132
136
  console.log(" --no-update Skip auto-update check on startup");
133
137
  console.log(" --debug Enable debug panel in the web UI");
134
138
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
@@ -599,10 +603,11 @@ function toClayStudioUrl(ip, port, protocol) {
599
603
  }
600
604
 
601
605
  function ensureCerts(ip) {
602
- // Check builtin cert first (unless --local-cert flag is set)
603
- if (!forceMkcert) {
606
+ // --builtin-cert: skip mkcert entirely, go straight to builtin
607
+ if (forceBuiltin) {
604
608
  var builtin = getBuiltinCert();
605
609
  if (builtin) return builtin;
610
+ return null;
606
611
  }
607
612
 
608
613
  var homeDir = os.homedir();
@@ -619,24 +624,30 @@ function ensureCerts(ip) {
619
624
  fs.copyFileSync(legacyCert, certPath);
620
625
  }
621
626
 
627
+ var mkcertInstalled = hasMkcert();
628
+
622
629
  var caRoot = null;
623
- try {
624
- caRoot = path.join(
625
- execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
626
- "rootCA.pem"
627
- );
628
- if (!fs.existsSync(caRoot)) caRoot = null;
629
- } catch (e) {}
630
+ if (mkcertInstalled) {
631
+ try {
632
+ caRoot = path.join(
633
+ execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
634
+ "rootCA.pem"
635
+ );
636
+ if (!fs.existsSync(caRoot)) caRoot = null;
637
+ } catch (e) {}
638
+ }
630
639
 
631
640
  // Collect all IPv4 addresses (Tailscale + LAN)
632
641
  var allIPs = getAllIPs();
633
642
 
634
643
  if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
635
644
  var needRegen = false;
645
+ var isMkcertCert = false;
636
646
  try {
637
647
  var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
638
648
  // If cert is from an external CA (e.g. Tailscale/Let's Encrypt), never regenerate
639
649
  if (certText.indexOf("mkcert") === -1) return { key: keyPath, cert: certPath, caRoot: caRoot };
650
+ isMkcertCert = true;
640
651
  for (var i = 0; i < allIPs.length; i++) {
641
652
  if (certText.indexOf(allIPs[i]) === -1) {
642
653
  needRegen = true;
@@ -644,24 +655,41 @@ function ensureCerts(ip) {
644
655
  }
645
656
  }
646
657
  } catch (e) { needRegen = true; }
647
- if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
658
+ // mkcert cert but mkcert uninstalled: CA is gone, cert is untrusted. Skip it.
659
+ if (isMkcertCert && !mkcertInstalled) needRegen = true;
660
+ if (!needRegen) {
661
+ return { key: keyPath, cert: certPath, caRoot: caRoot, mkcertDetected: mkcertInstalled && !forceMkcert };
662
+ }
648
663
  }
649
664
 
650
- fs.mkdirSync(certDir, { recursive: true });
665
+ // mkcert installed: generate local cert (legacy behavior)
666
+ if (mkcertInstalled) {
667
+ fs.mkdirSync(certDir, { recursive: true });
668
+
669
+ var domains = ["localhost", "127.0.0.1", "::1"];
670
+ for (var i = 0; i < allIPs.length; i++) {
671
+ if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
672
+ }
673
+
674
+ try {
675
+ var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
676
+ execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
677
+ } catch (err) {
678
+ // mkcert generation failed, fall through to builtin
679
+ }
651
680
 
652
- var domains = ["localhost", "127.0.0.1", "::1"];
653
- for (var i = 0; i < allIPs.length; i++) {
654
- if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
681
+ if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
682
+ return { key: keyPath, cert: certPath, caRoot: caRoot, mkcertDetected: !forceMkcert };
683
+ }
655
684
  }
656
685
 
657
- try {
658
- var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
659
- execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
660
- } catch (err) {
661
- return null;
686
+ // Fallback: builtin cert (unless --local-cert forces mkcert-only)
687
+ if (!forceMkcert) {
688
+ var builtin = getBuiltinCert();
689
+ if (builtin) return builtin;
662
690
  }
663
691
 
664
- return { key: keyPath, cert: certPath, caRoot: caRoot };
692
+ return null;
665
693
  }
666
694
 
667
695
  // --- Logo ---
@@ -1436,12 +1464,14 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1436
1464
  var ip = getLocalIP();
1437
1465
  var hasTls = false;
1438
1466
  var hasBuiltinCert = false;
1467
+ var mkcertDetected = false;
1439
1468
 
1440
1469
  if (useHttps) {
1441
1470
  var certPaths = ensureCerts(ip);
1442
1471
  if (certPaths) {
1443
1472
  hasTls = true;
1444
1473
  if (certPaths.builtin) hasBuiltinCert = true;
1474
+ if (certPaths.mkcertDetected) mkcertDetected = true;
1445
1475
  } else {
1446
1476
  log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
1447
1477
  }
@@ -1516,6 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1516
1546
  pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
1517
1547
  tls: hasTls,
1518
1548
  builtinCert: hasBuiltinCert,
1549
+ mkcertDetected: mkcertDetected,
1519
1550
  debug: debugMode,
1520
1551
  keepAwake: keepAwake,
1521
1552
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1595,6 +1626,8 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1595
1626
  : protocol + "://" + ip + ":" + config.port;
1596
1627
  console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
1597
1628
  console.log(" " + sym.done + " " + url);
1629
+ if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network.");
1630
+ if (config.mkcertDetected) console.log(" " + sym.warn + " Clay now ships with a builtin HTTPS certificate. To use it, pass --builtin-cert or uninstall mkcert.");
1598
1631
  console.log(" " + sym.done + " Headless mode — exiting CLI");
1599
1632
  process.exit(0);
1600
1633
  return;
@@ -1611,12 +1644,14 @@ async function devMode(mode, keepAwake, existingPinHash) {
1611
1644
  var ip = getLocalIP();
1612
1645
  var hasTls = false;
1613
1646
  var hasBuiltinCert = false;
1647
+ var mkcertDetected = false;
1614
1648
 
1615
1649
  if (useHttps) {
1616
1650
  var certPaths = ensureCerts(ip);
1617
1651
  if (certPaths) {
1618
1652
  hasTls = true;
1619
1653
  if (certPaths.builtin) hasBuiltinCert = true;
1654
+ if (certPaths.mkcertDetected) mkcertDetected = true;
1620
1655
  }
1621
1656
  }
1622
1657
 
@@ -1680,6 +1715,7 @@ async function devMode(mode, keepAwake, existingPinHash) {
1680
1715
  pinHash: existingPinHash || null,
1681
1716
  tls: hasTls,
1682
1717
  builtinCert: hasBuiltinCert,
1718
+ mkcertDetected: mkcertDetected,
1683
1719
  debug: true,
1684
1720
  keepAwake: keepAwake || false,
1685
1721
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1829,6 +1865,7 @@ async function restartDaemonWithTLS(config, callback) {
1829
1865
  return;
1830
1866
  }
1831
1867
  var hasBuiltinCert = !!(certPaths && certPaths.builtin);
1868
+ var mkcertDetected = !!(certPaths && certPaths.mkcertDetected);
1832
1869
 
1833
1870
  // Shut down old daemon
1834
1871
  stopDaemonWatcher();
@@ -1853,6 +1890,7 @@ async function restartDaemonWithTLS(config, callback) {
1853
1890
  pinHash: config.pinHash || null,
1854
1891
  tls: true,
1855
1892
  builtinCert: hasBuiltinCert,
1893
+ mkcertDetected: mkcertDetected,
1856
1894
  debug: config.debug || false,
1857
1895
  keepAwake: config.keepAwake || false,
1858
1896
  dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
@@ -1951,6 +1989,7 @@ function showMainMenu(config, ip) {
1951
1989
  function afterQr() {
1952
1990
  // Status line
1953
1991
  log(" " + a.dim + "clay" + a.reset + " " + a.dim + "v" + currentVersion + a.reset + a.dim + " — " + url + a.reset);
1992
+ if (config.builtinCert) log(" " + a.dim + "d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network." + a.reset);
1954
1993
  var parts = [];
1955
1994
  parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
1956
1995
  parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
@@ -1961,6 +2000,13 @@ function showMainMenu(config, ip) {
1961
2000
  log(" Press " + a.bold + "o" + a.reset + " to open in browser");
1962
2001
  log("");
1963
2002
 
2003
+ if (config.mkcertDetected) {
2004
+ log(" " + sym.warn + " " + a.yellow + "Clay now ships with a builtin HTTPS certificate." + a.reset);
2005
+ log(" " + a.dim + "No more CA setup on each device." + a.reset);
2006
+ log(" " + a.dim + "To use it, pass --builtin-cert or uninstall mkcert." + a.reset);
2007
+ log("");
2008
+ }
2009
+
1964
2010
  showMenuItems();
1965
2011
  }
1966
2012
 
@@ -2030,9 +2076,8 @@ function showMainMenu(config, ip) {
2030
2076
  }
2031
2077
  }, {
2032
2078
  hint: [
2033
- "claude-relay has been renamed to clay-server · npx clay-server",
2034
2079
  "Run npx clay-server in other directories to add more projects.",
2035
- "★ github.com/chadbyte/claude-relay — Press s to star the repo",
2080
+ "★ github.com/chadbyte/clay — Press s to star the repo",
2036
2081
  ],
2037
2082
  keys: [
2038
2083
  { key: "o", onKey: function () {
@@ -2040,7 +2085,7 @@ function showMainMenu(config, ip) {
2040
2085
  showMainMenu(config, ip);
2041
2086
  }},
2042
2087
  { key: "s", onKey: function () {
2043
- openUrl("https://github.com/chadbyte/claude-relay");
2088
+ openUrl("https://github.com/chadbyte/clay");
2044
2089
  showMainMenu(config, ip);
2045
2090
  }},
2046
2091
  ],
@@ -2206,7 +2251,7 @@ function showSetupGuide(config, ip, goBack) {
2206
2251
  // mkcert: use HTTP onboarding server for CA install flow
2207
2252
  var setupUrl;
2208
2253
  if (config.builtinCert) {
2209
- setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "?playbook=push-notifications";
2254
+ setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "/pwa";
2210
2255
  } else if (config.tls) {
2211
2256
  setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
2212
2257
  } else {
@@ -2725,6 +2770,7 @@ var currentVersion = require("../package.json").version;
2725
2770
  : protocol + "://" + ip + ":" + config.port;
2726
2771
  console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
2727
2772
  console.log(" " + sym.done + " " + url);
2773
+ if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network.");
2728
2774
  process.exit(0);
2729
2775
  return;
2730
2776
  }
package/lib/daemon.js CHANGED
@@ -94,7 +94,7 @@ var caRoot = null;
94
94
  try {
95
95
  var { execSync } = require("child_process");
96
96
  caRoot = path.join(
97
- execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
97
+ execSync("mkcert -CAROOT", { encoding: "utf8", stdio: "pipe" }).trim(),
98
98
  "rootCA.pem"
99
99
  );
100
100
  if (!fs.existsSync(caRoot)) caRoot = null;
package/lib/pages.js CHANGED
@@ -48,56 +48,58 @@ function setupPageHtml(httpsUrl, httpUrl, hasCert, lanMode) {
48
48
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
49
49
  <title>Setup - Clay</title>
50
50
  <style>
51
+ :root{--s-bg:#282a36;--s-text:#f8f8f2;--s-accent:#ffb86c;--s-muted:#6272a4;--s-border:#44475a;--s-dimmer:#6272a4;--s-success:#50fa7b;--s-accent-15:rgba(255,184,108,0.15);--s-success-10:rgba(80,250,123,0.1);--s-success-15:rgba(80,250,123,0.15);--s-accent-06:rgba(255,184,108,0.06);--s-muted-06:rgba(98,114,164,0.06);--s-muted-15:rgba(98,114,164,0.15)}
52
+ @media(prefers-color-scheme:light){:root{--s-bg:#FAFAFA;--s-text:#5C6166;--s-accent:#FA8D3E;--s-muted:#A0A6AC;--s-border:#D2D4D8;--s-dimmer:#8A9199;--s-success:#6CBF49;--s-accent-15:rgba(250,141,62,0.15);--s-success-10:rgba(108,191,73,0.1);--s-success-15:rgba(108,191,73,0.15);--s-accent-06:rgba(250,141,62,0.06);--s-muted-06:rgba(160,166,172,0.06);--s-muted-15:rgba(160,166,172,0.15)}}
51
53
  *{margin:0;padding:0;box-sizing:border-box}
52
- body{background:#2F2E2B;color:#E8E5DE;font-family:system-ui,-apple-system,sans-serif;min-height:100dvh;display:flex;justify-content:center;padding:env(safe-area-inset-top,0) 20px 40px}
54
+ body{background:var(--s-bg);color:var(--s-text);font-family:system-ui,-apple-system,sans-serif;min-height:100dvh;display:flex;justify-content:center;padding:env(safe-area-inset-top,0) 20px 40px}
53
55
  .c{max-width:480px;width:100%;padding-top:40px}
54
- h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
55
- .subtitle{text-align:center;color:#908B81;font-size:13px;margin-bottom:28px}
56
+ h1{color:var(--s-accent);font-size:22px;margin:0 0 4px;text-align:center}
57
+ .subtitle{text-align:center;color:var(--s-muted);font-size:13px;margin-bottom:28px}
56
58
 
57
59
  /* Steps indicator */
58
60
  .steps-bar{display:flex;gap:6px;margin-bottom:32px}
59
- .steps-bar .pip{flex:1;height:3px;border-radius:2px;background:#3E3C37;transition:background 0.3s}
60
- .steps-bar .pip.done{background:#57AB5A}
61
- .steps-bar .pip.active{background:#DA7756}
61
+ .steps-bar .pip{flex:1;height:3px;border-radius:2px;background:var(--s-border);transition:background 0.3s}
62
+ .steps-bar .pip.done{background:var(--s-success)}
63
+ .steps-bar .pip.active{background:var(--s-accent)}
62
64
 
63
65
  /* Step card */
64
66
  .step-card{display:none;animation:fadeIn 0.25s ease}
65
67
  .step-card.active{display:block}
66
68
  @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
67
69
 
68
- .step-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#DA7756;font-weight:600;margin-bottom:8px}
70
+ .step-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--s-accent);font-weight:600;margin-bottom:8px}
69
71
  .step-title{font-size:18px;font-weight:600;margin-bottom:6px}
70
- .step-desc{font-size:14px;line-height:1.6;color:#908B81;margin-bottom:20px}
72
+ .step-desc{font-size:14px;line-height:1.6;color:var(--s-muted);margin-bottom:20px}
71
73
 
72
74
  .instruction{display:flex;gap:12px;margin-bottom:16px}
73
- .inst-num{width:24px;height:24px;border-radius:50%;background:rgba(218,119,86,0.15);color:#DA7756;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0;margin-top:1px}
75
+ .inst-num{width:24px;height:24px;border-radius:50%;background:var(--s-accent-15);color:var(--s-accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0;margin-top:1px}
74
76
  .inst-text{font-size:14px;line-height:1.6}
75
- .inst-text .note{font-size:12px;color:#6D6860;margin-top:4px}
77
+ .inst-text .note{font-size:12px;color:var(--s-dimmer);margin-top:4px}
76
78
 
77
- .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:#DA7756;color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;font-size:14px;margin:4px 0;border:none;cursor:pointer;font-family:inherit;transition:opacity 0.15s}
79
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:var(--s-accent);color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;font-size:14px;margin:4px 0;border:none;cursor:pointer;font-family:inherit;transition:opacity 0.15s}
78
80
  .btn:hover{opacity:0.9}
79
- .btn.outline{background:transparent;border:1.5px solid #3E3C37;color:#E8E5DE}
80
- .btn.outline:hover{border-color:#6D6860}
81
- .btn.success{background:#57AB5A}
81
+ .btn.outline{background:transparent;border:1.5px solid var(--s-border);color:var(--s-text)}
82
+ .btn.outline:hover{border-color:var(--s-dimmer)}
83
+ .btn.success{background:var(--s-success)}
82
84
  .btn:disabled{opacity:0.4;cursor:default}
83
85
 
84
86
  .btn-row{display:flex;gap:8px;margin-top:20px}
85
87
  .btn-row .btn{flex:1}
86
88
 
87
89
  .check-status{display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:10px;font-size:13px;margin:16px 0}
88
- .check-status.ok{background:rgba(87,171,90,0.1);color:#57AB5A;border:1px solid rgba(87,171,90,0.15)}
89
- .check-status.warn{background:rgba(218,119,86,0.06);border:1px solid rgba(218,119,86,0.15);color:#DA7756}
90
- .check-status.pending{background:rgba(144,139,129,0.06);border:1px solid rgba(144,139,129,0.15);color:#908B81}
90
+ .check-status.ok{background:var(--s-success-10);color:var(--s-success);border:1px solid var(--s-success-15)}
91
+ .check-status.warn{background:var(--s-accent-06);border:1px solid var(--s-accent-15);color:var(--s-accent)}
92
+ .check-status.pending{background:var(--s-muted-06);border:1px solid var(--s-muted-15);color:var(--s-muted)}
91
93
 
92
94
  .platform-ios,.platform-android,.platform-desktop{display:none}
93
95
 
94
96
  .done-card{text-align:center;padding:40px 0}
95
97
  .done-icon{font-size:48px;margin-bottom:16px}
96
98
  .done-title{font-size:20px;font-weight:600;margin-bottom:8px}
97
- .done-desc{font-size:14px;color:#908B81;margin-bottom:24px}
99
+ .done-desc{font-size:14px;color:var(--s-muted);margin-bottom:24px}
98
100
 
99
- .skip-link{display:block;text-align:center;color:#6D6860;font-size:13px;text-decoration:none;margin-top:12px;cursor:pointer;border:none;background:none;font-family:inherit}
100
- .skip-link:hover{color:#908B81}
101
+ .skip-link{display:block;text-align:center;color:var(--s-dimmer);font-size:13px;text-decoration:none;margin-top:12px;cursor:pointer;border:none;background:none;font-family:inherit}
102
+ .skip-link:hover{color:var(--s-muted)}
101
103
  </style></head><body>
102
104
  <div class="c">
103
105
  <h1>Clay</h1>
package/lib/project.js CHANGED
@@ -246,6 +246,7 @@ function createProjectContext(opts) {
246
246
  username: u.username,
247
247
  avatarStyle: p.avatarStyle || "thumbs",
248
248
  avatarSeed: p.avatarSeed || u.username,
249
+ avatarCustom: p.avatarCustom || "",
249
250
  });
250
251
  }
251
252
  msg.users = userList;
@@ -1646,11 +1647,11 @@ function createProjectContext(opts) {
1646
1647
  var switchTarget = sm.sessions.get(msg.id);
1647
1648
  if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return;
1648
1649
  ws._clayActiveSession = msg.id;
1649
- sm.switchSession(msg.id, ws);
1650
+ sm.switchSession(msg.id, ws, hydrateImageRefs);
1650
1651
  broadcastPresence();
1651
1652
  } else {
1652
1653
  ws._clayActiveSession = msg.id;
1653
- sm.switchSession(msg.id, ws);
1654
+ sm.switchSession(msg.id, ws, hydrateImageRefs);
1654
1655
  }
1655
1656
  }
1656
1657
  return;
@@ -2005,7 +2006,7 @@ function createProjectContext(opts) {
2005
2006
  onProcessingChanged();
2006
2007
 
2007
2008
  sm.saveSessionFile(session);
2008
- sm.switchSession(session.localId, ws);
2009
+ sm.switchSession(session.localId, ws, hydrateImageRefs);
2009
2010
  sm.sendAndRecord(session, { type: "rewind_complete", mode: mode });
2010
2011
  sm.broadcastSessionList();
2011
2012
  } catch (err) {
@@ -2076,6 +2077,7 @@ function createProjectContext(opts) {
2076
2077
  displayName: p.name || u.displayName || u.username,
2077
2078
  avatarStyle: p.avatarStyle || "thumbs",
2078
2079
  avatarSeed: p.avatarSeed || u.username,
2080
+ avatarCustom: p.avatarCustom || "",
2079
2081
  };
2080
2082
  if (msg.type === "cursor_move") {
2081
2083
  cursorMsg.turn = msg.turn;
@@ -3162,7 +3164,7 @@ function createProjectContext(opts) {
3162
3164
  judgeCraftSession.ralphCraftingMode = true;
3163
3165
  judgeCraftSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
3164
3166
  sm.saveSessionFile(judgeCraftSession);
3165
- sm.switchSession(judgeCraftSession.localId);
3167
+ sm.switchSession(judgeCraftSession.localId, null, hydrateImageRefs);
3166
3168
  loopState.craftingSessionId = judgeCraftSession.localId;
3167
3169
 
3168
3170
  loopRegistry.updateRecord(newLoopId, { craftingSessionId: judgeCraftSession.localId });
@@ -3206,7 +3208,7 @@ function createProjectContext(opts) {
3206
3208
  craftingSession.ralphCraftingMode = true;
3207
3209
  craftingSession.loop = { active: true, iteration: 0, role: "crafting", loopId: newLoopId, name: craftName, source: recordSource, startedAt: loopState.startedAt };
3208
3210
  sm.saveSessionFile(craftingSession);
3209
- sm.switchSession(craftingSession.localId);
3211
+ sm.switchSession(craftingSession.localId, null, hydrateImageRefs);
3210
3212
  loopState.craftingSessionId = craftingSession.localId;
3211
3213
 
3212
3214
  // Store crafting session ID in the registry record
@@ -3504,6 +3506,7 @@ function createProjectContext(opts) {
3504
3506
  username: u.username,
3505
3507
  avatarStyle: p.avatarStyle || "thumbs",
3506
3508
  avatarSeed: p.avatarSeed || u.username,
3509
+ avatarCustom: p.avatarCustom || "",
3507
3510
  });
3508
3511
  }
3509
3512
  send({ type: "session_presence", presence: presence });
@@ -4124,6 +4127,7 @@ function createProjectContext(opts) {
4124
4127
  username: u.username,
4125
4128
  avatarStyle: p.avatarStyle || "thumbs",
4126
4129
  avatarSeed: p.avatarSeed || u.username,
4130
+ avatarCustom: p.avatarCustom || "",
4127
4131
  });
4128
4132
  }
4129
4133
  status.onlineUsers = onlineUsers;