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 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 has been shut down." + a.reset);
283
- log(a.dim + " Run " + a.reset + "npx claude-relay" + a.dim + " to start again." + a.reset);
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
- process.exit(0);
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(sub.toJSON()),
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