clay-server 2.16.0 → 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 +20 -7
- package/bin/cli.js +158 -239
- package/lib/certs/fullchain.pem +47 -0
- package/lib/certs/privkey.pem +5 -0
- package/lib/daemon.js +20 -3
- package/lib/pages.js +22 -20
- package/lib/project.js +9 -5
- package/lib/public/app.js +45 -25
- package/lib/public/css/command-palette.css +1 -1
- package/lib/public/css/mates.css +13 -14
- package/lib/public/css/menus.css +19 -0
- package/lib/public/css/overlays.css +44 -18
- package/lib/public/css/profile.css +128 -0
- package/lib/public/css/title-bar.css +0 -4
- package/lib/public/index.html +9 -5
- package/lib/public/modules/avatar.js +36 -0
- package/lib/public/modules/command-palette.js +4 -7
- package/lib/public/modules/mate-sidebar.js +5 -8
- package/lib/public/modules/profile.js +351 -24
- package/lib/public/modules/qrcode.js +23 -3
- package/lib/public/modules/sidebar.js +26 -9
- package/lib/public/sw.js +4 -1
- package/lib/server.js +224 -3
- package/lib/sessions.js +4 -4
- package/package.json +1 -1
- package/lib/themes/clay-light.json +0 -10
- package/lib/themes/clay.json +0 -10
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. Available from `v2.17.0-beta.2`. 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,8 @@ 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;
|
|
56
|
+
var forceBuiltin = false;
|
|
55
57
|
var skipUpdate = false;
|
|
56
58
|
var debugMode = false;
|
|
57
59
|
var autoYes = false;
|
|
@@ -81,6 +83,10 @@ for (var i = 0; i < args.length; i++) {
|
|
|
81
83
|
i++;
|
|
82
84
|
} else if (args[i] === "--no-https") {
|
|
83
85
|
useHttps = false;
|
|
86
|
+
} else if (args[i] === "--local-cert") {
|
|
87
|
+
forceMkcert = true;
|
|
88
|
+
} else if (args[i] === "--builtin-cert") {
|
|
89
|
+
forceBuiltin = true;
|
|
84
90
|
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
85
91
|
skipUpdate = true;
|
|
86
92
|
} else if (args[i] === "--dev") {
|
|
@@ -124,7 +130,9 @@ for (var i = 0; i < args.length; i++) {
|
|
|
124
130
|
console.log("Options:");
|
|
125
131
|
console.log(" -p, --port <port> Port to listen on (default: 2633)");
|
|
126
132
|
console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
|
|
127
|
-
console.log(" --no-https Disable HTTPS (enabled by default
|
|
133
|
+
console.log(" --no-https Disable HTTPS (enabled by default)");
|
|
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");
|
|
128
136
|
console.log(" --no-update Skip auto-update check on startup");
|
|
129
137
|
console.log(" --debug Enable debug panel in the web UI");
|
|
130
138
|
console.log(" -y, --yes Skip interactive prompts (accept defaults)");
|
|
@@ -564,7 +572,44 @@ function getAllIPs() {
|
|
|
564
572
|
return ips;
|
|
565
573
|
}
|
|
566
574
|
|
|
575
|
+
function getBuiltinCert() {
|
|
576
|
+
try {
|
|
577
|
+
var certDir = path.join(__dirname, "..", "lib", "certs");
|
|
578
|
+
var keyPath = path.join(certDir, "privkey.pem");
|
|
579
|
+
var certPath = path.join(certDir, "fullchain.pem");
|
|
580
|
+
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) return null;
|
|
581
|
+
|
|
582
|
+
// Check expiry
|
|
583
|
+
var certText = execFileSync("openssl", [
|
|
584
|
+
"x509", "-in", certPath, "-noout", "-enddate"
|
|
585
|
+
], { encoding: "utf8" });
|
|
586
|
+
var m = certText.match(/notAfter=(.+)/);
|
|
587
|
+
if (m) {
|
|
588
|
+
var expiry = new Date(m[1]);
|
|
589
|
+
var now = new Date();
|
|
590
|
+
// Skip if expiring within 7 days
|
|
591
|
+
if (expiry.getTime() - now.getTime() < 7 * 24 * 60 * 60 * 1000) return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return { key: keyPath, cert: certPath, caRoot: null, builtin: true };
|
|
595
|
+
} catch (e) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function toClayStudioUrl(ip, port, protocol) {
|
|
601
|
+
var dashed = ip.replace(/\./g, "-");
|
|
602
|
+
return protocol + "://" + dashed + ".d.clay.studio:" + port;
|
|
603
|
+
}
|
|
604
|
+
|
|
567
605
|
function ensureCerts(ip) {
|
|
606
|
+
// --builtin-cert: skip mkcert entirely, go straight to builtin
|
|
607
|
+
if (forceBuiltin) {
|
|
608
|
+
var builtin = getBuiltinCert();
|
|
609
|
+
if (builtin) return builtin;
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
568
613
|
var homeDir = os.homedir();
|
|
569
614
|
var certDir = path.join(process.env.CLAY_HOME || path.join(homeDir, ".clay"), "certs");
|
|
570
615
|
var keyPath = path.join(certDir, "key.pem");
|
|
@@ -579,24 +624,30 @@ function ensureCerts(ip) {
|
|
|
579
624
|
fs.copyFileSync(legacyCert, certPath);
|
|
580
625
|
}
|
|
581
626
|
|
|
627
|
+
var mkcertInstalled = hasMkcert();
|
|
628
|
+
|
|
582
629
|
var caRoot = null;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
+
}
|
|
590
639
|
|
|
591
640
|
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
592
641
|
var allIPs = getAllIPs();
|
|
593
642
|
|
|
594
643
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
595
644
|
var needRegen = false;
|
|
645
|
+
var isMkcertCert = false;
|
|
596
646
|
try {
|
|
597
647
|
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
598
648
|
// If cert is from an external CA (e.g. Tailscale/Let's Encrypt), never regenerate
|
|
599
649
|
if (certText.indexOf("mkcert") === -1) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
650
|
+
isMkcertCert = true;
|
|
600
651
|
for (var i = 0; i < allIPs.length; i++) {
|
|
601
652
|
if (certText.indexOf(allIPs[i]) === -1) {
|
|
602
653
|
needRegen = true;
|
|
@@ -604,24 +655,41 @@ function ensureCerts(ip) {
|
|
|
604
655
|
}
|
|
605
656
|
}
|
|
606
657
|
} catch (e) { needRegen = true; }
|
|
607
|
-
|
|
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
|
+
}
|
|
608
663
|
}
|
|
609
664
|
|
|
610
|
-
|
|
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
|
+
}
|
|
611
680
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
681
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
682
|
+
return { key: keyPath, cert: certPath, caRoot: caRoot, mkcertDetected: !forceMkcert };
|
|
683
|
+
}
|
|
615
684
|
}
|
|
616
685
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return null;
|
|
686
|
+
// Fallback: builtin cert (unless --local-cert forces mkcert-only)
|
|
687
|
+
if (!forceMkcert) {
|
|
688
|
+
var builtin = getBuiltinCert();
|
|
689
|
+
if (builtin) return builtin;
|
|
622
690
|
}
|
|
623
691
|
|
|
624
|
-
return
|
|
692
|
+
return null;
|
|
625
693
|
}
|
|
626
694
|
|
|
627
695
|
// --- Logo ---
|
|
@@ -1395,11 +1463,15 @@ function setup(callback) {
|
|
|
1395
1463
|
async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
1396
1464
|
var ip = getLocalIP();
|
|
1397
1465
|
var hasTls = false;
|
|
1466
|
+
var hasBuiltinCert = false;
|
|
1467
|
+
var mkcertDetected = false;
|
|
1398
1468
|
|
|
1399
1469
|
if (useHttps) {
|
|
1400
1470
|
var certPaths = ensureCerts(ip);
|
|
1401
1471
|
if (certPaths) {
|
|
1402
1472
|
hasTls = true;
|
|
1473
|
+
if (certPaths.builtin) hasBuiltinCert = true;
|
|
1474
|
+
if (certPaths.mkcertDetected) mkcertDetected = true;
|
|
1403
1475
|
} else {
|
|
1404
1476
|
log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
|
|
1405
1477
|
}
|
|
@@ -1473,6 +1545,8 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1473
1545
|
host: host,
|
|
1474
1546
|
pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
|
|
1475
1547
|
tls: hasTls,
|
|
1548
|
+
builtinCert: hasBuiltinCert,
|
|
1549
|
+
mkcertDetected: mkcertDetected,
|
|
1476
1550
|
debug: debugMode,
|
|
1477
1551
|
keepAwake: keepAwake,
|
|
1478
1552
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -1547,9 +1621,13 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1547
1621
|
// Headless mode — print status and exit immediately
|
|
1548
1622
|
if (headlessMode) {
|
|
1549
1623
|
var protocol = config.tls ? "https" : "http";
|
|
1550
|
-
var url =
|
|
1624
|
+
var url = config.builtinCert
|
|
1625
|
+
? toClayStudioUrl(ip, config.port, protocol)
|
|
1626
|
+
: protocol + "://" + ip + ":" + config.port;
|
|
1551
1627
|
console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
|
|
1552
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.");
|
|
1553
1631
|
console.log(" " + sym.done + " Headless mode — exiting CLI");
|
|
1554
1632
|
process.exit(0);
|
|
1555
1633
|
return;
|
|
@@ -1565,10 +1643,16 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1565
1643
|
async function devMode(mode, keepAwake, existingPinHash) {
|
|
1566
1644
|
var ip = getLocalIP();
|
|
1567
1645
|
var hasTls = false;
|
|
1646
|
+
var hasBuiltinCert = false;
|
|
1647
|
+
var mkcertDetected = false;
|
|
1568
1648
|
|
|
1569
1649
|
if (useHttps) {
|
|
1570
1650
|
var certPaths = ensureCerts(ip);
|
|
1571
|
-
if (certPaths)
|
|
1651
|
+
if (certPaths) {
|
|
1652
|
+
hasTls = true;
|
|
1653
|
+
if (certPaths.builtin) hasBuiltinCert = true;
|
|
1654
|
+
if (certPaths.mkcertDetected) mkcertDetected = true;
|
|
1655
|
+
}
|
|
1572
1656
|
}
|
|
1573
1657
|
|
|
1574
1658
|
var portFree = await isPortFree(port);
|
|
@@ -1630,6 +1714,8 @@ async function devMode(mode, keepAwake, existingPinHash) {
|
|
|
1630
1714
|
host: host,
|
|
1631
1715
|
pinHash: existingPinHash || null,
|
|
1632
1716
|
tls: hasTls,
|
|
1717
|
+
builtinCert: hasBuiltinCert,
|
|
1718
|
+
mkcertDetected: mkcertDetected,
|
|
1633
1719
|
debug: true,
|
|
1634
1720
|
keepAwake: keepAwake || false,
|
|
1635
1721
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -1778,6 +1864,8 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1778
1864
|
callback(config);
|
|
1779
1865
|
return;
|
|
1780
1866
|
}
|
|
1867
|
+
var hasBuiltinCert = !!(certPaths && certPaths.builtin);
|
|
1868
|
+
var mkcertDetected = !!(certPaths && certPaths.mkcertDetected);
|
|
1781
1869
|
|
|
1782
1870
|
// Shut down old daemon
|
|
1783
1871
|
stopDaemonWatcher();
|
|
@@ -1801,6 +1889,8 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1801
1889
|
port: config.port,
|
|
1802
1890
|
pinHash: config.pinHash || null,
|
|
1803
1891
|
tls: true,
|
|
1892
|
+
builtinCert: hasBuiltinCert,
|
|
1893
|
+
mkcertDetected: mkcertDetected,
|
|
1804
1894
|
debug: config.debug || false,
|
|
1805
1895
|
keepAwake: config.keepAwake || false,
|
|
1806
1896
|
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
@@ -1879,7 +1969,9 @@ function showServerStarted(config, ip) {
|
|
|
1879
1969
|
function showMainMenu(config, ip) {
|
|
1880
1970
|
startDaemonWatcher();
|
|
1881
1971
|
var protocol = config.tls ? "https" : "http";
|
|
1882
|
-
var url =
|
|
1972
|
+
var url = config.builtinCert
|
|
1973
|
+
? toClayStudioUrl(ip, config.port, protocol)
|
|
1974
|
+
: protocol + "://" + ip + ":" + config.port;
|
|
1883
1975
|
|
|
1884
1976
|
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
1885
1977
|
var projs = (status && status.projects) || [];
|
|
@@ -1897,6 +1989,7 @@ function showMainMenu(config, ip) {
|
|
|
1897
1989
|
function afterQr() {
|
|
1898
1990
|
// Status line
|
|
1899
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);
|
|
1900
1993
|
var parts = [];
|
|
1901
1994
|
parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
|
|
1902
1995
|
parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
|
|
@@ -1907,6 +2000,13 @@ function showMainMenu(config, ip) {
|
|
|
1907
2000
|
log(" Press " + a.bold + "o" + a.reset + " to open in browser");
|
|
1908
2001
|
log("");
|
|
1909
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
|
+
|
|
1910
2010
|
showMenuItems();
|
|
1911
2011
|
}
|
|
1912
2012
|
|
|
@@ -1925,7 +2025,6 @@ function showMainMenu(config, ip) {
|
|
|
1925
2025
|
function showMenuItems() {
|
|
1926
2026
|
var items = [
|
|
1927
2027
|
{ label: "Setup notifications", value: "notifications" },
|
|
1928
|
-
{ label: "Projects", value: "projects" },
|
|
1929
2028
|
{ label: "Settings", value: "settings" },
|
|
1930
2029
|
{ label: "Shut down server", value: "shutdown" },
|
|
1931
2030
|
{ label: "Keep server alive & exit", value: "exit" },
|
|
@@ -1940,10 +2039,6 @@ function showMainMenu(config, ip) {
|
|
|
1940
2039
|
});
|
|
1941
2040
|
break;
|
|
1942
2041
|
|
|
1943
|
-
case "projects":
|
|
1944
|
-
showProjectsMenu(config, ip);
|
|
1945
|
-
break;
|
|
1946
|
-
|
|
1947
2042
|
case "settings":
|
|
1948
2043
|
showSettingsMenu(config, ip);
|
|
1949
2044
|
break;
|
|
@@ -1981,9 +2076,8 @@ function showMainMenu(config, ip) {
|
|
|
1981
2076
|
}
|
|
1982
2077
|
}, {
|
|
1983
2078
|
hint: [
|
|
1984
|
-
"claude-relay has been renamed to clay-server · npx clay-server",
|
|
1985
2079
|
"Run npx clay-server in other directories to add more projects.",
|
|
1986
|
-
"★ github.com/chadbyte/
|
|
2080
|
+
"★ github.com/chadbyte/clay — Press s to star the repo",
|
|
1987
2081
|
],
|
|
1988
2082
|
keys: [
|
|
1989
2083
|
{ key: "o", onKey: function () {
|
|
@@ -1991,7 +2085,7 @@ function showMainMenu(config, ip) {
|
|
|
1991
2085
|
showMainMenu(config, ip);
|
|
1992
2086
|
}},
|
|
1993
2087
|
{ key: "s", onKey: function () {
|
|
1994
|
-
openUrl("https://github.com/chadbyte/
|
|
2088
|
+
openUrl("https://github.com/chadbyte/clay");
|
|
1995
2089
|
showMainMenu(config, ip);
|
|
1996
2090
|
}},
|
|
1997
2091
|
],
|
|
@@ -2000,203 +2094,6 @@ function showMainMenu(config, ip) {
|
|
|
2000
2094
|
});
|
|
2001
2095
|
}
|
|
2002
2096
|
|
|
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
2097
|
// ==============================
|
|
2201
2098
|
// Setup guide (2x2 toggle flow)
|
|
2202
2099
|
// ==============================
|
|
@@ -2229,7 +2126,7 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
2229
2126
|
promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
|
|
2230
2127
|
wantRemote = remote;
|
|
2231
2128
|
log(sym.bar);
|
|
2232
|
-
promptToggle("Want push notifications?", "Requires HTTPS
|
|
2129
|
+
promptToggle("Want push notifications?", "Requires HTTPS", false, function (push) {
|
|
2233
2130
|
wantPush = push;
|
|
2234
2131
|
log(sym.bar);
|
|
2235
2132
|
afterToggles();
|
|
@@ -2293,6 +2190,15 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
2293
2190
|
return;
|
|
2294
2191
|
}
|
|
2295
2192
|
|
|
2193
|
+
// Builtin cert: HTTPS already active, skip mkcert flow entirely
|
|
2194
|
+
if (config.builtinCert) {
|
|
2195
|
+
log(sym.pointer + " " + a.bold + "HTTPS" + a.reset + a.dim + " · Enabled (builtin certificate)" + a.reset);
|
|
2196
|
+
log(sym.bar);
|
|
2197
|
+
showSetupQR();
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// mkcert flow (--mkcert or fallback)
|
|
2296
2202
|
var mcReady = hasMkcert();
|
|
2297
2203
|
log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
|
|
2298
2204
|
if (mcReady) {
|
|
@@ -2341,10 +2247,16 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
2341
2247
|
}
|
|
2342
2248
|
var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
|
|
2343
2249
|
var setupQuery = wantRemote ? "" : "?mode=lan";
|
|
2344
|
-
//
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2250
|
+
// Builtin cert: link directly to the app with push notification guide
|
|
2251
|
+
// mkcert: use HTTP onboarding server for CA install flow
|
|
2252
|
+
var setupUrl;
|
|
2253
|
+
if (config.builtinCert) {
|
|
2254
|
+
setupUrl = toClayStudioUrl(setupIP, config.port, "https") + "/pwa";
|
|
2255
|
+
} else if (config.tls) {
|
|
2256
|
+
setupUrl = "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery;
|
|
2257
|
+
} else {
|
|
2258
|
+
setupUrl = "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
|
|
2259
|
+
}
|
|
2348
2260
|
log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
|
|
2349
2261
|
log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
|
|
2350
2262
|
log(sym.bar + " " + a.bold + setupUrl + a.reset);
|
|
@@ -2435,11 +2347,13 @@ function showSettingsMenu(config, ip) {
|
|
|
2435
2347
|
{ label: "Setup notifications", value: "guide" },
|
|
2436
2348
|
];
|
|
2437
2349
|
|
|
2438
|
-
if (
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2350
|
+
if (!muEnabled) {
|
|
2351
|
+
if (config.pinHash) {
|
|
2352
|
+
items.push({ label: "Change PIN", value: "pin" });
|
|
2353
|
+
items.push({ label: "Remove PIN", value: "remove_pin" });
|
|
2354
|
+
} else {
|
|
2355
|
+
items.push({ label: "Set PIN", value: "pin" });
|
|
2356
|
+
}
|
|
2443
2357
|
}
|
|
2444
2358
|
if (muEnabled) {
|
|
2445
2359
|
items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
|
|
@@ -2759,7 +2673,9 @@ function showSettingsMenu(config, ip) {
|
|
|
2759
2673
|
return;
|
|
2760
2674
|
}
|
|
2761
2675
|
var protocol = config.tls ? "https" : "http";
|
|
2762
|
-
var recoveryUrl =
|
|
2676
|
+
var recoveryUrl = config.builtinCert
|
|
2677
|
+
? toClayStudioUrl(ip, config.port, protocol) + "/recover/" + recoveryUrlPath
|
|
2678
|
+
: protocol + "://" + ip + ":" + config.port + "/recover/" + recoveryUrlPath;
|
|
2763
2679
|
log(sym.bar);
|
|
2764
2680
|
log(sym.bar + " " + a.yellow + sym.warn + " Admin Password Recovery" + a.reset);
|
|
2765
2681
|
log(sym.bar);
|
|
@@ -2849,9 +2765,12 @@ var currentVersion = require("../package.json").version;
|
|
|
2849
2765
|
if (headlessMode) {
|
|
2850
2766
|
var protocol = config.tls ? "https" : "http";
|
|
2851
2767
|
var ip = getLocalIP();
|
|
2852
|
-
var url =
|
|
2768
|
+
var url = config.builtinCert
|
|
2769
|
+
? toClayStudioUrl(ip, config.port, protocol)
|
|
2770
|
+
: protocol + "://" + ip + ":" + config.port;
|
|
2853
2771
|
console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
|
|
2854
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.");
|
|
2855
2774
|
process.exit(0);
|
|
2856
2775
|
return;
|
|
2857
2776
|
}
|