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 +74 -73
- package/bin/cli.js +301 -16
- package/lib/public/app.js +1078 -114
- package/lib/public/index.html +74 -22
- package/lib/public/manifest.json +11 -1
- package/lib/public/style.css +768 -126
- package/lib/public/sw.js +62 -0
- package/lib/push.js +103 -0
- package/lib/server.js +1010 -128
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,120 +1,121 @@
|
|
|
1
1
|
# claude-relay
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Claude Code on your phone with push notifications. One command, zero install.
|
|
4
4
|
|
|
5
5
|

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
## Use Claude Code from your phone
|
|
38
18
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
24
|
+
Your phone/tablet <--> claude-relay (your machine) <--> Claude Code
|
|
25
|
+
browser WebSocket + HTTPS
|
|
46
26
|
```
|
|
47
27
|
|
|
48
|
-
##
|
|
28
|
+
## Push notifications for Claude Code
|
|
49
29
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
- Node.js 18+
|
|
68
|
-
- [mkcert](https://github.com/FiloSottile/mkcert) (optional, for HTTPS)
|
|
34
|
+
## Approve Claude Code permissions remotely
|
|
69
35
|
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
70
|
+
# 1. Run in your project directory
|
|
81
71
|
npx claude-relay
|
|
82
72
|
|
|
83
|
-
#
|
|
84
|
-
|
|
73
|
+
# 2. Scan the QR code with your phone
|
|
74
|
+
# or open the URL shown in the terminal
|
|
85
75
|
|
|
86
|
-
#
|
|
87
|
-
|
|
76
|
+
# 3. Press 's' for the setup wizard
|
|
77
|
+
# to enable push notifications and remote access
|
|
88
78
|
```
|
|
89
79
|
|
|
90
|
-
|
|
80
|
+
## HTTPS setup for push notifications
|
|
91
81
|
|
|
92
|
-
|
|
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
|
-
|
|
89
|
+
Certificates are generated automatically on first run. The setup wizard checks for mkcert and guides you if it is missing.
|
|
102
90
|
|
|
103
|
-
|
|
91
|
+
## CLI options
|
|
104
92
|
|
|
105
|
-
|
|
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
|
-
##
|
|
101
|
+
## Requirements
|
|
108
102
|
|
|
109
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|