clay-server 2.6.0 → 2.7.1

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 (38) hide show
  1. package/bin/cli.js +53 -4
  2. package/lib/config.js +15 -6
  3. package/lib/daemon.js +47 -5
  4. package/lib/ipc.js +12 -0
  5. package/lib/notes.js +2 -2
  6. package/lib/project.js +883 -2
  7. package/lib/public/app.js +862 -14
  8. package/lib/public/css/diff.css +12 -0
  9. package/lib/public/css/filebrowser.css +1 -1
  10. package/lib/public/css/loop.css +841 -0
  11. package/lib/public/css/menus.css +5 -0
  12. package/lib/public/css/mobile-nav.css +15 -15
  13. package/lib/public/css/rewind.css +23 -0
  14. package/lib/public/css/scheduler-modal.css +546 -0
  15. package/lib/public/css/scheduler.css +944 -0
  16. package/lib/public/css/sidebar.css +1 -0
  17. package/lib/public/css/skills.css +59 -0
  18. package/lib/public/css/sticky-notes.css +486 -0
  19. package/lib/public/css/title-bar.css +83 -3
  20. package/lib/public/index.html +181 -3
  21. package/lib/public/modules/diff.js +3 -3
  22. package/lib/public/modules/filebrowser.js +169 -45
  23. package/lib/public/modules/input.js +17 -3
  24. package/lib/public/modules/markdown.js +10 -0
  25. package/lib/public/modules/qrcode.js +23 -26
  26. package/lib/public/modules/scheduler.js +1240 -0
  27. package/lib/public/modules/server-settings.js +40 -0
  28. package/lib/public/modules/sidebar.js +12 -0
  29. package/lib/public/modules/skills.js +84 -0
  30. package/lib/public/modules/sticky-notes.js +617 -52
  31. package/lib/public/modules/theme.js +9 -19
  32. package/lib/public/modules/tools.js +16 -2
  33. package/lib/public/style.css +3 -0
  34. package/lib/scheduler.js +362 -0
  35. package/lib/sdk-bridge.js +36 -0
  36. package/lib/sessions.js +9 -5
  37. package/lib/utils.js +49 -3
  38. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,5 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // --- Node version check (must run before any require that may use Node 20+ features) ---
4
+ var _nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
5
+ if (_nodeMajor < 20) {
6
+ console.error("");
7
+ console.error("\x1b[31m[clay] Node.js 20+ is required (current: " + process.version + ")\x1b[0m");
8
+ console.error("[clay] The Claude Agent SDK 0.2.40+ requires Node 20 for Symbol.dispose support.");
9
+ console.error("[clay] If you cannot upgrade Node, use claude-relay@2.4.3 which supports Node 18.");
10
+ console.error("");
11
+ console.error(" Upgrade Node: nvm install 22 && nvm use 22");
12
+ console.error(" Or use older: npx claude-relay@2.4.3");
13
+ console.error("");
14
+ process.exit(78);
15
+ }
16
+
3
17
  var os = require("os");
4
18
  var fs = require("fs");
5
19
  var path = require("path");
@@ -7,8 +21,9 @@ var { execSync, execFileSync, spawn } = require("child_process");
7
21
  var qrcode = require("qrcode-terminal");
8
22
  var net = require("net");
9
23
 
10
- // Detect dev mode (no separate storage — dev and prod share ~/.clay)
24
+ // Detect dev mode — dev and prod use separate daemon files so they can run simultaneously
11
25
  var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "clay-dev") || process.argv.includes("--dev");
26
+ if (_isDev) process.env.CLAY_DEV = "1";
12
27
 
13
28
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
14
29
  var { sendIPCCommand } = require("../lib/ipc");
@@ -33,12 +48,14 @@ var debugMode = false;
33
48
  var autoYes = false;
34
49
  var cliPin = null;
35
50
  var shutdownMode = false;
51
+ var restartMode = false;
36
52
  var addPath = null;
37
53
  var removePath = null;
38
54
  var listMode = false;
39
55
  var dangerouslySkipPermissions = false;
40
56
  var headlessMode = false;
41
57
  var watchMode = false;
58
+ var host = null;
42
59
 
43
60
  for (var i = 0; i < args.length; i++) {
44
61
  if (args[i] === "-p" || args[i] === "--port") {
@@ -48,6 +65,9 @@ for (var i = 0; i < args.length; i++) {
48
65
  process.exit(1);
49
66
  }
50
67
  i++;
68
+ } else if (args[i] === "--host" || args[i] === "--bind") {
69
+ host = args[i + 1] || null;
70
+ i++;
51
71
  } else if (args[i] === "--no-https") {
52
72
  useHttps = false;
53
73
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
@@ -65,6 +85,8 @@ for (var i = 0; i < args.length; i++) {
65
85
  i++;
66
86
  } else if (args[i] === "--shutdown") {
67
87
  shutdownMode = true;
88
+ } else if (args[i] === "--restart") {
89
+ restartMode = true;
68
90
  } else if (args[i] === "--add") {
69
91
  addPath = args[i + 1] || ".";
70
92
  i++;
@@ -79,19 +101,21 @@ for (var i = 0; i < args.length; i++) {
79
101
  } else if (args[i] === "--dangerously-skip-permissions") {
80
102
  dangerouslySkipPermissions = true;
81
103
  } else if (args[i] === "-h" || args[i] === "--help") {
82
- console.log("Usage: clay-server [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
104
+ console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
83
105
  console.log(" clay-server --add <path> Add a project to the running daemon");
84
106
  console.log(" clay-server --remove <path> Remove a project from the running daemon");
85
107
  console.log(" clay-server --list List registered projects");
86
108
  console.log("");
87
109
  console.log("Options:");
88
110
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
111
+ console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
89
112
  console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
90
113
  console.log(" --no-update Skip auto-update check on startup");
91
114
  console.log(" --debug Enable debug panel in the web UI");
92
115
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
93
116
  console.log(" --pin <pin> Set 6-digit PIN (use with --yes)");
94
117
  console.log(" --shutdown Shut down the running relay daemon");
118
+ console.log(" --restart Restart the running relay daemon");
95
119
  console.log(" --add <path> Add a project directory (use '.' for current)");
96
120
  console.log(" --remove <path> Remove a project directory");
97
121
  console.log(" --list List all registered projects");
@@ -128,6 +152,25 @@ if (shutdownMode) {
128
152
  return;
129
153
  }
130
154
 
155
+ // --- Handle --restart before anything else ---
156
+ if (restartMode) {
157
+ var restartConfig = loadConfig();
158
+ isDaemonAliveAsync(restartConfig).then(function (alive) {
159
+ if (!alive) {
160
+ console.error("No running daemon found.");
161
+ process.exit(1);
162
+ }
163
+ sendIPCCommand(socketPath(), { cmd: "restart" }).then(function () {
164
+ console.log("Server restarted.");
165
+ process.exit(0);
166
+ }).catch(function (err) {
167
+ console.error("Restart failed:", err.message);
168
+ process.exit(1);
169
+ });
170
+ });
171
+ return;
172
+ }
173
+
131
174
  // --- Handle --add before anything else ---
132
175
  if (addPath !== null) {
133
176
  var absAdd = path.resolve(addPath);
@@ -1277,6 +1320,7 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1277
1320
  var config = {
1278
1321
  pid: null,
1279
1322
  port: port,
1323
+ host: host,
1280
1324
  pinHash: pin ? generateAuthToken(pin) : null,
1281
1325
  tls: hasTls,
1282
1326
  debug: debugMode,
@@ -1386,6 +1430,7 @@ async function devMode(pin, keepAwake, existingPinHash) {
1386
1430
  var config = {
1387
1431
  pid: null,
1388
1432
  port: port,
1433
+ host: host,
1389
1434
  pinHash: existingPinHash || (pin ? generateAuthToken(pin) : null),
1390
1435
  tls: hasTls,
1391
1436
  debug: true,
@@ -2370,8 +2415,12 @@ var currentVersion = require("../package.json").version;
2370
2415
  if (autoRestorable.length > 0) {
2371
2416
  console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
2372
2417
  }
2373
- var hasRestorable = autoRestorable.length > 0;
2374
- await forkDaemon(pin, false, hasRestorable ? autoRestorable : undefined, !hasRestorable);
2418
+ // Add cwd if it has history in .clayrc, or if there are no other projects to restore
2419
+ var cwdInRc = (autoRc.recentProjects || []).some(function (p) {
2420
+ return p.path === cwd;
2421
+ });
2422
+ var addCwd = cwdInRc || autoRestorable.length === 0;
2423
+ await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
2375
2424
  } else {
2376
2425
  setup(function (pin, keepAwake) {
2377
2426
  if (dangerouslySkipPermissions && !pin) {
package/lib/config.js CHANGED
@@ -70,20 +70,23 @@ var CONFIG_DIR = CLAY_HOME;
70
70
  var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
71
71
  var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
72
72
 
73
+ // Dev mode uses separate daemon files so dev and prod can run simultaneously
74
+ var _devMode = !!process.env.CLAY_DEV;
75
+
73
76
  function configPath() {
74
- return path.join(CONFIG_DIR, "daemon.json");
77
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.json" : "daemon.json");
75
78
  }
76
79
 
77
80
  function socketPath() {
78
81
  if (process.platform === "win32") {
79
- var pipeName = "clay-daemon";
82
+ var pipeName = _devMode ? "clay-daemon-dev" : "clay-daemon";
80
83
  return "\\\\.\\pipe\\" + pipeName;
81
84
  }
82
- return path.join(CONFIG_DIR, "daemon.sock");
85
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.sock" : "daemon.sock");
83
86
  }
84
87
 
85
88
  function logPath() {
86
- return path.join(CONFIG_DIR, "daemon.log");
89
+ return path.join(CONFIG_DIR, _devMode ? "daemon-dev.log" : "daemon.log");
87
90
  }
88
91
 
89
92
  function ensureConfigDir() {
@@ -117,7 +120,10 @@ function isPidAlive(pid) {
117
120
 
118
121
  function isDaemonAlive(config) {
119
122
  if (!config || !config.pid) return false;
120
- if (!isPidAlive(config.pid)) return false;
123
+ if (!isPidAlive(config.pid)) {
124
+ clearStaleConfig();
125
+ return false;
126
+ }
121
127
  // Named pipes on Windows can't be stat'd, just check PID
122
128
  if (process.platform === "win32") return true;
123
129
  try {
@@ -131,7 +137,10 @@ function isDaemonAlive(config) {
131
137
  function isDaemonAliveAsync(config) {
132
138
  return new Promise(function (resolve) {
133
139
  if (!config || !config.pid) return resolve(false);
134
- if (!isPidAlive(config.pid)) return resolve(false);
140
+ if (!isPidAlive(config.pid)) {
141
+ clearStaleConfig();
142
+ return resolve(false);
143
+ }
135
144
 
136
145
  var sock = socketPath();
137
146
  var client = net.connect(sock);
package/lib/daemon.js CHANGED
@@ -22,7 +22,7 @@ delete process.env.CLAUDECODE;
22
22
 
23
23
  var fs = require("fs");
24
24
  var path = require("path");
25
- var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
25
+ var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo, isPidAlive, clearStaleConfig } = require("./config");
26
26
  var { createIPCServer } = require("./ipc");
27
27
  var { createServer, generateAuthToken } = require("./server");
28
28
 
@@ -81,6 +81,8 @@ var lanIp = (function () {
81
81
  })();
82
82
 
83
83
  // --- Create multi-project server ---
84
+ var listenHost = config.host || "0.0.0.0";
85
+
84
86
  var relay = createServer({
85
87
  tlsOptions: tlsOptions,
86
88
  caPath: caRoot,
@@ -387,6 +389,10 @@ var relay = createServer({
387
389
  console.log("[daemon] Shutdown requested via web UI");
388
390
  gracefulShutdown();
389
391
  },
392
+ onRestart: function () {
393
+ console.log("[daemon] Restart requested via web UI");
394
+ spawnAndRestart();
395
+ },
390
396
  });
391
397
 
392
398
  // --- Register projects ---
@@ -405,6 +411,14 @@ for (var i = 0; i < projects.length; i++) {
405
411
  try { syncClayrc(config.projects); } catch (e) {}
406
412
 
407
413
  // --- IPC server ---
414
+ // Clean up stale socket/config left by a previously killed daemon
415
+ var existingConfig = loadConfig();
416
+ if (existingConfig && existingConfig.pid && existingConfig.pid !== process.pid) {
417
+ if (!isPidAlive(existingConfig.pid)) {
418
+ console.log("[daemon] Clearing stale config from dead PID " + existingConfig.pid);
419
+ clearStaleConfig();
420
+ }
421
+ }
408
422
  var ipc = createIPCServer(socketPath(), function (msg) {
409
423
  switch (msg.cmd) {
410
424
  case "add_project": {
@@ -523,6 +537,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
523
537
  gracefulShutdown();
524
538
  return { ok: true };
525
539
 
540
+ case "restart":
541
+ console.log("[daemon] Restart requested via IPC");
542
+ spawnAndRestart();
543
+ return { ok: true };
544
+
526
545
  case "update": {
527
546
  console.log("[daemon] Update & restart requested via IPC");
528
547
 
@@ -584,9 +603,9 @@ var listenRetries = 0;
584
603
  var MAX_LISTEN_RETRIES = 15;
585
604
 
586
605
  function startListening() {
587
- relay.server.listen(config.port, function () {
606
+ relay.server.listen(config.port, listenHost, function () {
588
607
  var protocol = tlsOptions ? "https" : "http";
589
- console.log("[daemon] Listening on " + protocol + "://0.0.0.0:" + config.port);
608
+ console.log("[daemon] Listening on " + protocol + "://" + listenHost + ":" + config.port);
590
609
  console.log("[daemon] PID:", process.pid);
591
610
  console.log("[daemon] Projects:", config.projects.length);
592
611
 
@@ -637,8 +656,8 @@ if (relay.onboardingServer) {
637
656
  relay.onboardingServer.on("error", function (err) {
638
657
  console.error("[daemon] Onboarding HTTP server error:", err.message);
639
658
  });
640
- relay.onboardingServer.listen(onboardingPort, function () {
641
- console.log("[daemon] Onboarding HTTP on http://0.0.0.0:" + onboardingPort);
659
+ relay.onboardingServer.listen(onboardingPort, listenHost, function () {
660
+ console.log("[daemon] Onboarding HTTP on http://" + listenHost + ":" + onboardingPort);
642
661
  });
643
662
  }
644
663
 
@@ -652,6 +671,29 @@ if (config.keepAwake && process.platform === "darwin") {
652
671
  } catch (e) {}
653
672
  }
654
673
 
674
+ // --- Spawn new daemon and graceful restart ---
675
+ function spawnAndRestart() {
676
+ var { spawn: spawnRestart } = require("child_process");
677
+ var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
678
+ var daemonScript = path.join(__dirname, "daemon.js");
679
+ var logFd = fs.openSync(restartLogPath(), "a");
680
+ var child = spawnRestart(process.execPath, [daemonScript], {
681
+ detached: true,
682
+ windowsHide: true,
683
+ stdio: ["ignore", logFd, logFd],
684
+ env: Object.assign({}, process.env, {
685
+ CLAY_CONFIG: restartConfigPath(),
686
+ }),
687
+ });
688
+ child.unref();
689
+ fs.closeSync(logFd);
690
+ config.pid = child.pid;
691
+ saveConfig(config);
692
+ console.log("[daemon] Spawned new daemon (PID " + child.pid + "), shutting down...");
693
+ updateHandoff = true;
694
+ setTimeout(function () { gracefulShutdown(); }, 100);
695
+ }
696
+
655
697
  // --- Graceful shutdown ---
656
698
  var updateHandoff = false; // true when shutting down for update (new daemon already spawned)
657
699
 
package/lib/ipc.js CHANGED
@@ -46,6 +46,18 @@ function createIPCServer(sockPath, handler) {
46
46
  conn.on("error", function () {});
47
47
  });
48
48
 
49
+ var retried = false;
50
+ server.on("error", function (err) {
51
+ if (err.code === "EADDRINUSE" && !retried) {
52
+ retried = true;
53
+ console.log("[ipc] Socket in use, removing stale socket and retrying...");
54
+ try { fs.unlinkSync(sockPath); } catch (e) {}
55
+ server.listen(sockPath);
56
+ } else {
57
+ console.error("[ipc] Failed to bind socket:", err.message);
58
+ process.exit(1);
59
+ }
60
+ });
49
61
  server.listen(sockPath);
50
62
 
51
63
  return {
package/lib/notes.js CHANGED
@@ -8,8 +8,8 @@ function createNotesManager(opts) {
8
8
  var cwd = opts.cwd;
9
9
 
10
10
  // Storage path: ~/.clay/notes/{encodedCwd}.json
11
- var encodedCwd = utils.encodeCwd(cwd);
12
11
  var notesDir = path.join(config.CONFIG_DIR, "notes");
12
+ var encodedCwd = utils.resolveEncodedFile(notesDir, cwd, ".json");
13
13
  var notesFile = path.join(notesDir, encodedCwd + ".json");
14
14
 
15
15
  // In-memory cache
@@ -67,7 +67,7 @@ function createNotesManager(opts) {
67
67
  function update(id, changes) {
68
68
  for (var i = 0; i < notes.length; i++) {
69
69
  if (notes[i].id === id) {
70
- var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "zIndex"];
70
+ var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "hidden", "zIndex"];
71
71
  for (var j = 0; j < allowed.length; j++) {
72
72
  var key = allowed[j];
73
73
  if (changes[key] !== undefined) {