clay-server 2.8.2 → 2.9.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 CHANGED
@@ -276,6 +276,8 @@ If you are using claude-relay, let us know how you are using it in Discussions:
276
276
 
277
277
  This is an independent project and is not affiliated with Anthropic. Claude is a trademark of Anthropic.
278
278
 
279
+ Clay is provided "as is" without warranty of any kind. Users are responsible for ensuring their use of this software complies with all applicable terms of service of the underlying AI providers (e.g., Anthropic, OpenAI) and any other third-party services. Features such as multi-user mode are experimental and may involve sharing access to API-backed services — please review your provider's usage policies regarding account sharing, acceptable use, and any applicable rate limits or restrictions before enabling such features. The authors assume no liability for any misuse or violations arising from the use of this software.
280
+
279
281
  ## License
280
282
 
281
283
  MIT
package/bin/cli.js CHANGED
@@ -28,6 +28,7 @@ if (_isDev) process.env.CLAY_DEV = "1";
28
28
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
29
29
  var { sendIPCCommand } = require("../lib/ipc");
30
30
  var { generateAuthToken } = require("../lib/server");
31
+ var { enableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
31
32
 
32
33
  function openUrl(url) {
33
34
  try {
@@ -56,6 +57,7 @@ var dangerouslySkipPermissions = false;
56
57
  var headlessMode = false;
57
58
  var watchMode = false;
58
59
  var host = null;
60
+ var multiUserMode = false;
59
61
 
60
62
  for (var i = 0; i < args.length; i++) {
61
63
  if (args[i] === "-p" || args[i] === "--port") {
@@ -100,6 +102,8 @@ for (var i = 0; i < args.length; i++) {
100
102
  autoYes = true;
101
103
  } else if (args[i] === "--dangerously-skip-permissions") {
102
104
  dangerouslySkipPermissions = true;
105
+ } else if (args[i] === "--multi-user") {
106
+ multiUserMode = true;
103
107
  } else if (args[i] === "-h" || args[i] === "--help") {
104
108
  console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
105
109
  console.log(" clay-server --add <path> Add a project to the running daemon");
@@ -120,6 +124,7 @@ for (var i = 0; i < args.length; i++) {
120
124
  console.log(" --remove <path> Remove a project directory");
121
125
  console.log(" --list List all registered projects");
122
126
  console.log(" --headless Start daemon and exit immediately (implies --yes)");
127
+ console.log(" --multi-user Enable multi-user mode (generates setup code)");
123
128
  console.log(" --dangerously-skip-permissions");
124
129
  console.log(" Bypass all permission prompts");
125
130
  process.exit(0);
@@ -258,6 +263,34 @@ if (listMode) {
258
263
  return;
259
264
  }
260
265
 
266
+ // --- Handle --multi-user before anything else ---
267
+ if (multiUserMode) {
268
+ var muResult = enableMultiUser();
269
+ if (muResult.alreadyEnabled && muResult.hasAdmin) {
270
+ console.log("");
271
+ console.log("Multi-user mode is already enabled and an admin account exists.");
272
+ console.log("No changes made.");
273
+ console.log("");
274
+ } else if (muResult.setupCode) {
275
+ console.log("");
276
+ console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
277
+ console.log("");
278
+ console.log(" Multi-user mode is experimental and may change in future releases.");
279
+ console.log(" Sharing access to AI-powered tools may be subject to your provider's");
280
+ console.log(" terms of service. Please review the applicable usage policies before");
281
+ console.log(" granting access to other users.");
282
+ console.log("");
283
+ console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
284
+ console.log("");
285
+ console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
286
+ console.log("");
287
+ console.log("Open Clay in your browser and enter this code to create the admin account.");
288
+ console.log("The code is single-use and will be cleared once the admin is set up.");
289
+ console.log("");
290
+ }
291
+ process.exit(0);
292
+ }
293
+
261
294
  var cwd = process.cwd();
262
295
 
263
296
  // --- ANSI helpers ---
@@ -1312,6 +1345,15 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1312
1345
  var allProjects = [];
1313
1346
  var usedSlugs = [];
1314
1347
 
1348
+ // Load previous config to preserve per-project settings (visibility, allowedUsers)
1349
+ var prevConfig = loadConfig();
1350
+ var prevProjectMap = {};
1351
+ if (prevConfig && prevConfig.projects) {
1352
+ for (var pi = 0; pi < prevConfig.projects.length; pi++) {
1353
+ prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
1354
+ }
1355
+ }
1356
+
1315
1357
  // Only include cwd if explicitly requested
1316
1358
  if (addCwd) {
1317
1359
  var slug = generateSlug(cwd, []);
@@ -1326,6 +1368,11 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1326
1368
  break;
1327
1369
  }
1328
1370
  }
1371
+ // Restore access settings from previous config
1372
+ if (prevProjectMap[cwd]) {
1373
+ if (prevProjectMap[cwd].visibility) cwdEntry.visibility = prevProjectMap[cwd].visibility;
1374
+ if (prevProjectMap[cwd].allowedUsers) cwdEntry.allowedUsers = prevProjectMap[cwd].allowedUsers;
1375
+ }
1329
1376
  allProjects.push(cwdEntry);
1330
1377
  usedSlugs.push(slug);
1331
1378
  }
@@ -1338,7 +1385,13 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1338
1385
  if (!fs.existsSync(rp.path)) continue; // skip missing directories
1339
1386
  var rpSlug = generateSlug(rp.path, usedSlugs);
1340
1387
  usedSlugs.push(rpSlug);
1341
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1388
+ var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1389
+ // Restore access settings from previous config
1390
+ if (prevProjectMap[rp.path]) {
1391
+ if (prevProjectMap[rp.path].visibility) rpEntry.visibility = prevProjectMap[rp.path].visibility;
1392
+ if (prevProjectMap[rp.path].allowedUsers) rpEntry.allowedUsers = prevProjectMap[rp.path].allowedUsers;
1393
+ }
1394
+ allProjects.push(rpEntry);
1342
1395
  }
1343
1396
  }
1344
1397
 
@@ -1429,6 +1482,15 @@ async function devMode(pin, keepAwake, existingPinHash) {
1429
1482
  var slug = generateSlug(cwd, []);
1430
1483
  var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1431
1484
 
1485
+ // Load previous config to preserve per-project settings (visibility, allowedUsers)
1486
+ var prevDevConfig = loadConfig();
1487
+ var prevDevProjectMap = {};
1488
+ if (prevDevConfig && prevDevConfig.projects) {
1489
+ for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
1490
+ prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
1491
+ }
1492
+ }
1493
+
1432
1494
  // Restore previous projects
1433
1495
  var rc = loadClayrc();
1434
1496
  var restorable = (rc.recentProjects || []).filter(function (p) {
@@ -1443,13 +1505,24 @@ async function devMode(pin, keepAwake, existingPinHash) {
1443
1505
  break;
1444
1506
  }
1445
1507
  }
1508
+ // Restore access settings for cwd from previous config
1509
+ if (prevDevProjectMap[cwd]) {
1510
+ if (prevDevProjectMap[cwd].visibility) cwdDevEntry.visibility = prevDevProjectMap[cwd].visibility;
1511
+ if (prevDevProjectMap[cwd].allowedUsers) cwdDevEntry.allowedUsers = prevDevProjectMap[cwd].allowedUsers;
1512
+ }
1446
1513
  var allProjects = [cwdDevEntry];
1447
1514
  var usedSlugs = [slug];
1448
1515
  for (var ri = 0; ri < restorable.length; ri++) {
1449
1516
  var rp = restorable[ri];
1450
1517
  var rpSlug = generateSlug(rp.path, usedSlugs);
1451
1518
  usedSlugs.push(rpSlug);
1452
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1519
+ var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1520
+ // Restore access settings from previous config
1521
+ if (prevDevProjectMap[rp.path]) {
1522
+ if (prevDevProjectMap[rp.path].visibility) rpDevEntry.visibility = prevDevProjectMap[rp.path].visibility;
1523
+ if (prevDevProjectMap[rp.path].allowedUsers) rpDevEntry.allowedUsers = prevDevProjectMap[rp.path].allowedUsers;
1524
+ }
1525
+ allProjects.push(rpDevEntry);
1453
1526
  }
1454
1527
 
1455
1528
  var config = {
@@ -2222,7 +2295,13 @@ function showSettingsMenu(config, ip) {
2222
2295
  log(sym.bar + " Tailscale " + tsStatus);
2223
2296
  log(sym.bar + " mkcert " + mcStatus);
2224
2297
  log(sym.bar + " HTTPS " + tlsStatus);
2298
+ var muEnabled = isMultiUser();
2299
+ var muStatus = muEnabled
2300
+ ? a.green + "Enabled" + a.reset
2301
+ : a.dim + "Off" + a.reset;
2302
+
2225
2303
  log(sym.bar + " PIN " + pinStatus);
2304
+ log(sym.bar + " Multi-user " + muStatus);
2226
2305
  if (process.platform === "darwin") {
2227
2306
  log(sym.bar + " Keep awake " + awakeStatus);
2228
2307
  }
@@ -2239,6 +2318,11 @@ function showSettingsMenu(config, ip) {
2239
2318
  } else {
2240
2319
  items.push({ label: "Set PIN", value: "pin" });
2241
2320
  }
2321
+ if (muEnabled) {
2322
+ items.push({ label: "Multi-user mode (enabled)", value: "multi_user" });
2323
+ } else {
2324
+ items.push({ label: "Enable multi-user mode", value: "multi_user" });
2325
+ }
2242
2326
  if (process.platform === "darwin") {
2243
2327
  items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
2244
2328
  }
@@ -2280,6 +2364,42 @@ function showSettingsMenu(config, ip) {
2280
2364
  });
2281
2365
  break;
2282
2366
 
2367
+ case "multi_user":
2368
+ if (muEnabled && hasAdmin()) {
2369
+ log(sym.bar);
2370
+ log(sym.bar + " " + a.dim + "Multi-user mode is already enabled and an admin account exists." + a.reset);
2371
+ log(sym.bar + " " + a.dim + "No changes made." + a.reset);
2372
+ log(sym.bar);
2373
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2374
+ showSettingsMenu(config, ip);
2375
+ });
2376
+ } else {
2377
+ var muResult = enableMultiUser();
2378
+ log(sym.bar);
2379
+ log(sym.bar + " " + a.yellow + sym.warn + " Experimental Feature" + a.reset);
2380
+ log(sym.bar);
2381
+ log(sym.bar + " " + a.dim + "Multi-user mode is experimental and may change in future releases." + a.reset);
2382
+ log(sym.bar + " " + a.dim + "Sharing access to AI-powered tools may be subject to your provider's" + a.reset);
2383
+ log(sym.bar + " " + a.dim + "terms of service. Please review the applicable usage policies before" + a.reset);
2384
+ log(sym.bar + " " + a.dim + "granting access to other users." + a.reset);
2385
+ log(sym.bar);
2386
+ if (muResult.setupCode) {
2387
+ log(sym.bar + " " + a.green + "Multi-user mode enabled." + a.reset);
2388
+ log(sym.bar);
2389
+ log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
2390
+ log(sym.bar);
2391
+ log(sym.bar + " " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
2392
+ log(sym.bar + " " + a.dim + "The code is single-use and will be cleared once the admin is set up." + a.reset);
2393
+ } else {
2394
+ log(sym.bar + " " + a.dim + "Multi-user mode is already enabled." + a.reset);
2395
+ }
2396
+ log(sym.bar);
2397
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2398
+ showSettingsMenu(config, ip);
2399
+ });
2400
+ }
2401
+ break;
2402
+
2283
2403
  case "logs":
2284
2404
  console.clear();
2285
2405
  log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
package/lib/config.js CHANGED
@@ -89,8 +89,14 @@ function logPath() {
89
89
  return path.join(CONFIG_DIR, _devMode ? "daemon-dev.log" : "daemon.log");
90
90
  }
91
91
 
92
+ function chmodSafe(filePath, mode) {
93
+ if (process.platform === "win32") return;
94
+ try { fs.chmodSync(filePath, mode); } catch (e) {}
95
+ }
96
+
92
97
  function ensureConfigDir() {
93
98
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
99
+ chmodSafe(CONFIG_DIR, 0o700);
94
100
  }
95
101
 
96
102
  function loadConfig() {
@@ -106,7 +112,9 @@ function saveConfig(config) {
106
112
  ensureConfigDir();
107
113
  var tmpPath = configPath() + ".tmp";
108
114
  fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
115
+ chmodSafe(tmpPath, 0o600);
109
116
  fs.renameSync(tmpPath, configPath());
117
+ chmodSafe(configPath(), 0o600);
110
118
  }
111
119
 
112
120
  function isPidAlive(pid) {
@@ -173,7 +181,17 @@ function generateSlug(projectPath, existingSlugs) {
173
181
  }
174
182
 
175
183
  function clearStaleConfig() {
176
- try { fs.unlinkSync(configPath()); } catch (e) {}
184
+ // Clear pid from config instead of deleting the file (preserves project settings)
185
+ try {
186
+ var data = fs.readFileSync(configPath(), "utf8");
187
+ var cfg = JSON.parse(data);
188
+ cfg.pid = null;
189
+ var tmpPath = configPath() + ".tmp";
190
+ fs.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2));
191
+ chmodSafe(tmpPath, 0o600);
192
+ fs.renameSync(tmpPath, configPath());
193
+ chmodSafe(configPath(), 0o600);
194
+ } catch (e) {}
177
195
  if (process.platform !== "win32") {
178
196
  try { fs.unlinkSync(socketPath()); } catch (e) {}
179
197
  }
@@ -316,4 +334,5 @@ module.exports = {
316
334
  saveClayrc: saveClayrc,
317
335
  syncClayrc: syncClayrc,
318
336
  removeFromClayrc: removeFromClayrc,
337
+ chmodSafe: chmodSafe,
319
338
  };
package/lib/daemon.js CHANGED
@@ -368,6 +368,12 @@ var relay = createServer({
368
368
  console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
369
369
  return { ok: true, pinEnabled: !!config.pinHash };
370
370
  },
371
+ onUpgradePin: function (newHash) {
372
+ config.pinHash = newHash;
373
+ relay.setAuthToken(newHash);
374
+ saveConfig(config);
375
+ console.log("[daemon] PIN hash auto-upgraded to scrypt");
376
+ },
371
377
  onSetKeepAwake: function (value) {
372
378
  var want = !!value;
373
379
  config.keepAwake = want;
@@ -393,6 +399,40 @@ var relay = createServer({
393
399
  console.log("[daemon] Restart requested via web UI");
394
400
  spawnAndRestart();
395
401
  },
402
+ onSetProjectVisibility: function (slug, visibility) {
403
+ for (var i = 0; i < config.projects.length; i++) {
404
+ if (config.projects[i].slug === slug) {
405
+ config.projects[i].visibility = visibility;
406
+ saveConfig(config);
407
+ console.log("[daemon] Set project visibility:", slug, "→", visibility);
408
+ return { ok: true };
409
+ }
410
+ }
411
+ return { error: "Project not found" };
412
+ },
413
+ onSetProjectAllowedUsers: function (slug, allowedUsers) {
414
+ for (var i = 0; i < config.projects.length; i++) {
415
+ if (config.projects[i].slug === slug) {
416
+ config.projects[i].allowedUsers = allowedUsers;
417
+ saveConfig(config);
418
+ console.log("[daemon] Set project allowed users:", slug, "→", allowedUsers.length, "users");
419
+ return { ok: true };
420
+ }
421
+ }
422
+ return { error: "Project not found" };
423
+ },
424
+ onGetProjectAccess: function (slug) {
425
+ for (var i = 0; i < config.projects.length; i++) {
426
+ if (config.projects[i].slug === slug) {
427
+ return {
428
+ slug: slug,
429
+ visibility: config.projects[i].visibility || "public",
430
+ allowedUsers: config.projects[i].allowedUsers || [],
431
+ };
432
+ }
433
+ }
434
+ return { error: "Project not found" };
435
+ },
396
436
  });
397
437
 
398
438
  // --- Register projects ---