claude-relay 2.4.2 → 2.5.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.
Files changed (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
package/lib/config.js DELETED
@@ -1,222 +0,0 @@
1
- var fs = require("fs");
2
- var path = require("path");
3
- var os = require("os");
4
- var net = require("net");
5
-
6
- var CONFIG_DIR = process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay");
7
- var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
8
- var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
9
-
10
- function configPath() {
11
- return path.join(CONFIG_DIR, "daemon.json");
12
- }
13
-
14
- function socketPath() {
15
- if (process.platform === "win32") {
16
- var pipeName = process.env.CLAUDE_RELAY_HOME ? "claude-relay-dev-daemon" : "claude-relay-daemon";
17
- return "\\\\.\\pipe\\" + pipeName;
18
- }
19
- return path.join(CONFIG_DIR, "daemon.sock");
20
- }
21
-
22
- function logPath() {
23
- return path.join(CONFIG_DIR, "daemon.log");
24
- }
25
-
26
- function ensureConfigDir() {
27
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
28
- }
29
-
30
- function loadConfig() {
31
- try {
32
- var data = fs.readFileSync(configPath(), "utf8");
33
- return JSON.parse(data);
34
- } catch (e) {
35
- return null;
36
- }
37
- }
38
-
39
- function saveConfig(config) {
40
- ensureConfigDir();
41
- var tmpPath = configPath() + ".tmp";
42
- fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
43
- fs.renameSync(tmpPath, configPath());
44
- }
45
-
46
- function isPidAlive(pid) {
47
- try {
48
- process.kill(pid, 0);
49
- return true;
50
- } catch (e) {
51
- return false;
52
- }
53
- }
54
-
55
- function isDaemonAlive(config) {
56
- if (!config || !config.pid) return false;
57
- if (!isPidAlive(config.pid)) return false;
58
- // Named pipes on Windows can't be stat'd, just check PID
59
- if (process.platform === "win32") return true;
60
- try {
61
- fs.statSync(socketPath());
62
- return true;
63
- } catch (e) {
64
- return false;
65
- }
66
- }
67
-
68
- function isDaemonAliveAsync(config) {
69
- return new Promise(function (resolve) {
70
- if (!config || !config.pid) return resolve(false);
71
- if (!isPidAlive(config.pid)) return resolve(false);
72
-
73
- var sock = socketPath();
74
- var client = net.connect(sock);
75
- var timer = setTimeout(function () {
76
- client.destroy();
77
- resolve(false);
78
- }, 1000);
79
-
80
- client.on("connect", function () {
81
- clearTimeout(timer);
82
- client.destroy();
83
- resolve(true);
84
- });
85
- client.on("error", function () {
86
- clearTimeout(timer);
87
- resolve(false);
88
- });
89
- });
90
- }
91
-
92
- function generateSlug(projectPath, existingSlugs) {
93
- var base = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
94
- if (!base) base = "project";
95
- if (!existingSlugs || existingSlugs.indexOf(base) === -1) return base;
96
- for (var i = 2; i < 100; i++) {
97
- var candidate = base + "-" + i;
98
- if (existingSlugs.indexOf(candidate) === -1) return candidate;
99
- }
100
- return base + "-" + Date.now();
101
- }
102
-
103
- function clearStaleConfig() {
104
- try { fs.unlinkSync(configPath()); } catch (e) {}
105
- if (process.platform !== "win32") {
106
- try { fs.unlinkSync(socketPath()); } catch (e) {}
107
- }
108
- }
109
-
110
- // --- Crash info ---
111
-
112
- function crashInfoPath() {
113
- return CRASH_INFO_PATH;
114
- }
115
-
116
- function writeCrashInfo(info) {
117
- try {
118
- ensureConfigDir();
119
- fs.writeFileSync(CRASH_INFO_PATH, JSON.stringify(info));
120
- } catch (e) {}
121
- }
122
-
123
- function readCrashInfo() {
124
- try {
125
- var data = fs.readFileSync(CRASH_INFO_PATH, "utf8");
126
- return JSON.parse(data);
127
- } catch (e) {
128
- return null;
129
- }
130
- }
131
-
132
- function clearCrashInfo() {
133
- try { fs.unlinkSync(CRASH_INFO_PATH); } catch (e) {}
134
- }
135
-
136
- // --- ~/.clayrc (recent projects persistence) ---
137
-
138
- function clayrcPath() {
139
- return CLAYRC_PATH;
140
- }
141
-
142
- function loadClayrc() {
143
- try {
144
- var data = fs.readFileSync(CLAYRC_PATH, "utf8");
145
- return JSON.parse(data);
146
- } catch (e) {
147
- return { recentProjects: [] };
148
- }
149
- }
150
-
151
- function saveClayrc(rc) {
152
- var tmpPath = CLAYRC_PATH + ".tmp";
153
- fs.writeFileSync(tmpPath, JSON.stringify(rc, null, 2) + "\n");
154
- fs.renameSync(tmpPath, CLAYRC_PATH);
155
- }
156
-
157
- /**
158
- * Update ~/.clayrc with the current project list from daemon config.
159
- * Merges with existing entries (preserves addedAt, updates lastUsed).
160
- */
161
- function syncClayrc(projects) {
162
- var rc = loadClayrc();
163
- var existing = rc.recentProjects || [];
164
-
165
- // Build a map by path for quick lookup
166
- var byPath = {};
167
- for (var i = 0; i < existing.length; i++) {
168
- byPath[existing[i].path] = existing[i];
169
- }
170
-
171
- // Update/add current projects
172
- for (var j = 0; j < projects.length; j++) {
173
- var p = projects[j];
174
- if (byPath[p.path]) {
175
- // Update existing entry
176
- byPath[p.path].slug = p.slug;
177
- byPath[p.path].lastUsed = Date.now();
178
- if (p.title) byPath[p.path].title = p.title;
179
- else delete byPath[p.path].title;
180
- } else {
181
- // New entry
182
- byPath[p.path] = {
183
- path: p.path,
184
- slug: p.slug,
185
- title: p.title || undefined,
186
- addedAt: p.addedAt || Date.now(),
187
- lastUsed: Date.now(),
188
- };
189
- }
190
- }
191
-
192
- // Rebuild array, sorted by lastUsed descending
193
- var all = Object.keys(byPath).map(function (k) { return byPath[k]; });
194
- all.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
195
-
196
- // Keep at most 20 recent projects
197
- rc.recentProjects = all.slice(0, 20);
198
- saveClayrc(rc);
199
- }
200
-
201
- module.exports = {
202
- CONFIG_DIR: CONFIG_DIR,
203
- configPath: configPath,
204
- socketPath: socketPath,
205
- logPath: logPath,
206
- ensureConfigDir: ensureConfigDir,
207
- loadConfig: loadConfig,
208
- saveConfig: saveConfig,
209
- isPidAlive: isPidAlive,
210
- isDaemonAlive: isDaemonAlive,
211
- isDaemonAliveAsync: isDaemonAliveAsync,
212
- generateSlug: generateSlug,
213
- clearStaleConfig: clearStaleConfig,
214
- crashInfoPath: crashInfoPath,
215
- writeCrashInfo: writeCrashInfo,
216
- readCrashInfo: readCrashInfo,
217
- clearCrashInfo: clearCrashInfo,
218
- clayrcPath: clayrcPath,
219
- loadClayrc: loadClayrc,
220
- saveClayrc: saveClayrc,
221
- syncClayrc: syncClayrc,
222
- };
package/lib/daemon.js DELETED
@@ -1,423 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Polyfill Symbol.dispose/asyncDispose for Node 18 (used by claude-agent-sdk)
4
- if (!Symbol.dispose) Symbol.dispose = Symbol("Symbol.dispose");
5
- if (!Symbol.asyncDispose) Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
6
-
7
- var fs = require("fs");
8
- var path = require("path");
9
- var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
10
- var { createIPCServer } = require("./ipc");
11
- var { createServer } = require("./server");
12
-
13
- var configFile = process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
14
- var config;
15
-
16
- try {
17
- config = JSON.parse(fs.readFileSync(configFile, "utf8"));
18
- } catch (e) {
19
- console.error("[daemon] Failed to read config:", e.message);
20
- process.exit(1);
21
- }
22
-
23
- // --- TLS ---
24
- var tlsOptions = null;
25
- if (config.tls) {
26
- var os = require("os");
27
- var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".claude-relay"), "certs");
28
- var keyPath = path.join(certDir, "key.pem");
29
- var certPath = path.join(certDir, "cert.pem");
30
- try {
31
- tlsOptions = {
32
- key: fs.readFileSync(keyPath),
33
- cert: fs.readFileSync(certPath),
34
- };
35
- } catch (e) {
36
- console.error("[daemon] TLS cert not found, falling back to HTTP");
37
- }
38
- }
39
-
40
- var caRoot = null;
41
- try {
42
- var { execSync } = require("child_process");
43
- caRoot = path.join(
44
- execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
45
- "rootCA.pem"
46
- );
47
- if (!fs.existsSync(caRoot)) caRoot = null;
48
- } catch (e) {}
49
-
50
- // --- Resolve LAN IP for share URL ---
51
- var os2 = require("os");
52
- var lanIp = (function () {
53
- var ifaces = os2.networkInterfaces();
54
- for (var addrs of Object.values(ifaces)) {
55
- for (var i = 0; i < addrs.length; i++) {
56
- if (addrs[i].family === "IPv4" && !addrs[i].internal && addrs[i].address.startsWith("100.")) return addrs[i].address;
57
- }
58
- }
59
- for (var addrs of Object.values(ifaces)) {
60
- for (var i = 0; i < addrs.length; i++) {
61
- if (addrs[i].family === "IPv4" && !addrs[i].internal) return addrs[i].address;
62
- }
63
- }
64
- return null;
65
- })();
66
-
67
- // --- Create multi-project server ---
68
- var relay = createServer({
69
- tlsOptions: tlsOptions,
70
- caPath: caRoot,
71
- pinHash: config.pinHash || null,
72
- port: config.port,
73
- debug: config.debug || false,
74
- dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
75
- lanHost: lanIp ? lanIp + ":" + config.port : null,
76
- onAddProject: function (absPath) {
77
- // Check if already registered
78
- for (var j = 0; j < config.projects.length; j++) {
79
- if (config.projects[j].path === absPath) {
80
- return { ok: true, slug: config.projects[j].slug, existing: true };
81
- }
82
- }
83
- var slugs = config.projects.map(function (p) { return p.slug; });
84
- var slug = generateSlug(absPath, slugs);
85
- relay.addProject(absPath, slug);
86
- config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
87
- saveConfig(config);
88
- try { syncClayrc(config.projects); } catch (e) {}
89
- console.log("[daemon] Added project (web):", slug, "→", absPath);
90
- // Broadcast updated project list to all clients
91
- relay.broadcastAll({
92
- type: "projects_updated",
93
- projects: relay.getProjects(),
94
- projectCount: config.projects.length,
95
- });
96
- return { ok: true, slug: slug };
97
- },
98
- onRemoveProject: function (slug) {
99
- var found = false;
100
- for (var j = 0; j < config.projects.length; j++) {
101
- if (config.projects[j].slug === slug) { found = true; break; }
102
- }
103
- if (!found) return { ok: false, error: "Project not found" };
104
- relay.removeProject(slug);
105
- config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
106
- saveConfig(config);
107
- try { syncClayrc(config.projects); } catch (e) {}
108
- console.log("[daemon] Removed project (web):", slug);
109
- relay.broadcastAll({
110
- type: "projects_updated",
111
- projects: relay.getProjects(),
112
- projectCount: config.projects.length,
113
- });
114
- return { ok: true };
115
- },
116
- });
117
-
118
- // --- Register projects ---
119
- var projects = config.projects || [];
120
- for (var i = 0; i < projects.length; i++) {
121
- var p = projects[i];
122
- if (fs.existsSync(p.path)) {
123
- console.log("[daemon] Adding project:", p.slug, "→", p.path);
124
- relay.addProject(p.path, p.slug, p.title);
125
- } else {
126
- console.log("[daemon] Skipping missing project:", p.path);
127
- }
128
- }
129
-
130
- // Sync ~/.clayrc on startup
131
- try { syncClayrc(config.projects); } catch (e) {}
132
-
133
- // --- IPC server ---
134
- var ipc = createIPCServer(socketPath(), function (msg) {
135
- switch (msg.cmd) {
136
- case "add_project": {
137
- if (!msg.path) return { ok: false, error: "missing path" };
138
- var absPath = path.resolve(msg.path);
139
- // Check if already registered
140
- for (var j = 0; j < config.projects.length; j++) {
141
- if (config.projects[j].path === absPath) {
142
- return { ok: true, slug: config.projects[j].slug, existing: true };
143
- }
144
- }
145
- var slugs = config.projects.map(function (p) { return p.slug; });
146
- var slug = generateSlug(absPath, slugs);
147
- relay.addProject(absPath, slug);
148
- config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
149
- saveConfig(config);
150
- try { syncClayrc(config.projects); } catch (e) {}
151
- console.log("[daemon] Added project:", slug, "→", absPath);
152
- return { ok: true, slug: slug };
153
- }
154
-
155
- case "remove_project": {
156
- if (!msg.path && !msg.slug) return { ok: false, error: "missing path or slug" };
157
- var target = msg.slug;
158
- if (!target) {
159
- var abs = path.resolve(msg.path);
160
- for (var k = 0; k < config.projects.length; k++) {
161
- if (config.projects[k].path === abs) {
162
- target = config.projects[k].slug;
163
- break;
164
- }
165
- }
166
- }
167
- if (!target) return { ok: false, error: "project not found" };
168
- relay.removeProject(target);
169
- config.projects = config.projects.filter(function (p) { return p.slug !== target; });
170
- saveConfig(config);
171
- try { syncClayrc(config.projects); } catch (e) {}
172
- console.log("[daemon] Removed project:", target);
173
- return { ok: true };
174
- }
175
-
176
- case "get_status":
177
- return {
178
- ok: true,
179
- pid: process.pid,
180
- port: config.port,
181
- tls: !!tlsOptions,
182
- keepAwake: !!config.keepAwake,
183
- projects: relay.getProjects(),
184
- uptime: process.uptime(),
185
- };
186
-
187
- case "set_pin": {
188
- config.pinHash = msg.pinHash || null;
189
- relay.setAuthToken(config.pinHash);
190
- saveConfig(config);
191
- return { ok: true };
192
- }
193
-
194
- case "set_project_title": {
195
- if (!msg.slug) return { ok: false, error: "missing slug" };
196
- var newTitle = msg.title || null;
197
- relay.setProjectTitle(msg.slug, newTitle);
198
- for (var ti = 0; ti < config.projects.length; ti++) {
199
- if (config.projects[ti].slug === msg.slug) {
200
- if (newTitle) {
201
- config.projects[ti].title = newTitle;
202
- } else {
203
- delete config.projects[ti].title;
204
- }
205
- break;
206
- }
207
- }
208
- saveConfig(config);
209
- try { syncClayrc(config.projects); } catch (e) {}
210
- console.log("[daemon] Project title:", msg.slug, "→", newTitle || "(default)");
211
- return { ok: true };
212
- }
213
-
214
- case "set_keep_awake": {
215
- var want = !!msg.value;
216
- config.keepAwake = want;
217
- saveConfig(config);
218
- if (want && !caffeinateProc && process.platform === "darwin") {
219
- try {
220
- var { spawn: spawnCaff } = require("child_process");
221
- caffeinateProc = spawnCaff("caffeinate", ["-di"], { stdio: "ignore", detached: false });
222
- caffeinateProc.on("error", function () { caffeinateProc = null; });
223
- } catch (e) {}
224
- } else if (!want && caffeinateProc) {
225
- try { caffeinateProc.kill(); } catch (e) {}
226
- caffeinateProc = null;
227
- }
228
- console.log("[daemon] Keep awake:", want);
229
- return { ok: true };
230
- }
231
-
232
- case "shutdown":
233
- console.log("[daemon] Shutdown requested via IPC");
234
- gracefulShutdown();
235
- return { ok: true };
236
-
237
- case "update": {
238
- console.log("[daemon] Update & restart requested via IPC");
239
-
240
- // Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
241
- if (config.debug) {
242
- console.log("[daemon] Dev mode — restarting via dev watcher");
243
- updateHandoff = true;
244
- setTimeout(function () { gracefulShutdown(); }, 100);
245
- return { ok: true };
246
- }
247
-
248
- // Production: fetch latest via npx, then spawn updated daemon
249
- var { execSync: execSyncUpd, spawn: spawnUpd } = require("child_process");
250
- var updDaemonScript;
251
- try {
252
- // npx downloads the package and puts a bin symlink; `which` prints its path
253
- var binPath = execSyncUpd(
254
- "npx --yes --package=claude-relay@latest -- which claude-relay",
255
- { stdio: ["ignore", "pipe", "pipe"], timeout: 120000, encoding: "utf8" }
256
- ).trim();
257
- // Resolve symlink to get the actual package directory
258
- var realBin = fs.realpathSync(binPath);
259
- updDaemonScript = path.join(path.dirname(realBin), "..", "lib", "daemon.js");
260
- updDaemonScript = path.resolve(updDaemonScript);
261
- console.log("[daemon] Resolved updated daemon:", updDaemonScript);
262
- } catch (updErr) {
263
- console.log("[daemon] npx resolve failed:", updErr.message);
264
- // Fallback: restart with current code
265
- updDaemonScript = path.join(__dirname, "daemon.js");
266
- }
267
- // Spawn new daemon process — it will retry if port is still in use
268
- var { logPath: updLogPath, configPath: updConfigPath } = require("./config");
269
- var updLogFd = fs.openSync(updLogPath(), "a");
270
- var updChild = spawnUpd(process.execPath, [updDaemonScript], {
271
- detached: true,
272
- windowsHide: true,
273
- stdio: ["ignore", updLogFd, updLogFd],
274
- env: Object.assign({}, process.env, {
275
- CLAUDE_RELAY_CONFIG: updConfigPath(),
276
- }),
277
- });
278
- updChild.unref();
279
- fs.closeSync(updLogFd);
280
- config.pid = updChild.pid;
281
- saveConfig(config);
282
- console.log("[daemon] Spawned new daemon (PID " + updChild.pid + "), shutting down...");
283
- updateHandoff = true;
284
- setTimeout(function () { gracefulShutdown(); }, 100);
285
- return { ok: true };
286
- }
287
-
288
- default:
289
- return { ok: false, error: "unknown command: " + msg.cmd };
290
- }
291
- });
292
-
293
- // --- Start listening (with retry for port-in-use during update handoff) ---
294
- var listenRetries = 0;
295
- var MAX_LISTEN_RETRIES = 15;
296
-
297
- function startListening() {
298
- relay.server.listen(config.port, function () {
299
- var protocol = tlsOptions ? "https" : "http";
300
- console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
301
- console.log("[daemon] PID:", process.pid);
302
- console.log("[daemon] Projects:", config.projects.length);
303
-
304
- // Update PID in config
305
- config.pid = process.pid;
306
- saveConfig(config);
307
-
308
- // Check for crash info from a previous crash and notify clients
309
- var crashInfo = readCrashInfo();
310
- if (crashInfo) {
311
- console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
312
- console.log("[daemon] Crash reason:", crashInfo.reason);
313
- // Delay notification so clients have time to reconnect
314
- setTimeout(function () {
315
- relay.broadcastAll({
316
- type: "toast",
317
- level: "warn",
318
- message: "Server recovered from a crash and was automatically restarted.",
319
- detail: crashInfo.reason || null,
320
- });
321
- }, 3000);
322
- clearCrashInfo();
323
- }
324
- });
325
- }
326
-
327
- relay.server.on("error", function (err) {
328
- if (err.code === "EADDRINUSE" && listenRetries < MAX_LISTEN_RETRIES) {
329
- listenRetries++;
330
- console.log("[daemon] Port " + config.port + " in use, retrying (" + listenRetries + "/" + MAX_LISTEN_RETRIES + ")...");
331
- setTimeout(startListening, 1000);
332
- return;
333
- }
334
- console.error("[daemon] Server error:", err.message);
335
- writeCrashInfo({
336
- reason: "Server error: " + err.message,
337
- pid: process.pid,
338
- time: Date.now(),
339
- });
340
- process.exit(1);
341
- });
342
-
343
- startListening();
344
-
345
- // --- HTTP onboarding server (only when TLS is active) ---
346
- if (relay.onboardingServer) {
347
- var onboardingPort = config.port + 1;
348
- relay.onboardingServer.on("error", function (err) {
349
- console.error("[daemon] Onboarding HTTP server error:", err.message);
350
- });
351
- relay.onboardingServer.listen(onboardingPort, function () {
352
- console.log("[daemon] Onboarding HTTP on http://0.0.0.0:" + onboardingPort);
353
- });
354
- }
355
-
356
- // --- Caffeinate (macOS) ---
357
- var caffeinateProc = null;
358
- if (config.keepAwake && process.platform === "darwin") {
359
- try {
360
- var { spawn } = require("child_process");
361
- caffeinateProc = spawn("caffeinate", ["-di"], { stdio: "ignore", detached: false });
362
- caffeinateProc.on("error", function () { caffeinateProc = null; });
363
- } catch (e) {}
364
- }
365
-
366
- // --- Graceful shutdown ---
367
- var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
368
-
369
- function gracefulShutdown() {
370
- console.log("[daemon] Shutting down...");
371
- var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
372
-
373
- if (caffeinateProc) {
374
- try { caffeinateProc.kill(); } catch (e) {}
375
- }
376
-
377
- ipc.close();
378
-
379
- // Remove PID from config (skip if update handoff — new daemon PID is already saved)
380
- if (!updateHandoff) {
381
- try {
382
- var c = loadConfig();
383
- if (c && c.pid === process.pid) {
384
- delete c.pid;
385
- saveConfig(c);
386
- }
387
- } catch (e) {}
388
- }
389
-
390
- relay.destroyAll();
391
-
392
- if (relay.onboardingServer) {
393
- relay.onboardingServer.close();
394
- }
395
-
396
- relay.server.close(function () {
397
- console.log("[daemon] Server closed");
398
- process.exit(exitCode);
399
- });
400
-
401
- // Force exit after 5 seconds
402
- setTimeout(function () {
403
- console.error("[daemon] Forced exit after timeout");
404
- process.exit(1);
405
- }, 5000);
406
- }
407
-
408
- process.on("SIGTERM", gracefulShutdown);
409
- process.on("SIGINT", gracefulShutdown);
410
- // Windows emits SIGHUP when console window closes
411
- if (process.platform === "win32") {
412
- process.on("SIGHUP", gracefulShutdown);
413
- }
414
-
415
- process.on("uncaughtException", function (err) {
416
- console.error("[daemon] Uncaught exception:", err);
417
- writeCrashInfo({
418
- reason: err ? (err.stack || err.message || String(err)) : "unknown",
419
- pid: process.pid,
420
- time: Date.now(),
421
- });
422
- gracefulShutdown();
423
- });