claude-relay 2.2.2 → 2.2.4
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/bin/cli.js +190 -44
- package/lib/config.js +8 -2
- package/lib/daemon.js +4 -0
- package/lib/ipc.js +7 -3
- package/lib/pages.js +24 -6
- package/lib/project.js +1 -1
- package/lib/sdk-bridge.js +2 -2
- package/lib/server.js +5 -3
- package/lib/terminal.js +2 -1
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,17 @@ var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir,
|
|
|
10
10
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
11
11
|
var { generateAuthToken } = require("../lib/server");
|
|
12
12
|
|
|
13
|
+
function openUrl(url) {
|
|
14
|
+
try {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, windowsHide: true }).unref();
|
|
17
|
+
} else {
|
|
18
|
+
var cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
19
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
var args = process.argv.slice(2);
|
|
14
25
|
var port = 2633;
|
|
15
26
|
var useHttps = true;
|
|
@@ -312,6 +323,33 @@ function getLocalIP() {
|
|
|
312
323
|
}
|
|
313
324
|
|
|
314
325
|
// --- Certs ---
|
|
326
|
+
function isRoutableIP(addr) {
|
|
327
|
+
if (addr.startsWith("10.")) return true;
|
|
328
|
+
if (addr.startsWith("192.168.")) return true;
|
|
329
|
+
if (addr.startsWith("100.")) {
|
|
330
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
331
|
+
return second >= 64 && second <= 127; // CGNAT (Tailscale)
|
|
332
|
+
}
|
|
333
|
+
if (addr.startsWith("172.")) {
|
|
334
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
335
|
+
return second >= 16 && second <= 31;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getAllIPs() {
|
|
341
|
+
var ips = [];
|
|
342
|
+
var ifaces = os.networkInterfaces();
|
|
343
|
+
for (var addrs of Object.values(ifaces)) {
|
|
344
|
+
for (var j = 0; j < addrs.length; j++) {
|
|
345
|
+
if (addrs[j].family === "IPv4" && !addrs[j].internal && isRoutableIP(addrs[j].address)) {
|
|
346
|
+
ips.push(addrs[j].address);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return ips;
|
|
351
|
+
}
|
|
352
|
+
|
|
315
353
|
function ensureCerts(ip) {
|
|
316
354
|
var homeDir = os.homedir();
|
|
317
355
|
var certDir = path.join(homeDir, ".claude-relay", "certs");
|
|
@@ -336,27 +374,33 @@ function ensureCerts(ip) {
|
|
|
336
374
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
337
375
|
} catch (e) {}
|
|
338
376
|
|
|
377
|
+
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
378
|
+
var allIPs = getAllIPs();
|
|
379
|
+
|
|
339
380
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
340
381
|
var needRegen = false;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (certText.indexOf(
|
|
345
|
-
|
|
346
|
-
|
|
382
|
+
try {
|
|
383
|
+
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
384
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
385
|
+
if (certText.indexOf(allIPs[i]) === -1) {
|
|
386
|
+
needRegen = true;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (e) { needRegen = true; }
|
|
347
391
|
if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
348
392
|
}
|
|
349
393
|
|
|
350
394
|
fs.mkdirSync(certDir, { recursive: true });
|
|
351
395
|
|
|
352
396
|
var domains = ["localhost", "127.0.0.1", "::1"];
|
|
353
|
-
|
|
397
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
398
|
+
if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
|
|
399
|
+
}
|
|
354
400
|
|
|
355
401
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
{ stdio: "pipe" }
|
|
359
|
-
);
|
|
402
|
+
var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
|
|
403
|
+
execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
|
|
360
404
|
} catch (err) {
|
|
361
405
|
return null;
|
|
362
406
|
}
|
|
@@ -991,9 +1035,13 @@ function setup(callback) {
|
|
|
991
1035
|
log(sym.bar);
|
|
992
1036
|
|
|
993
1037
|
promptPin(function (pin) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1038
|
+
if (process.platform === "darwin") {
|
|
1039
|
+
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1040
|
+
callback(pin, keepAwake);
|
|
1041
|
+
});
|
|
1042
|
+
} else {
|
|
1043
|
+
callback(pin, false);
|
|
1044
|
+
}
|
|
997
1045
|
});
|
|
998
1046
|
});
|
|
999
1047
|
});
|
|
@@ -1063,6 +1111,7 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1063
1111
|
|
|
1064
1112
|
var child = spawn(process.execPath, [daemonScript], {
|
|
1065
1113
|
detached: true,
|
|
1114
|
+
windowsHide: true,
|
|
1066
1115
|
stdio: ["ignore", logFd, logFd],
|
|
1067
1116
|
env: Object.assign({}, process.env, {
|
|
1068
1117
|
CLAUDE_RELAY_CONFIG: configPath(),
|
|
@@ -1092,6 +1141,96 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1092
1141
|
showServerStarted(config, ip);
|
|
1093
1142
|
}
|
|
1094
1143
|
|
|
1144
|
+
// ==============================
|
|
1145
|
+
// Restart daemon with TLS enabled
|
|
1146
|
+
// ==============================
|
|
1147
|
+
async function restartDaemonWithTLS(config, callback) {
|
|
1148
|
+
var ip = getLocalIP();
|
|
1149
|
+
var certPaths = ensureCerts(ip);
|
|
1150
|
+
if (!certPaths) {
|
|
1151
|
+
callback(config);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Shut down old daemon
|
|
1156
|
+
stopDaemonWatcher();
|
|
1157
|
+
try {
|
|
1158
|
+
await sendIPCCommand(socketPath(), { cmd: "shutdown" });
|
|
1159
|
+
} catch (e) {}
|
|
1160
|
+
|
|
1161
|
+
// Wait for port to be released
|
|
1162
|
+
var waited = 0;
|
|
1163
|
+
while (waited < 5000) {
|
|
1164
|
+
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
1165
|
+
waited += 300;
|
|
1166
|
+
var free = await isPortFree(config.port);
|
|
1167
|
+
if (free) break;
|
|
1168
|
+
}
|
|
1169
|
+
clearStaleConfig();
|
|
1170
|
+
|
|
1171
|
+
// Re-fork with TLS
|
|
1172
|
+
var newConfig = {
|
|
1173
|
+
pid: null,
|
|
1174
|
+
port: config.port,
|
|
1175
|
+
pinHash: config.pinHash || null,
|
|
1176
|
+
tls: true,
|
|
1177
|
+
debug: config.debug || false,
|
|
1178
|
+
keepAwake: config.keepAwake || false,
|
|
1179
|
+
projects: config.projects || [],
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
ensureConfigDir();
|
|
1183
|
+
saveConfig(newConfig);
|
|
1184
|
+
|
|
1185
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1186
|
+
var logFile = logPath();
|
|
1187
|
+
var logFd = fs.openSync(logFile, "a");
|
|
1188
|
+
|
|
1189
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
1190
|
+
detached: true,
|
|
1191
|
+
windowsHide: true,
|
|
1192
|
+
stdio: ["ignore", logFd, logFd],
|
|
1193
|
+
env: Object.assign({}, process.env, {
|
|
1194
|
+
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1195
|
+
}),
|
|
1196
|
+
});
|
|
1197
|
+
child.unref();
|
|
1198
|
+
fs.closeSync(logFd);
|
|
1199
|
+
|
|
1200
|
+
newConfig.pid = child.pid;
|
|
1201
|
+
saveConfig(newConfig);
|
|
1202
|
+
|
|
1203
|
+
await new Promise(function (resolve) { setTimeout(resolve, 800); });
|
|
1204
|
+
|
|
1205
|
+
var alive = await isDaemonAliveAsync(newConfig);
|
|
1206
|
+
if (!alive) {
|
|
1207
|
+
log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
|
|
1208
|
+
// Re-fork without TLS so the server is at least running
|
|
1209
|
+
newConfig.tls = false;
|
|
1210
|
+
saveConfig(newConfig);
|
|
1211
|
+
var logFd2 = fs.openSync(logFile, "a");
|
|
1212
|
+
var child2 = spawn(process.execPath, [daemonScript], {
|
|
1213
|
+
detached: true,
|
|
1214
|
+
windowsHide: true,
|
|
1215
|
+
stdio: ["ignore", logFd2, logFd2],
|
|
1216
|
+
env: Object.assign({}, process.env, {
|
|
1217
|
+
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1218
|
+
}),
|
|
1219
|
+
});
|
|
1220
|
+
child2.unref();
|
|
1221
|
+
fs.closeSync(logFd2);
|
|
1222
|
+
newConfig.pid = child2.pid;
|
|
1223
|
+
saveConfig(newConfig);
|
|
1224
|
+
await new Promise(function (resolve) { setTimeout(resolve, 800); });
|
|
1225
|
+
startDaemonWatcher();
|
|
1226
|
+
callback(newConfig);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
startDaemonWatcher();
|
|
1231
|
+
callback(newConfig);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1095
1234
|
// ==============================
|
|
1096
1235
|
// Show server started info
|
|
1097
1236
|
// ==============================
|
|
@@ -1161,6 +1300,7 @@ function showMainMenu(config, ip) {
|
|
|
1161
1300
|
switch (choice) {
|
|
1162
1301
|
case "notifications":
|
|
1163
1302
|
showSetupGuide(config, ip, function () {
|
|
1303
|
+
config = loadConfig() || config;
|
|
1164
1304
|
showMainMenu(config, ip);
|
|
1165
1305
|
});
|
|
1166
1306
|
break;
|
|
@@ -1211,17 +1351,11 @@ function showMainMenu(config, ip) {
|
|
|
1211
1351
|
],
|
|
1212
1352
|
keys: [
|
|
1213
1353
|
{ key: "o", onKey: function () {
|
|
1214
|
-
|
|
1215
|
-
var openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
1216
|
-
spawn(openCmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
1217
|
-
} catch (e) {}
|
|
1354
|
+
openUrl(url);
|
|
1218
1355
|
showMainMenu(config, ip);
|
|
1219
1356
|
}},
|
|
1220
1357
|
{ key: "s", onKey: function () {
|
|
1221
|
-
|
|
1222
|
-
var openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
1223
|
-
spawn(openCmd, ["https://github.com/chadbyte/claude-relay"], { stdio: "ignore", detached: true }).unref();
|
|
1224
|
-
} catch (e) {}
|
|
1358
|
+
openUrl("https://github.com/chadbyte/claude-relay");
|
|
1225
1359
|
showMainMenu(config, ip);
|
|
1226
1360
|
}},
|
|
1227
1361
|
],
|
|
@@ -1435,22 +1569,6 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1435
1569
|
var wantRemote = false;
|
|
1436
1570
|
var wantPush = false;
|
|
1437
1571
|
|
|
1438
|
-
// If everything is already set up, skip straight to QR
|
|
1439
|
-
var tsReady = getTailscaleIP() !== null;
|
|
1440
|
-
var mcReady = hasMkcert();
|
|
1441
|
-
if (tsReady && mcReady && config.tls) {
|
|
1442
|
-
console.clear();
|
|
1443
|
-
printLogo();
|
|
1444
|
-
log("");
|
|
1445
|
-
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
1446
|
-
log(sym.bar);
|
|
1447
|
-
log(sym.done + " " + a.green + "Tailscale" + a.reset + a.dim + " · " + getTailscaleIP() + a.reset);
|
|
1448
|
-
log(sym.done + " " + a.green + "HTTPS" + a.reset + a.dim + " · mkcert installed" + a.reset);
|
|
1449
|
-
log(sym.bar);
|
|
1450
|
-
showSetupQR();
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
1572
|
console.clear();
|
|
1455
1573
|
printLogo();
|
|
1456
1574
|
log("");
|
|
@@ -1543,11 +1661,25 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1543
1661
|
log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
|
|
1544
1662
|
if (mcReady) {
|
|
1545
1663
|
log(sym.bar + " " + a.green + "mkcert is installed" + a.reset);
|
|
1664
|
+
if (!config.tls) {
|
|
1665
|
+
log(sym.bar + " " + a.dim + "Restarting server with HTTPS..." + a.reset);
|
|
1666
|
+
restartDaemonWithTLS(config, function (newConfig) {
|
|
1667
|
+
config = newConfig;
|
|
1668
|
+
log(sym.bar);
|
|
1669
|
+
showSetupQR();
|
|
1670
|
+
});
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1546
1673
|
log(sym.bar);
|
|
1547
1674
|
showSetupQR();
|
|
1548
1675
|
} else {
|
|
1549
1676
|
log(sym.bar + " " + a.yellow + "mkcert not found." + a.reset);
|
|
1550
|
-
|
|
1677
|
+
var mkcertHint = process.platform === "win32"
|
|
1678
|
+
? "choco install mkcert && mkcert -install"
|
|
1679
|
+
: process.platform === "darwin"
|
|
1680
|
+
? "brew install mkcert && mkcert -install"
|
|
1681
|
+
: "apt install mkcert && mkcert -install";
|
|
1682
|
+
log(sym.bar + " " + a.dim + "Install: " + a.reset + mkcertHint);
|
|
1551
1683
|
log(sym.bar);
|
|
1552
1684
|
promptSelect("Select", [
|
|
1553
1685
|
{ label: "Re-check", value: "recheck" },
|
|
@@ -1564,10 +1696,19 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1564
1696
|
|
|
1565
1697
|
function showSetupQR() {
|
|
1566
1698
|
var tsIP = getTailscaleIP();
|
|
1699
|
+
var lanIP = null;
|
|
1700
|
+
if (!wantRemote) {
|
|
1701
|
+
var allIPs = getAllIPs();
|
|
1702
|
+
for (var j = 0; j < allIPs.length; j++) {
|
|
1703
|
+
if (!allIPs[j].startsWith("100.")) { lanIP = allIPs[j]; break; }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
|
|
1707
|
+
var setupQuery = wantRemote ? "" : "?mode=lan";
|
|
1567
1708
|
// Always use HTTP onboarding URL for QR/setup when TLS is active
|
|
1568
1709
|
var setupUrl = config.tls
|
|
1569
|
-
? "http://" +
|
|
1570
|
-
: "http://" +
|
|
1710
|
+
? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
|
|
1711
|
+
: "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
|
|
1571
1712
|
log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
|
|
1572
1713
|
log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
|
|
1573
1714
|
log(sym.bar + " " + a.bold + setupUrl + a.reset);
|
|
@@ -1576,7 +1717,7 @@ function showSetupGuide(config, ip, goBack) {
|
|
|
1576
1717
|
var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
|
|
1577
1718
|
console.log(lines);
|
|
1578
1719
|
log(sym.bar);
|
|
1579
|
-
if (
|
|
1720
|
+
if (wantRemote) {
|
|
1580
1721
|
log(sym.bar + " " + a.dim + "Can't connect? Make sure Tailscale is installed on your phone too." + a.reset);
|
|
1581
1722
|
} else {
|
|
1582
1723
|
log(sym.bar + " " + a.dim + "Can't connect? Your phone must be on the same Wi-Fi network." + a.reset);
|
|
@@ -1630,7 +1771,9 @@ function showSettingsMenu(config, ip) {
|
|
|
1630
1771
|
log(sym.bar + " mkcert " + mcStatus);
|
|
1631
1772
|
log(sym.bar + " HTTPS " + tlsStatus);
|
|
1632
1773
|
log(sym.bar + " PIN " + pinStatus);
|
|
1633
|
-
|
|
1774
|
+
if (process.platform === "darwin") {
|
|
1775
|
+
log(sym.bar + " Keep awake " + awakeStatus);
|
|
1776
|
+
}
|
|
1634
1777
|
log(sym.bar);
|
|
1635
1778
|
|
|
1636
1779
|
// Build items
|
|
@@ -1644,7 +1787,9 @@ function showSettingsMenu(config, ip) {
|
|
|
1644
1787
|
} else {
|
|
1645
1788
|
items.push({ label: "Set PIN", value: "pin" });
|
|
1646
1789
|
}
|
|
1647
|
-
|
|
1790
|
+
if (process.platform === "darwin") {
|
|
1791
|
+
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
1792
|
+
}
|
|
1648
1793
|
items.push({ label: "View logs", value: "logs" });
|
|
1649
1794
|
items.push({ label: "Back", value: "back" });
|
|
1650
1795
|
|
|
@@ -1652,6 +1797,7 @@ function showSettingsMenu(config, ip) {
|
|
|
1652
1797
|
switch (choice) {
|
|
1653
1798
|
case "guide":
|
|
1654
1799
|
showSetupGuide(config, ip, function () {
|
|
1800
|
+
config = loadConfig() || config;
|
|
1655
1801
|
showSettingsMenu(config, ip);
|
|
1656
1802
|
});
|
|
1657
1803
|
break;
|
package/lib/config.js
CHANGED
|
@@ -11,6 +11,9 @@ function configPath() {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function socketPath() {
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
return "\\\\.\\pipe\\claude-relay-daemon";
|
|
16
|
+
}
|
|
14
17
|
return path.join(CONFIG_DIR, "daemon.sock");
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -50,7 +53,8 @@ function isPidAlive(pid) {
|
|
|
50
53
|
function isDaemonAlive(config) {
|
|
51
54
|
if (!config || !config.pid) return false;
|
|
52
55
|
if (!isPidAlive(config.pid)) return false;
|
|
53
|
-
//
|
|
56
|
+
// Named pipes on Windows can't be stat'd, just check PID
|
|
57
|
+
if (process.platform === "win32") return true;
|
|
54
58
|
try {
|
|
55
59
|
fs.statSync(socketPath());
|
|
56
60
|
return true;
|
|
@@ -96,7 +100,9 @@ function generateSlug(projectPath, existingSlugs) {
|
|
|
96
100
|
|
|
97
101
|
function clearStaleConfig() {
|
|
98
102
|
try { fs.unlinkSync(configPath()); } catch (e) {}
|
|
99
|
-
|
|
103
|
+
if (process.platform !== "win32") {
|
|
104
|
+
try { fs.unlinkSync(socketPath()); } catch (e) {}
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
// --- ~/.clayrc (recent projects persistence) ---
|
package/lib/daemon.js
CHANGED
|
@@ -251,6 +251,10 @@ function gracefulShutdown() {
|
|
|
251
251
|
|
|
252
252
|
process.on("SIGTERM", gracefulShutdown);
|
|
253
253
|
process.on("SIGINT", gracefulShutdown);
|
|
254
|
+
// Windows emits SIGHUP when console window closes
|
|
255
|
+
if (process.platform === "win32") {
|
|
256
|
+
process.on("SIGHUP", gracefulShutdown);
|
|
257
|
+
}
|
|
254
258
|
|
|
255
259
|
process.on("uncaughtException", function (err) {
|
|
256
260
|
console.error("[daemon] Uncaught exception:", err);
|
package/lib/ipc.js
CHANGED
|
@@ -6,8 +6,10 @@ var fs = require("fs");
|
|
|
6
6
|
* handler(msg) should return a response object (or a Promise of one).
|
|
7
7
|
*/
|
|
8
8
|
function createIPCServer(sockPath, handler) {
|
|
9
|
-
// Remove stale socket file
|
|
10
|
-
|
|
9
|
+
// Remove stale socket file (not needed for Windows named pipes)
|
|
10
|
+
if (process.platform !== "win32") {
|
|
11
|
+
try { fs.unlinkSync(sockPath); } catch (e) {}
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
var server = net.createServer(function (conn) {
|
|
13
15
|
var buffer = "";
|
|
@@ -49,7 +51,9 @@ function createIPCServer(sockPath, handler) {
|
|
|
49
51
|
return {
|
|
50
52
|
close: function () {
|
|
51
53
|
server.close();
|
|
52
|
-
|
|
54
|
+
if (process.platform !== "win32") {
|
|
55
|
+
try { fs.unlinkSync(sockPath); } catch (e) {}
|
|
56
|
+
}
|
|
53
57
|
},
|
|
54
58
|
};
|
|
55
59
|
}
|
package/lib/pages.js
CHANGED
|
@@ -40,7 +40,7 @@ function pinPageHtml() {
|
|
|
40
40
|
'</script></div></body></html>';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function setupPageHtml(httpsUrl, httpUrl, hasCert) {
|
|
43
|
+
function setupPageHtml(httpsUrl, httpUrl, hasCert, lanMode) {
|
|
44
44
|
return `<!DOCTYPE html><html lang="en"><head>
|
|
45
45
|
<meta charset="UTF-8">
|
|
46
46
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
@@ -195,6 +195,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
195
195
|
<div class="step-desc">Install Claude Relay as an app for quick access and a full-screen experience.</div>
|
|
196
196
|
|
|
197
197
|
<div class="platform-ios">
|
|
198
|
+
<div class="check-status warn">On iOS, push notifications only work from the installed app. This step is required.</div>
|
|
198
199
|
<div id="ios-not-safari" class="check-status warn" style="display:none">You must use <b>Safari</b> to install. Open this page in Safari first.</div>
|
|
199
200
|
<div id="ios-safari-steps">
|
|
200
201
|
<div class="instruction"><div class="inst-num">1</div>
|
|
@@ -237,6 +238,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
237
238
|
</div>
|
|
238
239
|
|
|
239
240
|
<div id="pwa-status" class="check-status pending">After installing, open Claude Relay from your home screen to continue setup.</div>
|
|
241
|
+
<button class="skip-link" id="pwa-skip" onclick="nextStep()" style="display:none">Skip for now</button>
|
|
240
242
|
</div>
|
|
241
243
|
|
|
242
244
|
<!-- Step 3: Push Notifications -->
|
|
@@ -270,6 +272,7 @@ h1{color:#DA7756;font-size:22px;margin:0 0 4px;text-align:center}
|
|
|
270
272
|
var httpsUrl = ${JSON.stringify(httpsUrl)};
|
|
271
273
|
var httpUrl = ${JSON.stringify(httpUrl)};
|
|
272
274
|
var hasCert = ${hasCert ? 'true' : 'false'};
|
|
275
|
+
var lanMode = ${lanMode ? 'true' : 'false'};
|
|
273
276
|
var isHttps = location.protocol === "https:";
|
|
274
277
|
var ua = navigator.userAgent;
|
|
275
278
|
var isIOS = /iPhone|iPad|iPod/.test(ua);
|
|
@@ -326,10 +329,17 @@ if (isStandalone && localStorage.getItem("setup-pending")) {
|
|
|
326
329
|
|
|
327
330
|
function buildSteps(hasPushSub) {
|
|
328
331
|
steps = [];
|
|
329
|
-
if (!isTailscale && !isLocal) steps.push("tailscale");
|
|
332
|
+
if (!isTailscale && !isLocal && !lanMode) steps.push("tailscale");
|
|
330
333
|
if (hasCert && !isHttps) steps.push("cert");
|
|
331
|
-
if (
|
|
332
|
-
|
|
334
|
+
if (isAndroid) {
|
|
335
|
+
// Android: push first (works in browser), then PWA as optional
|
|
336
|
+
if ((isHttps || isLocal) && !hasPushSub) steps.push("push");
|
|
337
|
+
if (!isStandalone) steps.push("pwa");
|
|
338
|
+
} else {
|
|
339
|
+
// iOS: PWA required for push, so install first
|
|
340
|
+
if (!isStandalone) steps.push("pwa");
|
|
341
|
+
if ((isHttps || isLocal) && !hasPushSub) steps.push("push");
|
|
342
|
+
}
|
|
333
343
|
steps.push("done");
|
|
334
344
|
|
|
335
345
|
// Trigger HTTPS check now that steps are built
|
|
@@ -350,6 +360,14 @@ function buildSteps(hasPushSub) {
|
|
|
350
360
|
localStorage.setItem("setup-pending", String(stepsBeforePwa + 1));
|
|
351
361
|
}
|
|
352
362
|
|
|
363
|
+
// Android: PWA is optional, show skip button and update text
|
|
364
|
+
if (isAndroid && steps.indexOf("pwa") !== -1) {
|
|
365
|
+
var pwaSkip = document.getElementById("pwa-skip");
|
|
366
|
+
var pwaStatus = document.getElementById("pwa-status");
|
|
367
|
+
if (pwaSkip) pwaSkip.style.display = "block";
|
|
368
|
+
if (pwaStatus) pwaStatus.textContent = "Optional: install for quick access and full-screen experience.";
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
// Push: show warning if not on HTTPS
|
|
354
372
|
if (!isHttps && !isLocal) {
|
|
355
373
|
pushBtn.style.display = "none";
|
|
@@ -393,7 +411,7 @@ function showStep(idx) {
|
|
|
393
411
|
function nextStep() {
|
|
394
412
|
// After cert step on HTTP, redirect to HTTPS for remaining steps
|
|
395
413
|
if (!isHttps && steps[currentStep] === "cert") {
|
|
396
|
-
location.replace(httpsUrl + "/setup");
|
|
414
|
+
location.replace(httpsUrl + "/setup" + (lanMode ? "?mode=lan" : ""));
|
|
397
415
|
return;
|
|
398
416
|
}
|
|
399
417
|
if (currentStep < steps.length - 1) showStep(currentStep + 1);
|
|
@@ -616,7 +634,7 @@ if (!isHttps && !isLocal) {
|
|
|
616
634
|
var ac = new AbortController();
|
|
617
635
|
setTimeout(function() { ac.abort(); }, 3000);
|
|
618
636
|
fetch(info.httpsUrl + "/info", { signal: ac.signal, mode: "no-cors" })
|
|
619
|
-
.then(function() { location.replace(info.httpsUrl + "/setup"); })
|
|
637
|
+
.then(function() { location.replace(info.httpsUrl + "/setup" + (lanMode ? "?mode=lan" : "")); })
|
|
620
638
|
.catch(function() { init(); });
|
|
621
639
|
}).catch(function() { init(); });
|
|
622
640
|
} else {
|
package/lib/project.js
CHANGED
|
@@ -478,7 +478,7 @@ function createProjectContext(opts) {
|
|
|
478
478
|
entries.push({
|
|
479
479
|
name: item.name,
|
|
480
480
|
type: item.isDirectory() ? "dir" : "file",
|
|
481
|
-
path: path.relative(cwd, path.join(fsDir, item.name)),
|
|
481
|
+
path: path.relative(cwd, path.join(fsDir, item.name)).split(path.sep).join("/"),
|
|
482
482
|
});
|
|
483
483
|
}
|
|
484
484
|
sendTo(ws, { type: "fs_list_result", path: msg.path || ".", entries: entries });
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -466,7 +466,7 @@ function createSDKBridge(opts) {
|
|
|
466
466
|
|
|
467
467
|
function permissionPushTitle(toolName, input) {
|
|
468
468
|
if (!input) return "Claude wants to use " + toolName;
|
|
469
|
-
var file = input.file_path ? input.file_path.split(
|
|
469
|
+
var file = input.file_path ? input.file_path.split(/[/\\]/).pop() : "";
|
|
470
470
|
switch (toolName) {
|
|
471
471
|
case "Bash": return "Claude wants to run a command";
|
|
472
472
|
case "Edit": return "Claude wants to edit " + (file || "a file");
|
|
@@ -487,7 +487,7 @@ function createSDKBridge(opts) {
|
|
|
487
487
|
if (toolName === "Bash" && input.command) {
|
|
488
488
|
text = input.command;
|
|
489
489
|
} else if (toolName === "Edit" && input.file_path) {
|
|
490
|
-
text = input.file_path.split(
|
|
490
|
+
text = input.file_path.split(/[/\\]/).pop() + ": " + (input.old_string || "").substring(0, 40) + " \u2192 " + (input.new_string || "").substring(0, 40);
|
|
491
491
|
} else if (toolName === "Write" && input.file_path) {
|
|
492
492
|
text = input.file_path;
|
|
493
493
|
} else if (input.file_path) {
|
package/lib/server.js
CHANGED
|
@@ -202,16 +202,17 @@ function createServer(opts) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
// Setup page
|
|
205
|
-
if (
|
|
205
|
+
if (fullUrl === "/setup" && req.method === "GET") {
|
|
206
206
|
var host = req.headers.host || "localhost";
|
|
207
207
|
var hostname = host.split(":")[0];
|
|
208
208
|
var protocol = tlsOptions ? "https" : "http";
|
|
209
209
|
var setupUrl = protocol + "://" + hostname + ":" + portNum;
|
|
210
|
+
var lanMode = /[?&]mode=lan/.test(req.url);
|
|
210
211
|
res.writeHead(200, {
|
|
211
212
|
"Content-Type": "text/html; charset=utf-8",
|
|
212
213
|
"Access-Control-Allow-Origin": "*",
|
|
213
214
|
});
|
|
214
|
-
res.end(setupPageHtml(setupUrl, setupUrl, !!caContent));
|
|
215
|
+
res.end(setupPageHtml(setupUrl, setupUrl, !!caContent, lanMode));
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
218
|
|
|
@@ -364,8 +365,9 @@ function createServer(opts) {
|
|
|
364
365
|
var hostname = host.split(":")[0];
|
|
365
366
|
var httpsSetupUrl = "https://" + hostname + ":" + portNum;
|
|
366
367
|
var httpSetupUrl = "http://" + hostname + ":" + (portNum + 1);
|
|
368
|
+
var lanMode = /[?&]mode=lan/.test(req.url);
|
|
367
369
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
368
|
-
res.end(setupPageHtml(httpsSetupUrl, httpSetupUrl, !!caContent));
|
|
370
|
+
res.end(setupPageHtml(httpsSetupUrl, httpSetupUrl, !!caContent, lanMode));
|
|
369
371
|
return;
|
|
370
372
|
}
|
|
371
373
|
|
package/lib/terminal.js
CHANGED
|
@@ -8,7 +8,8 @@ try {
|
|
|
8
8
|
function createTerminal(cwd, cols, rows) {
|
|
9
9
|
if (!pty) return null;
|
|
10
10
|
|
|
11
|
-
var shell = process.env.SHELL
|
|
11
|
+
var shell = process.env.SHELL
|
|
12
|
+
|| (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/bash");
|
|
12
13
|
var term = pty.spawn(shell, [], {
|
|
13
14
|
name: "xterm-256color",
|
|
14
15
|
cols: cols || 80,
|