clay-server 2.8.2 → 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.
@@ -7,6 +7,7 @@ import { setSTTLang, getSTTLang } from './stt.js';
7
7
 
8
8
  var ctx;
9
9
  var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed' };
10
+ var profileUsername = '';
10
11
  var popoverEl = null;
11
12
  var saveTimer = null;
12
13
  var previewSeed = '';
@@ -95,6 +96,17 @@ function applyToIsland() {
95
96
  }
96
97
 
97
98
  nameEl.textContent = displayName;
99
+
100
+ // Show CTA if user hasn't personalized their name
101
+ var ctaEl = document.querySelector('.user-island-cta');
102
+ if (ctaEl) {
103
+ var isDefault = profileUsername && profile.name === profileUsername;
104
+ if (isDefault) {
105
+ ctaEl.classList.remove('hidden');
106
+ } else {
107
+ ctaEl.classList.add('hidden');
108
+ }
109
+ }
98
110
  }
99
111
 
100
112
  // --- Popover ---
@@ -325,12 +337,21 @@ export function initProfile(_ctx) {
325
337
  });
326
338
  }
327
339
 
340
+ var ctaEl = island.querySelector('.user-island-cta');
341
+ if (ctaEl) {
342
+ ctaEl.addEventListener('click', function(e) {
343
+ e.stopPropagation();
344
+ showPopover();
345
+ });
346
+ }
347
+
328
348
  fetchProfile().then(function(data) {
329
349
  if (data.name !== undefined) profile.name = data.name;
330
350
  if (data.lang) profile.lang = data.lang;
331
351
  if (data.avatarColor) profile.avatarColor = data.avatarColor;
332
352
  if (data.avatarStyle) profile.avatarStyle = data.avatarStyle;
333
353
  if (data.avatarSeed) profile.avatarSeed = data.avatarSeed;
354
+ if (data.username) profileUsername = data.username;
334
355
 
335
356
  // Auto-generate seed if none exists
336
357
  if (!profile.avatarSeed) {
@@ -1,6 +1,7 @@
1
1
  // project-settings.js — Project settings panel (profile, defaults, instructions, env)
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { showToast } from './utils.js';
4
+ import { parseEmojis } from './markdown.js';
4
5
 
5
6
  var ctx = null;
6
7
  var panelEl = null;
@@ -252,6 +253,7 @@ function updateIconPreview(icon) {
252
253
  var removeBtn = document.getElementById("ps-icon-remove-btn");
253
254
  if (preview) {
254
255
  preview.textContent = icon || "";
256
+ if (icon) parseEmojis(preview);
255
257
  }
256
258
  if (removeBtn) {
257
259
  removeBtn.classList.toggle("hidden", !icon);
@@ -308,6 +310,7 @@ function showPsEmojiPicker() {
308
310
  tabBtns.push(tab);
309
311
  })(EMOJI_CATEGORIES[t], t);
310
312
  }
313
+ parseEmojis(tabBar);
311
314
  picker.appendChild(tabBar);
312
315
 
313
316
  // Grid
@@ -336,7 +339,7 @@ function showPsEmojiPicker() {
336
339
  grid.appendChild(btn);
337
340
  })(emojis[i]);
338
341
  }
339
-
342
+ parseEmojis(grid);
340
343
  scrollArea.scrollTop = 0;
341
344
  }
342
345
 
@@ -2,6 +2,7 @@
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { showToast, copyToClipboard } from './utils.js';
4
4
  import { parseEnvString, looksLikeEnv } from './project-settings.js';
5
+ import { checkAdminAccess, loadAdminSection } from './admin.js';
5
6
 
6
7
  var ctx = null;
7
8
  var settingsEl = null;
@@ -240,6 +241,10 @@ function switchSection(sectionName) {
240
241
  // Lazy-load section data
241
242
  if (sectionName === "claudemd") loadGlobalClaudeMd();
242
243
  if (sectionName === "environment") loadSharedEnv();
244
+ if (sectionName === "admin-users" || sectionName === "admin-invites" || sectionName === "admin-projects" || sectionName === "admin-smtp") {
245
+ var adminBody = document.getElementById(sectionName + "-body");
246
+ if (adminBody) loadAdminSection(sectionName, adminBody);
247
+ }
243
248
  }
244
249
 
245
250
  function openSettings() {
@@ -251,6 +256,14 @@ function openSettings() {
251
256
  resetRestartButton();
252
257
  resetShutdownForm();
253
258
 
259
+ // Show/hide admin sections based on role
260
+ checkAdminAccess().then(function (isAdmin) {
261
+ var adminEls = settingsEl.querySelectorAll(".settings-admin-only");
262
+ for (var ai = 0; ai < adminEls.length; ai++) {
263
+ adminEls[ai].style.display = isAdmin ? "" : "none";
264
+ }
265
+ });
266
+
254
267
  // Start periodic stats refresh
255
268
  requestStats();
256
269
  statsTimer = setInterval(requestStats, 5000);
@@ -2,6 +2,7 @@ import { escapeHtml, copyToClipboard } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { openProjectSettings } from './project-settings.js';
4
4
  import { triggerShare } from './qrcode.js';
5
+ import { parseEmojis } from './markdown.js';
5
6
 
6
7
  var ctx;
7
8
 
@@ -16,6 +17,9 @@ var expandedLoopGroups = new Set();
16
17
  var cachedProjectList = [];
17
18
  var cachedCurrentSlug = null;
18
19
 
20
+ // --- Session presence (multi-user: who is viewing which session) ---
21
+ var sessionPresence = {}; // { sessionId: [{ id, displayName, avatarStyle, avatarSeed }] }
22
+
19
23
  // --- Countdown timer for upcoming schedules ---
20
24
  var countdownTimer = null;
21
25
  var countdownContainer = null;
@@ -32,7 +36,7 @@ function closeSessionCtxMenu() {
32
36
  }
33
37
  }
34
38
 
35
- function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid) {
39
+ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
36
40
  closeSessionCtxMenu();
37
41
  sessionCtxSessionId = sessionId;
38
42
 
@@ -49,6 +53,24 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid) {
49
53
  });
50
54
  menu.appendChild(renameItem);
51
55
 
56
+ // Session visibility toggle (only the session owner can change)
57
+ if (ctx.multiUser && sessionData && sessionData.ownerId && sessionData.ownerId === ctx.myUserId) {
58
+ var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
59
+ var isPrivate = currentVis === "private";
60
+ var visItem = document.createElement("button");
61
+ visItem.className = "session-ctx-item";
62
+ visItem.innerHTML = iconHtml(isPrivate ? "eye" : "eye-off") + " <span>" + (isPrivate ? "Make Shared" : "Make Private") + "</span>";
63
+ visItem.addEventListener("click", function (e) {
64
+ e.stopPropagation();
65
+ closeSessionCtxMenu();
66
+ var newVis = isPrivate ? "shared" : "private";
67
+ if (ctx.ws && ctx.connected) {
68
+ ctx.ws.send(JSON.stringify({ type: "set_session_visibility", sessionId: sessionId, visibility: newVis }));
69
+ }
70
+ });
71
+ menu.appendChild(visItem);
72
+ }
73
+
52
74
  var deleteItem = document.createElement("button");
53
75
  deleteItem.className = "session-ctx-item session-ctx-delete";
54
76
  deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
@@ -394,6 +416,9 @@ function renderSessionItem(s) {
394
416
  if (s.isProcessing) {
395
417
  textHtml += '<span class="session-processing"></span>';
396
418
  }
419
+ if (ctx.multiUser && s.sessionVisibility === "private") {
420
+ textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
421
+ }
397
422
  textHtml += highlightMatch(s.title || "New Session", searchQuery);
398
423
  textSpan.innerHTML = textHtml;
399
424
  el.appendChild(textSpan);
@@ -402,12 +427,12 @@ function renderSessionItem(s) {
402
427
  moreBtn.className = "session-more-btn";
403
428
  moreBtn.innerHTML = iconHtml("ellipsis");
404
429
  moreBtn.title = "More options";
405
- moreBtn.addEventListener("click", (function(id, title, cliSid, btn) {
430
+ moreBtn.addEventListener("click", (function(id, title, cliSid, btn, sData) {
406
431
  return function(e) {
407
432
  e.stopPropagation();
408
- showSessionCtxMenu(btn, id, title, cliSid);
433
+ showSessionCtxMenu(btn, id, title, cliSid, sData);
409
434
  };
410
- })(s.id, s.title, s.cliSessionId, moreBtn));
435
+ })(s.id, s.title, s.cliSessionId, moreBtn, s));
411
436
  el.appendChild(moreBtn);
412
437
 
413
438
  el.addEventListener("click", (function (id) {
@@ -419,6 +444,9 @@ function renderSessionItem(s) {
419
444
  };
420
445
  })(s.id));
421
446
 
447
+ // Presence avatars (multi-user)
448
+ renderPresenceAvatars(el, String(s.id));
449
+
422
450
  return el;
423
451
  }
424
452
 
@@ -510,6 +538,59 @@ export function handleSearchResults(msg) {
510
538
  }
511
539
  }
512
540
 
541
+ export function updateSessionPresence(presence) {
542
+ sessionPresence = presence;
543
+ // Update presence avatars on existing session items without full re-render
544
+ var items = ctx.sessionListEl.querySelectorAll("[data-session-id]");
545
+ for (var i = 0; i < items.length; i++) {
546
+ renderPresenceAvatars(items[i], items[i].dataset.sessionId);
547
+ }
548
+ }
549
+
550
+ function presenceAvatarUrl(style, seed) {
551
+ var s = encodeURIComponent(seed || "anonymous");
552
+ return "https://api.dicebear.com/9.x/" + (style || "thumbs") + "/svg?seed=" + s + "&size=24";
553
+ }
554
+
555
+ function renderPresenceAvatars(el, sessionId) {
556
+ // Remove existing presence container
557
+ var existing = el.querySelector(".session-presence");
558
+ if (existing) existing.remove();
559
+
560
+ var users = sessionPresence[sessionId];
561
+ if (!users || users.length === 0) return;
562
+
563
+ var container = document.createElement("span");
564
+ container.className = "session-presence";
565
+
566
+ var max = 3;
567
+ var shown = users.length > max ? max : users.length;
568
+ for (var i = 0; i < shown; i++) {
569
+ var u = users[i];
570
+ var img = document.createElement("img");
571
+ img.className = "session-presence-avatar";
572
+ img.src = presenceAvatarUrl(u.avatarStyle, u.avatarSeed);
573
+ img.alt = u.displayName;
574
+ img.dataset.tip = u.displayName + (u.username ? " (@" + u.username + ")" : "");
575
+ if (i > 0) img.style.marginLeft = "-6px";
576
+ container.appendChild(img);
577
+ }
578
+ if (users.length > max) {
579
+ var more = document.createElement("span");
580
+ more.className = "session-presence-more";
581
+ more.textContent = "+" + (users.length - max);
582
+ container.appendChild(more);
583
+ }
584
+
585
+ // Insert before the more-btn
586
+ var moreBtn = el.querySelector(".session-more-btn");
587
+ if (moreBtn) {
588
+ el.insertBefore(container, moreBtn);
589
+ } else {
590
+ el.appendChild(container);
591
+ }
592
+ }
593
+
513
594
  export function updatePageTitle() {
514
595
  var sessionTitle = "";
515
596
  var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
@@ -1685,6 +1766,7 @@ function showEmojiPicker(slug, anchorEl) {
1685
1766
  tabBtns.push(tab);
1686
1767
  })(EMOJI_CATEGORIES[t], t);
1687
1768
  }
1769
+ parseEmojis(tabBar);
1688
1770
  picker.appendChild(tabBar);
1689
1771
 
1690
1772
  // --- Scrollable grid area ---
@@ -1713,7 +1795,7 @@ function showEmojiPicker(slug, anchorEl) {
1713
1795
  grid.appendChild(btn);
1714
1796
  })(emojis[i]);
1715
1797
  }
1716
-
1798
+ parseEmojis(grid);
1717
1799
  scrollArea.scrollTop = 0;
1718
1800
  }
1719
1801
 
@@ -1970,6 +2052,29 @@ function setupDragHandlers(el, slug) {
1970
2052
  });
1971
2053
  }
1972
2054
 
2055
+ export function renderSidebarPresence(onlineUsers) {
2056
+ var container = document.getElementById("sidebar-presence");
2057
+ if (!container) return;
2058
+ container.innerHTML = "";
2059
+ if (!onlineUsers || onlineUsers.length < 2) return;
2060
+ var maxShow = 4;
2061
+ for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
2062
+ var ou = onlineUsers[i];
2063
+ var img = document.createElement("img");
2064
+ img.className = "sidebar-presence-avatar";
2065
+ img.src = presenceAvatarUrl(ou.avatarStyle, ou.avatarSeed);
2066
+ img.alt = ou.displayName;
2067
+ img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
2068
+ container.appendChild(img);
2069
+ }
2070
+ if (onlineUsers.length > maxShow) {
2071
+ var more = document.createElement("span");
2072
+ more.className = "sidebar-presence-more";
2073
+ more.textContent = "+" + (onlineUsers.length - maxShow);
2074
+ container.appendChild(more);
2075
+ }
2076
+ }
2077
+
1973
2078
  export function renderIconStrip(projects, currentSlug) {
1974
2079
  // Cache for mobile sheet
1975
2080
  cachedProjectList = projects;
@@ -1991,6 +2096,7 @@ export function renderIconStrip(projects, currentSlug) {
1991
2096
  var emojiSpan = document.createElement("span");
1992
2097
  emojiSpan.className = "project-emoji";
1993
2098
  emojiSpan.textContent = p.icon;
2099
+ parseEmojis(emojiSpan);
1994
2100
  el.appendChild(emojiSpan);
1995
2101
  } else {
1996
2102
  el.appendChild(document.createTextNode(getProjectAbbrev(p.name)));
@@ -21,3 +21,4 @@
21
21
  @import url("css/playbook.css");
22
22
  @import url("css/stt.css");
23
23
  @import url("css/profile.css");
24
+ @import url("css/admin.css");
package/lib/push.js CHANGED
@@ -17,6 +17,9 @@ function loadOrCreateVapidKeys() {
17
17
  var keys = webpush.generateVAPIDKeys();
18
18
  fs.mkdirSync(dir, { recursive: true });
19
19
  fs.writeFileSync(keyFile, JSON.stringify(keys, null, 2));
20
+ if (process.platform !== "win32") {
21
+ try { fs.chmodSync(keyFile, 0o600); } catch (e) {}
22
+ }
20
23
  return keys;
21
24
  }
22
25
 
@@ -53,6 +56,9 @@ function initPush() {
53
56
  vapidKey: keys.publicKey,
54
57
  subs: [...subscriptions.values()],
55
58
  }));
59
+ if (process.platform !== "win32") {
60
+ try { fs.chmodSync(subFile, 0o600); } catch (e2) {}
61
+ }
56
62
  } catch (e) {}
57
63
  }
58
64