claude-relay 2.2.4 → 2.3.1
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 +13 -0
- package/bin/cli.js +144 -6
- package/lib/config.js +34 -2
- package/lib/daemon.js +54 -2
- package/lib/pages.js +22 -1
- package/lib/project.js +312 -26
- package/lib/public/app.js +339 -18
- package/lib/public/css/base.css +5 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +571 -0
- package/lib/public/css/input.css +3 -0
- package/lib/public/css/menus.css +89 -5
- package/lib/public/css/messages.css +89 -50
- package/lib/public/css/overlays.css +40 -0
- package/lib/public/index.html +102 -19
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/filebrowser.js +1023 -11
- package/lib/public/modules/input.js +96 -2
- package/lib/public/modules/notifications.js +29 -3
- package/lib/public/modules/qrcode.js +11 -2
- package/lib/public/modules/rewind.js +51 -2
- package/lib/public/modules/terminal.js +73 -0
- package/lib/public/modules/tools.js +45 -104
- package/lib/public/modules/utils.js +10 -2
- package/lib/public/style.css +1 -0
- package/lib/public/sw.js +21 -7
- package/lib/push.js +5 -1
- package/lib/sdk-bridge.js +38 -5
- package/lib/server.js +41 -7
- package/lib/sessions.js +14 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ Scan the QR code with your phone to connect instantly, or open the URL displayed
|
|
|
133
133
|
* **Send While Processing** - Queue messages without waiting for the current response to finish.
|
|
134
134
|
* **Sticky Todo Overlay** - TodoWrite tasks float as a progress bar while you scroll through the conversation.
|
|
135
135
|
* **Scroll Position Hold** - Reading earlier messages will not get interrupted by new content arriving.
|
|
136
|
+
* **RTL Text Support** - Automatic bidirectional text rendering for Arabic, Hebrew, and other RTL languages.
|
|
136
137
|
|
|
137
138
|
**Server and Security**
|
|
138
139
|
|
|
@@ -182,6 +183,9 @@ If push registration fails: check whether your browser trusts HTTPS and whether
|
|
|
182
183
|
```bash
|
|
183
184
|
npx claude-relay # Default (port 2633)
|
|
184
185
|
npx claude-relay -p 8080 # Specify port
|
|
186
|
+
npx claude-relay -y # Skip interactive prompts (accept defaults)
|
|
187
|
+
npx claude-relay -y --pin 123456
|
|
188
|
+
# Non-interactive with PIN (for scripts/CI)
|
|
185
189
|
npx claude-relay --no-https # Disable HTTPS
|
|
186
190
|
npx claude-relay --no-update # Skip update check
|
|
187
191
|
npx claude-relay --debug # Enable debug panel
|
|
@@ -189,6 +193,9 @@ npx claude-relay --add . # Add current directory to running daemon
|
|
|
189
193
|
npx claude-relay --add /path # Add a project by path
|
|
190
194
|
npx claude-relay --remove . # Remove a project
|
|
191
195
|
npx claude-relay --list # List registered projects
|
|
196
|
+
npx claude-relay --shutdown # Stop the running daemon
|
|
197
|
+
npx claude-relay --dangerously-skip-permissions
|
|
198
|
+
# Bypass all permission prompts (PIN required during setup)
|
|
192
199
|
```
|
|
193
200
|
|
|
194
201
|
## Requirements
|
|
@@ -236,6 +243,12 @@ For a detailed sequence diagram, daemon structure, and design decisions, refer t
|
|
|
236
243
|
|
|
237
244
|
---
|
|
238
245
|
|
|
246
|
+
## Contributors
|
|
247
|
+
|
|
248
|
+
<a href="https://github.com/chadbyte/claude-relay/graphs/contributors">
|
|
249
|
+
<img src="https://contrib.rocks/image?repo=chadbyte/claude-relay" />
|
|
250
|
+
</a>
|
|
251
|
+
|
|
239
252
|
## Contributing
|
|
240
253
|
|
|
241
254
|
Bug fixes and typos are welcome. For feature suggestions, please open an issue first:
|
package/bin/cli.js
CHANGED
|
@@ -6,7 +6,14 @@ var path = require("path");
|
|
|
6
6
|
var { execSync, execFileSync, spawn } = require("child_process");
|
|
7
7
|
var qrcode = require("qrcode-terminal");
|
|
8
8
|
var net = require("net");
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// Detect dev mode before loading config (env must be set before require)
|
|
11
|
+
var _isDev = process.argv[1] && path.basename(process.argv[1]) === "claude-relay-dev";
|
|
12
|
+
if (_isDev) {
|
|
13
|
+
process.env.CLAUDE_RELAY_HOME = path.join(os.homedir(), ".claude-relay-dev");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
10
17
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
11
18
|
var { generateAuthToken } = require("../lib/server");
|
|
12
19
|
|
|
@@ -22,7 +29,7 @@ function openUrl(url) {
|
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
var args = process.argv.slice(2);
|
|
25
|
-
var port = 2633;
|
|
32
|
+
var port = _isDev ? 2635 : 2633;
|
|
26
33
|
var useHttps = true;
|
|
27
34
|
var skipUpdate = false;
|
|
28
35
|
var debugMode = false;
|
|
@@ -32,6 +39,7 @@ var shutdownMode = false;
|
|
|
32
39
|
var addPath = null;
|
|
33
40
|
var removePath = null;
|
|
34
41
|
var listMode = false;
|
|
42
|
+
var dangerouslySkipPermissions = false;
|
|
35
43
|
|
|
36
44
|
for (var i = 0; i < args.length; i++) {
|
|
37
45
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -62,6 +70,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
62
70
|
i++;
|
|
63
71
|
} else if (args[i] === "--list") {
|
|
64
72
|
listMode = true;
|
|
73
|
+
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
74
|
+
dangerouslySkipPermissions = true;
|
|
65
75
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
66
76
|
console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
|
|
67
77
|
console.log(" claude-relay --add <path> Add a project to the running daemon");
|
|
@@ -79,6 +89,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
79
89
|
console.log(" --add <path> Add a project directory (use '.' for current)");
|
|
80
90
|
console.log(" --remove <path> Remove a project directory");
|
|
81
91
|
console.log(" --list List all registered projects");
|
|
92
|
+
console.log(" --dangerously-skip-permissions");
|
|
93
|
+
console.log(" Bypass all permission prompts (requires --pin)");
|
|
82
94
|
process.exit(0);
|
|
83
95
|
}
|
|
84
96
|
}
|
|
@@ -270,6 +282,10 @@ function stopDaemonWatcher() {
|
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
|
|
285
|
+
var _restartAttempts = 0;
|
|
286
|
+
var MAX_RESTART_ATTEMPTS = 5;
|
|
287
|
+
var _restartBackoffStart = 0;
|
|
288
|
+
|
|
273
289
|
function onDaemonDied() {
|
|
274
290
|
stopDaemonWatcher();
|
|
275
291
|
// Clean up stdin in case a prompt is active
|
|
@@ -278,11 +294,117 @@ function onDaemonDied() {
|
|
|
278
294
|
process.stdin.pause();
|
|
279
295
|
process.stdin.removeAllListeners("data");
|
|
280
296
|
} catch (e) {}
|
|
297
|
+
|
|
298
|
+
// Check if this was a crash (crash.json exists) vs intentional shutdown
|
|
299
|
+
var crashInfo = readCrashInfo();
|
|
300
|
+
if (!crashInfo) {
|
|
301
|
+
// Intentional shutdown, no restart
|
|
302
|
+
log("");
|
|
303
|
+
log(sym.warn + " " + a.yellow + "Server has been shut down." + a.reset);
|
|
304
|
+
log(a.dim + " Run " + a.reset + "npx claude-relay" + a.dim + " to start again." + a.reset);
|
|
305
|
+
log("");
|
|
306
|
+
process.exit(0);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Reset backoff counter if enough time has passed since last restart burst
|
|
311
|
+
var now = Date.now();
|
|
312
|
+
if (_restartBackoffStart && now - _restartBackoffStart > 60000) {
|
|
313
|
+
_restartAttempts = 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_restartAttempts++;
|
|
317
|
+
if (_restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
318
|
+
log("");
|
|
319
|
+
log(sym.warn + " " + a.red + "Server crashed too many times (" + MAX_RESTART_ATTEMPTS + " attempts). Giving up." + a.reset);
|
|
320
|
+
if (crashInfo.reason) {
|
|
321
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
322
|
+
}
|
|
323
|
+
log(a.dim + " Check logs: " + a.reset + logPath());
|
|
324
|
+
log("");
|
|
325
|
+
process.exit(1);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (_restartAttempts === 1) _restartBackoffStart = now;
|
|
330
|
+
|
|
281
331
|
log("");
|
|
282
|
-
log(sym.warn + " " + a.yellow + "Server
|
|
283
|
-
|
|
332
|
+
log(sym.warn + " " + a.yellow + "Server crashed. Restarting... (" + _restartAttempts + "/" + MAX_RESTART_ATTEMPTS + ")" + a.reset);
|
|
333
|
+
if (crashInfo.reason) {
|
|
334
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Re-fork the daemon from saved config
|
|
338
|
+
restartDaemonFromConfig();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function restartDaemonFromConfig() {
|
|
342
|
+
var lastConfig = loadConfig();
|
|
343
|
+
if (!lastConfig || !lastConfig.projects) {
|
|
344
|
+
log(a.red + " No config found. Cannot restart." + a.reset);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
clearStaleConfig();
|
|
350
|
+
|
|
351
|
+
// Wait for port to be released
|
|
352
|
+
var targetPort = lastConfig.port || port;
|
|
353
|
+
var waited = 0;
|
|
354
|
+
while (waited < 3000) {
|
|
355
|
+
var free = await isPortFree(targetPort);
|
|
356
|
+
if (free) break;
|
|
357
|
+
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
358
|
+
waited += 300;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Rebuild config (preserve everything except pid)
|
|
362
|
+
var newConfig = {
|
|
363
|
+
pid: null,
|
|
364
|
+
port: targetPort,
|
|
365
|
+
pinHash: lastConfig.pinHash || null,
|
|
366
|
+
tls: lastConfig.tls !== undefined ? lastConfig.tls : useHttps,
|
|
367
|
+
debug: lastConfig.debug || false,
|
|
368
|
+
keepAwake: lastConfig.keepAwake || false,
|
|
369
|
+
dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
|
|
370
|
+
projects: (lastConfig.projects || []).filter(function (p) {
|
|
371
|
+
return fs.existsSync(p.path);
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
ensureConfigDir();
|
|
376
|
+
saveConfig(newConfig);
|
|
377
|
+
|
|
378
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
379
|
+
var logFile = logPath();
|
|
380
|
+
var logFd = fs.openSync(logFile, "a");
|
|
381
|
+
|
|
382
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
383
|
+
detached: true,
|
|
384
|
+
windowsHide: true,
|
|
385
|
+
stdio: ["ignore", logFd, logFd],
|
|
386
|
+
env: Object.assign({}, process.env, {
|
|
387
|
+
CLAUDE_RELAY_CONFIG: configPath(),
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
child.unref();
|
|
391
|
+
fs.closeSync(logFd);
|
|
392
|
+
|
|
393
|
+
newConfig.pid = child.pid;
|
|
394
|
+
saveConfig(newConfig);
|
|
395
|
+
|
|
396
|
+
// Wait and verify
|
|
397
|
+
await new Promise(function (resolve) { setTimeout(resolve, 1200); });
|
|
398
|
+
var alive = await isDaemonAliveAsync(newConfig);
|
|
399
|
+
if (!alive) {
|
|
400
|
+
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
var ip = getLocalIP();
|
|
405
|
+
log(sym.done + " " + a.green + "Server restarted successfully." + a.reset);
|
|
284
406
|
log("");
|
|
285
|
-
|
|
407
|
+
showMainMenu(newConfig, ip);
|
|
286
408
|
}
|
|
287
409
|
|
|
288
410
|
// --- Network ---
|
|
@@ -352,7 +474,7 @@ function getAllIPs() {
|
|
|
352
474
|
|
|
353
475
|
function ensureCerts(ip) {
|
|
354
476
|
var homeDir = os.homedir();
|
|
355
|
-
var certDir = path.join(homeDir, ".claude-relay", "certs");
|
|
477
|
+
var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(homeDir, ".claude-relay"), "certs");
|
|
356
478
|
var keyPath = path.join(certDir, "key.pem");
|
|
357
479
|
var certPath = path.join(certDir, "cert.pem");
|
|
358
480
|
|
|
@@ -1098,6 +1220,7 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1098
1220
|
tls: hasTls,
|
|
1099
1221
|
debug: debugMode,
|
|
1100
1222
|
keepAwake: keepAwake,
|
|
1223
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1101
1224
|
projects: allProjects,
|
|
1102
1225
|
};
|
|
1103
1226
|
|
|
@@ -1176,6 +1299,7 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1176
1299
|
tls: true,
|
|
1177
1300
|
debug: config.debug || false,
|
|
1178
1301
|
keepAwake: config.keepAwake || false,
|
|
1302
|
+
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1179
1303
|
projects: config.projects || [],
|
|
1180
1304
|
};
|
|
1181
1305
|
|
|
@@ -1938,8 +2062,16 @@ var currentVersion = require("../package.json").version;
|
|
|
1938
2062
|
// No daemon running — first-time setup
|
|
1939
2063
|
if (autoYes) {
|
|
1940
2064
|
var pin = cliPin || null;
|
|
2065
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
2066
|
+
console.error(" " + sym.warn + " " + a.red + "--dangerously-skip-permissions requires --pin <pin>" + a.reset);
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
1941
2070
|
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
1942
2071
|
console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
|
|
2072
|
+
if (dangerouslySkipPermissions) {
|
|
2073
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2074
|
+
}
|
|
1943
2075
|
var autoRc = loadClayrc();
|
|
1944
2076
|
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
1945
2077
|
return p.path !== cwd && fs.existsSync(p.path);
|
|
@@ -1950,6 +2082,12 @@ var currentVersion = require("../package.json").version;
|
|
|
1950
2082
|
await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined);
|
|
1951
2083
|
} else {
|
|
1952
2084
|
setup(function (pin, keepAwake) {
|
|
2085
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
2086
|
+
log(sym.warn + " " + a.red + "--dangerously-skip-permissions requires a PIN." + a.reset);
|
|
2087
|
+
log(a.dim + " Please set a PIN to use skip permissions mode." + a.reset);
|
|
2088
|
+
process.exit(1);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
1953
2091
|
// Check ~/.clayrc for previous projects to restore
|
|
1954
2092
|
var rc = loadClayrc();
|
|
1955
2093
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
package/lib/config.js
CHANGED
|
@@ -3,8 +3,9 @@ var path = require("path");
|
|
|
3
3
|
var os = require("os");
|
|
4
4
|
var net = require("net");
|
|
5
5
|
|
|
6
|
-
var CONFIG_DIR = path.join(os.homedir(), ".claude-relay");
|
|
6
|
+
var CONFIG_DIR = process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay");
|
|
7
7
|
var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
|
|
8
|
+
var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
|
|
8
9
|
|
|
9
10
|
function configPath() {
|
|
10
11
|
return path.join(CONFIG_DIR, "daemon.json");
|
|
@@ -12,7 +13,8 @@ function configPath() {
|
|
|
12
13
|
|
|
13
14
|
function socketPath() {
|
|
14
15
|
if (process.platform === "win32") {
|
|
15
|
-
|
|
16
|
+
var pipeName = process.env.CLAUDE_RELAY_HOME ? "claude-relay-dev-daemon" : "claude-relay-daemon";
|
|
17
|
+
return "\\\\.\\pipe\\" + pipeName;
|
|
16
18
|
}
|
|
17
19
|
return path.join(CONFIG_DIR, "daemon.sock");
|
|
18
20
|
}
|
|
@@ -105,6 +107,32 @@ function clearStaleConfig() {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// --- Crash info ---
|
|
111
|
+
|
|
112
|
+
function crashInfoPath() {
|
|
113
|
+
return CRASH_INFO_PATH;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeCrashInfo(info) {
|
|
117
|
+
try {
|
|
118
|
+
ensureConfigDir();
|
|
119
|
+
fs.writeFileSync(CRASH_INFO_PATH, JSON.stringify(info));
|
|
120
|
+
} catch (e) {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readCrashInfo() {
|
|
124
|
+
try {
|
|
125
|
+
var data = fs.readFileSync(CRASH_INFO_PATH, "utf8");
|
|
126
|
+
return JSON.parse(data);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function clearCrashInfo() {
|
|
133
|
+
try { fs.unlinkSync(CRASH_INFO_PATH); } catch (e) {}
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
// --- ~/.clayrc (recent projects persistence) ---
|
|
109
137
|
|
|
110
138
|
function clayrcPath() {
|
|
@@ -183,6 +211,10 @@ module.exports = {
|
|
|
183
211
|
isDaemonAliveAsync: isDaemonAliveAsync,
|
|
184
212
|
generateSlug: generateSlug,
|
|
185
213
|
clearStaleConfig: clearStaleConfig,
|
|
214
|
+
crashInfoPath: crashInfoPath,
|
|
215
|
+
writeCrashInfo: writeCrashInfo,
|
|
216
|
+
readCrashInfo: readCrashInfo,
|
|
217
|
+
clearCrashInfo: clearCrashInfo,
|
|
186
218
|
clayrcPath: clayrcPath,
|
|
187
219
|
loadClayrc: loadClayrc,
|
|
188
220
|
saveClayrc: saveClayrc,
|
package/lib/daemon.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Polyfill Symbol.dispose/asyncDispose for Node 18 (used by claude-agent-sdk)
|
|
4
|
+
if (!Symbol.dispose) Symbol.dispose = Symbol("Symbol.dispose");
|
|
5
|
+
if (!Symbol.asyncDispose) Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
|
|
6
|
+
|
|
3
7
|
var fs = require("fs");
|
|
4
8
|
var path = require("path");
|
|
5
|
-
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc } = require("./config");
|
|
9
|
+
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
|
|
6
10
|
var { createIPCServer } = require("./ipc");
|
|
7
11
|
var { createServer } = require("./server");
|
|
8
12
|
|
|
@@ -20,7 +24,7 @@ try {
|
|
|
20
24
|
var tlsOptions = null;
|
|
21
25
|
if (config.tls) {
|
|
22
26
|
var os = require("os");
|
|
23
|
-
var certDir = path.join(os.homedir(), ".claude-relay", "certs");
|
|
27
|
+
var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay"), "certs");
|
|
24
28
|
var keyPath = path.join(certDir, "key.pem");
|
|
25
29
|
var certPath = path.join(certDir, "cert.pem");
|
|
26
30
|
try {
|
|
@@ -43,6 +47,23 @@ try {
|
|
|
43
47
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
44
48
|
} catch (e) {}
|
|
45
49
|
|
|
50
|
+
// --- Resolve LAN IP for share URL ---
|
|
51
|
+
var os2 = require("os");
|
|
52
|
+
var lanIp = (function () {
|
|
53
|
+
var ifaces = os2.networkInterfaces();
|
|
54
|
+
for (var addrs of Object.values(ifaces)) {
|
|
55
|
+
for (var i = 0; i < addrs.length; i++) {
|
|
56
|
+
if (addrs[i].family === "IPv4" && !addrs[i].internal && addrs[i].address.startsWith("100.")) return addrs[i].address;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (var addrs of Object.values(ifaces)) {
|
|
60
|
+
for (var i = 0; i < addrs.length; i++) {
|
|
61
|
+
if (addrs[i].family === "IPv4" && !addrs[i].internal) return addrs[i].address;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
})();
|
|
66
|
+
|
|
46
67
|
// --- Create multi-project server ---
|
|
47
68
|
var relay = createServer({
|
|
48
69
|
tlsOptions: tlsOptions,
|
|
@@ -50,6 +71,8 @@ var relay = createServer({
|
|
|
50
71
|
pinHash: config.pinHash || null,
|
|
51
72
|
port: config.port,
|
|
52
73
|
debug: config.debug || false,
|
|
74
|
+
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
75
|
+
lanHost: lanIp ? lanIp + ":" + config.port : null,
|
|
53
76
|
});
|
|
54
77
|
|
|
55
78
|
// --- Register projects ---
|
|
@@ -179,6 +202,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
179
202
|
// --- Start listening ---
|
|
180
203
|
relay.server.on("error", function (err) {
|
|
181
204
|
console.error("[daemon] Server error:", err.message);
|
|
205
|
+
writeCrashInfo({
|
|
206
|
+
reason: "Server error: " + err.message,
|
|
207
|
+
pid: process.pid,
|
|
208
|
+
time: Date.now(),
|
|
209
|
+
});
|
|
182
210
|
process.exit(1);
|
|
183
211
|
});
|
|
184
212
|
|
|
@@ -191,6 +219,23 @@ relay.server.listen(config.port, function () {
|
|
|
191
219
|
// Update PID in config
|
|
192
220
|
config.pid = process.pid;
|
|
193
221
|
saveConfig(config);
|
|
222
|
+
|
|
223
|
+
// Check for crash info from a previous crash and notify clients
|
|
224
|
+
var crashInfo = readCrashInfo();
|
|
225
|
+
if (crashInfo) {
|
|
226
|
+
console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
|
|
227
|
+
console.log("[daemon] Crash reason:", crashInfo.reason);
|
|
228
|
+
// Delay notification so clients have time to reconnect
|
|
229
|
+
setTimeout(function () {
|
|
230
|
+
relay.broadcastAll({
|
|
231
|
+
type: "toast",
|
|
232
|
+
level: "warn",
|
|
233
|
+
message: "Server recovered from a crash and was automatically restarted.",
|
|
234
|
+
detail: crashInfo.reason || null,
|
|
235
|
+
});
|
|
236
|
+
}, 3000);
|
|
237
|
+
clearCrashInfo();
|
|
238
|
+
}
|
|
194
239
|
});
|
|
195
240
|
|
|
196
241
|
// --- HTTP onboarding server (only when TLS is active) ---
|
|
@@ -233,6 +278,8 @@ function gracefulShutdown() {
|
|
|
233
278
|
}
|
|
234
279
|
} catch (e) {}
|
|
235
280
|
|
|
281
|
+
relay.destroyAll();
|
|
282
|
+
|
|
236
283
|
if (relay.onboardingServer) {
|
|
237
284
|
relay.onboardingServer.close();
|
|
238
285
|
}
|
|
@@ -258,5 +305,10 @@ if (process.platform === "win32") {
|
|
|
258
305
|
|
|
259
306
|
process.on("uncaughtException", function (err) {
|
|
260
307
|
console.error("[daemon] Uncaught exception:", err);
|
|
308
|
+
writeCrashInfo({
|
|
309
|
+
reason: err ? (err.stack || err.message || String(err)) : "unknown",
|
|
310
|
+
pid: process.pid,
|
|
311
|
+
time: Date.now(),
|
|
312
|
+
});
|
|
261
313
|
gracefulShutdown();
|
|
262
314
|
});
|
package/lib/pages.js
CHANGED
|
@@ -549,6 +549,12 @@ function pushDone() {
|
|
|
549
549
|
pushStatus.className = "check-status ok";
|
|
550
550
|
pushStatus.textContent = "Push notifications enabled!";
|
|
551
551
|
fireConfetti();
|
|
552
|
+
navigator.serviceWorker.ready.then(function(reg) {
|
|
553
|
+
reg.showNotification("\ud83c\udf89 Welcome to Claude Relay!", {
|
|
554
|
+
body: "\ud83d\udd14 You\u2019ll be notified when Claude responds.",
|
|
555
|
+
tag: "claude-welcome",
|
|
556
|
+
});
|
|
557
|
+
}).catch(function() {});
|
|
552
558
|
setTimeout(function() { nextStep(); }, 1200);
|
|
553
559
|
}
|
|
554
560
|
|
|
@@ -582,10 +588,16 @@ function enablePush() {
|
|
|
582
588
|
});
|
|
583
589
|
})
|
|
584
590
|
.then(function(sub) {
|
|
591
|
+
var prevEndpoint = localStorage.getItem("push-endpoint");
|
|
592
|
+
localStorage.setItem("push-endpoint", sub.endpoint);
|
|
593
|
+
var payload = { subscription: sub.toJSON() };
|
|
594
|
+
if (prevEndpoint && prevEndpoint !== sub.endpoint) {
|
|
595
|
+
payload.replaceEndpoint = prevEndpoint;
|
|
596
|
+
}
|
|
585
597
|
return fetch("/api/push-subscribe", {
|
|
586
598
|
method: "POST",
|
|
587
599
|
headers: { "Content-Type": "application/json" },
|
|
588
|
-
body: JSON.stringify(
|
|
600
|
+
body: JSON.stringify(payload),
|
|
589
601
|
});
|
|
590
602
|
})
|
|
591
603
|
.then(pushDone)
|
|
@@ -683,6 +695,15 @@ function dashboardPageHtml(projects, version) {
|
|
|
683
695
|
'<div class="subtitle">Select a project</div>' +
|
|
684
696
|
'<div class="cards">' + cards + '</div>' +
|
|
685
697
|
'<div class="footer">v' + escapeHtml(version || "") + '</div>' +
|
|
698
|
+
'<style>' +
|
|
699
|
+
'.toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#3A3936;border:1px solid #DA7756;color:#E8E5DE;padding:12px 24px;border-radius:8px;font-size:14px;z-index:999;opacity:0;transition:opacity .3s}' +
|
|
700
|
+
'.toast.show{opacity:1}' +
|
|
701
|
+
'</style>' +
|
|
702
|
+
'<script>var s=window.matchMedia("(display-mode:standalone)").matches||navigator.standalone;' +
|
|
703
|
+
'if(s&&!localStorage.getItem("setup-done")){var t=/^100\\./.test(location.hostname);location.replace("/setup"+(t?"":"?mode=lan"));}' +
|
|
704
|
+
'var p=new URLSearchParams(location.search);var g=p.get("gone");' +
|
|
705
|
+
'if(g){history.replaceState(null,"","/");var d=document.createElement("div");d.className="toast";d.textContent="Project \\""+g+"\\" is no longer available";document.body.appendChild(d);' +
|
|
706
|
+
'requestAnimationFrame(function(){d.className="toast show"});setTimeout(function(){d.className="toast";setTimeout(function(){d.remove()},300)},5000);}</script>' +
|
|
686
707
|
'</body></html>';
|
|
687
708
|
}
|
|
688
709
|
|