claude-relay 2.2.2 → 2.2.3

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
@@ -312,6 +312,33 @@ function getLocalIP() {
312
312
  }
313
313
 
314
314
  // --- Certs ---
315
+ function isRoutableIP(addr) {
316
+ if (addr.startsWith("10.")) return true;
317
+ if (addr.startsWith("192.168.")) return true;
318
+ if (addr.startsWith("100.")) {
319
+ var second = parseInt(addr.split(".")[1], 10);
320
+ return second >= 64 && second <= 127; // CGNAT (Tailscale)
321
+ }
322
+ if (addr.startsWith("172.")) {
323
+ var second = parseInt(addr.split(".")[1], 10);
324
+ return second >= 16 && second <= 31;
325
+ }
326
+ return false;
327
+ }
328
+
329
+ function getAllIPs() {
330
+ var ips = [];
331
+ var ifaces = os.networkInterfaces();
332
+ for (var addrs of Object.values(ifaces)) {
333
+ for (var j = 0; j < addrs.length; j++) {
334
+ if (addrs[j].family === "IPv4" && !addrs[j].internal && isRoutableIP(addrs[j].address)) {
335
+ ips.push(addrs[j].address);
336
+ }
337
+ }
338
+ }
339
+ return ips;
340
+ }
341
+
315
342
  function ensureCerts(ip) {
316
343
  var homeDir = os.homedir();
317
344
  var certDir = path.join(homeDir, ".claude-relay", "certs");
@@ -336,21 +363,29 @@ function ensureCerts(ip) {
336
363
  if (!fs.existsSync(caRoot)) caRoot = null;
337
364
  } catch (e) {}
338
365
 
366
+ // Collect all IPv4 addresses (Tailscale + LAN)
367
+ var allIPs = getAllIPs();
368
+
339
369
  if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
340
370
  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
- }
371
+ try {
372
+ var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
373
+ for (var i = 0; i < allIPs.length; i++) {
374
+ if (certText.indexOf(allIPs[i]) === -1) {
375
+ needRegen = true;
376
+ break;
377
+ }
378
+ }
379
+ } catch (e) { needRegen = true; }
347
380
  if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
348
381
  }
349
382
 
350
383
  fs.mkdirSync(certDir, { recursive: true });
351
384
 
352
385
  var domains = ["localhost", "127.0.0.1", "::1"];
353
- if (ip && ip !== "localhost") domains.push(ip);
386
+ for (var i = 0; i < allIPs.length; i++) {
387
+ if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
388
+ }
354
389
 
355
390
  try {
356
391
  execSync(
@@ -1435,22 +1470,6 @@ function showSetupGuide(config, ip, goBack) {
1435
1470
  var wantRemote = false;
1436
1471
  var wantPush = false;
1437
1472
 
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
1473
  console.clear();
1455
1474
  printLogo();
1456
1475
  log("");
@@ -1564,10 +1583,19 @@ function showSetupGuide(config, ip, goBack) {
1564
1583
 
1565
1584
  function showSetupQR() {
1566
1585
  var tsIP = getTailscaleIP();
1586
+ var lanIP = null;
1587
+ if (!wantRemote) {
1588
+ var allIPs = getAllIPs();
1589
+ for (var j = 0; j < allIPs.length; j++) {
1590
+ if (!allIPs[j].startsWith("100.")) { lanIP = allIPs[j]; break; }
1591
+ }
1592
+ }
1593
+ var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
1594
+ var setupQuery = wantRemote ? "" : "?mode=lan";
1567
1595
  // Always use HTTP onboarding URL for QR/setup when TLS is active
1568
1596
  var setupUrl = config.tls
1569
- ? "http://" + (tsIP || ip) + ":" + (config.port + 1) + "/setup"
1570
- : "http://" + (tsIP || ip) + ":" + config.port + "/setup";
1597
+ ? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
1598
+ : "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
1571
1599
  log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
1572
1600
  log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
1573
1601
  log(sym.bar + " " + a.bold + setupUrl + a.reset);
@@ -1576,7 +1604,7 @@ function showSetupGuide(config, ip, goBack) {
1576
1604
  var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
1577
1605
  console.log(lines);
1578
1606
  log(sym.bar);
1579
- if (tsIP) {
1607
+ if (wantRemote) {
1580
1608
  log(sym.bar + " " + a.dim + "Can't connect? Make sure Tailscale is installed on your phone too." + a.reset);
1581
1609
  } else {
1582
1610
  log(sym.bar + " " + a.dim + "Can't connect? Your phone must be on the same Wi-Fi network." + a.reset);
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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"