claude-relay 2.2.3 → 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,10 +6,21 @@ 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
 
13
+ function openUrl(url) {
14
+ try {
15
+ if (process.platform === "win32") {
16
+ spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, windowsHide: true }).unref();
17
+ } else {
18
+ var cmd = process.platform === "darwin" ? "open" : "xdg-open";
19
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
20
+ }
21
+ } catch (e) {}
22
+ }
23
+
13
24
  var args = process.argv.slice(2);
14
25
  var port = 2633;
15
26
  var useHttps = true;
@@ -21,6 +32,7 @@ var shutdownMode = false;
21
32
  var addPath = null;
22
33
  var removePath = null;
23
34
  var listMode = false;
35
+ var dangerouslySkipPermissions = false;
24
36
 
25
37
  for (var i = 0; i < args.length; i++) {
26
38
  if (args[i] === "-p" || args[i] === "--port") {
@@ -51,6 +63,8 @@ for (var i = 0; i < args.length; i++) {
51
63
  i++;
52
64
  } else if (args[i] === "--list") {
53
65
  listMode = true;
66
+ } else if (args[i] === "--dangerously-skip-permissions") {
67
+ dangerouslySkipPermissions = true;
54
68
  } else if (args[i] === "-h" || args[i] === "--help") {
55
69
  console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
56
70
  console.log(" claude-relay --add <path> Add a project to the running daemon");
@@ -68,6 +82,8 @@ for (var i = 0; i < args.length; i++) {
68
82
  console.log(" --add <path> Add a project directory (use '.' for current)");
69
83
  console.log(" --remove <path> Remove a project directory");
70
84
  console.log(" --list List all registered projects");
85
+ console.log(" --dangerously-skip-permissions");
86
+ console.log(" Bypass all permission prompts (requires --pin)");
71
87
  process.exit(0);
72
88
  }
73
89
  }
@@ -259,6 +275,10 @@ function stopDaemonWatcher() {
259
275
  }
260
276
  }
261
277
 
278
+ var _restartAttempts = 0;
279
+ var MAX_RESTART_ATTEMPTS = 5;
280
+ var _restartBackoffStart = 0;
281
+
262
282
  function onDaemonDied() {
263
283
  stopDaemonWatcher();
264
284
  // Clean up stdin in case a prompt is active
@@ -267,11 +287,117 @@ function onDaemonDied() {
267
287
  process.stdin.pause();
268
288
  process.stdin.removeAllListeners("data");
269
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
+
270
324
  log("");
271
- log(sym.warn + " " + a.yellow + "Server has been shut down." + a.reset);
272
- 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);
273
399
  log("");
274
- process.exit(0);
400
+ showMainMenu(newConfig, ip);
275
401
  }
276
402
 
277
403
  // --- Network ---
@@ -388,10 +514,8 @@ function ensureCerts(ip) {
388
514
  }
389
515
 
390
516
  try {
391
- execSync(
392
- "mkcert -key-file " + keyPath + " -cert-file " + certPath + " " + domains.join(" "),
393
- { stdio: "pipe" }
394
- );
517
+ var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
518
+ execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
395
519
  } catch (err) {
396
520
  return null;
397
521
  }
@@ -1026,9 +1150,13 @@ function setup(callback) {
1026
1150
  log(sym.bar);
1027
1151
 
1028
1152
  promptPin(function (pin) {
1029
- promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
1030
- callback(pin, keepAwake);
1031
- });
1153
+ if (process.platform === "darwin") {
1154
+ promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
1155
+ callback(pin, keepAwake);
1156
+ });
1157
+ } else {
1158
+ callback(pin, false);
1159
+ }
1032
1160
  });
1033
1161
  });
1034
1162
  });
@@ -1085,6 +1213,7 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
1085
1213
  tls: hasTls,
1086
1214
  debug: debugMode,
1087
1215
  keepAwake: keepAwake,
1216
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
1088
1217
  projects: allProjects,
1089
1218
  };
1090
1219
 
@@ -1098,6 +1227,7 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
1098
1227
 
1099
1228
  var child = spawn(process.execPath, [daemonScript], {
1100
1229
  detached: true,
1230
+ windowsHide: true,
1101
1231
  stdio: ["ignore", logFd, logFd],
1102
1232
  env: Object.assign({}, process.env, {
1103
1233
  CLAUDE_RELAY_CONFIG: configPath(),
@@ -1127,6 +1257,97 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
1127
1257
  showServerStarted(config, ip);
1128
1258
  }
1129
1259
 
1260
+ // ==============================
1261
+ // Restart daemon with TLS enabled
1262
+ // ==============================
1263
+ async function restartDaemonWithTLS(config, callback) {
1264
+ var ip = getLocalIP();
1265
+ var certPaths = ensureCerts(ip);
1266
+ if (!certPaths) {
1267
+ callback(config);
1268
+ return;
1269
+ }
1270
+
1271
+ // Shut down old daemon
1272
+ stopDaemonWatcher();
1273
+ try {
1274
+ await sendIPCCommand(socketPath(), { cmd: "shutdown" });
1275
+ } catch (e) {}
1276
+
1277
+ // Wait for port to be released
1278
+ var waited = 0;
1279
+ while (waited < 5000) {
1280
+ await new Promise(function (resolve) { setTimeout(resolve, 300); });
1281
+ waited += 300;
1282
+ var free = await isPortFree(config.port);
1283
+ if (free) break;
1284
+ }
1285
+ clearStaleConfig();
1286
+
1287
+ // Re-fork with TLS
1288
+ var newConfig = {
1289
+ pid: null,
1290
+ port: config.port,
1291
+ pinHash: config.pinHash || null,
1292
+ tls: true,
1293
+ debug: config.debug || false,
1294
+ keepAwake: config.keepAwake || false,
1295
+ dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
1296
+ projects: config.projects || [],
1297
+ };
1298
+
1299
+ ensureConfigDir();
1300
+ saveConfig(newConfig);
1301
+
1302
+ var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
1303
+ var logFile = logPath();
1304
+ var logFd = fs.openSync(logFile, "a");
1305
+
1306
+ var child = spawn(process.execPath, [daemonScript], {
1307
+ detached: true,
1308
+ windowsHide: true,
1309
+ stdio: ["ignore", logFd, logFd],
1310
+ env: Object.assign({}, process.env, {
1311
+ CLAUDE_RELAY_CONFIG: configPath(),
1312
+ }),
1313
+ });
1314
+ child.unref();
1315
+ fs.closeSync(logFd);
1316
+
1317
+ newConfig.pid = child.pid;
1318
+ saveConfig(newConfig);
1319
+
1320
+ await new Promise(function (resolve) { setTimeout(resolve, 800); });
1321
+
1322
+ var alive = await isDaemonAliveAsync(newConfig);
1323
+ if (!alive) {
1324
+ log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
1325
+ // Re-fork without TLS so the server is at least running
1326
+ newConfig.tls = false;
1327
+ saveConfig(newConfig);
1328
+ var logFd2 = fs.openSync(logFile, "a");
1329
+ var child2 = spawn(process.execPath, [daemonScript], {
1330
+ detached: true,
1331
+ windowsHide: true,
1332
+ stdio: ["ignore", logFd2, logFd2],
1333
+ env: Object.assign({}, process.env, {
1334
+ CLAUDE_RELAY_CONFIG: configPath(),
1335
+ }),
1336
+ });
1337
+ child2.unref();
1338
+ fs.closeSync(logFd2);
1339
+ newConfig.pid = child2.pid;
1340
+ saveConfig(newConfig);
1341
+ await new Promise(function (resolve) { setTimeout(resolve, 800); });
1342
+ startDaemonWatcher();
1343
+ callback(newConfig);
1344
+ return;
1345
+ }
1346
+
1347
+ startDaemonWatcher();
1348
+ callback(newConfig);
1349
+ }
1350
+
1130
1351
  // ==============================
1131
1352
  // Show server started info
1132
1353
  // ==============================
@@ -1196,6 +1417,7 @@ function showMainMenu(config, ip) {
1196
1417
  switch (choice) {
1197
1418
  case "notifications":
1198
1419
  showSetupGuide(config, ip, function () {
1420
+ config = loadConfig() || config;
1199
1421
  showMainMenu(config, ip);
1200
1422
  });
1201
1423
  break;
@@ -1246,17 +1468,11 @@ function showMainMenu(config, ip) {
1246
1468
  ],
1247
1469
  keys: [
1248
1470
  { key: "o", onKey: function () {
1249
- try {
1250
- var openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1251
- spawn(openCmd, [url], { stdio: "ignore", detached: true }).unref();
1252
- } catch (e) {}
1471
+ openUrl(url);
1253
1472
  showMainMenu(config, ip);
1254
1473
  }},
1255
1474
  { key: "s", onKey: function () {
1256
- try {
1257
- var openCmd = process.platform === "darwin" ? "open" : "xdg-open";
1258
- spawn(openCmd, ["https://github.com/chadbyte/claude-relay"], { stdio: "ignore", detached: true }).unref();
1259
- } catch (e) {}
1475
+ openUrl("https://github.com/chadbyte/claude-relay");
1260
1476
  showMainMenu(config, ip);
1261
1477
  }},
1262
1478
  ],
@@ -1562,11 +1778,25 @@ function showSetupGuide(config, ip, goBack) {
1562
1778
  log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
1563
1779
  if (mcReady) {
1564
1780
  log(sym.bar + " " + a.green + "mkcert is installed" + a.reset);
1781
+ if (!config.tls) {
1782
+ log(sym.bar + " " + a.dim + "Restarting server with HTTPS..." + a.reset);
1783
+ restartDaemonWithTLS(config, function (newConfig) {
1784
+ config = newConfig;
1785
+ log(sym.bar);
1786
+ showSetupQR();
1787
+ });
1788
+ return;
1789
+ }
1565
1790
  log(sym.bar);
1566
1791
  showSetupQR();
1567
1792
  } else {
1568
1793
  log(sym.bar + " " + a.yellow + "mkcert not found." + a.reset);
1569
- log(sym.bar + " " + a.dim + "Install: " + a.reset + "brew install mkcert && mkcert -install");
1794
+ var mkcertHint = process.platform === "win32"
1795
+ ? "choco install mkcert && mkcert -install"
1796
+ : process.platform === "darwin"
1797
+ ? "brew install mkcert && mkcert -install"
1798
+ : "apt install mkcert && mkcert -install";
1799
+ log(sym.bar + " " + a.dim + "Install: " + a.reset + mkcertHint);
1570
1800
  log(sym.bar);
1571
1801
  promptSelect("Select", [
1572
1802
  { label: "Re-check", value: "recheck" },
@@ -1658,7 +1888,9 @@ function showSettingsMenu(config, ip) {
1658
1888
  log(sym.bar + " mkcert " + mcStatus);
1659
1889
  log(sym.bar + " HTTPS " + tlsStatus);
1660
1890
  log(sym.bar + " PIN " + pinStatus);
1661
- log(sym.bar + " Keep awake " + awakeStatus);
1891
+ if (process.platform === "darwin") {
1892
+ log(sym.bar + " Keep awake " + awakeStatus);
1893
+ }
1662
1894
  log(sym.bar);
1663
1895
 
1664
1896
  // Build items
@@ -1672,7 +1904,9 @@ function showSettingsMenu(config, ip) {
1672
1904
  } else {
1673
1905
  items.push({ label: "Set PIN", value: "pin" });
1674
1906
  }
1675
- items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
1907
+ if (process.platform === "darwin") {
1908
+ items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
1909
+ }
1676
1910
  items.push({ label: "View logs", value: "logs" });
1677
1911
  items.push({ label: "Back", value: "back" });
1678
1912
 
@@ -1680,6 +1914,7 @@ function showSettingsMenu(config, ip) {
1680
1914
  switch (choice) {
1681
1915
  case "guide":
1682
1916
  showSetupGuide(config, ip, function () {
1917
+ config = loadConfig() || config;
1683
1918
  showSettingsMenu(config, ip);
1684
1919
  });
1685
1920
  break;
@@ -1820,8 +2055,16 @@ var currentVersion = require("../package.json").version;
1820
2055
  // No daemon running — first-time setup
1821
2056
  if (autoYes) {
1822
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
+ }
1823
2063
  console.log(" " + sym.done + " Auto-accepted disclaimer");
1824
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
+ }
1825
2068
  var autoRc = loadClayrc();
1826
2069
  var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
1827
2070
  return p.path !== cwd && fs.existsSync(p.path);
@@ -1832,6 +2075,12 @@ var currentVersion = require("../package.json").version;
1832
2075
  await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined);
1833
2076
  } else {
1834
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
+ }
1835
2084
  // Check ~/.clayrc for previous projects to restore
1836
2085
  var rc = loadClayrc();
1837
2086
  var restorable = (rc.recentProjects || []).filter(function (p) {
package/lib/config.js CHANGED
@@ -5,12 +5,16 @@ 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");
11
12
  }
12
13
 
13
14
  function socketPath() {
15
+ if (process.platform === "win32") {
16
+ return "\\\\.\\pipe\\claude-relay-daemon";
17
+ }
14
18
  return path.join(CONFIG_DIR, "daemon.sock");
15
19
  }
16
20
 
@@ -50,7 +54,8 @@ function isPidAlive(pid) {
50
54
  function isDaemonAlive(config) {
51
55
  if (!config || !config.pid) return false;
52
56
  if (!isPidAlive(config.pid)) return false;
53
- // Also check socket exists
57
+ // Named pipes on Windows can't be stat'd, just check PID
58
+ if (process.platform === "win32") return true;
54
59
  try {
55
60
  fs.statSync(socketPath());
56
61
  return true;
@@ -96,7 +101,35 @@ function generateSlug(projectPath, existingSlugs) {
96
101
 
97
102
  function clearStaleConfig() {
98
103
  try { fs.unlinkSync(configPath()); } catch (e) {}
99
- try { fs.unlinkSync(socketPath()); } catch (e) {}
104
+ if (process.platform !== "win32") {
105
+ try { fs.unlinkSync(socketPath()); } catch (e) {}
106
+ }
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) {}
100
133
  }
101
134
 
102
135
  // --- ~/.clayrc (recent projects persistence) ---
@@ -177,6 +210,10 @@ module.exports = {
177
210
  isDaemonAliveAsync: isDaemonAliveAsync,
178
211
  generateSlug: generateSlug,
179
212
  clearStaleConfig: clearStaleConfig,
213
+ crashInfoPath: crashInfoPath,
214
+ writeCrashInfo: writeCrashInfo,
215
+ readCrashInfo: readCrashInfo,
216
+ clearCrashInfo: clearCrashInfo,
180
217
  clayrcPath: clayrcPath,
181
218
  loadClayrc: loadClayrc,
182
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
  }
@@ -251,8 +294,17 @@ function gracefulShutdown() {
251
294
 
252
295
  process.on("SIGTERM", gracefulShutdown);
253
296
  process.on("SIGINT", gracefulShutdown);
297
+ // Windows emits SIGHUP when console window closes
298
+ if (process.platform === "win32") {
299
+ process.on("SIGHUP", gracefulShutdown);
300
+ }
254
301
 
255
302
  process.on("uncaughtException", function (err) {
256
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
+ });
257
309
  gracefulShutdown();
258
310
  });
package/lib/ipc.js CHANGED
@@ -6,8 +6,10 @@ var fs = require("fs");
6
6
  * handler(msg) should return a response object (or a Promise of one).
7
7
  */
8
8
  function createIPCServer(sockPath, handler) {
9
- // Remove stale socket file
10
- try { fs.unlinkSync(sockPath); } catch (e) {}
9
+ // Remove stale socket file (not needed for Windows named pipes)
10
+ if (process.platform !== "win32") {
11
+ try { fs.unlinkSync(sockPath); } catch (e) {}
12
+ }
11
13
 
12
14
  var server = net.createServer(function (conn) {
13
15
  var buffer = "";
@@ -49,7 +51,9 @@ function createIPCServer(sockPath, handler) {
49
51
  return {
50
52
  close: function () {
51
53
  server.close();
52
- try { fs.unlinkSync(sockPath); } catch (e) {}
54
+ if (process.platform !== "win32") {
55
+ try { fs.unlinkSync(sockPath); } catch (e) {}
56
+ }
53
57
  },
54
58
  };
55
59
  }