claude-relay 1.2.8 → 1.4.0

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 CHANGED
@@ -1,120 +1,121 @@
1
1
  # claude-relay
2
2
 
3
- Run `npx claude-relay` in any directory. Access Claude Code on that directory from any device.
3
+ Claude Code on your phone with push notifications. One command, zero install.
4
4
 
5
5
  ![claude-relay demo](screenshot.gif)
6
6
 
7
+ You start a long task in Claude Code. You step away. Claude needs permission to edit a file. It waits. You come back 30 minutes later to a stalled session.
8
+
9
+ **claude-relay fixes this.** Run `npx claude-relay` and your phone gets push notifications when Claude needs you. Tap to approve. Claude keeps working. You keep living.
10
+
7
11
  ```
8
- $ cd ~/my-project
9
- $ npx claude-relay
10
-
11
- ◆ Claude Relay
12
-
13
- ▲ READ BEFORE CONTINUING
14
-
15
- │ Anyone with access to the URL gets full Claude Code access
16
- │ to this machine, including reading, writing, and executing
17
- │ files with your user permissions.
18
-
19
- ◆ PIN protection
20
- │ 6-digit PIN, or Enter to skip
21
- │ ●●●●●●
22
- ◇ PIN protection · Enabled
23
-
24
- ◆ Keep awake
25
- ◇ Keep awake · Yes
26
-
27
- └ Starting relay...
28
-
29
- Claude Relay running at http://100.64.1.5:2633
30
- my-project · /Users/you/my-project
12
+ npx claude-relay
31
13
  ```
32
14
 
33
- ## Why?
34
-
35
- You can use Claude Code from the Claude app, but it requires a GitHub repo, runs in a sandboxed VM, and comes with limitations. No local tools, no custom skills, no access to your actual dev environment.
15
+ No app to install. No cloud server. No account to create. Your data stays on your machine.
36
16
 
37
- **claude-relay** gives you the real thing. One command in any directory and you get full Claude Code on **your machine**, from any device. Your files, your tools, your skills, your environment. No GitHub required, no sandbox.
17
+ ## Use Claude Code from your phone
38
18
 
39
- ## How it works
19
+ claude-relay runs on your machine and connects Claude Code to a web UI over WebSocket. Open the URL on your phone, add it to your home screen, and you get push notifications whenever Claude needs input.
40
20
 
41
- claude-relay uses the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and bridges it to a web UI over WebSocket. Your browser talks to the relay, the relay talks to Claude Code. Sessions persist across reconnects.
21
+ Sessions are real-time synced across all connected devices. Type on your PC, see it on your phone. Approve on your phone, see it on your PC. Everything is live.
42
22
 
43
23
  ```
44
- Browser (any device) <--> claude-relay (your machine) <--> Claude Agent SDK
45
- WebSocket HTTP/HTTPS + WS
24
+ Your phone/tablet <--> claude-relay (your machine) <--> Claude Code
25
+ browser WebSocket + HTTPS
46
26
  ```
47
27
 
48
- ## Features
28
+ ## Push notifications for Claude Code
49
29
 
50
- - **One command** `npx claude-relay` and you're live
51
- - **Mobile-first UI** — designed for phones and tablets, works everywhere
52
- - **HTTPS** — automatic TLS via [mkcert](https://github.com/FiloSottile/mkcert), enabled by default
53
- - **PIN protection** — optional 6-digit PIN set at startup
54
- - **Permission control** — tool approval relayed to the browser (Allow / Allow for Session / Deny)
55
- - **Multi-session** — run multiple Claude Code sessions, switch between them
56
- - **Multi-device sync** — input, messages, and state sync across connected devices in real-time
57
- - **Streaming** — real-time token streaming, tool execution, thinking blocks
58
- - **Session persistence** — sessions survive server restarts and reconnects
59
- - **Tailscale-aware** — prefers Tailscale IP for secure remote access
60
- - **Slash commands** — full slash command support with autocomplete
61
- - **Keep awake** — optional macOS caffeinate to prevent sleep while running
62
- - **Zero config** — no API keys, no setup. Uses your local `claude` installation
30
+ Get notified on your phone when Claude needs approval, finishes a task, or hits an error. Works even when the browser is closed. Tap the notification to jump straight in.
63
31
 
64
- ## Requirements
32
+ No app required. Add to your home screen and notifications work like a native app (PWA). The built-in setup wizard walks you through it in 3 steps.
65
33
 
66
- - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
67
- - Node.js 18+
68
- - [mkcert](https://github.com/FiloSottile/mkcert) (optional, for HTTPS)
34
+ ## Approve Claude Code permissions remotely
69
35
 
70
- ## Install
36
+ Kick off a refactoring task, go make coffee. Your phone buzzes: "Claude wants to edit `src/auth.ts`". Tap approve. Claude continues. No need to walk back to your desk.
71
37
 
72
- ```bash
73
- # Run directly (no install needed)
74
- npx claude-relay@latest
75
- ```
38
+ Running tests, migrations, or multi-file changes? Watch the progress from your phone or tablet without staying at your desk.
39
+
40
+ ## Claude Code on iPad and tablets
41
+
42
+ Full Claude Code access from any browser. No SSH terminal app, no GitHub repo required, no sandboxed VM. Your actual dev environment, your tools, your MCP servers, your CLAUDE.md, your files.
43
+
44
+ ## Session handoff between CLI and browser
45
+
46
+ Start a session in the terminal. Pick it up on your phone. Hand it back to the terminal. Sessions survive server restarts, browser closes, and reconnects. Your conversation is never lost.
47
+
48
+ ## Run Claude Code remotely with Tailscale
49
+
50
+ To access Claude Code from outside your local network, use [Tailscale](https://tailscale.com). Install it on your machine and your phone, sign in with the same account, and you are connected. claude-relay detects Tailscale automatically.
76
51
 
77
- ## Usage
52
+ Tailscale creates a private encrypted tunnel between your devices. No port forwarding, no cloud relay, no data leaving your control.
53
+
54
+ ## Features
55
+
56
+ - **Push notifications** on permission requests, task completion, and errors
57
+ - **Real-time sync** across all connected devices via WebSocket
58
+ - **Session persistence** across server restarts and reconnects
59
+ - **Mobile-first UI** with big tap targets for approve/deny
60
+ - **Setup wizard** guides you through Tailscale, HTTPS, and push setup
61
+ - **Multi-session** support with automatic port selection
62
+ - **PIN protection** for access control
63
+ - **HTTPS** via mkcert, automatic certificate generation
64
+ - **Slash commands** with autocomplete
65
+ - **Zero config** uses your local Claude Code installation as-is
66
+
67
+ ## Quick start
78
68
 
79
69
  ```bash
80
- # Start in current directory
70
+ # 1. Run in your project directory
81
71
  npx claude-relay
82
72
 
83
- # Custom port (default: 2633)
84
- npx claude-relay -p 8080
73
+ # 2. Scan the QR code with your phone
74
+ # or open the URL shown in the terminal
85
75
 
86
- # Disable HTTPS
87
- npx claude-relay --no-https
76
+ # 3. Press 's' for the setup wizard
77
+ # to enable push notifications and remote access
88
78
  ```
89
79
 
90
- Then open the URL on any device connected to the same network, or scan the QR code shown in the terminal.
80
+ ## HTTPS setup for push notifications
91
81
 
92
- ### HTTPS setup
93
-
94
- HTTPS is enabled by default when [mkcert](https://github.com/FiloSottile/mkcert) is installed. To set it up:
82
+ Push notifications require HTTPS. claude-relay supports automatic HTTPS via [mkcert](https://github.com/FiloSottile/mkcert):
95
83
 
96
84
  ```bash
97
85
  brew install mkcert
98
86
  mkcert -install
99
87
  ```
100
88
 
101
- claude-relay will automatically generate certificates on first run. If mkcert is not installed, it falls back to HTTP.
89
+ Certificates are generated automatically on first run. The setup wizard checks for mkcert and guides you if it is missing.
102
90
 
103
- ### Remote access with Tailscale
91
+ ## CLI options
104
92
 
105
- claude-relay automatically detects [Tailscale](https://tailscale.com) and uses your Tailscale IP. Install Tailscale on your machine and phone, and you can access Claude Code from anywhere.
93
+ ```bash
94
+ npx claude-relay # Start with defaults
95
+ npx claude-relay -p 8080 # Custom port (default: 2633)
96
+ npx claude-relay --no-https # Disable HTTPS
97
+ npx claude-relay --no-update # Skip auto-update check
98
+ npx claude-relay --debug # Enable debug panel
99
+ ```
106
100
 
107
- ## Issues
101
+ ## Requirements
108
102
 
109
- Found a bug or have a feature request? [Open an issue](https://github.com/chadbyte/claude-relay/issues).
103
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
104
+ - Node.js 18+
105
+ - [mkcert](https://github.com/FiloSottile/mkcert) (for HTTPS and push notifications)
106
+ - [Tailscale](https://tailscale.com) (for remote access outside your network)
110
107
 
111
108
  ## Security
112
109
 
113
- **Anyone with access to the URL gets full Claude Code access to your machine**, including reading, writing, and executing files with your user permissions. PIN protection adds a layer of access control, but is not a substitute for network-level security.
110
+ **Anyone with access to the URL gets full Claude Code access to your machine**, including reading, writing, and executing files with your user permissions.
114
111
 
115
- We strongly recommend using a private network layer such as [Tailscale](https://tailscale.com), WireGuard, or a VPN. claude-relay automatically detects Tailscale and prefers its IP for this reason.
112
+ Use a private network. We strongly recommend [Tailscale](https://tailscale.com), WireGuard, or a VPN. PIN protection adds a layer of access control but is not a substitute for network-level security. Do not expose claude-relay to the public internet.
116
113
 
117
- If you choose to expose it beyond your private network, that's your call. **Entirely at your own risk.** The authors assume no responsibility for any damage, data loss, or security incidents.
114
+ **Entirely at your own risk.** The authors assume no responsibility for any damage, data loss, or security incidents.
115
+
116
+ ## Issues
117
+
118
+ Found a bug or have a feature request? [Open an issue](https://github.com/chadbyte/claude-relay/issues).
118
119
 
119
120
  ## Disclaimer
120
121
 
package/bin/cli.js CHANGED
@@ -11,6 +11,7 @@ const args = process.argv.slice(2);
11
11
  let port = 2633;
12
12
  let useHttps = true;
13
13
  let skipUpdate = false;
14
+ let debugMode = false;
14
15
 
15
16
  for (let i = 0; i < args.length; i++) {
16
17
  if (args[i] === "-p" || args[i] === "--port") {
@@ -24,13 +25,16 @@ for (let i = 0; i < args.length; i++) {
24
25
  useHttps = false;
25
26
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
26
27
  skipUpdate = true;
28
+ } else if (args[i] === "--debug") {
29
+ debugMode = true;
27
30
  } else if (args[i] === "-h" || args[i] === "--help") {
28
- console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update]");
31
+ console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug]");
29
32
  console.log("");
30
33
  console.log("Options:");
31
34
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
32
35
  console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
33
36
  console.log(" --no-update Skip auto-update check on startup");
37
+ console.log(" --debug Enable debug panel in the web UI");
34
38
  process.exit(0);
35
39
  }
36
40
  }
@@ -305,8 +309,276 @@ function promptToggle(title, desc, defaultValue, callback) {
305
309
  });
306
310
  }
307
311
 
312
+ // --- Port availability check ---
313
+ var net = require("net");
314
+
315
+ function isPortFree(p) {
316
+ return new Promise(function (resolve) {
317
+ var srv = net.createServer();
318
+ srv.once("error", function () { resolve(false); });
319
+ srv.once("listening", function () { srv.close(function () { resolve(true); }); });
320
+ srv.listen(p);
321
+ });
322
+ }
323
+
324
+ async function findAvailablePort(startPort) {
325
+ var p = startPort;
326
+ var maxAttempts = 20;
327
+ for (var i = 0; i < maxAttempts; i++) {
328
+ var httpFree = await isPortFree(p);
329
+ var httpsFree = await isPortFree(p + 1);
330
+ if (httpFree && httpsFree) return p;
331
+ p += 2;
332
+ }
333
+ return null;
334
+ }
335
+
336
+ // --- Detect tools ---
337
+ function getTailscaleIP() {
338
+ var interfaces = os.networkInterfaces();
339
+ for (var name in interfaces) {
340
+ if (/^(tailscale|utun)/.test(name)) {
341
+ for (var i = 0; i < interfaces[name].length; i++) {
342
+ var addr = interfaces[name][i];
343
+ if (addr.family === "IPv4" && !addr.internal && addr.address.startsWith("100.")) {
344
+ return addr.address;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ for (var addrs of Object.values(interfaces)) {
350
+ for (var j = 0; j < addrs.length; j++) {
351
+ if (addrs[j].family === "IPv4" && !addrs[j].internal && addrs[j].address.startsWith("100.")) {
352
+ return addrs[j].address;
353
+ }
354
+ }
355
+ }
356
+ return null;
357
+ }
358
+
359
+ function hasTailscale() {
360
+ return getTailscaleIP() !== null;
361
+ }
362
+
363
+ function hasMkcert() {
364
+ try {
365
+ execSync("mkcert -CAROOT", { stdio: "pipe", encoding: "utf8" });
366
+ return true;
367
+ } catch (e) { return false; }
368
+ }
369
+
370
+ // --- Re-check / back key listener ---
371
+ function listenForKey(keys, callback) {
372
+ if (!process.stdin.isTTY) return;
373
+ process.stdin.setRawMode(true);
374
+ process.stdin.resume();
375
+ process.stdin.setEncoding("utf8");
376
+
377
+ var handler = function (ch) {
378
+ var lower = ch.toLowerCase();
379
+ if (ch === "\x03") { process.exit(0); return; }
380
+ if (keys[lower]) {
381
+ process.stdin.setRawMode(false);
382
+ process.stdin.pause();
383
+ process.stdin.removeListener("data", handler);
384
+ keys[lower]();
385
+ }
386
+ };
387
+ process.stdin.on("data", handler);
388
+ }
389
+
390
+ // --- Post-startup setup guide ---
391
+ function showSetupGuide(serverIP, httpPort, httpsPort, showMainView) {
392
+ var wantRemote = false;
393
+ var wantPush = false;
394
+
395
+ function redraw(renderFn) {
396
+ console.clear();
397
+ printLogo();
398
+ log("");
399
+ log(sym.pointer + " " + a.bold + "Setup Guide" + a.reset);
400
+ log(sym.bar);
401
+ if (wantRemote) log(sym.done + " Access from outside your network? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
402
+ else log(sym.done + " Access from outside your network? " + a.dim + "· No" + a.reset);
403
+ log(sym.bar);
404
+ if (wantPush) log(sym.done + " Want push notifications? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
405
+ else log(sym.done + " Want push notifications? " + a.dim + "· No" + a.reset);
406
+ log(sym.bar);
407
+ renderFn();
408
+ }
409
+
410
+ log("");
411
+ log(sym.pointer + " " + a.bold + "Setup Guide" + a.reset);
412
+ log(sym.bar);
413
+
414
+ promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
415
+ wantRemote = remote;
416
+ log(sym.bar);
417
+ promptToggle("Want push notifications?", "Requires HTTPS (mkcert certificate)", false, function (push) {
418
+ wantPush = push;
419
+ log(sym.bar);
420
+ afterToggles();
421
+ });
422
+ });
423
+
424
+ function showSetupQR() {
425
+ var tsIP = getTailscaleIP();
426
+ var setupUrl = "http://" + (tsIP || serverIP) + ":" + httpPort + "/setup";
427
+ log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
428
+ log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
429
+ log(sym.bar + " " + a.bold + setupUrl + a.reset);
430
+ log(sym.bar);
431
+ qrcode.generate(setupUrl, { small: true }, function (code) {
432
+ var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
433
+ console.log(lines);
434
+ log(sym.bar);
435
+ log(sym.bar + " " + a.dim + "Can't connect?" + a.reset);
436
+ if (tsIP) {
437
+ log(sym.bar + " " + a.dim + "Make sure Tailscale is installed on your phone too." + a.reset);
438
+ } else {
439
+ log(sym.bar + " " + a.dim + "Your phone must be on the same Wi-Fi network." + a.reset);
440
+ }
441
+ log(sym.bar);
442
+ log(sym.done + " " + a.dim + "Server setup complete." + a.reset);
443
+ log(sym.end);
444
+ log("");
445
+ listenForBackKey(showMainView);
446
+ });
447
+ }
448
+
449
+ function afterToggles() {
450
+ if (!wantRemote && !wantPush) {
451
+ log(sym.done + " " + a.green + "All set!" + a.reset + a.dim + " · No additional setup needed." + a.reset);
452
+ log(sym.end);
453
+ log("");
454
+ listenForBackKey(showMainView);
455
+ return;
456
+ }
457
+ if (wantRemote) {
458
+ renderTailscale();
459
+ } else {
460
+ renderHttps();
461
+ }
462
+ }
463
+
464
+ function renderTailscale() {
465
+ var tsReady = hasTailscale();
466
+ var tsIP = tsReady ? getTailscaleIP() : null;
467
+
468
+ log(sym.pointer + " " + a.bold + "Tailscale Setup" + a.reset);
469
+ if (tsReady && tsIP) {
470
+ log(sym.bar + " " + a.green + "Tailscale is running" + a.reset + a.dim + " · " + tsIP + a.reset);
471
+ log(sym.bar);
472
+ log(sym.bar + " On your phone/tablet:");
473
+ log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
474
+ log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
475
+ log(sym.bar);
476
+ renderHttps();
477
+ } else if (tsReady) {
478
+ log(sym.bar + " " + a.yellow + "Tailscale is installed but no IP found." + a.reset);
479
+ log(sym.bar + " " + a.dim + "Run: tailscale up" + a.reset);
480
+ log(sym.bar);
481
+ log(sym.bar + " " + a.dim + "Press " + a.reset + "r" + a.dim + " to re-check, " + a.reset + "h" + a.dim + " to go back." + a.reset);
482
+ log(sym.end);
483
+ log("");
484
+ listenForKey({ r: function () { redraw(renderTailscale); }, h: showMainView });
485
+ } else {
486
+ log(sym.bar + " " + a.yellow + "Tailscale not found on this machine." + a.reset);
487
+ log(sym.bar + " " + a.dim + "Install: https://tailscale.com/download" + a.reset);
488
+ log(sym.bar + " " + a.dim + "Then run: tailscale up" + a.reset);
489
+ log(sym.bar);
490
+ log(sym.bar + " On your phone/tablet:");
491
+ log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
492
+ log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
493
+ log(sym.bar);
494
+ log(sym.bar + " " + a.dim + "Press " + a.reset + "r" + a.dim + " to re-check, " + a.reset + "h" + a.dim + " to go back." + a.reset);
495
+ log(sym.end);
496
+ log("");
497
+ listenForKey({ r: function () { redraw(renderTailscale); }, h: showMainView });
498
+ }
499
+ }
500
+
501
+ function renderHttps() {
502
+ if (!wantPush) {
503
+ showSetupQR();
504
+ return;
505
+ }
506
+
507
+ var mcReady = hasMkcert();
508
+ var tsIP = getTailscaleIP();
509
+ log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
510
+ if (mcReady) {
511
+ log(sym.bar + " " + a.green + "mkcert is installed" + a.reset);
512
+ log(sym.bar);
513
+ showSetupQR();
514
+ } else {
515
+ log(sym.bar + " " + a.yellow + "mkcert not found." + a.reset);
516
+ log(sym.bar + " " + a.dim + "Install: brew install mkcert && mkcert -install" + a.reset);
517
+ log(sym.bar);
518
+ log(sym.bar + " " + a.dim + "Press " + a.reset + "r" + a.dim + " to re-check, " + a.reset + "h" + a.dim + " to go back." + a.reset);
519
+ log(sym.end);
520
+ log("");
521
+ listenForKey({ r: function () { redraw(renderHttps); }, h: showMainView });
522
+ }
523
+ }
524
+ }
525
+
526
+ function listenForSetupKey(serverIP, httpPort, httpsPort, showMainView) {
527
+ if (!process.stdin.isTTY) return;
528
+ var bc = a.dim;
529
+ var rc = a.reset;
530
+ var msg1 = "Access from your phone or get notified when Claude is done?";
531
+ var msg2 = "Press " + a.cyan + a.bold + "s" + rc + " to set up.";
532
+ var w = msg1.length + 4;
533
+ var pad2 = " ".repeat(msg1.length - 18);
534
+ log(bc + "┌" + "─".repeat(w) + "┐" + rc);
535
+ log(bc + "│ " + rc + msg1 + bc + " │" + rc);
536
+ log(bc + "│ " + rc + msg2 + pad2 + bc + " │" + rc);
537
+ log(bc + "└" + "─".repeat(w) + "┘" + rc);
538
+ log("");
539
+
540
+ process.stdin.setRawMode(true);
541
+ process.stdin.resume();
542
+ process.stdin.setEncoding("utf8");
543
+
544
+ process.stdin.on("data", function onKey(ch) {
545
+ if (ch === "s" || ch === "S") {
546
+ process.stdin.setRawMode(false);
547
+ process.stdin.pause();
548
+ process.stdin.removeListener("data", onKey);
549
+ console.clear();
550
+ printLogo();
551
+ log("");
552
+ showSetupGuide(serverIP, httpPort, httpsPort, showMainView);
553
+ } else if (ch === "\x03") {
554
+ process.exit(0);
555
+ }
556
+ });
557
+ }
558
+
559
+ function listenForBackKey(showMainView) {
560
+ if (!process.stdin.isTTY) return;
561
+ log(a.dim + "Press " + a.reset + "h" + a.dim + " to go back." + a.reset);
562
+ log("");
563
+
564
+ process.stdin.setRawMode(true);
565
+ process.stdin.resume();
566
+ process.stdin.setEncoding("utf8");
567
+
568
+ process.stdin.on("data", function onBack(ch) {
569
+ if (ch === "h" || ch === "H") {
570
+ process.stdin.setRawMode(false);
571
+ process.stdin.pause();
572
+ process.stdin.removeListener("data", onBack);
573
+ showMainView();
574
+ } else if (ch === "\x03") {
575
+ process.exit(0);
576
+ }
577
+ });
578
+ }
579
+
308
580
  // --- Server start ---
309
- function start(pin) {
581
+ async function start(pin) {
310
582
  var ip = getLocalIP();
311
583
  var tlsOptions = null;
312
584
  var caRoot = null;
@@ -326,37 +598,46 @@ function start(pin) {
326
598
  }
327
599
  }
328
600
 
329
- var result = createServer(cwd, tlsOptions, caRoot, pin, port);
601
+ var actualPort = await findAvailablePort(port);
602
+ if (actualPort === null) {
603
+ log(a.red + "No available port found (tried " + port + " to " + (port + 38) + ")." + a.reset);
604
+ process.exit(1);
605
+ return;
606
+ }
607
+ if (actualPort !== port) {
608
+ log(sym.warn + " " + a.yellow + "Port " + port + " in use" + a.reset + a.dim + " · using " + actualPort + a.reset);
609
+ log(sym.bar);
610
+ }
611
+ port = actualPort;
612
+
613
+ var result = createServer(cwd, tlsOptions, caRoot, pin, port, debugMode);
330
614
  var entryServer = result.entryServer;
331
615
  var httpsServer = result.httpsServer;
332
616
 
333
617
  entryServer.on("error", function (err) {
334
- if (err.code === "EADDRINUSE") {
335
- log(a.red + "Port " + port + " is already in use." + a.reset);
336
- log(a.dim + "Run: claude-relay -p <port>" + a.reset);
337
- } else {
338
- log(a.red + "Server error: " + err.message + a.reset);
339
- }
618
+ log(a.red + "Server error: " + err.message + a.reset);
340
619
  process.exit(1);
341
620
  });
342
621
 
343
622
  var httpsPort = port + 1;
344
623
  if (httpsServer) {
345
624
  httpsServer.on("error", function (err) {
346
- if (err.code === "EADDRINUSE") {
347
- log(a.red + "HTTPS port " + httpsPort + " is already in use." + a.reset);
348
- } else {
349
- log(a.red + "HTTPS error: " + err.message + a.reset);
350
- }
625
+ log(a.red + "HTTPS error: " + err.message + a.reset);
351
626
  process.exit(1);
352
627
  });
353
628
  httpsServer.listen(httpsPort);
354
629
  }
355
630
 
356
- entryServer.listen(port, function () {
631
+ var hPort = httpsServer ? httpsPort : null;
632
+
633
+ function showMainView() {
357
634
  var project = path.basename(cwd);
358
635
  var url = "http://" + ip + ":" + port;
359
636
 
637
+ console.clear();
638
+ printLogo();
639
+ log("");
640
+
360
641
  if (ip !== "localhost") {
361
642
  qrcode.generate(url, { small: true }, function (code) {
362
643
  var lines = code.split("\n").map(function (l) { return " " + l; }).join("\n");
@@ -365,13 +646,17 @@ function start(pin) {
365
646
  log(a.bold + "Claude Relay" + a.reset + " running at " + a.bold + url + a.reset);
366
647
  log(a.dim + project + " · " + cwd + a.reset);
367
648
  log("");
649
+ listenForSetupKey(ip, port, hPort, showMainView);
368
650
  });
369
651
  } else {
370
652
  log(a.bold + "Claude Relay" + a.reset + " running at " + a.bold + url + a.reset);
371
653
  log(a.dim + project + " · " + cwd + a.reset);
372
654
  log("");
655
+ listenForSetupKey(ip, port, hPort, showMainView);
373
656
  }
374
- });
657
+ }
658
+
659
+ entryServer.listen(port, showMainView);
375
660
  }
376
661
 
377
662
  const { checkAndUpdate } = require("../lib/updater");