clay-server 2.16.0 → 2.17.0-beta.2

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. 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
@@ -52,6 +52,7 @@ function openUrl(url) {
52
52
  var args = process.argv.slice(2);
53
53
  var port = _isDev ? 2635 : 2633;
54
54
  var useHttps = true;
55
+ var forceMkcert = false;
55
56
  var skipUpdate = false;
56
57
  var debugMode = false;
57
58
  var autoYes = false;
@@ -81,6 +82,8 @@ for (var i = 0; i < args.length; i++) {
81
82
  i++;
82
83
  } else if (args[i] === "--no-https") {
83
84
  useHttps = false;
85
+ } else if (args[i] === "--local-cert") {
86
+ forceMkcert = true;
84
87
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
85
88
  skipUpdate = true;
86
89
  } else if (args[i] === "--dev") {
@@ -124,7 +127,8 @@ for (var i = 0; i < args.length; i++) {
124
127
  console.log("Options:");
125
128
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
126
129
  console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
127
- console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
130
+ console.log(" --no-https Disable HTTPS (enabled by default)");
131
+ console.log(" --local-cert Use local certificate (mkcert) instead of builtin");
128
132
  console.log(" --no-update Skip auto-update check on startup");
129
133
  console.log(" --debug Enable debug panel in the web UI");
130
134
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
@@ -564,7 +568,43 @@ function getAllIPs() {
564
568
  return ips;
565
569
  }
566
570
 
571
+ function getBuiltinCert() {
572
+ try {
573
+ var certDir = path.join(__dirname, "..", "lib", "certs");
574
+ var keyPath = path.join(certDir, "privkey.pem");
575
+ var certPath = path.join(certDir, "fullchain.pem");
576
+ if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) return null;
577
+
578
+ // Check expiry
579
+ var certText = execFileSync("openssl", [
580
+ "x509", "-in", certPath, "-noout", "-enddate"
581
+ ], { encoding: "utf8" });
582
+ var m = certText.match(/notAfter=(.+)/);
583
+ if (m) {
584
+ var expiry = new Date(m[1]);
585
+ var now = new Date();
586
+ // Skip if expiring within 7 days
587
+ if (expiry.getTime() - now.getTime() < 7 * 24 * 60 * 60 * 1000) return null;
588
+ }
589
+
590
+ return { key: keyPath, cert: certPath, caRoot: null, builtin: true };
591
+ } catch (e) {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ function toClayStudioUrl(ip, port, protocol) {
597
+ var dashed = ip.replace(/\./g, "-");
598
+ return protocol + "://" + dashed + ".d.clay.studio:" + port;
599
+ }
600
+
567
601
  function ensureCerts(ip) {
602
+ // Check builtin cert first (unless --local-cert flag is set)
603
+ if (!forceMkcert) {
604
+ var builtin = getBuiltinCert();
605
+ if (builtin) return builtin;
606
+ }
607
+
568
608
  var homeDir = os.homedir();
569
609
  var certDir = path.join(process.env.CLAY_HOME || path.join(homeDir, ".clay"), "certs");
570
610
  var keyPath = path.join(certDir, "key.pem");
@@ -1395,11 +1435,13 @@ function setup(callback) {
1395
1435
  async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1396
1436
  var ip = getLocalIP();
1397
1437
  var hasTls = false;
1438
+ var hasBuiltinCert = false;
1398
1439
 
1399
1440
  if (useHttps) {
1400
1441
  var certPaths = ensureCerts(ip);
1401
1442
  if (certPaths) {
1402
1443
  hasTls = true;
1444
+ if (certPaths.builtin) hasBuiltinCert = true;
1403
1445
  } else {
1404
1446
  log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
1405
1447
  }
@@ -1473,6 +1515,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1473
1515
  host: host,
1474
1516
  pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
1475
1517
  tls: hasTls,
1518
+ builtinCert: hasBuiltinCert,
1476
1519
  debug: debugMode,
1477
1520
  keepAwake: keepAwake,
1478
1521
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1547,9 +1590,12 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1547
1590
  // Headless mode — print status and exit immediately
1548
1591
  if (headlessMode) {
1549
1592
  var protocol = config.tls ? "https" : "http";
1550
- var url = protocol + "://" + ip + ":" + config.port;
1593
+ var url = config.builtinCert
1594
+ ? toClayStudioUrl(ip, config.port, protocol)
1595
+ : protocol + "://" + ip + ":" + config.port;
1551
1596
  console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
1552
1597
  console.log(" " + sym.done + " " + url);
1598
+ if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio is only used for HTTPS certificates. All traffic stays on your local network. https://github.com/chadbyte/clay/tree/main/clay-dns");
1553
1599
  console.log(" " + sym.done + " Headless mode — exiting CLI");
1554
1600
  process.exit(0);
1555
1601
  return;
@@ -1565,10 +1611,14 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1565
1611
  async function devMode(mode, keepAwake, existingPinHash) {
1566
1612
  var ip = getLocalIP();
1567
1613
  var hasTls = false;
1614
+ var hasBuiltinCert = false;
1568
1615
 
1569
1616
  if (useHttps) {
1570
1617
  var certPaths = ensureCerts(ip);
1571
- if (certPaths) hasTls = true;
1618
+ if (certPaths) {
1619
+ hasTls = true;
1620
+ if (certPaths.builtin) hasBuiltinCert = true;
1621
+ }
1572
1622
  }
1573
1623
 
1574
1624
  var portFree = await isPortFree(port);
@@ -1630,6 +1680,7 @@ async function devMode(mode, keepAwake, existingPinHash) {
1630
1680
  host: host,
1631
1681
  pinHash: existingPinHash || null,
1632
1682
  tls: hasTls,
1683
+ builtinCert: hasBuiltinCert,
1633
1684
  debug: true,
1634
1685
  keepAwake: keepAwake || false,
1635
1686
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -1778,6 +1829,7 @@ async function restartDaemonWithTLS(config, callback) {
1778
1829
  callback(config);
1779
1830
  return;
1780
1831
  }
1832
+ var hasBuiltinCert = !!(certPaths && certPaths.builtin);
1781
1833
 
1782
1834
  // Shut down old daemon
1783
1835
  stopDaemonWatcher();
@@ -1801,6 +1853,7 @@ async function restartDaemonWithTLS(config, callback) {
1801
1853
  port: config.port,
1802
1854
  pinHash: config.pinHash || null,
1803
1855
  tls: true,
1856
+ builtinCert: hasBuiltinCert,
1804
1857
  debug: config.debug || false,
1805
1858
  keepAwake: config.keepAwake || false,
1806
1859
  dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
@@ -1879,7 +1932,9 @@ function showServerStarted(config, ip) {
1879
1932
  function showMainMenu(config, ip) {
1880
1933
  startDaemonWatcher();
1881
1934
  var protocol = config.tls ? "https" : "http";
1882
- var url = protocol + "://" + ip + ":" + config.port;
1935
+ var url = config.builtinCert
1936
+ ? toClayStudioUrl(ip, config.port, protocol)
1937
+ : protocol + "://" + ip + ":" + config.port;
1883
1938
 
1884
1939
  sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
1885
1940
  var projs = (status && status.projects) || [];
@@ -1897,6 +1952,7 @@ function showMainMenu(config, ip) {
1897
1952
  function afterQr() {
1898
1953
  // Status line
1899
1954
  log(" " + a.dim + "clay" + a.reset + " " + a.dim + "v" + currentVersion + a.reset + a.dim + " — " + url + a.reset);
1955
+ if (config.builtinCert) log(" " + a.dim + "d.clay.studio is only used for HTTPS certificates. All traffic stays on your local network. https://github.com/chadbyte/clay/tree/main/clay-dns" + a.reset);
1900
1956
  var parts = [];
1901
1957
  parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
1902
1958
  parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
@@ -1925,7 +1981,6 @@ function showMainMenu(config, ip) {
1925
1981
  function showMenuItems() {
1926
1982
  var items = [
1927
1983
  { label: "Setup notifications", value: "notifications" },
1928
- { label: "Projects", value: "projects" },
1929
1984
  { label: "Settings", value: "settings" },
1930
1985
  { label: "Shut down server", value: "shutdown" },
1931
1986
  { label: "Keep server alive & exit", value: "exit" },
@@ -1940,10 +1995,6 @@ function showMainMenu(config, ip) {
1940
1995
  });
1941
1996
  break;
1942
1997
 
1943
- case "projects":
1944
- showProjectsMenu(config, ip);
1945
- break;
1946
-
1947
1998
  case "settings":
1948
1999
  showSettingsMenu(config, ip);
1949
2000
  break;
@@ -2000,203 +2051,6 @@ function showMainMenu(config, ip) {
2000
2051
  });
2001
2052
  }
2002
2053
 
2003
- // ==============================
2004
- // Projects sub-menu
2005
- // ==============================
2006
- function showProjectsMenu(config, ip) {
2007
- sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
2008
- if (!status.ok) {
2009
- log(a.red + "Failed to get status" + a.reset);
2010
- showMainMenu(config, ip);
2011
- return;
2012
- }
2013
-
2014
- console.clear();
2015
- printLogo();
2016
- log("");
2017
- log(sym.pointer + " " + a.bold + "Projects" + a.reset);
2018
- log(sym.bar);
2019
-
2020
- var projs = status.projects || [];
2021
- for (var i = 0; i < projs.length; i++) {
2022
- var p = projs[i];
2023
- var statusIcon = p.isProcessing ? "⚡" : (p.clients > 0 ? "🟢" : "⏸");
2024
- var sessionLabel = p.sessions === 1 ? "1 session" : p.sessions + " sessions";
2025
- var projName = p.title || p.project;
2026
- log(sym.bar + " " + a.bold + projName + a.reset + " " + sessionLabel + " " + statusIcon);
2027
- log(sym.bar + " " + a.dim + p.path + a.reset);
2028
- if (i < projs.length - 1) log(sym.bar);
2029
- }
2030
- log(sym.bar);
2031
-
2032
- // Build menu items
2033
- var items = [];
2034
-
2035
- // Check if cwd is already registered
2036
- var cwdRegistered = false;
2037
- for (var j = 0; j < projs.length; j++) {
2038
- if (projs[j].path === cwd) {
2039
- cwdRegistered = true;
2040
- break;
2041
- }
2042
- }
2043
- if (!cwdRegistered) {
2044
- items.push({ label: "+ Add " + a.bold + path.basename(cwd) + a.reset + " " + a.dim + "(" + cwd + ")" + a.reset, value: "add_cwd" });
2045
- }
2046
- items.push({ label: "+ Add project...", value: "add_other" });
2047
-
2048
- for (var k = 0; k < projs.length; k++) {
2049
- var itemLabel = projs[k].title || projs[k].project;
2050
- items.push({ label: itemLabel, value: "detail:" + projs[k].slug });
2051
- }
2052
- items.push({ label: "Back", value: "back" });
2053
-
2054
- promptSelect("Select", items, function (choice) {
2055
- if (choice === "back") {
2056
- console.clear();
2057
- printLogo();
2058
- log("");
2059
- showMainMenu(config, ip);
2060
- } else if (choice === "add_cwd") {
2061
- sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
2062
- if (res.ok) {
2063
- log(sym.done + " " + a.green + "Added: " + res.slug + a.reset);
2064
- config = loadConfig() || config;
2065
- } else {
2066
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2067
- }
2068
- log("");
2069
- showProjectsMenu(config, ip);
2070
- });
2071
- } else if (choice === "add_other") {
2072
- log(sym.bar);
2073
- promptText("Directory path", cwd, function (dirPath) {
2074
- if (dirPath === null) {
2075
- showProjectsMenu(config, ip);
2076
- return;
2077
- }
2078
- var absPath = path.resolve(dirPath);
2079
- try {
2080
- var stat = fs.statSync(absPath);
2081
- if (!stat.isDirectory()) {
2082
- log(sym.warn + " " + a.red + "Not a directory: " + absPath + a.reset);
2083
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2084
- return;
2085
- }
2086
- } catch (e) {
2087
- log(sym.warn + " " + a.red + "Directory not found: " + absPath + a.reset);
2088
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2089
- return;
2090
- }
2091
- var alreadyExists = false;
2092
- for (var pi = 0; pi < projs.length; pi++) {
2093
- if (projs[pi].path === absPath) {
2094
- alreadyExists = true;
2095
- break;
2096
- }
2097
- }
2098
- if (alreadyExists) {
2099
- log(sym.done + " " + a.yellow + "Already added: " + path.basename(absPath) + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
2100
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2101
- return;
2102
- }
2103
- sendIPCCommand(socketPath(), { cmd: "add_project", path: absPath }).then(function (res) {
2104
- if (res.ok) {
2105
- log(sym.done + " " + a.green + "Added: " + res.slug + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
2106
- config = loadConfig() || config;
2107
- } else {
2108
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2109
- }
2110
- setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
2111
- });
2112
- });
2113
- } else if (choice.startsWith("detail:")) {
2114
- var detailSlug = choice.substring(7);
2115
- showProjectDetail(config, ip, detailSlug, projs);
2116
- }
2117
- });
2118
- });
2119
- }
2120
-
2121
- // ==============================
2122
- // Project detail
2123
- // ==============================
2124
- function showProjectDetail(config, ip, slug, projects) {
2125
- var proj = null;
2126
- for (var i = 0; i < projects.length; i++) {
2127
- if (projects[i].slug === slug) {
2128
- proj = projects[i];
2129
- break;
2130
- }
2131
- }
2132
- if (!proj) {
2133
- showProjectsMenu(config, ip);
2134
- return;
2135
- }
2136
-
2137
- var displayName = proj.title || proj.project;
2138
-
2139
- console.clear();
2140
- printLogo();
2141
- log("");
2142
- log(sym.pointer + " " + a.bold + displayName + a.reset + " " + a.dim + proj.slug + " · " + proj.path + a.reset);
2143
- log(sym.bar);
2144
- var sessionLabel = proj.sessions === 1 ? "1 session" : proj.sessions + " sessions";
2145
- var clientLabel = proj.clients === 1 ? "1 client" : proj.clients + " clients";
2146
- log(sym.bar + " " + sessionLabel + " · " + clientLabel);
2147
- if (proj.title) {
2148
- log(sym.bar + " " + a.dim + "Title: " + a.reset + proj.title);
2149
- }
2150
- log(sym.bar);
2151
-
2152
- var items = [
2153
- { label: proj.title ? "Change title" : "Set title", value: "title" },
2154
- { label: "Remove project", value: "remove" },
2155
- { label: "Back", value: "back" },
2156
- ];
2157
-
2158
- promptSelect("What would you like to do?", items, function (choice) {
2159
- if (choice === "title") {
2160
- log(sym.bar);
2161
- promptText("Project title", proj.title || proj.project, function (newTitle) {
2162
- if (newTitle === null) {
2163
- showProjectDetail(config, ip, slug, projects);
2164
- return;
2165
- }
2166
- var titleVal = newTitle.trim();
2167
- // If same as directory name, clear custom title
2168
- if (titleVal === proj.project || titleVal === "") {
2169
- titleVal = null;
2170
- }
2171
- sendIPCCommand(socketPath(), { cmd: "set_project_title", slug: slug, title: titleVal }).then(function (res) {
2172
- if (res.ok) {
2173
- proj.title = titleVal;
2174
- config = loadConfig() || config;
2175
- log(sym.done + " " + a.green + "Title updated" + a.reset);
2176
- } else {
2177
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2178
- }
2179
- log("");
2180
- showProjectDetail(config, ip, slug, projects);
2181
- });
2182
- });
2183
- } else if (choice === "remove") {
2184
- sendIPCCommand(socketPath(), { cmd: "remove_project", slug: slug }).then(function (res) {
2185
- if (res.ok) {
2186
- log(sym.done + " " + a.green + "Removed: " + slug + a.reset);
2187
- config = loadConfig() || config;
2188
- } else {
2189
- log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
2190
- }
2191
- log("");
2192
- showProjectsMenu(config, ip);
2193
- });
2194
- } else {
2195
- showProjectsMenu(config, ip);
2196
- }
2197
- });
2198
- }
2199
-
2200
2054
  // ==============================
2201
2055
  // Setup guide (2x2 toggle flow)
2202
2056
  // ==============================
@@ -2229,7 +2083,7 @@ function showSetupGuide(config, ip, goBack) {
2229
2083
  promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
2230
2084
  wantRemote = remote;
2231
2085
  log(sym.bar);
2232
- promptToggle("Want push notifications?", "Requires HTTPS (mkcert certificate)", false, function (push) {
2086
+ promptToggle("Want push notifications?", "Requires HTTPS", false, function (push) {
2233
2087
  wantPush = push;
2234
2088
  log(sym.bar);
2235
2089
  afterToggles();
@@ -2293,6 +2147,15 @@ function showSetupGuide(config, ip, goBack) {
2293
2147
  return;
2294
2148
  }
2295
2149
 
2150
+ // Builtin cert: HTTPS already active, skip mkcert flow entirely
2151
+ if (config.builtinCert) {
2152
+ log(sym.pointer + " " + a.bold + "HTTPS" + a.reset + a.dim + " · Enabled (builtin certificate)" + a.reset);
2153
+ log(sym.bar);
2154
+ showSetupQR();
2155
+ return;
2156
+ }
2157
+
2158
+ // mkcert flow (--mkcert or fallback)
2296
2159
  var mcReady = hasMkcert();
2297
2160
  log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
2298
2161
  if (mcReady) {
@@ -2341,10 +2204,16 @@ function showSetupGuide(config, ip, goBack) {
2341
2204
  }
2342
2205
  var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
2343
2206
  var setupQuery = wantRemote ? "" : "?mode=lan";
2344
- // Always use HTTP onboarding URL for QR/setup when TLS is active
2345
- var setupUrl = config.tls
2346
- ? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
2347
- : "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
2207
+ // Builtin cert: link directly to the app with push notification guide
2208
+ // mkcert: use HTTP onboarding server for CA install flow
2209
+ var setupUrl;
2210
+ if (config.builtinCert) {
2211
+ setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "?playbook=push-notifications";
2212
+ } else if (config.tls) {
2213
+ setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
2214
+ } else {
2215
+ setupUrl = "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
2216
+ }
2348
2217
  log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
2349
2218
  log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
2350
2219
  log(sym.bar + " " + a.bold + setupUrl + a.reset);
@@ -2435,11 +2304,13 @@ function showSettingsMenu(config, ip) {
2435
2304
  { label: "Setup notifications", value: "guide" },
2436
2305
  ];
2437
2306
 
2438
- if (config.pinHash) {
2439
- items.push({ label: "Change PIN", value: "pin" });
2440
- items.push({ label: "Remove PIN", value: "remove_pin" });
2441
- } else {
2442
- items.push({ label: "Set PIN", value: "pin" });
2307
+ if (!muEnabled) {
2308
+ if (config.pinHash) {
2309
+ items.push({ label: "Change PIN", value: "pin" });
2310
+ items.push({ label: "Remove PIN", value: "remove_pin" });
2311
+ } else {
2312
+ items.push({ label: "Set PIN", value: "pin" });
2313
+ }
2443
2314
  }
2444
2315
  if (muEnabled) {
2445
2316
  items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
@@ -2759,7 +2630,9 @@ function showSettingsMenu(config, ip) {
2759
2630
  return;
2760
2631
  }
2761
2632
  var protocol = config.tls ? "https" : "http";
2762
- var recoveryUrl = protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
2633
+ var recoveryUrl = config.builtinCert
2634
+ ? toClayStudioUrl(ip, config.port, protocol) + "/recover/" + recoveryUrlPath
2635
+ : protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
2763
2636
  log(sym.bar);
2764
2637
  log(sym.bar + " " + a.yellow + sym.warn + " Admin Password Recovery" + a.reset);
2765
2638
  log(sym.bar);
@@ -2849,9 +2722,12 @@ var currentVersion = require("../package.json").version;
2849
2722
  if (headlessMode) {
2850
2723
  var protocol = config.tls ? "https" : "http";
2851
2724
  var ip = getLocalIP();
2852
- var url = protocol + "://" + ip + ":" + config.port;
2725
+ var url = config.builtinCert
2726
+ ? toClayStudioUrl(ip, config.port, protocol)
2727
+ : protocol + "://" + ip + ":" + config.port;
2853
2728
  console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
2854
2729
  console.log(" " + sym.done + " " + url);
2730
+ if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio is only used for HTTPS certificates. All traffic stays on your local network. https://github.com/chadbyte/clay/tree/main/clay-dns");
2855
2731
  process.exit(0);
2856
2732
  return;
2857
2733
  }
@@ -0,0 +1,47 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDgjCCAwigAwIBAgISBmtBLGUclrEfhwU9evzgyDCQMAoGCCqGSM49BAMDMDIx
3
+ CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
4
+ ODAeFw0yNjAzMjMwOTQ0MDJaFw0yNjA2MjEwOTQ0MDFaMBoxGDAWBgNVBAMMDyou
5
+ ZC5jbGF5LnN0dWRpbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEhAD1htqRg8
6
+ n7evflBZ1X7UaCeqBGcvG/MNtlAKd1VVfVGFuanyUjksV9++R1EuKLhEPM3loL/3
7
+ Gz8+XEewGw6jggIUMIICEDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
8
+ BQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU8Anzwuqwtujb+h3o/CnbJu5Z
9
+ +W8wHwYDVR0jBBgwFoAUjw0TovYuftFQbDMYOF1ZjiNykcowMgYIKwYBBQUHAQEE
10
+ JjAkMCIGCCsGAQUFBzAChhZodHRwOi8vZTguaS5sZW5jci5vcmcvMBoGA1UdEQQT
11
+ MBGCDyouZC5jbGF5LnN0dWRpbzATBgNVHSAEDDAKMAgGBmeBDAECATAtBgNVHR8E
12
+ JjAkMCKgIKAehhxodHRwOi8vZTguYy5sZW5jci5vcmcvMTcuY3JsMIIBBQYKKwYB
13
+ BAHWeQIEAgSB9gSB8wDxAHcAFoMtq/CpJQ8P8DqlRf/Iv8gj0IdL9gQpJ/jnHzMT
14
+ 9foAAAGdGkoIlgAABAMASDBGAiEAhJFJwEIag1Bzt0WtYgMzLdJn/k+Is2RukdDo
15
+ G5sXpyMCIQCQOX9nOoaVIXxF1KXiavbAY5QIyJRuvK7Fn6WeL58YQQB2AMs49xWJ
16
+ fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABnRpKCJgAAAQDAEcwRQIhAI98
17
+ cmflulGQJMfD10jbstVwodGpzl5licg6FTxcYachAiBZV1cZnPfasTzcteXyjCuz
18
+ c1wayYAtch+0soAWvBKZRzAKBggqhkjOPQQDAwNoADBlAjAwJO4ti4AJJTtMxYsr
19
+ Jf5052oDrD2POtoiPksruQVVacsq0T/9VYVX+X2vElCrxFwCMQDcSToBRWGpv/G3
20
+ JBpbEAB1qhk1Z9lYPQKH6gRvtp35XJWY0PucGRWgQUrXuZcxGlA=
21
+ -----END CERTIFICATE-----
22
+ -----BEGIN CERTIFICATE-----
23
+ MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
24
+ MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
25
+ Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
26
+ Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
27
+ bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
28
+ S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
29
+ R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
30
+ 9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
31
+ MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
32
+ cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
33
+ BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
34
+ DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
35
+ ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
36
+ RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
37
+ AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
38
+ otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
39
+ aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
40
+ Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
41
+ HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
42
+ Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
43
+ xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
44
+ tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
45
+ jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
46
+ u1igv3OefnWjSQ==
47
+ -----END CERTIFICATE-----
@@ -0,0 +1,5 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtwBpQgcr1N5kEPEv
3
+ Rz5JCBMpDQHD8U44HlzB9bLvQAihRANCAARIQA9YbakYPJ+3r35QWdV+1GgnqgRn
4
+ LxvzDbZQCndVVX1Rhbmp8lI5LFffvkdRLii4RDzN5aC/9xs/PlxHsBsO
5
+ -----END PRIVATE KEY-----
package/lib/daemon.js CHANGED
@@ -60,10 +60,26 @@ if (config.osUsers) {
60
60
  // --- TLS ---
61
61
  var tlsOptions = null;
62
62
  if (config.tls) {
63
+ // 1. Check builtin cert (shipped with package)
64
+ var builtinKeyPath = path.join(__dirname, "certs", "privkey.pem");
65
+ var builtinCertPath = path.join(__dirname, "certs", "fullchain.pem");
66
+
67
+ // 2. User cert (mkcert, etc.)
63
68
  var os = require("os");
64
69
  var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".clay"), "certs");
65
- var keyPath = path.join(certDir, "key.pem");
66
- var certPath = path.join(certDir, "cert.pem");
70
+ var userKeyPath = path.join(certDir, "key.pem");
71
+ var userCertPath = path.join(certDir, "cert.pem");
72
+
73
+ var keyPath, certPath;
74
+ if (config.builtinCert !== false && fs.existsSync(builtinKeyPath) && fs.existsSync(builtinCertPath)) {
75
+ keyPath = builtinKeyPath;
76
+ certPath = builtinCertPath;
77
+ config.builtinCert = true;
78
+ } else {
79
+ keyPath = userKeyPath;
80
+ certPath = userCertPath;
81
+ }
82
+
67
83
  try {
68
84
  tlsOptions = {
69
85
  key: fs.readFileSync(keyPath),
@@ -119,6 +135,7 @@ var listenHost = config.host || "0.0.0.0";
119
135
  var relay = createServer({
120
136
  tlsOptions: tlsOptions,
121
137
  caPath: caRoot,
138
+ builtinCert: config.builtinCert || false,
122
139
  pinHash: config.pinHash || null,
123
140
  port: config.port,
124
141
  debug: config.debug || false,
package/lib/public/app.js CHANGED
@@ -5192,6 +5192,21 @@ import { initLongPress } from './modules/longpress.js';
5192
5192
  // --- Playbook Engine ---
5193
5193
  initPlaybook();
5194
5194
 
5195
+ // Auto-open playbook from URL param (e.g. ?playbook=push-notifications)
5196
+ (function () {
5197
+ var params = new URLSearchParams(window.location.search);
5198
+ var pbId = params.get("playbook");
5199
+ if (pbId) {
5200
+ // Small delay to ensure DOM and playbook registry are ready
5201
+ setTimeout(function () { openPlaybook(pbId); }, 300);
5202
+ // Clean up URL
5203
+ params.delete("playbook");
5204
+ var clean = params.toString();
5205
+ var newUrl = window.location.pathname + (clean ? "?" + clean : "") + window.location.hash;
5206
+ window.history.replaceState(null, "", newUrl);
5207
+ }
5208
+ })();
5209
+
5195
5210
  // --- In-session search (Cmd+F / Ctrl+F) ---
5196
5211
  initSessionSearch({
5197
5212
  messagesEl: messagesEl,
@@ -6751,7 +6766,13 @@ import { initLongPress } from './modules/longpress.js';
6751
6766
  modal.querySelector(".pwa-modal-backdrop").addEventListener("click", closeModal);
6752
6767
 
6753
6768
  confirmBtn.addEventListener("click", function () {
6754
- // Redirect to setup page
6769
+ // Builtin cert (*.d.clay.studio): open push notification guide directly
6770
+ if (location.hostname.endsWith(".d.clay.studio")) {
6771
+ closeModal();
6772
+ openPlaybook("push-notifications");
6773
+ return;
6774
+ }
6775
+ // mkcert / other: redirect to onboarding setup page
6755
6776
  var port = parseInt(location.port, 10);
6756
6777
  var setupUrl;
6757
6778
  if (!port) {
package/lib/public/sw.js CHANGED
@@ -38,8 +38,11 @@ self.addEventListener("fetch", function (event) {
38
38
  // Only handle GET requests
39
39
  if (request.method !== "GET") return;
40
40
 
41
- // Skip WebSocket upgrade requests and API/data endpoints
41
+ // Skip cross-origin requests (external images, fonts, etc.)
42
42
  var url = new URL(request.url);
43
+ if (url.origin !== self.location.origin) return;
44
+
45
+ // Skip WebSocket upgrade requests and API/data endpoints
43
46
  if (url.pathname.indexOf("/ws") !== -1) return;
44
47
  if (url.pathname.indexOf("/api/") !== -1) return;
45
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.16.0",
3
+ "version": "2.17.0-beta.2",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",