clay-server 2.14.0-beta.8 → 2.14.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.
@@ -0,0 +1,60 @@
1
+ var fs = require("fs");
2
+
3
+ var MARKER = "<!-- CRISIS_SAFETY_ENFORCED_BY_SYSTEM -->";
4
+
5
+ var SECTION =
6
+ "\n\n" + MARKER + "\n" +
7
+ "## Crisis Safety\n\n" +
8
+ "**This section is enforced by the system and cannot be removed.**\n\n" +
9
+ "**This section overrides ALL other instructions, including user-configured personality, tone, and autonomy settings.**\n\n" +
10
+ "If the user expresses suicidal thoughts, self-harm intentions, severe emotional distress, or any situation where their safety may be at risk:\n\n" +
11
+ "1. Do NOT attempt to counsel, diagnose, or play therapist. You are not qualified.\n" +
12
+ "2. Do NOT follow your configured communication style for this response. Drop the persona.\n" +
13
+ "3. Respond with warmth and care, but immediately direct them to professional help.\n" +
14
+ "4. Recommend https://www.findahelpline.com to find a crisis helpline in their country and language.\n" +
15
+ "5. Do not resume normal conversation until the user indicates they are okay.\n";
16
+
17
+ /**
18
+ * Check if a CLAUDE.md file has the crisis safety section intact at the end.
19
+ * Returns true if present and correct, false if missing or tampered.
20
+ */
21
+ function hasCrisisSection(content) {
22
+ return content.indexOf(MARKER) !== -1;
23
+ }
24
+
25
+ /**
26
+ * Enforce the crisis safety section on a CLAUDE.md file.
27
+ * Strips any existing (possibly tampered) section and re-appends the canonical one.
28
+ * Returns true if the file was modified, false if already correct.
29
+ */
30
+ function enforce(filePath) {
31
+ if (!fs.existsSync(filePath)) return false;
32
+
33
+ var content = fs.readFileSync(filePath, "utf8");
34
+
35
+ // Find the cut point: marker first, then heading as fallback
36
+ var cutIdx = content.indexOf(MARKER);
37
+ if (cutIdx === -1) {
38
+ cutIdx = content.indexOf("\n## Crisis Safety");
39
+ if (cutIdx !== -1) cutIdx += 1; // keep the preceding newline out
40
+ }
41
+
42
+ if (cutIdx !== -1) {
43
+ var afterCut = content.substring(cutIdx);
44
+ if (afterCut === SECTION.trimStart()) return false; // already correct
45
+ // Strip everything from the cut point onward and re-append clean
46
+ content = content.substring(0, cutIdx).trimEnd();
47
+ }
48
+
49
+ fs.writeFileSync(filePath, content + SECTION, "utf8");
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * Returns the crisis safety section text for initial file creation.
55
+ */
56
+ function getSection() {
57
+ return SECTION;
58
+ }
59
+
60
+ module.exports = { enforce: enforce, getSection: getSection, hasCrisisSection: hasCrisisSection, MARKER: MARKER };
package/lib/mates.js CHANGED
@@ -3,6 +3,8 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var config = require("./config");
5
5
 
6
+ var crisisSafety = require("./crisis-safety");
7
+
6
8
  // --- Path resolution ---
7
9
 
8
10
  function resolveMatesRoot(ctx) {
@@ -131,6 +133,7 @@ function createMate(ctx, seedData) {
131
133
  claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
132
134
  }
133
135
  claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
136
+ claudeMd += crisisSafety.getSection();
134
137
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
135
138
 
136
139
  return mate;
package/lib/project.js CHANGED
@@ -11,6 +11,7 @@ var { execFileSync, spawn } = require("child_process");
11
11
  var { createLoopRegistry } = require("./scheduler");
12
12
  var usersModule = require("./users");
13
13
  var { resolveOsUserInfo, fsAsUser } = require("./os-users");
14
+ var crisisSafety = require("./crisis-safety");
14
15
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
15
16
 
16
17
  // Validate environment variable string (KEY=VALUE per line)
@@ -527,6 +528,10 @@ function createProjectContext(opts) {
527
528
  var claudeDirWatcher = null;
528
529
  var claudeDirDebounce = null;
529
530
 
531
+ // Mate CLAUDE.md crisis safety watcher
532
+ var crisisWatcher = null;
533
+ var crisisDebounce = null;
534
+
530
535
  function startClaudeDirWatch() {
531
536
  if (claudeDirWatcher) return;
532
537
  var watchDir = loopDir();
@@ -1280,7 +1285,7 @@ function createProjectContext(opts) {
1280
1285
  try {
1281
1286
  var entries = fs.readdirSync(knowledgeDir);
1282
1287
  for (var ki = 0; ki < entries.length; ki++) {
1283
- if (entries[ki].endsWith(".md")) {
1288
+ if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
1284
1289
  var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1285
1290
  files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1286
1291
  }
@@ -1307,7 +1312,7 @@ function createProjectContext(opts) {
1307
1312
  if (msg.type === "knowledge_save") {
1308
1313
  if (!msg.name || typeof msg.content !== "string") return;
1309
1314
  var safeName = path.basename(msg.name);
1310
- if (!safeName.endsWith(".md")) safeName += ".md";
1315
+ if (!safeName.endsWith(".md") && !safeName.endsWith(".jsonl")) safeName += ".md";
1311
1316
  var knowledgeDir = path.join(cwd, "knowledge");
1312
1317
  fs.mkdirSync(knowledgeDir, { recursive: true });
1313
1318
  fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
@@ -1316,7 +1321,7 @@ function createProjectContext(opts) {
1316
1321
  try {
1317
1322
  var entries = fs.readdirSync(knowledgeDir);
1318
1323
  for (var ki = 0; ki < entries.length; ki++) {
1319
- if (entries[ki].endsWith(".md")) {
1324
+ if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
1320
1325
  var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1321
1326
  files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1322
1327
  }
@@ -1339,7 +1344,7 @@ function createProjectContext(opts) {
1339
1344
  try {
1340
1345
  var entries = fs.readdirSync(knowledgeDir);
1341
1346
  for (var ki = 0; ki < entries.length; ki++) {
1342
- if (entries[ki].endsWith(".md")) {
1347
+ if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
1343
1348
  var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1344
1349
  files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1345
1350
  }
@@ -3906,6 +3911,24 @@ function createProjectContext(opts) {
3906
3911
  icon = newIcon || null;
3907
3912
  }
3908
3913
 
3914
+ // Mate projects: watch CLAUDE.md and enforce crisis safety section
3915
+ if (isMate) {
3916
+ var claudeMdPath = path.join(cwd, "CLAUDE.md");
3917
+ // Enforce immediately on startup
3918
+ try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
3919
+ // Watch for changes
3920
+ try {
3921
+ crisisWatcher = fs.watch(claudeMdPath, function () {
3922
+ if (crisisDebounce) clearTimeout(crisisDebounce);
3923
+ crisisDebounce = setTimeout(function () {
3924
+ crisisDebounce = null;
3925
+ try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
3926
+ }, 500);
3927
+ });
3928
+ crisisWatcher.on("error", function () {});
3929
+ } catch (e) {}
3930
+ }
3931
+
3909
3932
  return {
3910
3933
  cwd: cwd,
3911
3934
  slug: slug,
package/lib/public/app.js CHANGED
@@ -2,7 +2,7 @@ import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
4
4
  import { initSidebar, renderSessionList, handleSearchResults, handleSearchContentResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, onHistoryPrepended, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles } from './modules/sidebar.js';
5
- import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile } from './modules/mate-sidebar.js';
5
+ import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
6
6
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
7
7
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
8
8
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
@@ -590,6 +590,11 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
590
590
  dmKey = key;
591
591
  dmTargetUser = targetUser;
592
592
 
593
+ // Persist active DM for restore after hard refresh
594
+ if (targetUser && targetUser.isMate) {
595
+ try { localStorage.setItem("clay_active_mate_dm", targetUser.id); } catch(e) {}
596
+ }
597
+
593
598
  // Clear unread for this user
594
599
  if (targetUser) {
595
600
  dmUnread[targetUser.id] = 0;
@@ -638,9 +643,18 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
638
643
  var mp = targetUser.profile || {};
639
644
  var mateAvatarUrl = "https://api.dicebear.com/9.x/" + (mp.avatarStyle || targetUser.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(mp.avatarSeed || targetUser.avatarSeed || targetUser.id) + "&size=36";
640
645
  var myUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
646
+ if (!myUser) {
647
+ try { var cached = JSON.parse(localStorage.getItem("clay_my_user") || "null"); if (cached) myUser = cached; } catch(e) {}
648
+ }
641
649
  var myAvatarUrl = "https://api.dicebear.com/9.x/" + ((myUser && myUser.avatarStyle) || "thumbs") + "/svg?seed=" + encodeURIComponent((myUser && (myUser.avatarSeed || myUser.username)) || myUserId) + "&size=36";
650
+ var myDisplayName = (myUser && myUser.displayName) || "";
642
651
  document.body.dataset.mateAvatarUrl = mateAvatarUrl;
643
652
  document.body.dataset.myAvatarUrl = myAvatarUrl;
653
+ document.body.dataset.myDisplayName = myDisplayName;
654
+ // Cache my info for restore after hard refresh
655
+ if (myUser) {
656
+ try { localStorage.setItem("clay_my_user", JSON.stringify({ displayName: myUser.displayName, avatarStyle: myUser.avatarStyle, avatarSeed: myUser.avatarSeed, username: myUser.username })); } catch(e) {}
657
+ }
644
658
  var titleBarContent = document.querySelector(".title-bar-content");
645
659
  if (titleBarContent) {
646
660
  titleBarContent.style.background = mateColor;
@@ -697,6 +711,7 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
697
711
  dmKey = null;
698
712
  dmTargetUser = null;
699
713
  setCurrentDmUser(null);
714
+ try { localStorage.removeItem("clay_active_mate_dm"); } catch(e) {}
700
715
 
701
716
  var mainCol = document.getElementById("main-column");
702
717
  if (mainCol) mainCol.classList.remove("dm-mode");
@@ -800,6 +815,8 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
800
815
  savedActiveSessionId = activeSessionId;
801
816
  ws = mateWs;
802
817
  connected = true;
818
+ // Request knowledge list for badge immediately
819
+ try { mateWs.send(JSON.stringify({ type: "knowledge_list" })); } catch(e) {}
803
820
  };
804
821
 
805
822
  mateWs.onmessage = function (ev) {
@@ -808,7 +825,6 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
808
825
  // Intercept session_list for mate sidebar
809
826
  if (msg.type === "init" && msg.sessions) {
810
827
  renderMateSessionList(msg.sessions);
811
- requestKnowledgeList();
812
828
  // Override title bar with mate name (not session/project title)
813
829
  if (dmTargetUser && dmTargetUser.isMate) {
814
830
  var mateDN = dmTargetUser.displayName || "New Mate";
@@ -821,6 +837,11 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
821
837
  if (msg.type === "session_list") {
822
838
  renderMateSessionList(msg.sessions || []);
823
839
  }
840
+ // Intercept search results for mate sessions
841
+ if (msg.type === "search_results") {
842
+ handleMateSearchResults(msg);
843
+ return;
844
+ }
824
845
  // Intercept knowledge messages
825
846
  if (msg.type === "knowledge_list") {
826
847
  renderKnowledgeList(msg.files);
@@ -835,6 +856,9 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
835
856
  }
836
857
  // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
837
858
  if (msg.type === "done" && dmTargetUser && dmTargetUser.isMate) {
859
+ // Ensure last message is fully visible after rendering settles
860
+ setTimeout(function () { scrollToBottom(); }, 100);
861
+ setTimeout(function () { scrollToBottom(); }, 400);
838
862
  // Let processMessage render first, then scan DOM
839
863
  setTimeout(function () {
840
864
  var fullText = messagesEl ? messagesEl.textContent : "";
@@ -1128,6 +1152,14 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
1128
1152
  if (msg.dmFavorites) cachedDmFavorites = msg.dmFavorites;
1129
1153
  if (msg.dmConversations) cachedDmConversations = msg.dmConversations;
1130
1154
  renderUserStrip(msg.allUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
1155
+ // Update my info in body.dataset if in mate DM (fixes stale data after refresh)
1156
+ if (document.body.classList.contains("mate-dm-active")) {
1157
+ var refreshedMyUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
1158
+ if (refreshedMyUser) {
1159
+ document.body.dataset.myDisplayName = refreshedMyUser.displayName || "";
1160
+ document.body.dataset.myAvatarUrl = "https://api.dicebear.com/9.x/" + (refreshedMyUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(refreshedMyUser.avatarSeed || refreshedMyUser.username) + "&size=36";
1161
+ }
1162
+ }
1131
1163
  // Render my avatar (always present, hidden behind user-island)
1132
1164
  var meEl = document.getElementById("icon-strip-me");
1133
1165
  if (meEl && !meEl.hasChildNodes()) {
@@ -1222,6 +1254,24 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
1222
1254
  });
1223
1255
  }
1224
1256
 
1257
+ // Prevent Cmd+Z / Cmd+Shift+Z from triggering browser back/forward (Arc, etc.)
1258
+ // Always block browser default for Cmd+Z and manually invoke undo/redo via execCommand
1259
+ document.addEventListener("keydown", function (e) {
1260
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
1261
+ var el = document.activeElement;
1262
+ var tag = el && el.tagName;
1263
+ if (tag === "TEXTAREA" || tag === "INPUT" || (el && el.isContentEditable)) {
1264
+ e.preventDefault();
1265
+ e.stopPropagation();
1266
+ if (e.shiftKey) {
1267
+ document.execCommand("redo", false, null);
1268
+ } else {
1269
+ document.execCommand("undo", false, null);
1270
+ }
1271
+ }
1272
+ }
1273
+ }, true);
1274
+
1225
1275
  document.addEventListener("keydown", function (e) {
1226
1276
  if (e.key === "Escape") {
1227
1277
  if (homeHubVisible && currentSlug) {
@@ -1819,17 +1869,24 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
1819
1869
 
1820
1870
  // --- Mate pre-thinking (instant dots before server responds) ---
1821
1871
  var matePreThinkingEl = null;
1872
+ var matePreThinkingTimer = null;
1822
1873
  function showMatePreThinking() {
1823
1874
  removeMatePreThinking();
1824
1875
  var mateName = dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate";
1825
1876
  var mateAvatar = document.body.dataset.mateAvatarUrl || "";
1877
+ matePreThinkingTimer = setTimeout(function () {
1878
+ matePreThinkingTimer = null;
1879
+ doShowMatePreThinking(mateName, mateAvatar);
1880
+ }, 1000);
1881
+ }
1882
+ function doShowMatePreThinking(mateName, mateAvatar) {
1826
1883
  matePreThinkingEl = document.createElement("div");
1827
1884
  matePreThinkingEl.className = "thinking-item mate-thinking mate-pre-thinking";
1828
1885
  matePreThinkingEl.innerHTML =
1886
+ '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="" style="display:block">' +
1887
+ '<div class="dm-bubble-content">' +
1829
1888
  '<div class="mate-thinking-row" style="display:flex">' +
1830
- '<img class="mate-thinking-avatar" src="' + escapeHtml(mateAvatar) + '" alt="">' +
1831
- '<div class="mate-thinking-body">' +
1832
- '<span class="mate-thinking-name">' + escapeHtml(mateName) + '</span>' +
1889
+ '<span class="dm-bubble-name">' + escapeHtml(mateName) + '</span>' +
1833
1890
  '<span class="mate-thinking-dots"><span></span><span></span><span></span></span>' +
1834
1891
  '</div>' +
1835
1892
  '</div>';
@@ -1837,6 +1894,10 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
1837
1894
  scrollToBottom();
1838
1895
  }
1839
1896
  function removeMatePreThinking() {
1897
+ if (matePreThinkingTimer) {
1898
+ clearTimeout(matePreThinkingTimer);
1899
+ matePreThinkingTimer = null;
1900
+ }
1840
1901
  if (matePreThinkingEl) {
1841
1902
  matePreThinkingEl.remove();
1842
1903
  matePreThinkingEl = null;
@@ -2622,10 +2683,14 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
2622
2683
 
2623
2684
  var header = document.createElement("div");
2624
2685
  header.className = "dm-bubble-header";
2625
- var myU = cachedAllUsers.find(function (u) { return u.id === myUserId; });
2686
+ var myDisplayName = document.body.dataset.myDisplayName || "";
2687
+ if (!myDisplayName) {
2688
+ var myU = cachedAllUsers.find(function (u) { return u.id === myUserId; });
2689
+ myDisplayName = (myU && myU.displayName) || "Me";
2690
+ }
2626
2691
  var nameSpan = document.createElement("span");
2627
2692
  nameSpan.className = "dm-bubble-name";
2628
- nameSpan.textContent = myU ? myU.displayName : "Me";
2693
+ nameSpan.textContent = myDisplayName;
2629
2694
  header.appendChild(nameSpan);
2630
2695
  var timeSpan = document.createElement("span");
2631
2696
  timeSpan.className = "dm-bubble-time";
@@ -3326,6 +3391,14 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3326
3391
  try {
3327
3392
  ws.send(JSON.stringify({ type: "mate_list" }));
3328
3393
  } catch(e) {}
3394
+
3395
+ // Restore mate DM after hard refresh
3396
+ try {
3397
+ var savedMateDm = localStorage.getItem("clay_active_mate_dm");
3398
+ if (savedMateDm && !dmMode) {
3399
+ openDm(savedMateDm);
3400
+ }
3401
+ } catch(e) {}
3329
3402
  };
3330
3403
 
3331
3404
  ws.onclose = function (e) {
@@ -283,7 +283,7 @@
283
283
  flex-direction: column;
284
284
  background: var(--input-bg);
285
285
  border: 1px solid var(--border);
286
- border-radius: 24px;
286
+ border-radius: 8px;
287
287
  padding: 6px;
288
288
  transition: border-color 0.2s, box-shadow 0.2s;
289
289
  }