clay-server 2.11.0-beta.2 → 2.11.0-beta.20

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/bin/cli.js CHANGED
@@ -62,6 +62,7 @@ var headlessMode = false;
62
62
  var watchMode = false;
63
63
  var host = null;
64
64
  var multiUserMode = false;
65
+ var osUsersMode = false;
65
66
 
66
67
  for (var i = 0; i < args.length; i++) {
67
68
  if (args[i] === "-p" || args[i] === "--port") {
@@ -108,6 +109,8 @@ for (var i = 0; i < args.length; i++) {
108
109
  dangerouslySkipPermissions = true;
109
110
  } else if (args[i] === "--multi-user") {
110
111
  multiUserMode = true;
112
+ } else if (args[i] === "--os-users") {
113
+ osUsersMode = true;
111
114
  } else if (args[i] === "-h" || args[i] === "--help") {
112
115
  console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
113
116
  console.log(" clay-server --add <path> Add a project to the running daemon");
@@ -128,7 +131,8 @@ for (var i = 0; i < args.length; i++) {
128
131
  console.log(" --remove <path> Remove a project directory");
129
132
  console.log(" --list List all registered projects");
130
133
  console.log(" --headless Start daemon and exit immediately (implies --yes)");
131
- console.log(" --multi-user Enable multi-user mode (generates setup code)");
134
+ console.log(" --multi-user Start in multi-user mode (use with --yes for headless)");
135
+ console.log(" --os-users Enable OS-level user isolation (Linux, requires root + --multi-user)");
132
136
  console.log(" --dangerously-skip-permissions");
133
137
  console.log(" Bypass all permission prompts");
134
138
  process.exit(0);
@@ -267,33 +271,8 @@ if (listMode) {
267
271
  return;
268
272
  }
269
273
 
270
- // --- Handle --multi-user before anything else ---
271
- if (multiUserMode) {
272
- var muResult = enableMultiUser();
273
- if (muResult.alreadyEnabled && muResult.hasAdmin) {
274
- console.log("");
275
- console.log("Multi-user mode is already enabled and an admin account exists.");
276
- console.log("No changes made.");
277
- console.log("");
278
- } else if (muResult.setupCode) {
279
- console.log("");
280
- console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
281
- console.log("");
282
- console.log(" Multi-user mode is experimental and may change in future releases.");
283
- console.log(" Sharing access to AI-powered tools may be subject to your provider's");
284
- console.log(" terms of service. Please review the applicable usage policies before");
285
- console.log(" granting access to other users.");
286
- console.log("");
287
- console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
288
- console.log("");
289
- console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
290
- console.log("");
291
- console.log("Open Clay in your browser and enter this code to create the admin account.");
292
- console.log("The code is single-use and will be cleared once the admin is set up.");
293
- console.log("");
294
- }
295
- process.exit(0);
296
- }
274
+ // --multi-user / --os-users are now handled in the main entry flow (setup wizard or repeat run)
275
+ // Flags are parsed above and applied during forkDaemon()
297
276
 
298
277
  var cwd = process.cwd();
299
278
 
@@ -460,9 +439,11 @@ async function restartDaemonFromConfig() {
460
439
  debug: lastConfig.debug || false,
461
440
  keepAwake: lastConfig.keepAwake || false,
462
441
  dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
442
+ osUsers: lastConfig.osUsers || false,
463
443
  projects: (lastConfig.projects || []).filter(function (p) {
464
444
  return fs.existsSync(p.path);
465
445
  }),
446
+ removedProjects: lastConfig.removedProjects || [],
466
447
  };
467
448
 
468
449
  ensureConfigDir();
@@ -1243,13 +1224,29 @@ function setup(callback) {
1243
1224
  log("");
1244
1225
  log(sym.pointer + " " + a.bold + "Clay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
1245
1226
  log(sym.bar);
1246
- log(sym.bar + " " + a.dim + "Anyone with the URL gets full Claude Code access to this machine." + a.reset);
1247
- log(sym.bar + " " + a.dim + "Use a private network (Tailscale, VPN)." + a.reset);
1248
- log(sym.bar + " " + a.dim + "The authors assume no responsibility for any damage or data loss." + a.reset);
1227
+ log(sym.bar + " " + a.yellow + sym.warn + " Disclaimer" + a.reset);
1228
+ log(sym.bar);
1229
+ log(sym.bar + " " + a.dim + "This is an independent project and is not affiliated with Anthropic." + a.reset);
1230
+ log(sym.bar + " " + a.dim + "Claude is a trademark of Anthropic." + a.reset);
1231
+ log(sym.bar);
1232
+ log(sym.bar + " " + a.dim + "Clay is provided \"as is\" without warranty of any kind. Users are" + a.reset);
1233
+ log(sym.bar + " " + a.dim + "responsible for complying with the terms of service of underlying AI" + a.reset);
1234
+ log(sym.bar + " " + a.dim + "providers (e.g., Anthropic, OpenAI) and all applicable terms of any" + a.reset);
1235
+ log(sym.bar + " " + a.dim + "third-party services." + a.reset);
1236
+ log(sym.bar);
1237
+ log(sym.bar + " " + a.dim + "Features such as multi-user mode are experimental and may involve" + a.reset);
1238
+ log(sym.bar + " " + a.dim + "sharing access to API-based services. Before enabling such features," + a.reset);
1239
+ log(sym.bar + " " + a.dim + "review your provider's usage policies regarding account sharing," + a.reset);
1240
+ log(sym.bar + " " + a.dim + "acceptable use, and any applicable rate limits or restrictions." + a.reset);
1241
+ log(sym.bar);
1242
+ log(sym.bar + " " + a.dim + "The authors assume no liability for misuse or violations arising" + a.reset);
1243
+ log(sym.bar + " " + a.dim + "from the use of this software." + a.reset);
1244
+ log(sym.bar);
1245
+ log(sym.bar + " Type " + a.bold + "agree" + a.reset + " to accept and continue.");
1249
1246
  log(sym.bar);
1250
1247
 
1251
- promptToggle("Accept and continue", null, true, function (accepted) {
1252
- if (!accepted) {
1248
+ promptText("", "", function (val) {
1249
+ if (!val || val.trim().toLowerCase() !== "agree") {
1253
1250
  log(sym.end + " " + a.dim + "Aborted." + a.reset);
1254
1251
  log("");
1255
1252
  process.exit(0);
@@ -1279,44 +1276,99 @@ function setup(callback) {
1279
1276
  }
1280
1277
  port = p;
1281
1278
  log(sym.bar);
1279
+ askMode();
1280
+ });
1281
+ });
1282
+ }
1282
1283
 
1283
- function askPin() {
1284
- promptPin(function (pin) {
1285
- if (dangerouslySkipPermissions && !pin) {
1286
- log(sym.bar);
1287
- log(sym.warn + " " + a.yellow + "WARNING: No PIN + skip permissions = anyone with the URL" + a.reset);
1288
- log(sym.bar + " " + a.yellow + "can execute any command without approval." + a.reset);
1289
- log(sym.bar);
1290
- promptToggle("Continue without PIN?", null, false, function (confirmed) {
1291
- if (!confirmed) {
1292
- clearUp(6);
1293
- log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.yellow + "Required for skip permissions" + a.reset);
1294
- log(sym.bar);
1295
- askPin();
1296
- return;
1297
- }
1298
- afterPin(pin);
1299
- });
1300
- } else {
1301
- afterPin(pin);
1302
- }
1303
- });
1304
- }
1284
+ function askMode() {
1285
+ promptSelect("How will you use Clay?", [
1286
+ { label: "Just me (single user)", value: "single" },
1287
+ { label: "Multiple users", value: "multi" },
1288
+ ], function (mode) {
1289
+ if (mode === "single") {
1290
+ finishSetup(mode, false);
1291
+ } else {
1292
+ askOsUsers(mode);
1293
+ }
1294
+ });
1295
+ }
1305
1296
 
1306
- function afterPin(pin) {
1307
- if (process.platform === "darwin") {
1308
- promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
1309
- callback(pin, keepAwake);
1310
- });
1311
- } else {
1312
- callback(pin, false);
1313
- }
1297
+ function askOsUsers(mode) {
1298
+ // Only offer OS user isolation on Linux
1299
+ if (process.platform !== "linux") {
1300
+ finishSetup(mode, false);
1301
+ return;
1302
+ }
1303
+ log(sym.bar);
1304
+ promptSelect("Enable OS-level user isolation?", [
1305
+ { label: "Yes", value: "yes" },
1306
+ { label: "No", value: "no" },
1307
+ ], function (choice) {
1308
+ if (choice !== "yes") {
1309
+ finishSetup(mode, false);
1310
+ return;
1311
+ }
1312
+ log(sym.bar);
1313
+ log(sym.bar + " " + a.yellow + sym.warn + " OS-Level User Isolation" + a.reset);
1314
+ log(sym.bar);
1315
+ log(sym.bar + " " + a.dim + "This feature maps each Clay user to a Linux OS user account." + a.reset);
1316
+ log(sym.bar + " " + a.dim + "The daemon must run as root and will spawn processes (SDK workers," + a.reset);
1317
+ log(sym.bar + " " + a.dim + "terminals, file operations) as the mapped Linux user." + a.reset);
1318
+ log(sym.bar);
1319
+ log(sym.bar + " " + a.dim + "What this means:" + a.reset);
1320
+ log(sym.bar + " " + a.dim + "- Each mapped user uses their own ~/.claude/ credentials" + a.reset);
1321
+ log(sym.bar + " " + a.dim + "- Terminals and file access follow Linux permissions" + a.reset);
1322
+ log(sym.bar + " " + a.dim + "- Linux user accounts are created automatically (clay-username)" + a.reset);
1323
+ log(sym.bar);
1324
+ log(sym.bar + " " + a.dim + "Recommended: Run on a dedicated Clay server or cloud instance," + a.reset);
1325
+ log(sym.bar + " " + a.dim + "not on a personal computer or general-purpose server." + a.reset);
1326
+ log(sym.bar);
1327
+ promptSelect("Confirm", [
1328
+ { label: "Enable OS-level user isolation", value: "confirm" },
1329
+ { label: "Cancel", value: "cancel" },
1330
+ ], function (confirmChoice) {
1331
+ if (confirmChoice !== "confirm") {
1332
+ finishSetup(mode, false);
1333
+ return;
1314
1334
  }
1315
-
1316
- askPin();
1335
+ var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
1336
+ if (!isRoot) {
1337
+ // Save config so sudo clay can pick it up
1338
+ var partialConfig = {
1339
+ port: port,
1340
+ host: host,
1341
+ mode: "multi",
1342
+ osUsers: true,
1343
+ setupCompleted: true,
1344
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
1345
+ };
1346
+ saveConfig(partialConfig);
1347
+ log(sym.bar);
1348
+ log(sym.warn + " " + a.yellow + "OS user isolation requires root." + a.reset);
1349
+ log(sym.bar + " Run:");
1350
+ log(sym.bar + " " + a.bold + "sudo npx clay-server" + a.reset);
1351
+ log(sym.end);
1352
+ log("");
1353
+ process.exit(0);
1354
+ return;
1355
+ }
1356
+ finishSetup(mode, true);
1317
1357
  });
1318
1358
  });
1319
1359
  }
1360
+
1361
+ function finishSetup(mode, wantOsUsers) {
1362
+ if (process.platform === "darwin") {
1363
+ log(sym.bar);
1364
+ promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
1365
+ callback(mode, keepAwake, wantOsUsers);
1366
+ });
1367
+ } else {
1368
+ callback(mode, false, wantOsUsers);
1369
+ }
1370
+ }
1371
+
1320
1372
  askPort();
1321
1373
  });
1322
1374
  }
@@ -1324,7 +1376,7 @@ function setup(callback) {
1324
1376
  // ==============================
1325
1377
  // Fork the daemon process
1326
1378
  // ==============================
1327
- async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1379
+ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1328
1380
  var ip = getLocalIP();
1329
1381
  var hasTls = false;
1330
1382
 
@@ -1403,11 +1455,14 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1403
1455
  pid: null,
1404
1456
  port: port,
1405
1457
  host: host,
1406
- pinHash: pin ? generateAuthToken(pin) : null,
1458
+ pinHash: mode === "multi" && cliPin ? generateAuthToken(cliPin) : null,
1407
1459
  tls: hasTls,
1408
1460
  debug: debugMode,
1409
1461
  keepAwake: keepAwake,
1410
1462
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1463
+ osUsers: wantOsUsers || osUsersMode,
1464
+ mode: mode || "single",
1465
+ setupCompleted: true,
1411
1466
  projects: allProjects,
1412
1467
  };
1413
1468
 
@@ -1449,6 +1504,18 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1449
1504
  return;
1450
1505
  }
1451
1506
 
1507
+ // Enable multi-user mode if requested
1508
+ if (config.mode === "multi") {
1509
+ var muResult = enableMultiUser();
1510
+ if (muResult.setupCode) {
1511
+ log("");
1512
+ log(sym.done + " " + a.green + "Multi-user mode enabled." + a.reset);
1513
+ log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
1514
+ log(sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
1515
+ log("");
1516
+ }
1517
+ }
1518
+
1452
1519
  // Headless mode — print status and exit immediately
1453
1520
  if (headlessMode) {
1454
1521
  var protocol = config.tls ? "https" : "http";
@@ -1467,7 +1534,7 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1467
1534
  // ==============================
1468
1535
  // Dev mode — foreground daemon with file watching
1469
1536
  // ==============================
1470
- async function devMode(pin, keepAwake, existingPinHash) {
1537
+ async function devMode(mode, keepAwake, existingPinHash) {
1471
1538
  var ip = getLocalIP();
1472
1539
  var hasTls = false;
1473
1540
 
@@ -1533,11 +1600,13 @@ async function devMode(pin, keepAwake, existingPinHash) {
1533
1600
  pid: null,
1534
1601
  port: port,
1535
1602
  host: host,
1536
- pinHash: existingPinHash || (pin ? generateAuthToken(pin) : null),
1603
+ pinHash: existingPinHash || null,
1537
1604
  tls: hasTls,
1538
1605
  debug: true,
1539
1606
  keepAwake: keepAwake || false,
1540
1607
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1608
+ mode: mode || "single",
1609
+ setupCompleted: true,
1541
1610
  projects: allProjects,
1542
1611
  };
1543
1612
 
@@ -2268,6 +2337,7 @@ function showSetupGuide(config, ip, goBack) {
2268
2337
  function showSettingsMenu(config, ip) {
2269
2338
  sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
2270
2339
  var isAwake = status && status.keepAwake;
2340
+ var isOsUsers = status && status.osUsers;
2271
2341
 
2272
2342
  console.clear();
2273
2343
  printLogo();
@@ -2304,8 +2374,19 @@ function showSettingsMenu(config, ip) {
2304
2374
  ? a.green + "Enabled" + a.reset
2305
2375
  : a.dim + "Off" + a.reset;
2306
2376
 
2377
+ var modeLabel = config.mode === "multi" ? "Multi-user" : "Single user";
2378
+ var modeStatus = config.mode === "multi"
2379
+ ? a.clay + modeLabel + a.reset
2380
+ : a.dim + modeLabel + a.reset;
2381
+ log(sym.bar + " Mode " + modeStatus);
2307
2382
  log(sym.bar + " PIN " + pinStatus);
2308
2383
  log(sym.bar + " Multi-user " + muStatus);
2384
+ var osUsersStatus = isOsUsers
2385
+ ? a.green + "Enabled" + a.reset
2386
+ : a.dim + "Off" + a.reset;
2387
+ if (muEnabled) {
2388
+ log(sym.bar + " OS users " + osUsersStatus);
2389
+ }
2309
2390
  if (process.platform === "darwin") {
2310
2391
  log(sym.bar + " Keep awake " + awakeStatus);
2311
2392
  }
@@ -2324,6 +2405,11 @@ function showSettingsMenu(config, ip) {
2324
2405
  }
2325
2406
  if (muEnabled) {
2326
2407
  items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
2408
+ if (isOsUsers) {
2409
+ items.push({ label: "Disable OS-level user isolation", value: "disable_os_users" });
2410
+ } else {
2411
+ items.push({ label: "Enable OS-level user isolation", value: "os_users" });
2412
+ }
2327
2413
  } else {
2328
2414
  items.push({ label: "Enable multi-user mode", value: "multi_user" });
2329
2415
  }
@@ -2331,6 +2417,7 @@ function showSettingsMenu(config, ip) {
2331
2417
  items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
2332
2418
  }
2333
2419
  items.push({ label: "View logs", value: "logs" });
2420
+ items.push({ label: "Re-run setup wizard", value: "rerun_setup" });
2334
2421
  items.push({ label: "Back", value: "back" });
2335
2422
 
2336
2423
  promptSelect("Select", items, function (choice) {
@@ -2416,6 +2503,182 @@ function showSettingsMenu(config, ip) {
2416
2503
  });
2417
2504
  break;
2418
2505
 
2506
+ case "os_users":
2507
+ if (process.platform === "win32") {
2508
+ log(sym.bar);
2509
+ log(sym.bar + " " + a.red + "OS-level user isolation is not supported on Windows." + a.reset);
2510
+ log(sym.bar);
2511
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2512
+ showSettingsMenu(config, ip);
2513
+ });
2514
+ break;
2515
+ }
2516
+ if (process.getuid() !== 0) {
2517
+ log(sym.bar);
2518
+ log(sym.bar + " " + a.red + "Requires running as root." + a.reset);
2519
+ log(sym.bar);
2520
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2521
+ showSettingsMenu(config, ip);
2522
+ });
2523
+ break;
2524
+ }
2525
+ if (process.platform !== "linux") {
2526
+ log(sym.bar);
2527
+ log(sym.bar + " " + a.red + sym.warn + " OS-level user isolation requires Linux." + a.reset);
2528
+ log(sym.bar + " " + a.dim + "This feature depends on setfacl, getent, and uid/gid process spawning." + a.reset);
2529
+ log(sym.bar + " " + a.dim + "Use Docker or a Linux VM to run Clay with OS user isolation." + a.reset);
2530
+ log(sym.bar);
2531
+ showSettingsMenu(config, ip);
2532
+ return;
2533
+ }
2534
+ log(sym.bar);
2535
+ log(sym.bar + " " + a.yellow + sym.warn + " OS-Level User Isolation" + a.reset);
2536
+ log(sym.bar);
2537
+ log(sym.bar + " " + a.dim + "This feature maps each Clay user to a Linux OS user account." + a.reset);
2538
+ log(sym.bar + " " + a.dim + "The daemon must run as root and will spawn processes (SDK workers," + a.reset);
2539
+ log(sym.bar + " " + a.dim + "terminals, file operations) as the mapped Linux user." + a.reset);
2540
+ log(sym.bar);
2541
+ log(sym.bar + " " + a.dim + "What this means:" + a.reset);
2542
+ log(sym.bar + " " + a.dim + "- Each mapped user uses their own ~/.claude/ credentials" + a.reset);
2543
+ log(sym.bar + " " + a.dim + "- Terminals and file access follow Linux permissions" + a.reset);
2544
+ log(sym.bar + " " + a.dim + "- Linux user accounts are created automatically (clay-username)" + a.reset);
2545
+ log(sym.bar);
2546
+ log(sym.bar + " " + a.dim + "Recommended: Run on a dedicated Clay server or cloud instance," + a.reset);
2547
+ log(sym.bar + " " + a.dim + "not on a personal computer or general-purpose server." + a.reset);
2548
+ log(sym.bar);
2549
+ promptSelect("Select", [
2550
+ { label: "Enable OS-level user isolation", value: "confirm" },
2551
+ { label: "Cancel", value: "cancel" },
2552
+ ], function (confirmChoice) {
2553
+ if (confirmChoice === "confirm") {
2554
+ sendIPCCommand(socketPath(), { cmd: "set_os_users", value: true }).then(function (res) {
2555
+ if (res.ok) {
2556
+ config.osUsers = true;
2557
+ log(sym.bar);
2558
+ log(sym.done + " " + a.green + "OS-level user isolation enabled." + a.reset);
2559
+ if (res.provisioning) {
2560
+ var p = res.provisioning;
2561
+ if (p.provisioned.length > 0) {
2562
+ log(sym.bar);
2563
+ log(sym.bar + " " + a.green + "Provisioned " + p.provisioned.length + " Linux account(s):" + a.reset);
2564
+ for (var pi = 0; pi < p.provisioned.length; pi++) {
2565
+ log(sym.bar + " " + a.dim + p.provisioned[pi].username + " -> " + p.provisioned[pi].linuxUser + a.reset);
2566
+ }
2567
+ }
2568
+ if (p.skipped.length > 0) {
2569
+ log(sym.bar + " " + a.dim + p.skipped.length + " user(s) already mapped." + a.reset);
2570
+ }
2571
+ if (p.errors.length > 0) {
2572
+ log(sym.bar);
2573
+ log(sym.bar + " " + a.red + p.errors.length + " user(s) failed to provision:" + a.reset);
2574
+ for (var ei = 0; ei < p.errors.length; ei++) {
2575
+ log(sym.bar + " " + a.red + p.errors[ei].username + ": " + p.errors[ei].error + a.reset);
2576
+ }
2577
+ }
2578
+ }
2579
+ log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
2580
+ log(sym.bar);
2581
+ }
2582
+ showSettingsMenu(config, ip);
2583
+ });
2584
+ } else {
2585
+ showSettingsMenu(config, ip);
2586
+ }
2587
+ });
2588
+ break;
2589
+
2590
+ case "disable_os_users":
2591
+ log(sym.bar);
2592
+ log(sym.bar + " " + a.yellow + sym.warn + " Disable OS-level user isolation?" + a.reset);
2593
+ log(sym.bar);
2594
+ log(sym.bar + " " + a.dim + "Processes will no longer be spawned as mapped Linux users." + a.reset);
2595
+ log(sym.bar + " " + a.dim + "User mappings will be preserved and restored if re-enabled." + a.reset);
2596
+ log(sym.bar);
2597
+ promptSelect("Confirm", [
2598
+ { label: "Disable OS-level user isolation", value: "confirm" },
2599
+ { label: "Cancel", value: "cancel" },
2600
+ ], function (confirmChoice) {
2601
+ if (confirmChoice === "confirm") {
2602
+ sendIPCCommand(socketPath(), { cmd: "set_os_users", value: false }).then(function (res) {
2603
+ if (res.ok) {
2604
+ config.osUsers = false;
2605
+ log(sym.bar);
2606
+ log(sym.done + " " + a.green + "OS-level user isolation disabled." + a.reset);
2607
+ log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
2608
+ log(sym.bar);
2609
+ }
2610
+ showSettingsMenu(config, ip);
2611
+ });
2612
+ } else {
2613
+ showSettingsMenu(config, ip);
2614
+ }
2615
+ });
2616
+ break;
2617
+
2618
+ case "rerun_setup":
2619
+ log(sym.bar);
2620
+ log(sym.bar + " " + a.yellow + sym.warn + " Re-run setup wizard?" + a.reset);
2621
+ log(sym.bar);
2622
+ log(sym.bar + " " + a.dim + "This will shut down the running daemon, reset your setup" + a.reset);
2623
+ log(sym.bar + " " + a.dim + "preferences (mode, port), and walk you through the wizard again." + a.reset);
2624
+ log(sym.bar + " " + a.dim + "Your projects and user accounts will be preserved." + a.reset);
2625
+ log(sym.bar);
2626
+ promptSelect("Confirm", [
2627
+ { label: "Re-run setup wizard", value: "confirm" },
2628
+ { label: "Cancel", value: "cancel" },
2629
+ ], function (confirmChoice) {
2630
+ if (confirmChoice === "confirm") {
2631
+ // Clear setupCompleted so setup() runs fresh
2632
+ var cfg = loadConfig() || {};
2633
+ delete cfg.setupCompleted;
2634
+ delete cfg.mode;
2635
+ cfg.pid = null;
2636
+ saveConfig(cfg);
2637
+ // Shut down the daemon
2638
+ sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
2639
+ clearStaleConfig();
2640
+ // Run the setup wizard, then fork a new daemon
2641
+ setup(function (mode, keepAwake, wantOsUsers) {
2642
+ var rc = loadClayrc();
2643
+ var restorable = (rc.recentProjects || []).filter(function (p) {
2644
+ return p.path !== cwd && fs.existsSync(p.path);
2645
+ });
2646
+ if (restorable.length > 0) {
2647
+ promptRestoreProjects(restorable, function (selected) {
2648
+ forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
2649
+ });
2650
+ } else {
2651
+ log(sym.bar);
2652
+ log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
2653
+ log("");
2654
+ forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
2655
+ }
2656
+ });
2657
+ }).catch(function () {
2658
+ clearStaleConfig();
2659
+ setup(function (mode, keepAwake, wantOsUsers) {
2660
+ var rc = loadClayrc();
2661
+ var restorable = (rc.recentProjects || []).filter(function (p) {
2662
+ return p.path !== cwd && fs.existsSync(p.path);
2663
+ });
2664
+ if (restorable.length > 0) {
2665
+ promptRestoreProjects(restorable, function (selected) {
2666
+ forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
2667
+ });
2668
+ } else {
2669
+ log(sym.bar);
2670
+ log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
2671
+ log("");
2672
+ forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
2673
+ }
2674
+ });
2675
+ });
2676
+ } else {
2677
+ showSettingsMenu(config, ip);
2678
+ }
2679
+ });
2680
+ break;
2681
+
2419
2682
  case "logs":
2420
2683
  console.clear();
2421
2684
  log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
@@ -2477,14 +2740,14 @@ var currentVersion = require("../package.json").version;
2477
2740
  if (devConfig.pid) clearStaleConfig();
2478
2741
  devConfig = null;
2479
2742
  }
2480
- // No config — go through setup (disclaimer, port, PIN, etc.)
2743
+ // No config — go through setup (disclaimer, port, mode, etc.)
2481
2744
  if (!devConfig) {
2482
- setup(function (pin, keepAwake) {
2483
- devMode(pin, keepAwake, null);
2745
+ setup(function (mode, keepAwake, wantOsUsers) {
2746
+ devMode(mode, keepAwake, null);
2484
2747
  });
2485
2748
  } else {
2486
- // Reuse existing PIN hash from previous config
2487
- await devMode(cliPin || null, devConfig.keepAwake || false, devConfig.pinHash || null);
2749
+ // Reuse existing config (repeat run)
2750
+ await devMode(devConfig.mode || "single", devConfig.keepAwake || false, devConfig.pinHash || null);
2488
2751
  }
2489
2752
  return;
2490
2753
  }
@@ -2561,19 +2824,55 @@ var currentVersion = require("../package.json").version;
2561
2824
  showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
2562
2825
  }
2563
2826
  } else {
2564
- // No daemon running — first-time setup
2565
- if (autoYes) {
2566
- var pin = cliPin || null;
2567
- console.log(" " + sym.done + " Auto-accepted disclaimer");
2568
- console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
2569
- if (dangerouslySkipPermissions) {
2570
- console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + (pin ? "" : " (no PIN)") + a.reset);
2827
+ // No daemon running — check for saved config (repeat run)
2828
+ var savedConfig = loadConfig();
2829
+ var isRepeatRun = savedConfig && savedConfig.setupCompleted;
2830
+
2831
+ // --multi-user / --os-users CLI flags set config directly for headless/scripted usage
2832
+ if (multiUserMode) {
2833
+ if (!savedConfig) savedConfig = {};
2834
+ savedConfig.mode = "multi";
2835
+ savedConfig.setupCompleted = true;
2836
+ }
2837
+ if (osUsersMode) {
2838
+ if (!savedConfig) savedConfig = {};
2839
+ savedConfig.osUsers = true;
2840
+ savedConfig.mode = "multi";
2841
+ savedConfig.setupCompleted = true;
2842
+ }
2843
+ isRepeatRun = savedConfig && savedConfig.setupCompleted;
2844
+
2845
+ if (isRepeatRun || autoYes) {
2846
+ // Repeat run or --yes: skip wizard, reuse saved config
2847
+ var savedMode = (savedConfig && savedConfig.mode) || "single";
2848
+ var savedKeepAwake = (savedConfig && savedConfig.keepAwake) || false;
2849
+ var savedOsUsers = (savedConfig && savedConfig.osUsers) || false;
2850
+
2851
+ // os-users requires root
2852
+ if (savedOsUsers && typeof process.getuid === "function" && process.getuid() !== 0) {
2853
+ console.error(a.red + "OS user isolation requires root." + a.reset);
2854
+ console.error("Run: " + a.bold + "sudo npx clay-server" + a.reset);
2855
+ process.exit(1);
2856
+ return;
2571
2857
  }
2858
+
2859
+ if (savedConfig && savedConfig.port) port = savedConfig.port;
2860
+ if (savedConfig && savedConfig.host) host = savedConfig.host;
2861
+ if (savedConfig && savedConfig.dangerouslySkipPermissions) dangerouslySkipPermissions = true;
2862
+
2863
+ if (autoYes) {
2864
+ console.log(" " + sym.done + " Auto-accepted disclaimer");
2865
+ console.log(" " + sym.done + " Mode: " + savedMode);
2866
+ if (dangerouslySkipPermissions) {
2867
+ console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
2868
+ }
2869
+ }
2870
+
2572
2871
  var autoRc = loadClayrc();
2573
2872
  var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
2574
2873
  return p.path !== cwd && fs.existsSync(p.path);
2575
2874
  });
2576
- if (autoRestorable.length > 0) {
2875
+ if (autoRestorable.length > 0 && autoYes) {
2577
2876
  console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
2578
2877
  }
2579
2878
  // Add cwd if it has history in .clayrc, or if there are no other projects to restore
@@ -2581,9 +2880,10 @@ var currentVersion = require("../package.json").version;
2581
2880
  return p.path === cwd;
2582
2881
  });
2583
2882
  var addCwd = cwdInRc || autoRestorable.length === 0;
2584
- await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
2883
+ await forkDaemon(savedMode, savedKeepAwake, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd, savedOsUsers);
2585
2884
  } else {
2586
- setup(function (pin, keepAwake) {
2885
+ // First run: interactive wizard
2886
+ setup(function (mode, keepAwake, wantOsUsers) {
2587
2887
  // Check ~/.clayrc for previous projects to restore
2588
2888
  var rc = loadClayrc();
2589
2889
  var restorable = (rc.recentProjects || []).filter(function (p) {
@@ -2592,13 +2892,13 @@ var currentVersion = require("../package.json").version;
2592
2892
 
2593
2893
  if (restorable.length > 0) {
2594
2894
  promptRestoreProjects(restorable, function (selected) {
2595
- forkDaemon(pin, keepAwake, selected, false);
2895
+ forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
2596
2896
  });
2597
2897
  } else {
2598
2898
  log(sym.bar);
2599
2899
  log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
2600
2900
  log("");
2601
- forkDaemon(pin, keepAwake, undefined, true);
2901
+ forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
2602
2902
  }
2603
2903
  });
2604
2904
  }