claude-relay 2.2.4 → 2.3.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 +12 -0
- package/bin/cli.js +135 -4
- package/lib/config.js +31 -0
- package/lib/daemon.js +49 -1
- package/lib/pages.js +15 -1
- package/lib/project.js +323 -26
- package/lib/public/app.js +313 -7
- package/lib/public/css/base.css +5 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +541 -0
- package/lib/public/css/input.css +1 -0
- package/lib/public/css/menus.css +89 -5
- package/lib/public/css/messages.css +84 -49
- package/lib/public/css/overlays.css +40 -0
- package/lib/public/index.html +100 -17
- 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/tools.js +43 -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 +37 -4
- package/lib/sessions.js +14 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -182,6 +182,9 @@ If push registration fails: check whether your browser trusts HTTPS and whether
|
|
|
182
182
|
```bash
|
|
183
183
|
npx claude-relay # Default (port 2633)
|
|
184
184
|
npx claude-relay -p 8080 # Specify port
|
|
185
|
+
npx claude-relay -y # Skip interactive prompts (accept defaults)
|
|
186
|
+
npx claude-relay -y --pin 123456
|
|
187
|
+
# Non-interactive with PIN (for scripts/CI)
|
|
185
188
|
npx claude-relay --no-https # Disable HTTPS
|
|
186
189
|
npx claude-relay --no-update # Skip update check
|
|
187
190
|
npx claude-relay --debug # Enable debug panel
|
|
@@ -189,6 +192,9 @@ npx claude-relay --add . # Add current directory to running daemon
|
|
|
189
192
|
npx claude-relay --add /path # Add a project by path
|
|
190
193
|
npx claude-relay --remove . # Remove a project
|
|
191
194
|
npx claude-relay --list # List registered projects
|
|
195
|
+
npx claude-relay --shutdown # Stop the running daemon
|
|
196
|
+
npx claude-relay --dangerously-skip-permissions
|
|
197
|
+
# Bypass all permission prompts (PIN required during setup)
|
|
192
198
|
```
|
|
193
199
|
|
|
194
200
|
## Requirements
|
|
@@ -236,6 +242,12 @@ For a detailed sequence diagram, daemon structure, and design decisions, refer t
|
|
|
236
242
|
|
|
237
243
|
---
|
|
238
244
|
|
|
245
|
+
## Contributors
|
|
246
|
+
|
|
247
|
+
<a href="https://github.com/chadbyte/claude-relay/graphs/contributors">
|
|
248
|
+
<img src="https://contrib.rocks/image?repo=chadbyte/claude-relay" />
|
|
249
|
+
</a>
|
|
250
|
+
|
|
239
251
|
## Contributing
|
|
240
252
|
|
|
241
253
|
Bug fixes and typos are welcome. For feature suggestions, please open an issue first:
|
package/bin/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
-
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc } = require("../lib/config");
|
|
9
|
+
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
10
10
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
11
11
|
var { generateAuthToken } = require("../lib/server");
|
|
12
12
|
|
|
@@ -32,6 +32,7 @@ var shutdownMode = false;
|
|
|
32
32
|
var addPath = null;
|
|
33
33
|
var removePath = null;
|
|
34
34
|
var listMode = false;
|
|
35
|
+
var dangerouslySkipPermissions = false;
|
|
35
36
|
|
|
36
37
|
for (var i = 0; i < args.length; i++) {
|
|
37
38
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -62,6 +63,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
62
63
|
i++;
|
|
63
64
|
} else if (args[i] === "--list") {
|
|
64
65
|
listMode = true;
|
|
66
|
+
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
67
|
+
dangerouslySkipPermissions = true;
|
|
65
68
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
66
69
|
console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
|
|
67
70
|
console.log(" claude-relay --add <path> Add a project to the running daemon");
|
|
@@ -79,6 +82,8 @@ for (var i = 0; i < args.length; i++) {
|
|
|
79
82
|
console.log(" --add <path> Add a project directory (use '.' for current)");
|
|
80
83
|
console.log(" --remove <path> Remove a project directory");
|
|
81
84
|
console.log(" --list List all registered projects");
|
|
85
|
+
console.log(" --dangerously-skip-permissions");
|
|
86
|
+
console.log(" Bypass all permission prompts (requires --pin)");
|
|
82
87
|
process.exit(0);
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -270,6 +275,10 @@ function stopDaemonWatcher() {
|
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
var _restartAttempts = 0;
|
|
279
|
+
var MAX_RESTART_ATTEMPTS = 5;
|
|
280
|
+
var _restartBackoffStart = 0;
|
|
281
|
+
|
|
273
282
|
function onDaemonDied() {
|
|
274
283
|
stopDaemonWatcher();
|
|
275
284
|
// Clean up stdin in case a prompt is active
|
|
@@ -278,11 +287,117 @@ function onDaemonDied() {
|
|
|
278
287
|
process.stdin.pause();
|
|
279
288
|
process.stdin.removeAllListeners("data");
|
|
280
289
|
} catch (e) {}
|
|
290
|
+
|
|
291
|
+
// Check if this was a crash (crash.json exists) vs intentional shutdown
|
|
292
|
+
var crashInfo = readCrashInfo();
|
|
293
|
+
if (!crashInfo) {
|
|
294
|
+
// Intentional shutdown, no restart
|
|
295
|
+
log("");
|
|
296
|
+
log(sym.warn + " " + a.yellow + "Server has been shut down." + a.reset);
|
|
297
|
+
log(a.dim + " Run " + a.reset + "npx claude-relay" + a.dim + " to start again." + a.reset);
|
|
298
|
+
log("");
|
|
299
|
+
process.exit(0);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Reset backoff counter if enough time has passed since last restart burst
|
|
304
|
+
var now = Date.now();
|
|
305
|
+
if (_restartBackoffStart && now - _restartBackoffStart > 60000) {
|
|
306
|
+
_restartAttempts = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_restartAttempts++;
|
|
310
|
+
if (_restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
311
|
+
log("");
|
|
312
|
+
log(sym.warn + " " + a.red + "Server crashed too many times (" + MAX_RESTART_ATTEMPTS + " attempts). Giving up." + a.reset);
|
|
313
|
+
if (crashInfo.reason) {
|
|
314
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
315
|
+
}
|
|
316
|
+
log(a.dim + " Check logs: " + a.reset + logPath());
|
|
317
|
+
log("");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (_restartAttempts === 1) _restartBackoffStart = now;
|
|
323
|
+
|
|
281
324
|
log("");
|
|
282
|
-
log(sym.warn + " " + a.yellow + "Server
|
|
283
|
-
|
|
325
|
+
log(sym.warn + " " + a.yellow + "Server crashed. Restarting... (" + _restartAttempts + "/" + MAX_RESTART_ATTEMPTS + ")" + a.reset);
|
|
326
|
+
if (crashInfo.reason) {
|
|
327
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Re-fork the daemon from saved config
|
|
331
|
+
restartDaemonFromConfig();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function restartDaemonFromConfig() {
|
|
335
|
+
var lastConfig = loadConfig();
|
|
336
|
+
if (!lastConfig || !lastConfig.projects) {
|
|
337
|
+
log(a.red + " No config found. Cannot restart." + a.reset);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
clearStaleConfig();
|
|
343
|
+
|
|
344
|
+
// Wait for port to be released
|
|
345
|
+
var targetPort = lastConfig.port || port;
|
|
346
|
+
var waited = 0;
|
|
347
|
+
while (waited < 3000) {
|
|
348
|
+
var free = await isPortFree(targetPort);
|
|
349
|
+
if (free) break;
|
|
350
|
+
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
351
|
+
waited += 300;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Rebuild config (preserve everything except pid)
|
|
355
|
+
var newConfig = {
|
|
356
|
+
pid: null,
|
|
357
|
+
port: targetPort,
|
|
358
|
+
pinHash: lastConfig.pinHash || null,
|
|
359
|
+
tls: lastConfig.tls !== undefined ? lastConfig.tls : useHttps,
|
|
360
|
+
debug: lastConfig.debug || false,
|
|
361
|
+
keepAwake: lastConfig.keepAwake || false,
|
|
362
|
+
dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
|
|
363
|
+
projects: (lastConfig.projects || []).filter(function (p) {
|
|
364
|
+
return fs.existsSync(p.path);
|
|
365
|
+
}),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
ensureConfigDir();
|
|
369
|
+
saveConfig(newConfig);
|
|
370
|
+
|
|
371
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
372
|
+
var logFile = logPath();
|
|
373
|
+
var logFd = fs.openSync(logFile, "a");
|
|
374
|
+
|
|
375
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
376
|
+
detached: true,
|
|
377
|
+
windowsHide: true,
|
|
378
|
+
stdio: ["ignore", logFd, logFd],
|
|
379
|
+
env: Object.assign({}, process.env, {
|
|
380
|
+
CLAUDE_RELAY_CONFIG: configPath(),
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
child.unref();
|
|
384
|
+
fs.closeSync(logFd);
|
|
385
|
+
|
|
386
|
+
newConfig.pid = child.pid;
|
|
387
|
+
saveConfig(newConfig);
|
|
388
|
+
|
|
389
|
+
// Wait and verify
|
|
390
|
+
await new Promise(function (resolve) { setTimeout(resolve, 1200); });
|
|
391
|
+
var alive = await isDaemonAliveAsync(newConfig);
|
|
392
|
+
if (!alive) {
|
|
393
|
+
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
var ip = getLocalIP();
|
|
398
|
+
log(sym.done + " " + a.green + "Server restarted successfully." + a.reset);
|
|
284
399
|
log("");
|
|
285
|
-
|
|
400
|
+
showMainMenu(newConfig, ip);
|
|
286
401
|
}
|
|
287
402
|
|
|
288
403
|
// --- Network ---
|
|
@@ -1098,6 +1213,7 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1098
1213
|
tls: hasTls,
|
|
1099
1214
|
debug: debugMode,
|
|
1100
1215
|
keepAwake: keepAwake,
|
|
1216
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1101
1217
|
projects: allProjects,
|
|
1102
1218
|
};
|
|
1103
1219
|
|
|
@@ -1176,6 +1292,7 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1176
1292
|
tls: true,
|
|
1177
1293
|
debug: config.debug || false,
|
|
1178
1294
|
keepAwake: config.keepAwake || false,
|
|
1295
|
+
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1179
1296
|
projects: config.projects || [],
|
|
1180
1297
|
};
|
|
1181
1298
|
|
|
@@ -1938,8 +2055,16 @@ var currentVersion = require("../package.json").version;
|
|
|
1938
2055
|
// No daemon running — first-time setup
|
|
1939
2056
|
if (autoYes) {
|
|
1940
2057
|
var pin = cliPin || null;
|
|
2058
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
2059
|
+
console.error(" " + sym.warn + " " + a.red + "--dangerously-skip-permissions requires --pin <pin>" + a.reset);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
1941
2063
|
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
1942
2064
|
console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
|
|
2065
|
+
if (dangerouslySkipPermissions) {
|
|
2066
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2067
|
+
}
|
|
1943
2068
|
var autoRc = loadClayrc();
|
|
1944
2069
|
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
1945
2070
|
return p.path !== cwd && fs.existsSync(p.path);
|
|
@@ -1950,6 +2075,12 @@ var currentVersion = require("../package.json").version;
|
|
|
1950
2075
|
await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined);
|
|
1951
2076
|
} else {
|
|
1952
2077
|
setup(function (pin, keepAwake) {
|
|
2078
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
2079
|
+
log(sym.warn + " " + a.red + "--dangerously-skip-permissions requires a PIN." + a.reset);
|
|
2080
|
+
log(a.dim + " Please set a PIN to use skip permissions mode." + a.reset);
|
|
2081
|
+
process.exit(1);
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
1953
2084
|
// Check ~/.clayrc for previous projects to restore
|
|
1954
2085
|
var rc = loadClayrc();
|
|
1955
2086
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@ var net = require("net");
|
|
|
5
5
|
|
|
6
6
|
var CONFIG_DIR = 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");
|
|
@@ -105,6 +106,32 @@ function clearStaleConfig() {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// --- Crash info ---
|
|
110
|
+
|
|
111
|
+
function crashInfoPath() {
|
|
112
|
+
return CRASH_INFO_PATH;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeCrashInfo(info) {
|
|
116
|
+
try {
|
|
117
|
+
ensureConfigDir();
|
|
118
|
+
fs.writeFileSync(CRASH_INFO_PATH, JSON.stringify(info));
|
|
119
|
+
} catch (e) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readCrashInfo() {
|
|
123
|
+
try {
|
|
124
|
+
var data = fs.readFileSync(CRASH_INFO_PATH, "utf8");
|
|
125
|
+
return JSON.parse(data);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function clearCrashInfo() {
|
|
132
|
+
try { fs.unlinkSync(CRASH_INFO_PATH); } catch (e) {}
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
// --- ~/.clayrc (recent projects persistence) ---
|
|
109
136
|
|
|
110
137
|
function clayrcPath() {
|
|
@@ -183,6 +210,10 @@ module.exports = {
|
|
|
183
210
|
isDaemonAliveAsync: isDaemonAliveAsync,
|
|
184
211
|
generateSlug: generateSlug,
|
|
185
212
|
clearStaleConfig: clearStaleConfig,
|
|
213
|
+
crashInfoPath: crashInfoPath,
|
|
214
|
+
writeCrashInfo: writeCrashInfo,
|
|
215
|
+
readCrashInfo: readCrashInfo,
|
|
216
|
+
clearCrashInfo: clearCrashInfo,
|
|
186
217
|
clayrcPath: clayrcPath,
|
|
187
218
|
loadClayrc: loadClayrc,
|
|
188
219
|
saveClayrc: saveClayrc,
|
package/lib/daemon.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var fs = require("fs");
|
|
4
4
|
var path = require("path");
|
|
5
|
-
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc } = require("./config");
|
|
5
|
+
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
|
|
6
6
|
var { createIPCServer } = require("./ipc");
|
|
7
7
|
var { createServer } = require("./server");
|
|
8
8
|
|
|
@@ -43,6 +43,23 @@ try {
|
|
|
43
43
|
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
44
44
|
} catch (e) {}
|
|
45
45
|
|
|
46
|
+
// --- Resolve LAN IP for share URL ---
|
|
47
|
+
var os2 = require("os");
|
|
48
|
+
var lanIp = (function () {
|
|
49
|
+
var ifaces = os2.networkInterfaces();
|
|
50
|
+
for (var addrs of Object.values(ifaces)) {
|
|
51
|
+
for (var i = 0; i < addrs.length; i++) {
|
|
52
|
+
if (addrs[i].family === "IPv4" && !addrs[i].internal && addrs[i].address.startsWith("100.")) return addrs[i].address;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (var addrs of Object.values(ifaces)) {
|
|
56
|
+
for (var i = 0; i < addrs.length; i++) {
|
|
57
|
+
if (addrs[i].family === "IPv4" && !addrs[i].internal) return addrs[i].address;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
})();
|
|
62
|
+
|
|
46
63
|
// --- Create multi-project server ---
|
|
47
64
|
var relay = createServer({
|
|
48
65
|
tlsOptions: tlsOptions,
|
|
@@ -50,6 +67,8 @@ var relay = createServer({
|
|
|
50
67
|
pinHash: config.pinHash || null,
|
|
51
68
|
port: config.port,
|
|
52
69
|
debug: config.debug || false,
|
|
70
|
+
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
71
|
+
lanHost: lanIp ? lanIp + ":" + config.port : null,
|
|
53
72
|
});
|
|
54
73
|
|
|
55
74
|
// --- Register projects ---
|
|
@@ -179,6 +198,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
179
198
|
// --- Start listening ---
|
|
180
199
|
relay.server.on("error", function (err) {
|
|
181
200
|
console.error("[daemon] Server error:", err.message);
|
|
201
|
+
writeCrashInfo({
|
|
202
|
+
reason: "Server error: " + err.message,
|
|
203
|
+
pid: process.pid,
|
|
204
|
+
time: Date.now(),
|
|
205
|
+
});
|
|
182
206
|
process.exit(1);
|
|
183
207
|
});
|
|
184
208
|
|
|
@@ -191,6 +215,23 @@ relay.server.listen(config.port, function () {
|
|
|
191
215
|
// Update PID in config
|
|
192
216
|
config.pid = process.pid;
|
|
193
217
|
saveConfig(config);
|
|
218
|
+
|
|
219
|
+
// Check for crash info from a previous crash and notify clients
|
|
220
|
+
var crashInfo = readCrashInfo();
|
|
221
|
+
if (crashInfo) {
|
|
222
|
+
console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
|
|
223
|
+
console.log("[daemon] Crash reason:", crashInfo.reason);
|
|
224
|
+
// Delay notification so clients have time to reconnect
|
|
225
|
+
setTimeout(function () {
|
|
226
|
+
relay.broadcastAll({
|
|
227
|
+
type: "toast",
|
|
228
|
+
level: "warn",
|
|
229
|
+
message: "Server recovered from a crash and was automatically restarted.",
|
|
230
|
+
detail: crashInfo.reason || null,
|
|
231
|
+
});
|
|
232
|
+
}, 3000);
|
|
233
|
+
clearCrashInfo();
|
|
234
|
+
}
|
|
194
235
|
});
|
|
195
236
|
|
|
196
237
|
// --- HTTP onboarding server (only when TLS is active) ---
|
|
@@ -233,6 +274,8 @@ function gracefulShutdown() {
|
|
|
233
274
|
}
|
|
234
275
|
} catch (e) {}
|
|
235
276
|
|
|
277
|
+
relay.destroyAll();
|
|
278
|
+
|
|
236
279
|
if (relay.onboardingServer) {
|
|
237
280
|
relay.onboardingServer.close();
|
|
238
281
|
}
|
|
@@ -258,5 +301,10 @@ if (process.platform === "win32") {
|
|
|
258
301
|
|
|
259
302
|
process.on("uncaughtException", function (err) {
|
|
260
303
|
console.error("[daemon] Uncaught exception:", err);
|
|
304
|
+
writeCrashInfo({
|
|
305
|
+
reason: err ? (err.stack || err.message || String(err)) : "unknown",
|
|
306
|
+
pid: process.pid,
|
|
307
|
+
time: Date.now(),
|
|
308
|
+
});
|
|
261
309
|
gracefulShutdown();
|
|
262
310
|
});
|
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,8 @@ 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
|
+
'<script>var s=window.matchMedia("(display-mode:standalone)").matches||navigator.standalone;' +
|
|
699
|
+
'if(s&&!localStorage.getItem("setup-done")){var t=/^100\\./.test(location.hostname);location.replace("/setup"+(t?"":"?mode=lan"));}</script>' +
|
|
686
700
|
'</body></html>';
|
|
687
701
|
}
|
|
688
702
|
|