clay-server 2.5.1 → 2.7.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 (41) hide show
  1. package/bin/claude-relay.js +6 -0
  2. package/bin/cli.js +77 -11
  3. package/lib/cli-sessions.js +2 -7
  4. package/lib/config.js +86 -7
  5. package/lib/daemon.js +279 -6
  6. package/lib/ipc.js +12 -0
  7. package/lib/notes.js +4 -3
  8. package/lib/project.js +1174 -28
  9. package/lib/public/app.js +879 -31
  10. package/lib/public/css/diff.css +15 -4
  11. package/lib/public/css/filebrowser.css +363 -3
  12. package/lib/public/css/icon-strip.css +317 -1
  13. package/lib/public/css/input.css +127 -50
  14. package/lib/public/css/loop.css +780 -0
  15. package/lib/public/css/messages.css +1 -1
  16. package/lib/public/css/mobile-nav.css +6 -2
  17. package/lib/public/css/rewind.css +23 -0
  18. package/lib/public/css/server-settings.css +67 -20
  19. package/lib/public/css/sidebar.css +10 -4
  20. package/lib/public/css/skills.css +789 -0
  21. package/lib/public/css/sticky-notes.css +486 -0
  22. package/lib/public/css/title-bar.css +157 -7
  23. package/lib/public/index.html +366 -55
  24. package/lib/public/modules/diff.js +3 -3
  25. package/lib/public/modules/filebrowser.js +169 -45
  26. package/lib/public/modules/input.js +123 -56
  27. package/lib/public/modules/project-settings.js +906 -0
  28. package/lib/public/modules/qrcode.js +23 -26
  29. package/lib/public/modules/server-settings.js +449 -53
  30. package/lib/public/modules/sidebar.js +732 -1
  31. package/lib/public/modules/skills.js +794 -0
  32. package/lib/public/modules/sticky-notes.js +617 -52
  33. package/lib/public/modules/terminal.js +7 -0
  34. package/lib/public/modules/theme.js +9 -19
  35. package/lib/public/modules/tools.js +16 -2
  36. package/lib/public/style.css +2 -0
  37. package/lib/sdk-bridge.js +46 -7
  38. package/lib/server.js +305 -1
  39. package/lib/sessions.js +11 -5
  40. package/lib/utils.js +18 -0
  41. package/package.json +3 -2
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.warn("\x1b[33m⚠ 'claude-relay' is deprecated and will be removed in a future version.\x1b[0m");
4
+ console.warn("\x1b[33m Use \x1b[1mnpx clay-server\x1b[22m instead.\x1b[0m\n");
5
+
6
+ require("./cli.js");
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,11 +21,8 @@ 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 before loading config (env must be set before require)
24
+ // Detect dev mode (no separate storage dev and prod share ~/.clay)
11
25
  var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "clay-dev") || process.argv.includes("--dev");
12
- if (_isDev) {
13
- process.env.CLAY_HOME = path.join(os.homedir(), ".clay-dev");
14
- }
15
26
 
16
27
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
17
28
  var { sendIPCCommand } = require("../lib/ipc");
@@ -36,12 +47,14 @@ var debugMode = false;
36
47
  var autoYes = false;
37
48
  var cliPin = null;
38
49
  var shutdownMode = false;
50
+ var restartMode = false;
39
51
  var addPath = null;
40
52
  var removePath = null;
41
53
  var listMode = false;
42
54
  var dangerouslySkipPermissions = false;
43
55
  var headlessMode = false;
44
56
  var watchMode = false;
57
+ var host = null;
45
58
 
46
59
  for (var i = 0; i < args.length; i++) {
47
60
  if (args[i] === "-p" || args[i] === "--port") {
@@ -51,6 +64,9 @@ for (var i = 0; i < args.length; i++) {
51
64
  process.exit(1);
52
65
  }
53
66
  i++;
67
+ } else if (args[i] === "--host" || args[i] === "--bind") {
68
+ host = args[i + 1] || null;
69
+ i++;
54
70
  } else if (args[i] === "--no-https") {
55
71
  useHttps = false;
56
72
  } else if (args[i] === "--no-update" || args[i] === "--skip-update") {
@@ -68,6 +84,8 @@ for (var i = 0; i < args.length; i++) {
68
84
  i++;
69
85
  } else if (args[i] === "--shutdown") {
70
86
  shutdownMode = true;
87
+ } else if (args[i] === "--restart") {
88
+ restartMode = true;
71
89
  } else if (args[i] === "--add") {
72
90
  addPath = args[i + 1] || ".";
73
91
  i++;
@@ -82,19 +100,21 @@ for (var i = 0; i < args.length; i++) {
82
100
  } else if (args[i] === "--dangerously-skip-permissions") {
83
101
  dangerouslySkipPermissions = true;
84
102
  } else if (args[i] === "-h" || args[i] === "--help") {
85
- console.log("Usage: clay-server [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
103
+ console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
86
104
  console.log(" clay-server --add <path> Add a project to the running daemon");
87
105
  console.log(" clay-server --remove <path> Remove a project from the running daemon");
88
106
  console.log(" clay-server --list List registered projects");
89
107
  console.log("");
90
108
  console.log("Options:");
91
109
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
110
+ console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
92
111
  console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
93
112
  console.log(" --no-update Skip auto-update check on startup");
94
113
  console.log(" --debug Enable debug panel in the web UI");
95
114
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
96
115
  console.log(" --pin <pin> Set 6-digit PIN (use with --yes)");
97
116
  console.log(" --shutdown Shut down the running relay daemon");
117
+ console.log(" --restart Restart the running relay daemon");
98
118
  console.log(" --add <path> Add a project directory (use '.' for current)");
99
119
  console.log(" --remove <path> Remove a project directory");
100
120
  console.log(" --list List all registered projects");
@@ -131,6 +151,25 @@ if (shutdownMode) {
131
151
  return;
132
152
  }
133
153
 
154
+ // --- Handle --restart before anything else ---
155
+ if (restartMode) {
156
+ var restartConfig = loadConfig();
157
+ isDaemonAliveAsync(restartConfig).then(function (alive) {
158
+ if (!alive) {
159
+ console.error("No running daemon found.");
160
+ process.exit(1);
161
+ }
162
+ sendIPCCommand(socketPath(), { cmd: "restart" }).then(function () {
163
+ console.log("Server restarted.");
164
+ process.exit(0);
165
+ }).catch(function (err) {
166
+ console.error("Restart failed:", err.message);
167
+ process.exit(1);
168
+ });
169
+ });
170
+ return;
171
+ }
172
+
134
173
  // --- Handle --add before anything else ---
135
174
  if (addPath !== null) {
136
175
  var absAdd = path.resolve(addPath);
@@ -1250,7 +1289,18 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1250
1289
  // Only include cwd if explicitly requested
1251
1290
  if (addCwd) {
1252
1291
  var slug = generateSlug(cwd, []);
1253
- allProjects.push({ path: cwd, slug: slug, addedAt: Date.now() });
1292
+ var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1293
+ // Restore title/icon from .clayrc if available
1294
+ var cwdRc = loadClayrc();
1295
+ var cwdRecent = cwdRc.recentProjects || [];
1296
+ for (var cr = 0; cr < cwdRecent.length; cr++) {
1297
+ if (cwdRecent[cr].path === cwd) {
1298
+ if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
1299
+ if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
1300
+ break;
1301
+ }
1302
+ }
1303
+ allProjects.push(cwdEntry);
1254
1304
  usedSlugs.push(slug);
1255
1305
  }
1256
1306
 
@@ -1262,13 +1312,14 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1262
1312
  if (!fs.existsSync(rp.path)) continue; // skip missing directories
1263
1313
  var rpSlug = generateSlug(rp.path, usedSlugs);
1264
1314
  usedSlugs.push(rpSlug);
1265
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
1315
+ allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1266
1316
  }
1267
1317
  }
1268
1318
 
1269
1319
  var config = {
1270
1320
  pid: null,
1271
1321
  port: port,
1322
+ host: host,
1272
1323
  pinHash: pin ? generateAuthToken(pin) : null,
1273
1324
  tls: hasTls,
1274
1325
  debug: debugMode,
@@ -1350,24 +1401,35 @@ async function devMode(pin, keepAwake, existingPinHash) {
1350
1401
  }
1351
1402
 
1352
1403
  var slug = generateSlug(cwd, []);
1353
- var allProjects = [{ path: cwd, slug: slug, addedAt: Date.now() }];
1404
+ var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1354
1405
 
1355
1406
  // Restore previous projects
1356
1407
  var rc = loadClayrc();
1357
1408
  var restorable = (rc.recentProjects || []).filter(function (p) {
1358
1409
  return p.path !== cwd && fs.existsSync(p.path);
1359
1410
  });
1411
+ // Restore title/icon for cwd from .clayrc
1412
+ var rcAll = rc.recentProjects || [];
1413
+ for (var ci = 0; ci < rcAll.length; ci++) {
1414
+ if (rcAll[ci].path === cwd) {
1415
+ if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
1416
+ if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
1417
+ break;
1418
+ }
1419
+ }
1420
+ var allProjects = [cwdDevEntry];
1360
1421
  var usedSlugs = [slug];
1361
1422
  for (var ri = 0; ri < restorable.length; ri++) {
1362
1423
  var rp = restorable[ri];
1363
1424
  var rpSlug = generateSlug(rp.path, usedSlugs);
1364
1425
  usedSlugs.push(rpSlug);
1365
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
1426
+ allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1366
1427
  }
1367
1428
 
1368
1429
  var config = {
1369
1430
  pid: null,
1370
1431
  port: port,
1432
+ host: host,
1371
1433
  pinHash: existingPinHash || (pin ? generateAuthToken(pin) : null),
1372
1434
  tls: hasTls,
1373
1435
  debug: true,
@@ -2352,8 +2414,12 @@ var currentVersion = require("../package.json").version;
2352
2414
  if (autoRestorable.length > 0) {
2353
2415
  console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
2354
2416
  }
2355
- var hasRestorable = autoRestorable.length > 0;
2356
- await forkDaemon(pin, false, hasRestorable ? autoRestorable : undefined, !hasRestorable);
2417
+ // Add cwd if it has history in .clayrc, or if there are no other projects to restore
2418
+ var cwdInRc = (autoRc.recentProjects || []).some(function (p) {
2419
+ return p.path === cwd;
2420
+ });
2421
+ var addCwd = cwdInRc || autoRestorable.length === 0;
2422
+ await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
2357
2423
  } else {
2358
2424
  setup(function (pin, keepAwake) {
2359
2425
  if (dangerouslySkipPermissions && !pin) {
@@ -2,14 +2,9 @@ var fs = require("fs");
2
2
  var path = require("path");
3
3
  var os = require("os");
4
4
  var readline = require("readline");
5
+ var utils = require("./utils");
5
6
 
6
- /**
7
- * Compute the encoded project directory name used by the Claude CLI.
8
- * Replaces all "/" with "-", e.g. "/Users/foo/project" -> "-Users-foo-project"
9
- */
10
- function encodeCwd(cwd) {
11
- return cwd.replace(/\//g, "-");
12
- }
7
+ var encodeCwd = utils.encodeCwd;
13
8
 
14
9
  /**
15
10
  * Parse the first ~20 lines of a CLI session JSONL file to extract metadata.
package/lib/config.js CHANGED
@@ -18,6 +18,54 @@ if (!fs.existsSync(CLAY_HOME) && fs.existsSync(LEGACY_HOME)) {
18
18
  }
19
19
  }
20
20
 
21
+ // Auto-migrate dev sessions: merge ~/.clay-dev/sessions/ into ~/.clay/sessions/
22
+ var CLAY_DEV_HOME = path.join(os.homedir(), ".clay-dev");
23
+ var devSessionsRoot = path.join(CLAY_DEV_HOME, "sessions");
24
+ if (fs.existsSync(devSessionsRoot)) {
25
+ try {
26
+ var prodSessionsRoot = path.join(CLAY_HOME, "sessions");
27
+ fs.mkdirSync(prodSessionsRoot, { recursive: true });
28
+ var projectDirs = fs.readdirSync(devSessionsRoot);
29
+ var totalMigrated = 0;
30
+ for (var di = 0; di < projectDirs.length; di++) {
31
+ var srcDir = path.join(devSessionsRoot, projectDirs[di]);
32
+ if (!fs.statSync(srcDir).isDirectory()) continue;
33
+ var destDir = path.join(prodSessionsRoot, projectDirs[di]);
34
+ fs.mkdirSync(destDir, { recursive: true });
35
+ var files = fs.readdirSync(srcDir);
36
+ for (var fi = 0; fi < files.length; fi++) {
37
+ if (!files[fi].endsWith(".jsonl")) continue;
38
+ var srcFile = path.join(srcDir, files[fi]);
39
+ var destFile = path.join(destDir, files[fi]);
40
+ if (fs.existsSync(destFile)) continue;
41
+ try {
42
+ fs.renameSync(srcFile, destFile);
43
+ totalMigrated++;
44
+ } catch (e2) {
45
+ try {
46
+ fs.copyFileSync(srcFile, destFile);
47
+ fs.unlinkSync(srcFile);
48
+ totalMigrated++;
49
+ } catch (e3) {}
50
+ }
51
+ }
52
+ // Clean up empty project dir
53
+ try {
54
+ if (fs.readdirSync(srcDir).length === 0) fs.rmdirSync(srcDir);
55
+ } catch (e4) {}
56
+ }
57
+ if (totalMigrated > 0) {
58
+ console.log("[config] Migrated " + totalMigrated + " dev session(s) from " + devSessionsRoot + " → " + prodSessionsRoot);
59
+ }
60
+ // Clean up empty dev sessions root
61
+ try {
62
+ if (fs.readdirSync(devSessionsRoot).length === 0) fs.rmdirSync(devSessionsRoot);
63
+ } catch (e5) {}
64
+ } catch (e) {
65
+ console.error("[config] Dev session migration failed:", e.message);
66
+ }
67
+ }
68
+
21
69
  var CONFIG_DIR = CLAY_HOME;
22
70
  var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
23
71
  var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
@@ -28,7 +76,7 @@ function configPath() {
28
76
 
29
77
  function socketPath() {
30
78
  if (process.platform === "win32") {
31
- var pipeName = process.env.CLAY_HOME ? "clay-dev-daemon" : "clay-daemon";
79
+ var pipeName = "clay-daemon";
32
80
  return "\\\\.\\pipe\\" + pipeName;
33
81
  }
34
82
  return path.join(CONFIG_DIR, "daemon.sock");
@@ -69,7 +117,10 @@ function isPidAlive(pid) {
69
117
 
70
118
  function isDaemonAlive(config) {
71
119
  if (!config || !config.pid) return false;
72
- if (!isPidAlive(config.pid)) return false;
120
+ if (!isPidAlive(config.pid)) {
121
+ clearStaleConfig();
122
+ return false;
123
+ }
73
124
  // Named pipes on Windows can't be stat'd, just check PID
74
125
  if (process.platform === "win32") return true;
75
126
  try {
@@ -83,7 +134,10 @@ function isDaemonAlive(config) {
83
134
  function isDaemonAliveAsync(config) {
84
135
  return new Promise(function (resolve) {
85
136
  if (!config || !config.pid) return resolve(false);
86
- if (!isPidAlive(config.pid)) return resolve(false);
137
+ if (!isPidAlive(config.pid)) {
138
+ clearStaleConfig();
139
+ return resolve(false);
140
+ }
87
141
 
88
142
  var sock = socketPath();
89
143
  var client = net.connect(sock);
@@ -191,28 +245,52 @@ function syncClayrc(projects) {
191
245
  byPath[p.path].slug = p.slug;
192
246
  byPath[p.path].lastUsed = Date.now();
193
247
  if (p.title) byPath[p.path].title = p.title;
194
- else delete byPath[p.path].title;
248
+ else if ("title" in p) delete byPath[p.path].title;
249
+ if (p.icon) byPath[p.path].icon = p.icon;
250
+ else if ("icon" in p) delete byPath[p.path].icon;
195
251
  } else {
196
252
  // New entry
197
253
  byPath[p.path] = {
198
254
  path: p.path,
199
255
  slug: p.slug,
200
256
  title: p.title || undefined,
257
+ icon: p.icon || undefined,
201
258
  addedAt: p.addedAt || Date.now(),
202
259
  lastUsed: Date.now(),
203
260
  };
204
261
  }
205
262
  }
206
263
 
207
- // Rebuild array, sorted by lastUsed descending
208
- var all = Object.keys(byPath).map(function (k) { return byPath[k]; });
209
- all.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
264
+ // Active projects first, preserving config order (user's drag-and-drop order),
265
+ // then inactive recent projects sorted by lastUsed descending
266
+ var activePaths = {};
267
+ var ordered = [];
268
+ for (var k = 0; k < projects.length; k++) {
269
+ activePaths[projects[k].path] = true;
270
+ if (byPath[projects[k].path]) ordered.push(byPath[projects[k].path]);
271
+ }
272
+ var inactive = [];
273
+ var paths = Object.keys(byPath);
274
+ for (var m = 0; m < paths.length; m++) {
275
+ if (!activePaths[paths[m]]) inactive.push(byPath[paths[m]]);
276
+ }
277
+ inactive.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
278
+ var all = ordered.concat(inactive);
210
279
 
211
280
  // Keep at most 20 recent projects
212
281
  rc.recentProjects = all.slice(0, 20);
213
282
  saveClayrc(rc);
214
283
  }
215
284
 
285
+ function removeFromClayrc(projectPath) {
286
+ var rc = loadClayrc();
287
+ var before = (rc.recentProjects || []).length;
288
+ rc.recentProjects = (rc.recentProjects || []).filter(function (p) {
289
+ return p.path !== projectPath;
290
+ });
291
+ if (rc.recentProjects.length !== before) saveClayrc(rc);
292
+ }
293
+
216
294
  module.exports = {
217
295
  CONFIG_DIR: CONFIG_DIR,
218
296
  configPath: configPath,
@@ -234,4 +312,5 @@ module.exports = {
234
312
  loadClayrc: loadClayrc,
235
313
  saveClayrc: saveClayrc,
236
314
  syncClayrc: syncClayrc,
315
+ removeFromClayrc: removeFromClayrc,
237
316
  };