clay-server 2.8.0 → 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,8 +124,9 @@ 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
- console.log(" Bypass all permission prompts (requires --pin)");
129
+ console.log(" Bypass all permission prompts");
125
130
  process.exit(0);
126
131
  }
127
132
  }
@@ -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 ---
@@ -1243,7 +1276,30 @@ function setup(callback) {
1243
1276
  port = p;
1244
1277
  log(sym.bar);
1245
1278
 
1246
- promptPin(function (pin) {
1279
+ function askPin() {
1280
+ promptPin(function (pin) {
1281
+ if (dangerouslySkipPermissions && !pin) {
1282
+ log(sym.bar);
1283
+ log(sym.warn + " " + a.yellow + "WARNING: No PIN + skip permissions = anyone with the URL" + a.reset);
1284
+ log(sym.bar + " " + a.yellow + "can execute any command without approval." + a.reset);
1285
+ log(sym.bar);
1286
+ promptToggle("Continue without PIN?", null, false, function (confirmed) {
1287
+ if (!confirmed) {
1288
+ clearUp(6);
1289
+ log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.yellow + "Required for skip permissions" + a.reset);
1290
+ log(sym.bar);
1291
+ askPin();
1292
+ return;
1293
+ }
1294
+ afterPin(pin);
1295
+ });
1296
+ } else {
1297
+ afterPin(pin);
1298
+ }
1299
+ });
1300
+ }
1301
+
1302
+ function afterPin(pin) {
1247
1303
  if (process.platform === "darwin") {
1248
1304
  promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
1249
1305
  callback(pin, keepAwake);
@@ -1251,7 +1307,9 @@ function setup(callback) {
1251
1307
  } else {
1252
1308
  callback(pin, false);
1253
1309
  }
1254
- });
1310
+ }
1311
+
1312
+ askPin();
1255
1313
  });
1256
1314
  });
1257
1315
  }
@@ -1287,6 +1345,15 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1287
1345
  var allProjects = [];
1288
1346
  var usedSlugs = [];
1289
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
+
1290
1357
  // Only include cwd if explicitly requested
1291
1358
  if (addCwd) {
1292
1359
  var slug = generateSlug(cwd, []);
@@ -1301,6 +1368,11 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1301
1368
  break;
1302
1369
  }
1303
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
+ }
1304
1376
  allProjects.push(cwdEntry);
1305
1377
  usedSlugs.push(slug);
1306
1378
  }
@@ -1313,7 +1385,13 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1313
1385
  if (!fs.existsSync(rp.path)) continue; // skip missing directories
1314
1386
  var rpSlug = generateSlug(rp.path, usedSlugs);
1315
1387
  usedSlugs.push(rpSlug);
1316
- 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);
1317
1395
  }
1318
1396
  }
1319
1397
 
@@ -1404,6 +1482,15 @@ async function devMode(pin, keepAwake, existingPinHash) {
1404
1482
  var slug = generateSlug(cwd, []);
1405
1483
  var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1406
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
+
1407
1494
  // Restore previous projects
1408
1495
  var rc = loadClayrc();
1409
1496
  var restorable = (rc.recentProjects || []).filter(function (p) {
@@ -1418,13 +1505,24 @@ async function devMode(pin, keepAwake, existingPinHash) {
1418
1505
  break;
1419
1506
  }
1420
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
+ }
1421
1513
  var allProjects = [cwdDevEntry];
1422
1514
  var usedSlugs = [slug];
1423
1515
  for (var ri = 0; ri < restorable.length; ri++) {
1424
1516
  var rp = restorable[ri];
1425
1517
  var rpSlug = generateSlug(rp.path, usedSlugs);
1426
1518
  usedSlugs.push(rpSlug);
1427
- 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);
1428
1526
  }
1429
1527
 
1430
1528
  var config = {
@@ -2197,7 +2295,13 @@ function showSettingsMenu(config, ip) {
2197
2295
  log(sym.bar + " Tailscale " + tsStatus);
2198
2296
  log(sym.bar + " mkcert " + mcStatus);
2199
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
+
2200
2303
  log(sym.bar + " PIN " + pinStatus);
2304
+ log(sym.bar + " Multi-user " + muStatus);
2201
2305
  if (process.platform === "darwin") {
2202
2306
  log(sym.bar + " Keep awake " + awakeStatus);
2203
2307
  }
@@ -2214,6 +2318,11 @@ function showSettingsMenu(config, ip) {
2214
2318
  } else {
2215
2319
  items.push({ label: "Set PIN", value: "pin" });
2216
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
+ }
2217
2326
  if (process.platform === "darwin") {
2218
2327
  items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
2219
2328
  }
@@ -2255,6 +2364,42 @@ function showSettingsMenu(config, ip) {
2255
2364
  });
2256
2365
  break;
2257
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
+
2258
2403
  case "logs":
2259
2404
  console.clear();
2260
2405
  log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
@@ -2398,15 +2543,10 @@ var currentVersion = require("../package.json").version;
2398
2543
  // No daemon running — first-time setup
2399
2544
  if (autoYes) {
2400
2545
  var pin = cliPin || null;
2401
- if (dangerouslySkipPermissions && !pin) {
2402
- console.error(" " + sym.warn + " " + a.red + "--dangerously-skip-permissions requires --pin <pin>" + a.reset);
2403
- process.exit(1);
2404
- return;
2405
- }
2406
2546
  console.log(" " + sym.done + " Auto-accepted disclaimer");
2407
2547
  console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
2408
2548
  if (dangerouslySkipPermissions) {
2409
- console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
2549
+ console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + (pin ? "" : " (no PIN)") + a.reset);
2410
2550
  }
2411
2551
  var autoRc = loadClayrc();
2412
2552
  var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
@@ -2423,13 +2563,6 @@ var currentVersion = require("../package.json").version;
2423
2563
  await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
2424
2564
  } else {
2425
2565
  setup(function (pin, keepAwake) {
2426
- if (dangerouslySkipPermissions && !pin) {
2427
- log(sym.warn + " " + a.red + "--dangerously-skip-permissions requires a PIN." + a.reset);
2428
- log(a.dim + " Please set a PIN to use skip permissions mode." + a.reset);
2429
- process.exit(1);
2430
- return;
2431
- }
2432
-
2433
2566
  // Check ~/.clayrc for previous projects to restore
2434
2567
  var rc = loadClayrc();
2435
2568
  var restorable = (rc.recentProjects || []).filter(function (p) {
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 ---