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 +12 -0
- package/bin/cli.js +271 -22
- package/lib/config.js +39 -2
- package/lib/daemon.js +53 -1
- package/lib/ipc.js +7 -3
- package/lib/pages.js +15 -1
- package/lib/project.js +324 -27
- 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 +40 -7
- package/lib/server.js +37 -4
- package/lib/sessions.js +14 -5
- package/lib/terminal.js +2 -1
- 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,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
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
if (process.platform !== "win32") {
|
|
55
|
+
try { fs.unlinkSync(sockPath); } catch (e) {}
|
|
56
|
+
}
|
|
53
57
|
},
|
|
54
58
|
};
|
|
55
59
|
}
|