clay-server 2.20.0 → 2.20.1-beta.2

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center"><img src="media/hero.png" alt="Clay workspace" /></p>
10
10
 
11
- AI teammates who remember your decisions, challenge your thinking, and grow with your codebase. Runs on your machine.
11
+ A team workspace built on Claude Code. AI teammates who remember your decisions, challenge your thinking, and grow with your codebase. Runs on your machine.
12
12
 
13
13
  ```bash
14
14
  npx clay-server
package/bin/cli.js CHANGED
@@ -36,7 +36,7 @@ var crypto = require("crypto");
36
36
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo, REAL_HOME } = require("../lib/config");
37
37
  var { sendIPCCommand } = require("../lib/ipc");
38
38
  var { generateAuthToken } = require("../lib/server");
39
- var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
39
+ var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser, getSetupCode } = require("../lib/users");
40
40
 
41
41
  function openUrl(url) {
42
42
  try {
@@ -1417,16 +1417,15 @@ function setup(callback) {
1417
1417
  }
1418
1418
  var isRoot = typeof process.getuid === "function" && process.getuid() === 0;
1419
1419
  if (!isRoot) {
1420
- // Save config so sudo clay can pick it up
1421
- var partialConfig = {
1422
- port: port,
1423
- host: host,
1424
- mode: "multi",
1425
- osUsers: true,
1426
- setupCompleted: true,
1427
- dangerouslySkipPermissions: dangerouslySkipPermissions,
1428
- };
1429
- saveConfig(partialConfig);
1420
+ // Merge into existing config (preserve projects, TLS, etc.)
1421
+ var existingCfg = loadConfig() || {};
1422
+ existingCfg.port = port;
1423
+ existingCfg.host = host;
1424
+ existingCfg.mode = "multi";
1425
+ existingCfg.osUsers = true;
1426
+ existingCfg.setupCompleted = true;
1427
+ if (dangerouslySkipPermissions) existingCfg.dangerouslySkipPermissions = true;
1428
+ saveConfig(existingCfg);
1430
1429
  log(sym.bar);
1431
1430
  log(sym.warn + " " + a.yellow + "OS user isolation requires root." + a.reset);
1432
1431
  log(sym.bar + " Run:");
@@ -2009,8 +2008,10 @@ function showMainMenu(config, ip, setupCode) {
2009
2008
  log("");
2010
2009
  }
2011
2010
 
2012
- if (setupCode) {
2013
- log(" " + a.yellow + sym.warn + " Setup code: " + a.bold + setupCode + a.reset);
2011
+ // Always show setup code if one exists (persists until admin is created)
2012
+ var displayCode = setupCode || getSetupCode();
2013
+ if (displayCode) {
2014
+ log(" " + a.yellow + sym.warn + " Setup code: " + a.bold + displayCode + a.reset);
2014
2015
  log(" " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
2015
2016
  log("");
2016
2017
  }
@@ -2373,6 +2374,10 @@ function showSettingsMenu(config, ip) {
2373
2374
  } else {
2374
2375
  items.push({ label: "Enable multi-user mode", value: "multi_user" });
2375
2376
  }
2377
+ var pendingSetupCode = getSetupCode();
2378
+ if (muEnabled && pendingSetupCode) {
2379
+ items.push({ label: "Show setup code", value: "show_setup_code" });
2380
+ }
2376
2381
  if (muEnabled && hasAdmin()) {
2377
2382
  items.push({ label: "Recover admin password", value: "recover_admin" });
2378
2383
  }
@@ -2478,7 +2483,9 @@ function showSettingsMenu(config, ip) {
2478
2483
  }
2479
2484
  if (process.getuid() !== 0) {
2480
2485
  log(sym.bar);
2481
- log(sym.bar + " " + a.red + "Requires running as root." + a.reset);
2486
+ log(sym.bar + " " + a.red + sym.warn + " OS user isolation requires root." + a.reset);
2487
+ log(sym.bar + " " + a.dim + "Shut down this server, then restart with:" + a.reset);
2488
+ log(sym.bar + " " + a.bold + "sudo npx clay-server" + a.reset);
2482
2489
  log(sym.bar);
2483
2490
  promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2484
2491
  showSettingsMenu(config, ip);
@@ -2603,33 +2610,48 @@ function showSettingsMenu(config, ip) {
2603
2610
  { label: "Cancel", value: "cancel" },
2604
2611
  ], function (confirmChoice) {
2605
2612
  if (confirmChoice === "confirm") {
2606
- // Clear setupCompleted so setup() runs fresh
2613
+ // Save old PID before clearing, so we can force-kill if needed
2607
2614
  var cfg = loadConfig() || {};
2615
+ var oldPid = cfg.pid;
2616
+ // Clear setupCompleted so setup() runs fresh
2608
2617
  delete cfg.setupCompleted;
2609
2618
  delete cfg.mode;
2610
2619
  cfg.pid = null;
2611
2620
  saveConfig(cfg);
2612
- // Shut down the daemon
2613
- sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
2614
- clearStaleConfig();
2615
- // Run the setup wizard, then fork a new daemon
2616
- setup(function (mode, keepAwake, wantOsUsers) {
2617
- var rc = loadClayrc();
2618
- var restorable = (rc.recentProjects || []).filter(function (p) {
2619
- return p.path !== cwd && fs.existsSync(p.path);
2621
+
2622
+ // Helper: wait for port to be free, force-kill if needed
2623
+ function waitForPortFree(cb) {
2624
+ var attempts = 0;
2625
+ var maxAttempts = 12; // 6 seconds total
2626
+ function check() {
2627
+ isPortFree(port).then(function (free) {
2628
+ if (free) return cb();
2629
+ attempts++;
2630
+ if (attempts >= maxAttempts) {
2631
+ // Port still busy, force-kill old daemon
2632
+ if (oldPid) {
2633
+ try { process.kill(oldPid, "SIGKILL"); } catch (e) {}
2634
+ }
2635
+ // Wait a bit more after SIGKILL
2636
+ setTimeout(function () {
2637
+ isPortFree(port).then(function (free2) {
2638
+ if (!free2) {
2639
+ log(sym.warn + " " + a.yellow + "Port " + port + " still in use. Kill the process manually:" + a.reset);
2640
+ log(sym.bar + " " + a.bold + "lsof -ti:" + port + " | xargs kill -9" + a.reset);
2641
+ }
2642
+ cb();
2643
+ });
2644
+ }, 1000);
2645
+ return;
2646
+ }
2647
+ setTimeout(check, 500);
2620
2648
  });
2621
- if (restorable.length > 0) {
2622
- promptRestoreProjects(restorable, function (selected) {
2623
- forkDaemon(mode, keepAwake, selected, false, wantOsUsers);
2624
- });
2625
- } else {
2626
- log(sym.bar);
2627
- log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
2628
- log("");
2629
- forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
2630
- }
2631
- });
2632
- }).catch(function () {
2649
+ }
2650
+ check();
2651
+ }
2652
+
2653
+ // Helper: run setup wizard after daemon is dead
2654
+ function proceedWithSetup() {
2633
2655
  clearStaleConfig();
2634
2656
  setup(function (mode, keepAwake, wantOsUsers) {
2635
2657
  var rc = loadClayrc();
@@ -2647,6 +2669,17 @@ function showSettingsMenu(config, ip) {
2647
2669
  forkDaemon(mode, keepAwake, undefined, true, wantOsUsers);
2648
2670
  }
2649
2671
  });
2672
+ }
2673
+
2674
+ // Shut down the daemon, then wait for port to be free
2675
+ sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
2676
+ waitForPortFree(proceedWithSetup);
2677
+ }).catch(function () {
2678
+ // IPC failed, daemon may be unresponsive. Try SIGTERM, then wait.
2679
+ if (oldPid) {
2680
+ try { process.kill(oldPid, "SIGTERM"); } catch (e) {}
2681
+ }
2682
+ waitForPortFree(proceedWithSetup);
2650
2683
  });
2651
2684
  } else {
2652
2685
  showSettingsMenu(config, ip);
@@ -2654,6 +2687,23 @@ function showSettingsMenu(config, ip) {
2654
2687
  });
2655
2688
  break;
2656
2689
 
2690
+ case "show_setup_code":
2691
+ var currentCode = getSetupCode();
2692
+ if (currentCode) {
2693
+ log(sym.bar);
2694
+ log(sym.bar + " " + a.yellow + sym.warn + " Setup code: " + a.bold + currentCode + a.reset);
2695
+ log(sym.bar + " " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
2696
+ log(sym.bar);
2697
+ } else {
2698
+ log(sym.bar);
2699
+ log(sym.bar + " " + a.dim + "No pending setup code (admin already exists)." + a.reset);
2700
+ log(sym.bar);
2701
+ }
2702
+ promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
2703
+ showSettingsMenu(config, ip);
2704
+ });
2705
+ break;
2706
+
2657
2707
  case "logs":
2658
2708
  console.clear();
2659
2709
  log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
package/lib/daemon.js CHANGED
@@ -43,6 +43,11 @@ try {
43
43
  process.exit(1);
44
44
  }
45
45
 
46
+ console.log("[daemon] Config: " + configFile);
47
+ console.log("[daemon] Users: " + usersModule.USERS_FILE);
48
+ if (process.env.SUDO_USER) console.log("[daemon] SUDO_USER: " + process.env.SUDO_USER);
49
+ console.log("[daemon] UID: " + (typeof process.getuid === "function" ? process.getuid() : "N/A"));
50
+
46
51
  // --- OS users mode: check required system dependencies ---
47
52
  if (config.osUsers) {
48
53
  var { execSync: checkExec } = require("child_process");
package/lib/users.js CHANGED
@@ -115,7 +115,15 @@ function generateSetupCode() {
115
115
 
116
116
  function getSetupCode() {
117
117
  var data = loadUsers();
118
- return data.setupCode || null;
118
+ if (data.setupCode) return data.setupCode;
119
+ // Defensive: if multi-user is on, no admin, and no code, auto-generate one
120
+ if (data.multiUser && !findAdmin(data)) {
121
+ var code = generateSetupCode();
122
+ data.setupCode = code;
123
+ saveUsers(data);
124
+ return code;
125
+ }
126
+ return null;
119
127
  }
120
128
 
121
129
  function clearSetupCode() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.20.0",
3
+ "version": "2.20.1-beta.2",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",