clay-server 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
package/lib/daemon.js ADDED
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+
3
+ // --- Node version check ---
4
+ var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
5
+ if (nodeMajor < 20) {
6
+ console.error("\x1b[31m[clay] Node.js 20+ is required (current: " + process.version + ")\x1b[0m");
7
+ console.error("[clay] The Claude Agent SDK 0.2.40+ requires Node 20 for Symbol.dispose support.");
8
+ console.error("[clay] If you cannot upgrade Node, use claude-relay@2.4.3 which supports Node 18.");
9
+ console.error("");
10
+ console.error(" Upgrade Node: nvm install 22 && nvm use 22");
11
+ console.error(" Or use older: npx claude-relay@2.4.3");
12
+ process.exit(78); // EX_CONFIG — fatal config error, don't auto-restart
13
+ }
14
+
15
+ // Polyfill Symbol.dispose/asyncDispose if missing (Node 20.x may not have it)
16
+ if (!Symbol.dispose) Symbol.dispose = Symbol("Symbol.dispose");
17
+ if (!Symbol.asyncDispose) Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
18
+
19
+ // Remove CLAUDECODE env var so the SDK can spawn Claude Code child processes
20
+ // (prevents "cannot be launched inside another Claude Code session" error)
21
+ delete process.env.CLAUDECODE;
22
+
23
+ var fs = require("fs");
24
+ var path = require("path");
25
+ var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
26
+ var { createIPCServer } = require("./ipc");
27
+ var { createServer, generateAuthToken } = require("./server");
28
+
29
+ var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
30
+ var config;
31
+
32
+ try {
33
+ config = JSON.parse(fs.readFileSync(configFile, "utf8"));
34
+ } catch (e) {
35
+ console.error("[daemon] Failed to read config:", e.message);
36
+ process.exit(1);
37
+ }
38
+
39
+ // --- TLS ---
40
+ var tlsOptions = null;
41
+ if (config.tls) {
42
+ var os = require("os");
43
+ var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(os.homedir(), ".clay"), "certs");
44
+ var keyPath = path.join(certDir, "key.pem");
45
+ var certPath = path.join(certDir, "cert.pem");
46
+ try {
47
+ tlsOptions = {
48
+ key: fs.readFileSync(keyPath),
49
+ cert: fs.readFileSync(certPath),
50
+ };
51
+ } catch (e) {
52
+ console.error("[daemon] TLS cert not found, falling back to HTTP");
53
+ }
54
+ }
55
+
56
+ var caRoot = null;
57
+ try {
58
+ var { execSync } = require("child_process");
59
+ caRoot = path.join(
60
+ execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
61
+ "rootCA.pem"
62
+ );
63
+ if (!fs.existsSync(caRoot)) caRoot = null;
64
+ } catch (e) {}
65
+
66
+ // --- Resolve LAN IP for share URL ---
67
+ var os2 = require("os");
68
+ var lanIp = (function () {
69
+ var ifaces = os2.networkInterfaces();
70
+ for (var addrs of Object.values(ifaces)) {
71
+ for (var i = 0; i < addrs.length; i++) {
72
+ if (addrs[i].family === "IPv4" && !addrs[i].internal && addrs[i].address.startsWith("100.")) return addrs[i].address;
73
+ }
74
+ }
75
+ for (var addrs of Object.values(ifaces)) {
76
+ for (var i = 0; i < addrs.length; i++) {
77
+ if (addrs[i].family === "IPv4" && !addrs[i].internal) return addrs[i].address;
78
+ }
79
+ }
80
+ return null;
81
+ })();
82
+
83
+ // --- Create multi-project server ---
84
+ var relay = createServer({
85
+ tlsOptions: tlsOptions,
86
+ caPath: caRoot,
87
+ pinHash: config.pinHash || null,
88
+ port: config.port,
89
+ debug: config.debug || false,
90
+ dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
91
+ lanHost: lanIp ? lanIp + ":" + config.port : null,
92
+ onAddProject: function (absPath) {
93
+ // Check if already registered
94
+ for (var j = 0; j < config.projects.length; j++) {
95
+ if (config.projects[j].path === absPath) {
96
+ return { ok: true, slug: config.projects[j].slug, existing: true };
97
+ }
98
+ }
99
+ var slugs = config.projects.map(function (p) { return p.slug; });
100
+ var slug = generateSlug(absPath, slugs);
101
+ relay.addProject(absPath, slug);
102
+ config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
103
+ saveConfig(config);
104
+ try { syncClayrc(config.projects); } catch (e) {}
105
+ console.log("[daemon] Added project (web):", slug, "→", absPath);
106
+ // Broadcast updated project list to all clients
107
+ relay.broadcastAll({
108
+ type: "projects_updated",
109
+ projects: relay.getProjects(),
110
+ projectCount: config.projects.length,
111
+ });
112
+ return { ok: true, slug: slug };
113
+ },
114
+ onRemoveProject: function (slug) {
115
+ var found = false;
116
+ for (var j = 0; j < config.projects.length; j++) {
117
+ if (config.projects[j].slug === slug) { found = true; break; }
118
+ }
119
+ if (!found) return { ok: false, error: "Project not found" };
120
+ relay.removeProject(slug);
121
+ config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
122
+ saveConfig(config);
123
+ try { syncClayrc(config.projects); } catch (e) {}
124
+ console.log("[daemon] Removed project (web):", slug);
125
+ relay.broadcastAll({
126
+ type: "projects_updated",
127
+ projects: relay.getProjects(),
128
+ projectCount: config.projects.length,
129
+ });
130
+ return { ok: true };
131
+ },
132
+ onGetDaemonConfig: function () {
133
+ return {
134
+ port: config.port,
135
+ tls: !!tlsOptions,
136
+ debug: !!config.debug,
137
+ keepAwake: !!config.keepAwake,
138
+ pinEnabled: !!config.pinHash,
139
+ platform: process.platform,
140
+ };
141
+ },
142
+ onSetPin: function (pin) {
143
+ if (pin) {
144
+ config.pinHash = generateAuthToken(pin);
145
+ } else {
146
+ config.pinHash = null;
147
+ }
148
+ relay.setAuthToken(config.pinHash);
149
+ saveConfig(config);
150
+ console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
151
+ return { ok: true, pinEnabled: !!config.pinHash };
152
+ },
153
+ onSetKeepAwake: function (value) {
154
+ var want = !!value;
155
+ config.keepAwake = want;
156
+ saveConfig(config);
157
+ if (want && !caffeinateProc && process.platform === "darwin") {
158
+ try {
159
+ var { spawn: spawnCaff } = require("child_process");
160
+ caffeinateProc = spawnCaff("caffeinate", ["-di"], { stdio: "ignore", detached: false });
161
+ caffeinateProc.on("error", function () { caffeinateProc = null; });
162
+ } catch (e) {}
163
+ } else if (!want && caffeinateProc) {
164
+ try { caffeinateProc.kill(); } catch (e) {}
165
+ caffeinateProc = null;
166
+ }
167
+ console.log("[daemon] Keep awake:", want, "(web)");
168
+ return { ok: true, keepAwake: want };
169
+ },
170
+ onShutdown: function () {
171
+ console.log("[daemon] Shutdown requested via web UI");
172
+ gracefulShutdown();
173
+ },
174
+ });
175
+
176
+ // --- Register projects ---
177
+ var projects = config.projects || [];
178
+ for (var i = 0; i < projects.length; i++) {
179
+ var p = projects[i];
180
+ if (fs.existsSync(p.path)) {
181
+ console.log("[daemon] Adding project:", p.slug, "→", p.path);
182
+ relay.addProject(p.path, p.slug, p.title);
183
+ } else {
184
+ console.log("[daemon] Skipping missing project:", p.path);
185
+ }
186
+ }
187
+
188
+ // Sync ~/.clayrc on startup
189
+ try { syncClayrc(config.projects); } catch (e) {}
190
+
191
+ // --- IPC server ---
192
+ var ipc = createIPCServer(socketPath(), function (msg) {
193
+ switch (msg.cmd) {
194
+ case "add_project": {
195
+ if (!msg.path) return { ok: false, error: "missing path" };
196
+ var absPath = path.resolve(msg.path);
197
+ // Check if already registered
198
+ for (var j = 0; j < config.projects.length; j++) {
199
+ if (config.projects[j].path === absPath) {
200
+ return { ok: true, slug: config.projects[j].slug, existing: true };
201
+ }
202
+ }
203
+ var slugs = config.projects.map(function (p) { return p.slug; });
204
+ var slug = generateSlug(absPath, slugs);
205
+ relay.addProject(absPath, slug);
206
+ config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
207
+ saveConfig(config);
208
+ try { syncClayrc(config.projects); } catch (e) {}
209
+ console.log("[daemon] Added project:", slug, "→", absPath);
210
+ return { ok: true, slug: slug };
211
+ }
212
+
213
+ case "remove_project": {
214
+ if (!msg.path && !msg.slug) return { ok: false, error: "missing path or slug" };
215
+ var target = msg.slug;
216
+ if (!target) {
217
+ var abs = path.resolve(msg.path);
218
+ for (var k = 0; k < config.projects.length; k++) {
219
+ if (config.projects[k].path === abs) {
220
+ target = config.projects[k].slug;
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ if (!target) return { ok: false, error: "project not found" };
226
+ relay.removeProject(target);
227
+ config.projects = config.projects.filter(function (p) { return p.slug !== target; });
228
+ saveConfig(config);
229
+ try { syncClayrc(config.projects); } catch (e) {}
230
+ console.log("[daemon] Removed project:", target);
231
+ return { ok: true };
232
+ }
233
+
234
+ case "get_status":
235
+ return {
236
+ ok: true,
237
+ pid: process.pid,
238
+ port: config.port,
239
+ tls: !!tlsOptions,
240
+ keepAwake: !!config.keepAwake,
241
+ projects: relay.getProjects(),
242
+ uptime: process.uptime(),
243
+ };
244
+
245
+ case "set_pin": {
246
+ config.pinHash = msg.pinHash || null;
247
+ relay.setAuthToken(config.pinHash);
248
+ saveConfig(config);
249
+ return { ok: true };
250
+ }
251
+
252
+ case "set_project_title": {
253
+ if (!msg.slug) return { ok: false, error: "missing slug" };
254
+ var newTitle = msg.title || null;
255
+ relay.setProjectTitle(msg.slug, newTitle);
256
+ for (var ti = 0; ti < config.projects.length; ti++) {
257
+ if (config.projects[ti].slug === msg.slug) {
258
+ if (newTitle) {
259
+ config.projects[ti].title = newTitle;
260
+ } else {
261
+ delete config.projects[ti].title;
262
+ }
263
+ break;
264
+ }
265
+ }
266
+ saveConfig(config);
267
+ try { syncClayrc(config.projects); } catch (e) {}
268
+ console.log("[daemon] Project title:", msg.slug, "→", newTitle || "(default)");
269
+ return { ok: true };
270
+ }
271
+
272
+ case "set_keep_awake": {
273
+ var want = !!msg.value;
274
+ config.keepAwake = want;
275
+ saveConfig(config);
276
+ if (want && !caffeinateProc && process.platform === "darwin") {
277
+ try {
278
+ var { spawn: spawnCaff } = require("child_process");
279
+ caffeinateProc = spawnCaff("caffeinate", ["-di"], { stdio: "ignore", detached: false });
280
+ caffeinateProc.on("error", function () { caffeinateProc = null; });
281
+ } catch (e) {}
282
+ } else if (!want && caffeinateProc) {
283
+ try { caffeinateProc.kill(); } catch (e) {}
284
+ caffeinateProc = null;
285
+ }
286
+ console.log("[daemon] Keep awake:", want);
287
+ return { ok: true };
288
+ }
289
+
290
+ case "shutdown":
291
+ console.log("[daemon] Shutdown requested via IPC");
292
+ gracefulShutdown();
293
+ return { ok: true };
294
+
295
+ case "update": {
296
+ console.log("[daemon] Update & restart requested via IPC");
297
+
298
+ // Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
299
+ if (config.debug) {
300
+ console.log("[daemon] Dev mode — restarting via dev watcher");
301
+ updateHandoff = true;
302
+ setTimeout(function () { gracefulShutdown(); }, 100);
303
+ return { ok: true };
304
+ }
305
+
306
+ // Production: fetch latest via npx, then spawn updated daemon
307
+ var { execSync: execSyncUpd, spawn: spawnUpd } = require("child_process");
308
+ var updDaemonScript;
309
+ try {
310
+ // npx downloads the package and puts a bin symlink; `which` prints its path
311
+ var binPath = execSyncUpd(
312
+ "npx --yes --package=clay-server@latest -- which clay-server",
313
+ { stdio: ["ignore", "pipe", "pipe"], timeout: 120000, encoding: "utf8" }
314
+ ).trim();
315
+ // Resolve symlink to get the actual package directory
316
+ var realBin = fs.realpathSync(binPath);
317
+ updDaemonScript = path.join(path.dirname(realBin), "..", "lib", "daemon.js");
318
+ updDaemonScript = path.resolve(updDaemonScript);
319
+ console.log("[daemon] Resolved updated daemon:", updDaemonScript);
320
+ } catch (updErr) {
321
+ console.log("[daemon] npx resolve failed:", updErr.message);
322
+ // Fallback: restart with current code
323
+ updDaemonScript = path.join(__dirname, "daemon.js");
324
+ }
325
+ // Spawn new daemon process — it will retry if port is still in use
326
+ var { logPath: updLogPath, configPath: updConfigPath } = require("./config");
327
+ var updLogFd = fs.openSync(updLogPath(), "a");
328
+ var updChild = spawnUpd(process.execPath, [updDaemonScript], {
329
+ detached: true,
330
+ windowsHide: true,
331
+ stdio: ["ignore", updLogFd, updLogFd],
332
+ env: Object.assign({}, process.env, {
333
+ CLAY_CONFIG: updConfigPath(),
334
+ }),
335
+ });
336
+ updChild.unref();
337
+ fs.closeSync(updLogFd);
338
+ config.pid = updChild.pid;
339
+ saveConfig(config);
340
+ console.log("[daemon] Spawned new daemon (PID " + updChild.pid + "), shutting down...");
341
+ updateHandoff = true;
342
+ setTimeout(function () { gracefulShutdown(); }, 100);
343
+ return { ok: true };
344
+ }
345
+
346
+ default:
347
+ return { ok: false, error: "unknown command: " + msg.cmd };
348
+ }
349
+ });
350
+
351
+ // --- Start listening (with retry for port-in-use during update handoff) ---
352
+ var listenRetries = 0;
353
+ var MAX_LISTEN_RETRIES = 15;
354
+
355
+ function startListening() {
356
+ relay.server.listen(config.port, function () {
357
+ var protocol = tlsOptions ? "https" : "http";
358
+ console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
359
+ console.log("[daemon] PID:", process.pid);
360
+ console.log("[daemon] Projects:", config.projects.length);
361
+
362
+ // Update PID in config
363
+ config.pid = process.pid;
364
+ saveConfig(config);
365
+
366
+ // Check for crash info from a previous crash and notify clients
367
+ var crashInfo = readCrashInfo();
368
+ if (crashInfo) {
369
+ console.log("[daemon] Recovered from crash at", new Date(crashInfo.time).toISOString());
370
+ console.log("[daemon] Crash reason:", crashInfo.reason);
371
+ // Delay notification so clients have time to reconnect
372
+ setTimeout(function () {
373
+ relay.broadcastAll({
374
+ type: "toast",
375
+ level: "warn",
376
+ message: "Server recovered from a crash and was automatically restarted.",
377
+ detail: crashInfo.reason || null,
378
+ });
379
+ }, 3000);
380
+ clearCrashInfo();
381
+ }
382
+ });
383
+ }
384
+
385
+ relay.server.on("error", function (err) {
386
+ if (err.code === "EADDRINUSE" && listenRetries < MAX_LISTEN_RETRIES) {
387
+ listenRetries++;
388
+ console.log("[daemon] Port " + config.port + " in use, retrying (" + listenRetries + "/" + MAX_LISTEN_RETRIES + ")...");
389
+ setTimeout(startListening, 1000);
390
+ return;
391
+ }
392
+ console.error("[daemon] Server error:", err.message);
393
+ writeCrashInfo({
394
+ reason: "Server error: " + err.message,
395
+ pid: process.pid,
396
+ time: Date.now(),
397
+ });
398
+ process.exit(1);
399
+ });
400
+
401
+ startListening();
402
+
403
+ // --- HTTP onboarding server (only when TLS is active) ---
404
+ if (relay.onboardingServer) {
405
+ var onboardingPort = config.port + 1;
406
+ relay.onboardingServer.on("error", function (err) {
407
+ console.error("[daemon] Onboarding HTTP server error:", err.message);
408
+ });
409
+ relay.onboardingServer.listen(onboardingPort, function () {
410
+ console.log("[daemon] Onboarding HTTP on http://0.0.0.0:" + onboardingPort);
411
+ });
412
+ }
413
+
414
+ // --- Caffeinate (macOS) ---
415
+ var caffeinateProc = null;
416
+ if (config.keepAwake && process.platform === "darwin") {
417
+ try {
418
+ var { spawn } = require("child_process");
419
+ caffeinateProc = spawn("caffeinate", ["-di"], { stdio: "ignore", detached: false });
420
+ caffeinateProc.on("error", function () { caffeinateProc = null; });
421
+ } catch (e) {}
422
+ }
423
+
424
+ // --- Graceful shutdown ---
425
+ var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
426
+
427
+ function gracefulShutdown() {
428
+ console.log("[daemon] Shutting down...");
429
+ var exitCode = updateHandoff ? 120 : 0; // 120 = update handoff, don't auto-restart
430
+
431
+ if (caffeinateProc) {
432
+ try { caffeinateProc.kill(); } catch (e) {}
433
+ }
434
+
435
+ ipc.close();
436
+
437
+ // Remove PID from config (skip if update handoff — new daemon PID is already saved)
438
+ if (!updateHandoff) {
439
+ try {
440
+ var c = loadConfig();
441
+ if (c && c.pid === process.pid) {
442
+ delete c.pid;
443
+ saveConfig(c);
444
+ }
445
+ } catch (e) {}
446
+ }
447
+
448
+ relay.destroyAll();
449
+
450
+ if (relay.onboardingServer) {
451
+ relay.onboardingServer.close();
452
+ }
453
+
454
+ relay.server.close(function () {
455
+ console.log("[daemon] Server closed");
456
+ process.exit(exitCode);
457
+ });
458
+
459
+ // Force exit after 5 seconds
460
+ setTimeout(function () {
461
+ console.error("[daemon] Forced exit after timeout");
462
+ process.exit(1);
463
+ }, 5000);
464
+ }
465
+
466
+ process.on("SIGTERM", gracefulShutdown);
467
+ process.on("SIGINT", gracefulShutdown);
468
+
469
+ // Last-resort cleanup: kill caffeinate if process exits without graceful shutdown
470
+ process.on("exit", function () {
471
+ if (caffeinateProc) {
472
+ try { caffeinateProc.kill(); } catch (e) {}
473
+ }
474
+ });
475
+
476
+ // Windows emits SIGHUP when console window closes
477
+ if (process.platform === "win32") {
478
+ process.on("SIGHUP", gracefulShutdown);
479
+ }
480
+
481
+ process.on("uncaughtException", function (err) {
482
+ console.error("[daemon] Uncaught exception:", err);
483
+ writeCrashInfo({
484
+ reason: err ? (err.stack || err.message || String(err)) : "unknown",
485
+ pid: process.pid,
486
+ time: Date.now(),
487
+ });
488
+ gracefulShutdown();
489
+ });
package/lib/ipc.js ADDED
@@ -0,0 +1,112 @@
1
+ var net = require("net");
2
+ var fs = require("fs");
3
+
4
+ /**
5
+ * Create IPC server on a Unix domain socket.
6
+ * handler(msg) should return a response object (or a Promise of one).
7
+ */
8
+ function createIPCServer(sockPath, handler) {
9
+ // Remove stale socket file (not needed for Windows named pipes)
10
+ if (process.platform !== "win32") {
11
+ try { fs.unlinkSync(sockPath); } catch (e) {}
12
+ }
13
+
14
+ var server = net.createServer(function (conn) {
15
+ var buffer = "";
16
+ conn.setEncoding("utf8");
17
+
18
+ conn.on("data", function (chunk) {
19
+ buffer += chunk;
20
+ var lines = buffer.split("\n");
21
+ buffer = lines.pop(); // keep incomplete line
22
+
23
+ for (var i = 0; i < lines.length; i++) {
24
+ if (!lines[i].trim()) continue;
25
+ try {
26
+ var msg = JSON.parse(lines[i]);
27
+ var result = handler(msg);
28
+ // Support both sync and async handlers
29
+ if (result && typeof result.then === "function") {
30
+ (function (c) {
31
+ result.then(function (res) {
32
+ try { c.write(JSON.stringify(res) + "\n"); } catch (e) {}
33
+ }).catch(function (err) {
34
+ try { c.write(JSON.stringify({ ok: false, error: err.message }) + "\n"); } catch (e) {}
35
+ });
36
+ })(conn);
37
+ } else {
38
+ conn.write(JSON.stringify(result) + "\n");
39
+ }
40
+ } catch (e) {
41
+ try { conn.write(JSON.stringify({ ok: false, error: "parse error" }) + "\n"); } catch (e2) {}
42
+ }
43
+ }
44
+ });
45
+
46
+ conn.on("error", function () {});
47
+ });
48
+
49
+ server.listen(sockPath);
50
+
51
+ return {
52
+ close: function () {
53
+ server.close();
54
+ if (process.platform !== "win32") {
55
+ try { fs.unlinkSync(sockPath); } catch (e) {}
56
+ }
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Send a command to the daemon IPC server and wait for response.
63
+ * Returns a Promise resolving to the parsed response.
64
+ */
65
+ function sendIPCCommand(sockPath, message) {
66
+ return new Promise(function (resolve) {
67
+ var client = net.connect(sockPath);
68
+ var buffer = "";
69
+ var done = false;
70
+
71
+ var timer = setTimeout(function () {
72
+ if (!done) {
73
+ done = true;
74
+ client.destroy();
75
+ resolve({ ok: false, error: "timeout" });
76
+ }
77
+ }, 3000);
78
+
79
+ client.on("connect", function () {
80
+ client.write(JSON.stringify(message) + "\n");
81
+ });
82
+
83
+ client.on("data", function (chunk) {
84
+ buffer += chunk;
85
+ var idx = buffer.indexOf("\n");
86
+ if (idx !== -1 && !done) {
87
+ done = true;
88
+ clearTimeout(timer);
89
+ try {
90
+ var resp = JSON.parse(buffer.substring(0, idx));
91
+ resolve(resp);
92
+ } catch (e) {
93
+ resolve({ ok: false, error: "invalid response" });
94
+ }
95
+ client.destroy();
96
+ }
97
+ });
98
+
99
+ client.on("error", function (err) {
100
+ if (!done) {
101
+ done = true;
102
+ clearTimeout(timer);
103
+ resolve({ ok: false, error: err.code === "ECONNREFUSED" ? "daemon not responding" : err.message });
104
+ }
105
+ });
106
+ });
107
+ }
108
+
109
+ module.exports = {
110
+ createIPCServer: createIPCServer,
111
+ sendIPCCommand: sendIPCCommand,
112
+ };