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.
- package/README.md +2 -0
- package/bin/cli.js +122 -2
- package/lib/config.js +20 -1
- package/lib/daemon.js +40 -0
- package/lib/pages.js +670 -27
- package/lib/project.js +267 -16
- package/lib/public/app.js +74 -14
- package/lib/public/css/admin.css +576 -0
- package/lib/public/css/icon-strip.css +1 -0
- package/lib/public/css/menus.css +16 -11
- package/lib/public/css/overlays.css +2 -4
- package/lib/public/css/sidebar.css +49 -0
- package/lib/public/css/title-bar.css +45 -1
- package/lib/public/index.html +38 -8
- package/lib/public/modules/admin.js +631 -0
- package/lib/public/modules/markdown.js +9 -5
- package/lib/public/modules/profile.js +21 -0
- package/lib/public/modules/project-settings.js +4 -1
- package/lib/public/modules/server-settings.js +13 -0
- package/lib/public/modules/sidebar.js +111 -5
- package/lib/public/style.css +1 -0
- package/lib/push.js +6 -0
- package/lib/server.js +1075 -27
- package/lib/sessions.js +127 -41
- package/lib/smtp.js +221 -0
- package/lib/users.js +459 -0
- package/package.json +2 -1
|
@@ -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)));
|
package/lib/public/style.css
CHANGED
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
|
|