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 +20 -7
- package/bin/cli.js +94 -218
- package/lib/certs/fullchain.pem +47 -0
- package/lib/certs/privkey.pem +5 -0
- package/lib/daemon.js +19 -2
- package/lib/public/app.js +22 -1
- package/lib/public/sw.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/clay-server) [](https://www.npmjs.com/package/clay-server) [](https://github.com/chadbyte/clay) [](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
|
|
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
|
|
169
|
+
## HTTPS
|
|
170
170
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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)
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
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 (
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
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 =
|
|
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 =
|
|
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-----
|
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
|
|
66
|
-
var
|
|
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
|
-
//
|
|
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
|
|
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
|
|