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 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
- if (ip && ip !== "localhost") {
342
- try {
343
- var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
344
- if (certText.indexOf(ip) === -1) needRegen = true;
345
- } catch (e) {}
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
- if (ip && ip !== "localhost") domains.push(ip);
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
- execSync(
357
- "mkcert -key-file " + keyPath + " -cert-file " + certPath + " " + domains.join(" "),
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
- promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
995
- callback(pin, keepAwake);
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
- try {
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
- try {
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
- log(sym.bar + " " + a.dim + "Install: " + a.reset + "brew install mkcert && mkcert -install");
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://" + (tsIP || ip) + ":" + (config.port + 1) + "/setup"
1570
- : "http://" + (tsIP || ip) + ":" + config.port + "/setup";
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 (tsIP) {
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
- log(sym.bar + " Keep awake " + awakeStatus);
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
- items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
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
- // Also check socket exists
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
- try { fs.unlinkSync(socketPath()); } catch (e) {}
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
- try { fs.unlinkSync(sockPath); } catch (e) {}
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
- try { fs.unlinkSync(sockPath); } catch (e) {}
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 (!isStandalone) steps.push("pwa");
332
- if ((isHttps || isLocal) && !hasPushSub) steps.push("push");
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("/").pop() : "";
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("/").pop() + ": " + (input.old_string || "").substring(0, 40) + " \u2192 " + (input.new_string || "").substring(0, 40);
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 (req.url === "/setup" && req.method === "GET") {
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 || "/bin/bash";
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"