clay-server 2.22.0-beta.2 → 2.22.0-beta.4

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
@@ -1546,6 +1546,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1546
1546
  builtinCert: hasBuiltinCert,
1547
1547
  mkcertDetected: mkcertDetected,
1548
1548
  debug: debugMode,
1549
+ headless: headlessMode,
1549
1550
  keepAwake: keepAwake,
1550
1551
  dangerouslySkipPermissions: dangerouslySkipPermissions,
1551
1552
  osUsers: wantOsUsers || osUsersMode,
package/lib/daemon.js CHANGED
@@ -98,14 +98,6 @@ if (config.tls) {
98
98
  }
99
99
 
100
100
  var caRoot = null;
101
- try {
102
- var { execSync } = require("child_process");
103
- caRoot = path.join(
104
- execSync("mkcert -CAROOT", { encoding: "utf8", stdio: "pipe" }).trim(),
105
- "rootCA.pem"
106
- );
107
- if (!fs.existsSync(caRoot)) caRoot = null;
108
- } catch (e) {}
109
101
 
110
102
  // --- Resolve LAN IP for share URL ---
111
103
  var os2 = require("os");
@@ -160,11 +152,10 @@ var relay = createServer({
160
152
  var slugs = config.projects.map(function (p) { return p.slug; });
161
153
  var slug = generateSlug(absPath, slugs);
162
154
  relay.addProject(absPath, slug);
163
- var projectEntry = { path: absPath, slug: slug, addedAt: Date.now() };
164
- // Non-admin users own their projects and they default to private
165
- if (wsUser && wsUser.id && wsUser.role !== "admin") {
155
+ var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
156
+ // The user who adds a project always becomes the owner
157
+ if (wsUser && wsUser.id) {
166
158
  projectEntry.ownerId = wsUser.id;
167
- projectEntry.visibility = "private";
168
159
  }
169
160
  config.projects.push(projectEntry);
170
161
  // Remove from removedProjects if present
@@ -237,15 +228,10 @@ var relay = createServer({
237
228
  try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
238
229
  return { ok: false, error: "Failed to create project: " + e.message };
239
230
  }
240
- // Register project
241
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
231
+ // Register project - creator always becomes owner, default private
232
+ var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
242
233
  if (wsUser && wsUser.id) {
243
- if (config.osUsers || wsUser.role !== "admin") {
244
- projectEntry.ownerId = wsUser.id;
245
- }
246
- if (wsUser.role !== "admin") {
247
- projectEntry.visibility = "private";
248
- }
234
+ projectEntry.ownerId = wsUser.id;
249
235
  }
250
236
  relay.addProject(targetDir, slug);
251
237
  config.projects.push(projectEntry);
@@ -312,15 +298,11 @@ var relay = createServer({
312
298
  execSync("chown -R " + wsUser.linuxUser + ":" + wsUser.linuxUser + " " + JSON.stringify(targetDir));
313
299
  } catch (e) {}
314
300
  }
315
- // Register project
316
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
301
+ // Register project - creator always becomes owner
302
+ // Creator always becomes owner, default private
303
+ var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
317
304
  if (wsUser && wsUser.id) {
318
- if (config.osUsers || wsUser.role !== "admin") {
319
- projectEntry.ownerId = wsUser.id;
320
- }
321
- if (wsUser.role !== "admin") {
322
- projectEntry.visibility = "private";
323
- }
305
+ projectEntry.ownerId = wsUser.id;
324
306
  }
325
307
  relay.addProject(targetDir, slug);
326
308
  config.projects.push(projectEntry);
@@ -665,6 +647,7 @@ var relay = createServer({
665
647
  port: config.port,
666
648
  tls: !!tlsOptions,
667
649
  debug: !!config.debug,
650
+ headless: !!config.headless,
668
651
  keepAwake: !!config.keepAwake,
669
652
  autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
670
653
  pinEnabled: !!config.pinHash,
@@ -1041,7 +1024,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1041
1024
  var slugs = config.projects.map(function (p) { return p.slug; });
1042
1025
  var slug = generateSlug(absPath, slugs);
1043
1026
  relay.addProject(absPath, slug);
1044
- config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
1027
+ config.projects.push({ path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" });
1045
1028
  saveConfig(config);
1046
1029
  try { syncClayrc(config.projects); } catch (e) {}
1047
1030
  console.log("[daemon] Added project:", slug, "→", absPath);
@@ -1192,6 +1175,10 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1192
1175
  return { ok: true };
1193
1176
 
1194
1177
  case "update": {
1178
+ if (config.headless) {
1179
+ console.log("[daemon] Update & restart requested via IPC — blocked (headless mode)");
1180
+ return { ok: false, error: "Auto-update is disabled in headless mode" };
1181
+ }
1195
1182
  console.log("[daemon] Update & restart requested via IPC");
1196
1183
 
1197
1184
  // Dev mode (config.debug): just exit with code 120, cli.js dev watcher respawns daemon
package/lib/project.js CHANGED
@@ -1171,6 +1171,15 @@ function createProjectContext(opts) {
1171
1171
  setTimeout(function() { resumeLoop(); }, 500);
1172
1172
  }
1173
1173
 
1174
+ // Auto-assign owner if project has none and a user connects (e.g. IPC-added projects)
1175
+ if (!projectOwnerId && ws._clayUser && ws._clayUser.id && !isMate) {
1176
+ projectOwnerId = ws._clayUser.id;
1177
+ if (opts.onProjectOwnerChanged) {
1178
+ opts.onProjectOwnerChanged(slug, projectOwnerId);
1179
+ }
1180
+ console.log("[project] Auto-assigned owner for " + slug + ": " + projectOwnerId);
1181
+ }
1182
+
1174
1183
  // Send cached state
1175
1184
  var _userId = ws._clayUser ? ws._clayUser.id : null;
1176
1185
  var _filteredProjects = getProjectList(_userId);
package/lib/public/app.js CHANGED
@@ -2212,6 +2212,9 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
2212
2212
  sendBtn.disabled = false;
2213
2213
  setSendBtnMode("send");
2214
2214
  connectOverlay.classList.add("hidden");
2215
+ // Hide update banner on reconnect; server will re-send update_available if still needed
2216
+ var updPill = $("update-pill-wrap");
2217
+ if (updPill) updPill.classList.add("hidden");
2215
2218
  stopVerbCycle();
2216
2219
  } else if (status === "processing") {
2217
2220
  if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
@@ -4024,6 +4027,48 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4024
4027
  if (savedMainWs === this) return;
4025
4028
  };
4026
4029
 
4030
+ function showUpdateAvailable(msg) {
4031
+ var updatePillWrap = $("update-pill-wrap");
4032
+ var updateVersion = $("update-version");
4033
+ if (updatePillWrap && updateVersion && msg.version) {
4034
+ updateVersion.textContent = "v" + msg.version;
4035
+ updatePillWrap.classList.remove("hidden");
4036
+ var updPill = $("update-pill");
4037
+ var updResetBtn = $("update-now");
4038
+ if (isHeadlessMode) {
4039
+ // In headless mode, hide auto-update button and show manual guide only
4040
+ if (updPill) updPill.innerHTML = '<i data-lucide="arrow-up-circle"></i> <span id="update-version">v' + msg.version + '</span> available. Update manually';
4041
+ if (updResetBtn) updResetBtn.style.display = "none";
4042
+ } else {
4043
+ // Reset button state (may be stuck on "Updating..." after restart)
4044
+ if (updResetBtn) {
4045
+ updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
4046
+ updResetBtn.disabled = false;
4047
+ updResetBtn.style.display = "";
4048
+ }
4049
+ }
4050
+ // Update manual command based on version (beta vs stable)
4051
+ var updManualCmd = $("update-manual-cmd");
4052
+ if (updManualCmd) {
4053
+ var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
4054
+ updManualCmd.textContent = "npx clay-server@" + updTag;
4055
+ }
4056
+ refreshIcons();
4057
+ }
4058
+ // Update the settings check-for-updates button
4059
+ var settingsUpdBtn = $("settings-update-check");
4060
+ if (settingsUpdBtn && msg.version) {
4061
+ settingsUpdBtn.innerHTML = "";
4062
+ var ic = document.createElement("i");
4063
+ ic.setAttribute("data-lucide", "arrow-up-circle");
4064
+ settingsUpdBtn.appendChild(ic);
4065
+ settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
4066
+ settingsUpdBtn.classList.add("settings-btn-update-available");
4067
+ settingsUpdBtn.disabled = false;
4068
+ refreshIcons();
4069
+ }
4070
+ }
4071
+
4027
4072
  ws.onmessage = function (event) {
4028
4073
  // If this WS is stashed while in mate DM, only allow skill_installed through
4029
4074
  if (savedMainWs === this) {
@@ -4173,36 +4218,14 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4173
4218
  break;
4174
4219
 
4175
4220
  case "update_available":
4176
- var updatePillWrap = $("update-pill-wrap");
4177
- var updateVersion = $("update-version");
4178
- if (updatePillWrap && updateVersion && msg.version) {
4179
- updateVersion.textContent = "v" + msg.version;
4180
- updatePillWrap.classList.remove("hidden");
4181
- // Reset button state (may be stuck on "Updating..." after restart)
4182
- var updResetBtn = $("update-now");
4183
- if (updResetBtn) {
4184
- updResetBtn.innerHTML = '<i data-lucide="download"></i> Update now';
4185
- updResetBtn.disabled = false;
4186
- }
4187
- // Update manual command based on version (beta vs stable)
4188
- var updManualCmd = $("update-manual-cmd");
4189
- if (updManualCmd) {
4190
- var updTag = msg.version.indexOf("-beta") !== -1 ? "beta" : "latest";
4191
- updManualCmd.textContent = "npx clay-server@" + updTag;
4192
- }
4193
- refreshIcons();
4194
- }
4195
- // Update the settings check-for-updates button
4196
- var settingsUpdBtn = $("settings-update-check");
4197
- if (settingsUpdBtn && msg.version) {
4198
- settingsUpdBtn.innerHTML = "";
4199
- var ic = document.createElement("i");
4200
- ic.setAttribute("data-lucide", "arrow-up-circle");
4201
- settingsUpdBtn.appendChild(ic);
4202
- settingsUpdBtn.appendChild(document.createTextNode(" Update available (v" + msg.version + ")"));
4203
- settingsUpdBtn.classList.add("settings-btn-update-available");
4204
- settingsUpdBtn.disabled = false;
4205
- refreshIcons();
4221
+ // In multi-user mode, only show update UI to admins
4222
+ if (isMultiUserMode) {
4223
+ checkAdminAccess().then(function (isAdmin) {
4224
+ if (!isAdmin) return;
4225
+ showUpdateAvailable(msg);
4226
+ });
4227
+ } else {
4228
+ showUpdateAvailable(msg);
4206
4229
  }
4207
4230
  break;
4208
4231
 
@@ -5147,6 +5170,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5147
5170
  break;
5148
5171
 
5149
5172
  case "daemon_config":
5173
+ if (msg.config && msg.config.headless) isHeadlessMode = true;
5150
5174
  updateDaemonConfig(msg.config);
5151
5175
  break;
5152
5176
 
@@ -5634,6 +5658,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
5634
5658
 
5635
5659
  // --- Admin (multi-user mode) ---
5636
5660
  var isMultiUserMode = false;
5661
+ var isHeadlessMode = false;
5637
5662
  var myUserId = null;
5638
5663
  initAdmin({
5639
5664
  get projectList() { return cachedProjects; },
@@ -498,6 +498,113 @@
498
498
  .project-ctx-item.project-ctx-delete { color: var(--error); }
499
499
  .project-ctx-item.project-ctx-delete:hover { background: var(--error-8); }
500
500
 
501
+ /* --- Project Access Popover --- */
502
+ .project-access-popover {
503
+ position: fixed;
504
+ background: var(--sidebar-bg);
505
+ border: 1px solid var(--border);
506
+ border-radius: 12px;
507
+ padding: 0;
508
+ width: 260px;
509
+ box-shadow: 0 8px 30px rgba(var(--shadow-rgb), 0.35);
510
+ z-index: 9999;
511
+ animation: ctxMenuAppear 0.12s ease-out;
512
+ overflow: hidden;
513
+ }
514
+ .project-access-header {
515
+ display: flex;
516
+ align-items: center;
517
+ justify-content: space-between;
518
+ padding: 12px 14px 8px;
519
+ border-bottom: 1px solid var(--border);
520
+ }
521
+ .project-access-title {
522
+ font-size: 13px;
523
+ font-weight: 600;
524
+ color: var(--text);
525
+ }
526
+ .project-access-close {
527
+ background: none;
528
+ border: none;
529
+ color: var(--text-secondary);
530
+ font-size: 18px;
531
+ cursor: pointer;
532
+ padding: 0 2px;
533
+ line-height: 1;
534
+ }
535
+ .project-access-close:hover { color: var(--text); }
536
+ .project-access-section {
537
+ padding: 10px 14px;
538
+ }
539
+ .project-access-label {
540
+ display: block;
541
+ font-size: 11px;
542
+ font-weight: 600;
543
+ color: var(--text-secondary);
544
+ text-transform: uppercase;
545
+ letter-spacing: 0.04em;
546
+ margin-bottom: 6px;
547
+ }
548
+ .project-access-vis-row {
549
+ display: flex;
550
+ gap: 6px;
551
+ }
552
+ .project-access-vis-btn {
553
+ flex: 1;
554
+ display: flex;
555
+ align-items: center;
556
+ justify-content: center;
557
+ gap: 6px;
558
+ padding: 7px 0;
559
+ font-size: 12px;
560
+ font-family: inherit;
561
+ color: var(--text-secondary);
562
+ background: rgba(var(--overlay-rgb), 0.04);
563
+ border: 1px solid var(--border);
564
+ border-radius: 8px;
565
+ cursor: pointer;
566
+ transition: all 0.15s;
567
+ }
568
+ .project-access-vis-btn .lucide { width: 14px; height: 14px; }
569
+ .project-access-vis-btn:hover { background: rgba(var(--overlay-rgb), 0.08); }
570
+ .project-access-vis-btn.active {
571
+ background: var(--accent-8, rgba(99, 102, 241, 0.08));
572
+ border-color: var(--accent, #6366f1);
573
+ color: var(--accent, #6366f1);
574
+ font-weight: 500;
575
+ }
576
+ .project-access-user-list {
577
+ max-height: 200px;
578
+ overflow-y: auto;
579
+ }
580
+ .project-access-user-item {
581
+ display: flex;
582
+ align-items: center;
583
+ gap: 8px;
584
+ padding: 5px 0;
585
+ font-size: 13px;
586
+ color: var(--text);
587
+ cursor: pointer;
588
+ }
589
+ .project-access-user-item input[type="checkbox"] {
590
+ accent-color: var(--accent, #6366f1);
591
+ width: 15px;
592
+ height: 15px;
593
+ cursor: pointer;
594
+ }
595
+ .project-access-user-item:hover { color: var(--text); }
596
+ .project-access-empty {
597
+ font-size: 12px;
598
+ color: var(--text-tertiary);
599
+ padding: 8px 0;
600
+ }
601
+ .project-access-loading {
602
+ padding: 20px 14px;
603
+ text-align: center;
604
+ font-size: 12px;
605
+ color: var(--text-secondary);
606
+ }
607
+
501
608
  /* --- Emoji picker popover --- */
502
609
  .emoji-picker {
503
610
  position: fixed;
@@ -2672,6 +2672,158 @@ var EMOJI_CATEGORIES = [
2672
2672
  ]},
2673
2673
  ];
2674
2674
 
2675
+ // --- Project Access Popover ---
2676
+ var projectAccessPopover = null;
2677
+
2678
+ function closeProjectAccessPopover() {
2679
+ if (projectAccessPopover) {
2680
+ projectAccessPopover.remove();
2681
+ projectAccessPopover = null;
2682
+ document.removeEventListener("click", closeAccessOnOutside);
2683
+ document.removeEventListener("keydown", closeAccessOnEscape);
2684
+ }
2685
+ }
2686
+
2687
+ function closeAccessOnOutside(e) {
2688
+ if (projectAccessPopover && !projectAccessPopover.contains(e.target)) closeProjectAccessPopover();
2689
+ }
2690
+ function closeAccessOnEscape(e) {
2691
+ if (e.key === "Escape") closeProjectAccessPopover();
2692
+ }
2693
+
2694
+ function showProjectAccessPopover(anchorEl, slug) {
2695
+ closeProjectAccessPopover();
2696
+
2697
+ var popover = document.createElement("div");
2698
+ popover.className = "project-access-popover";
2699
+ popover.innerHTML = '<div class="project-access-loading">Loading...</div>';
2700
+ popover.addEventListener("click", function (e) { e.stopPropagation(); });
2701
+ document.body.appendChild(popover);
2702
+ projectAccessPopover = popover;
2703
+
2704
+ // Position near anchor
2705
+ requestAnimationFrame(function () {
2706
+ var rect = anchorEl.getBoundingClientRect();
2707
+ popover.style.position = "fixed";
2708
+ popover.style.left = (rect.right + 8) + "px";
2709
+ popover.style.top = rect.top + "px";
2710
+ popover.style.zIndex = "9999";
2711
+ var popRect = popover.getBoundingClientRect();
2712
+ if (popRect.right > window.innerWidth - 8) {
2713
+ popover.style.left = (rect.left - popRect.width - 8) + "px";
2714
+ }
2715
+ if (popRect.bottom > window.innerHeight - 8) {
2716
+ popover.style.top = (window.innerHeight - popRect.height - 8) + "px";
2717
+ }
2718
+ });
2719
+
2720
+ setTimeout(function () {
2721
+ document.addEventListener("click", closeAccessOnOutside);
2722
+ document.addEventListener("keydown", closeAccessOnEscape);
2723
+ }, 0);
2724
+
2725
+ // Fetch access info and user list in parallel
2726
+ Promise.all([
2727
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/access").then(function (r) { return r.json(); }),
2728
+ fetch("/api/admin/users").then(function (r) { return r.json(); }),
2729
+ ]).then(function (results) {
2730
+ var access = results[0];
2731
+ var usersData = results[1];
2732
+ if (access.error || usersData.error) {
2733
+ popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
2734
+ return;
2735
+ }
2736
+ renderAccessPopover(popover, slug, access, usersData.users || []);
2737
+ }).catch(function () {
2738
+ popover.innerHTML = '<div class="project-access-loading">Failed to load</div>';
2739
+ });
2740
+ }
2741
+
2742
+ function renderAccessPopover(popover, slug, access, allUsers) {
2743
+ var visibility = access.visibility || "public";
2744
+ var allowedUsers = access.allowedUsers || [];
2745
+ var ownerId = access.ownerId;
2746
+
2747
+ // Filter out the owner from the user list (owner always has access)
2748
+ var selectableUsers = allUsers.filter(function (u) { return u.id !== ownerId; });
2749
+
2750
+ var html = '';
2751
+ html += '<div class="project-access-header">';
2752
+ html += '<span class="project-access-title">Project Access</span>';
2753
+ html += '<button class="project-access-close">&times;</button>';
2754
+ html += '</div>';
2755
+
2756
+ // Visibility toggle
2757
+ html += '<div class="project-access-section">';
2758
+ html += '<label class="project-access-label">Visibility</label>';
2759
+ html += '<div class="project-access-vis-row">';
2760
+ html += '<button class="project-access-vis-btn' + (visibility === "private" ? ' active' : '') + '" data-vis="private">';
2761
+ html += iconHtml("lock") + ' Private';
2762
+ html += '</button>';
2763
+ html += '<button class="project-access-vis-btn' + (visibility === "public" ? ' active' : '') + '" data-vis="public">';
2764
+ html += iconHtml("globe") + ' Public';
2765
+ html += '</button>';
2766
+ html += '</div>';
2767
+ html += '</div>';
2768
+
2769
+ // Allowed users (only when private)
2770
+ html += '<div class="project-access-section project-access-users-section"' + (visibility !== "private" ? ' style="display:none"' : '') + '>';
2771
+ html += '<label class="project-access-label">Allowed Users</label>';
2772
+ html += '<div class="project-access-user-list">';
2773
+ for (var i = 0; i < selectableUsers.length; i++) {
2774
+ var u = selectableUsers[i];
2775
+ var checked = allowedUsers.indexOf(u.id) !== -1 ? " checked" : "";
2776
+ html += '<label class="project-access-user-item">';
2777
+ html += '<input type="checkbox" data-uid="' + u.id + '"' + checked + '>';
2778
+ html += '<span>' + escapeHtml(u.displayName || u.username || u.id) + '</span>';
2779
+ html += '</label>';
2780
+ }
2781
+ if (selectableUsers.length === 0) {
2782
+ html += '<div class="project-access-empty">No other users</div>';
2783
+ }
2784
+ html += '</div>';
2785
+ html += '</div>';
2786
+
2787
+ popover.innerHTML = html;
2788
+ refreshIcons();
2789
+
2790
+ // Close button
2791
+ popover.querySelector(".project-access-close").addEventListener("click", function () {
2792
+ closeProjectAccessPopover();
2793
+ });
2794
+
2795
+ // Visibility toggle
2796
+ popover.querySelectorAll(".project-access-vis-btn").forEach(function (btn) {
2797
+ btn.addEventListener("click", function () {
2798
+ var newVis = btn.dataset.vis;
2799
+ popover.querySelectorAll(".project-access-vis-btn").forEach(function (b) { b.classList.remove("active"); });
2800
+ btn.classList.add("active");
2801
+ var usersSection = popover.querySelector(".project-access-users-section");
2802
+ if (usersSection) usersSection.style.display = newVis === "private" ? "" : "none";
2803
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/visibility", {
2804
+ method: "PUT",
2805
+ headers: { "Content-Type": "application/json" },
2806
+ body: JSON.stringify({ visibility: newVis }),
2807
+ });
2808
+ });
2809
+ });
2810
+
2811
+ // User checkboxes
2812
+ popover.querySelectorAll('.project-access-user-item input[type="checkbox"]').forEach(function (cb) {
2813
+ cb.addEventListener("change", function () {
2814
+ var selected = [];
2815
+ popover.querySelectorAll('.project-access-user-item input[type="checkbox"]:checked').forEach(function (c) {
2816
+ selected.push(c.dataset.uid);
2817
+ });
2818
+ fetch("/api/admin/projects/" + encodeURIComponent(slug) + "/users", {
2819
+ method: "PUT",
2820
+ headers: { "Content-Type": "application/json" },
2821
+ body: JSON.stringify({ allowedUsers: selected }),
2822
+ });
2823
+ });
2824
+ });
2825
+ }
2826
+
2675
2827
  function closeProjectCtxMenu() {
2676
2828
  if (projectCtxMenu) {
2677
2829
  projectCtxMenu.remove();
@@ -2797,6 +2949,23 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
2797
2949
  });
2798
2950
  menu.appendChild(shareItem);
2799
2951
 
2952
+ // --- Manage Access (owner or admin, multi-user only) ---
2953
+ if (ctx.multiUser && slug.indexOf("--") === -1) {
2954
+ var isProjectOwner = ctx.myUserId && ctx.projectOwnerId && ctx.myUserId === ctx.projectOwnerId;
2955
+ var isAdmin = ctx.permissions && ctx.permissions.projectSettings !== false;
2956
+ if (isProjectOwner || isAdmin) {
2957
+ var accessItem = document.createElement("button");
2958
+ accessItem.className = "project-ctx-item";
2959
+ accessItem.innerHTML = iconHtml("users") + " <span>Manage Access</span>";
2960
+ accessItem.addEventListener("click", function (e) {
2961
+ e.stopPropagation();
2962
+ closeProjectCtxMenu();
2963
+ showProjectAccessPopover(anchorEl, slug);
2964
+ });
2965
+ menu.appendChild(accessItem);
2966
+ }
2967
+ }
2968
+
2800
2969
  if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
2801
2970
  // --- Separator ---
2802
2971
  var sep = document.createElement("div");
package/lib/server.js CHANGED
@@ -1512,13 +1512,23 @@ function createServer(opts) {
1512
1512
  return;
1513
1513
  }
1514
1514
  var mu = getMultiUserFromReq(req);
1515
- if (!mu || mu.role !== "admin") {
1516
- res.writeHead(403, { "Content-Type": "application/json" });
1517
- res.end('{"error":"Admin access required"}');
1515
+ if (!mu) {
1516
+ res.writeHead(401, { "Content-Type": "application/json" });
1517
+ res.end('{"error":"Authentication required"}');
1518
1518
  return;
1519
1519
  }
1520
- res.writeHead(200, { "Content-Type": "application/json" });
1521
- res.end(JSON.stringify({ users: users.getAllUsers() }));
1520
+ // Admins get full user list; project owners get limited list (id, displayName, username)
1521
+ if (mu.role === "admin") {
1522
+ res.writeHead(200, { "Content-Type": "application/json" });
1523
+ res.end(JSON.stringify({ users: users.getAllUsers() }));
1524
+ } else {
1525
+ var allU = users.getAllUsers();
1526
+ var safeUsers = allU.map(function (u) {
1527
+ return { id: u.id, displayName: u.displayName, username: u.username };
1528
+ });
1529
+ res.writeHead(200, { "Content-Type": "application/json" });
1530
+ res.end(JSON.stringify({ users: safeUsers }));
1531
+ }
1522
1532
  return;
1523
1533
  }
1524
1534
 
@@ -1990,9 +2000,12 @@ function createServer(opts) {
1990
2000
  return;
1991
2001
  }
1992
2002
  var mu = getMultiUserFromReq(req);
1993
- if (!mu || mu.role !== "admin") {
2003
+ var _visSlug = fullUrl.split("/")[4];
2004
+ var _visAccess = onGetProjectAccess ? onGetProjectAccess(_visSlug) : null;
2005
+ var _isOwner = mu && _visAccess && _visAccess.ownerId && mu.id === _visAccess.ownerId;
2006
+ if (!mu || (mu.role !== "admin" && !_isOwner)) {
1994
2007
  res.writeHead(403, { "Content-Type": "application/json" });
1995
- res.end('{"error":"Admin access required"}');
2008
+ res.end('{"error":"Admin or project owner access required"}');
1996
2009
  return;
1997
2010
  }
1998
2011
  var projSlug = fullUrl.split("/")[4];
@@ -2092,9 +2105,12 @@ function createServer(opts) {
2092
2105
  return;
2093
2106
  }
2094
2107
  var mu = getMultiUserFromReq(req);
2095
- if (!mu || mu.role !== "admin") {
2108
+ var _usrSlug = fullUrl.split("/")[4];
2109
+ var _usrAccess = onGetProjectAccess ? onGetProjectAccess(_usrSlug) : null;
2110
+ var _isOwnerU = mu && _usrAccess && _usrAccess.ownerId && mu.id === _usrAccess.ownerId;
2111
+ if (!mu || (mu.role !== "admin" && !_isOwnerU)) {
2096
2112
  res.writeHead(403, { "Content-Type": "application/json" });
2097
- res.end('{"error":"Admin access required"}');
2113
+ res.end('{"error":"Admin or project owner access required"}');
2098
2114
  return;
2099
2115
  }
2100
2116
  var projSlug = fullUrl.split("/")[4];
@@ -2134,7 +2150,7 @@ function createServer(opts) {
2134
2150
  return;
2135
2151
  }
2136
2152
 
2137
- // Get project access info (admin only)
2153
+ // Get project access info (admin or project owner)
2138
2154
  if (req.method === "GET" && /^\/api\/admin\/projects\/[a-z0-9_-]+\/access$/.test(fullUrl)) {
2139
2155
  if (!users.isMultiUser()) {
2140
2156
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -2142,9 +2158,12 @@ function createServer(opts) {
2142
2158
  return;
2143
2159
  }
2144
2160
  var mu = getMultiUserFromReq(req);
2145
- if (!mu || mu.role !== "admin") {
2161
+ var _accSlug = fullUrl.split("/")[4];
2162
+ var _accAccess = onGetProjectAccess ? onGetProjectAccess(_accSlug) : null;
2163
+ var _isOwnerA = mu && _accAccess && _accAccess.ownerId && mu.id === _accAccess.ownerId;
2164
+ if (!mu || (mu.role !== "admin" && !_isOwnerA)) {
2146
2165
  res.writeHead(403, { "Content-Type": "application/json" });
2147
- res.end('{"error":"Admin access required"}');
2166
+ res.end('{"error":"Admin or project owner access required"}');
2148
2167
  return;
2149
2168
  }
2150
2169
  var projSlug = fullUrl.split("/")[4];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.22.0-beta.2",
3
+ "version": "2.22.0-beta.4",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",