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

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/lib/daemon.js CHANGED
@@ -160,11 +160,10 @@ var relay = createServer({
160
160
  var slugs = config.projects.map(function (p) { return p.slug; });
161
161
  var slug = generateSlug(absPath, slugs);
162
162
  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") {
163
+ var projectEntry = { path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" };
164
+ // The user who adds a project always becomes the owner
165
+ if (wsUser && wsUser.id) {
166
166
  projectEntry.ownerId = wsUser.id;
167
- projectEntry.visibility = "private";
168
167
  }
169
168
  config.projects.push(projectEntry);
170
169
  // Remove from removedProjects if present
@@ -237,15 +236,10 @@ var relay = createServer({
237
236
  try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch (ce) {}
238
237
  return { ok: false, error: "Failed to create project: " + e.message };
239
238
  }
240
- // Register project
241
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
239
+ // Register project - creator always becomes owner, default private
240
+ var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
242
241
  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
- }
242
+ projectEntry.ownerId = wsUser.id;
249
243
  }
250
244
  relay.addProject(targetDir, slug);
251
245
  config.projects.push(projectEntry);
@@ -312,15 +306,11 @@ var relay = createServer({
312
306
  execSync("chown -R " + wsUser.linuxUser + ":" + wsUser.linuxUser + " " + JSON.stringify(targetDir));
313
307
  } catch (e) {}
314
308
  }
315
- // Register project
316
- var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
309
+ // Register project - creator always becomes owner
310
+ // Creator always becomes owner, default private
311
+ var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now(), visibility: "private" };
317
312
  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
- }
313
+ projectEntry.ownerId = wsUser.id;
324
314
  }
325
315
  relay.addProject(targetDir, slug);
326
316
  config.projects.push(projectEntry);
@@ -1041,7 +1031,7 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1041
1031
  var slugs = config.projects.map(function (p) { return p.slug; });
1042
1032
  var slug = generateSlug(absPath, slugs);
1043
1033
  relay.addProject(absPath, slug);
1044
- config.projects.push({ path: absPath, slug: slug, addedAt: Date.now() });
1034
+ config.projects.push({ path: absPath, slug: slug, addedAt: Date.now(), visibility: "private" });
1045
1035
  saveConfig(config);
1046
1036
  try { syncClayrc(config.projects); } catch (e) {}
1047
1037
  console.log("[daemon] Added project:", slug, "→", absPath);
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"); }
@@ -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.3",
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",