clay-server 2.24.4-beta.1 → 2.25.0-beta.1

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/project.js CHANGED
@@ -90,6 +90,18 @@ function safePath(base, requested) {
90
90
  }
91
91
  }
92
92
 
93
+ // Resolve an absolute path without requiring it to be within cwd.
94
+ // Used as fallback in OS user mode where ACL enforces access at the OS level.
95
+ function safeAbsPath(requested) {
96
+ if (!requested) return null;
97
+ var resolved = path.resolve(requested);
98
+ try {
99
+ return fs.realpathSync(resolved);
100
+ } catch (e) {
101
+ return null;
102
+ }
103
+ }
104
+
93
105
  /**
94
106
  * Create a project context — per-project state and handlers.
95
107
  * opts: { cwd, slug, title, pushModule, debug, dangerouslySkipPermissions, currentVersion }
@@ -1758,7 +1770,8 @@ function createProjectContext(opts) {
1758
1770
  }
1759
1771
 
1760
1772
  if (msg.type === "push_subscribe") {
1761
- if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1773
+ var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
1774
+ if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint, _pushUserId);
1762
1775
  return;
1763
1776
  }
1764
1777
 
@@ -2832,6 +2845,10 @@ function createProjectContext(opts) {
2832
2845
  }
2833
2846
  if (msg.type === "fs_list") {
2834
2847
  var fsDir = safePath(cwd, msg.path || ".");
2848
+ // In OS user mode, fall back to absolute path resolution (ACL enforces access)
2849
+ if (!fsDir && getOsUserInfoForWs(ws)) {
2850
+ fsDir = safeAbsPath(msg.path);
2851
+ }
2835
2852
  if (!fsDir) {
2836
2853
  sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: "Access denied" });
2837
2854
  return;
@@ -2874,6 +2891,9 @@ function createProjectContext(opts) {
2874
2891
 
2875
2892
  if (msg.type === "fs_read") {
2876
2893
  var fsFile = safePath(cwd, msg.path);
2894
+ if (!fsFile && getOsUserInfoForWs(ws)) {
2895
+ fsFile = safeAbsPath(msg.path);
2896
+ }
2877
2897
  if (!fsFile) {
2878
2898
  sendTo(ws, { type: "fs_read_result", path: msg.path, error: "Access denied" });
2879
2899
  return;
@@ -2920,6 +2940,9 @@ function createProjectContext(opts) {
2920
2940
  // --- File write ---
2921
2941
  if (msg.type === "fs_write") {
2922
2942
  var fsWriteFile = safePath(cwd, msg.path);
2943
+ if (!fsWriteFile && getOsUserInfoForWs(ws)) {
2944
+ fsWriteFile = safeAbsPath(msg.path);
2945
+ }
2923
2946
  if (!fsWriteFile) {
2924
2947
  sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: "Access denied" });
2925
2948
  return;
@@ -3749,6 +3772,11 @@ function createProjectContext(opts) {
3749
3772
  // Keep any pending scheduled message alive when user sends a regular message
3750
3773
 
3751
3774
  var userMsg = { type: "user_message", text: msg.text || "" };
3775
+ // Attach sender info for multi-user attribution (backward-compatible: old clients ignore these)
3776
+ if (ws._clayUser) {
3777
+ userMsg.from = ws._clayUser.id;
3778
+ userMsg.fromName = ws._clayUser.displayName || ws._clayUser.username || "";
3779
+ }
3752
3780
  var savedImagePaths = [];
3753
3781
  if (msg.images && msg.images.length > 0) {
3754
3782
  userMsg.imageCount = msg.images.length;
@@ -5361,6 +5389,9 @@ function createProjectContext(opts) {
5361
5389
  var reqFilePath = params.get("path");
5362
5390
  if (!reqFilePath) { res.writeHead(400); res.end("Missing path"); return true; }
5363
5391
  var absFile = safePath(cwd, reqFilePath);
5392
+ if (!absFile && getOsUserInfoForReq(req)) {
5393
+ absFile = safeAbsPath(reqFilePath);
5394
+ }
5364
5395
  if (!absFile) { res.writeHead(403); res.end("Access denied"); return true; }
5365
5396
  var fileExt = path.extname(absFile).toLowerCase();
5366
5397
  if (!IMAGE_EXTS.has(fileExt)) { res.writeHead(403); res.end("Only image files"); return true; }
package/lib/public/app.js CHANGED
@@ -2865,10 +2865,11 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2865
2865
  // AskUserQuestion, PermissionRequest, Plan, Todo, Thinking, Tool items -> modules/tools.js
2866
2866
 
2867
2867
  // --- DOM: Messages ---
2868
- function addUserMessage(text, images, pastes) {
2868
+ function addUserMessage(text, images, pastes, fromUserId, fromUserName) {
2869
2869
  if (!text && (!images || images.length === 0) && (!pastes || pastes.length === 0)) return;
2870
+ var isOtherUser = fromUserId && fromUserId !== myUserId;
2870
2871
  var div = document.createElement("div");
2871
- div.className = "msg-user";
2872
+ div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
2872
2873
  div.dataset.turn = ++turnCounter;
2873
2874
  var bubble = document.createElement("div");
2874
2875
  bubble.className = "bubble";
@@ -2926,14 +2927,27 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2926
2927
 
2927
2928
 
2928
2929
  // Always render avatar + header structure (CSS controls visibility)
2929
- var _myU = cachedAllUsers.find(function (u) { return u.id === myUserId; });
2930
- if (!_myU) {
2931
- try { _myU = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {}
2930
+ var _targetUser;
2931
+ var _displayName;
2932
+ if (isOtherUser) {
2933
+ _targetUser = cachedAllUsers.find(function (u) { return u.id === fromUserId; });
2934
+ _displayName = fromUserName || (_targetUser && (_targetUser.displayName || _targetUser.username)) || "User";
2935
+ } else {
2936
+ _targetUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
2937
+ if (!_targetUser) {
2938
+ try { _targetUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch(e) {}
2939
+ }
2940
+ _displayName = document.body.dataset.myDisplayName || "";
2941
+ if (!_displayName) {
2942
+ _displayName = (_targetUser && (_targetUser.displayName || _targetUser.username)) || "Me";
2943
+ }
2932
2944
  }
2933
2945
 
2934
2946
  var avi = document.createElement("img");
2935
- avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
2936
- avi.src = document.body.dataset.myAvatarUrl || userAvatarUrl(_myU || { id: myUserId }, 36);
2947
+ avi.className = "dm-bubble-avatar" + (isOtherUser ? " dm-bubble-avatar-other" : " dm-bubble-avatar-me");
2948
+ avi.src = isOtherUser
2949
+ ? userAvatarUrl(_targetUser || { id: fromUserId }, 36)
2950
+ : (document.body.dataset.myAvatarUrl || userAvatarUrl(_targetUser || { id: myUserId }, 36));
2937
2951
  div.appendChild(avi);
2938
2952
 
2939
2953
  var contentWrap = document.createElement("div");
@@ -2941,13 +2955,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2941
2955
 
2942
2956
  var header = document.createElement("div");
2943
2957
  header.className = "dm-bubble-header";
2944
- var myDisplayName = document.body.dataset.myDisplayName || "";
2945
- if (!myDisplayName) {
2946
- myDisplayName = (_myU && (_myU.displayName || _myU.username)) || "Me";
2947
- }
2948
2958
  var nameSpan = document.createElement("span");
2949
2959
  nameSpan.className = "dm-bubble-name";
2950
- nameSpan.textContent = myDisplayName;
2960
+ nameSpan.textContent = _displayName;
2951
2961
  header.appendChild(nameSpan);
2952
2962
  var timeSpan = document.createElement("span");
2953
2963
  timeSpan.className = "dm-bubble-time";
@@ -4408,9 +4418,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4408
4418
  if (msg.planContent) {
4409
4419
  setPlanContent(msg.planContent);
4410
4420
  renderPlanCard(msg.planContent);
4411
- addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null);
4421
+ addUserMessage("Execute the following plan. Do NOT re-enter plan mode — just implement it step by step.", msg.images || null, msg.pastes || null, msg.from, msg.fromName);
4412
4422
  } else {
4413
- addUserMessage(msg.text, msg.images || null, msg.pastes || null);
4423
+ addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName);
4414
4424
  }
4415
4425
  break;
4416
4426
 
@@ -732,7 +732,7 @@ body.mate-dm-active #layout.sidebar-collapsed .mate-collapsed-info {
732
732
  right: 12px;
733
733
  margin-top: 4px;
734
734
  padding: 10px 14px;
735
- background: var(--bg-primary, #fff);
735
+ background: var(--bg);
736
736
  border: 1px solid var(--border-subtle);
737
737
  border-radius: 8px;
738
738
  box-shadow: 0 4px 16px rgba(0,0,0,0.15);
@@ -823,7 +823,7 @@ body.mate-dm-active #layout.sidebar-collapsed .mate-collapsed-info {
823
823
  justify-content: center;
824
824
  }
825
825
  .mate-confirm-box {
826
- background: var(--bg-primary, #fff);
826
+ background: var(--bg);
827
827
  border: 1px solid var(--border-subtle);
828
828
  border-radius: 12px;
829
829
  padding: 20px 24px;
@@ -846,7 +846,7 @@ body.mate-dm-active #layout.sidebar-collapsed .mate-collapsed-info {
846
846
  border: 1px solid var(--border-subtle);
847
847
  font-size: 13px;
848
848
  cursor: pointer;
849
- background: var(--bg-primary);
849
+ background: var(--bg);
850
850
  color: var(--text);
851
851
  }
852
852
  .mate-confirm-delete {
@@ -873,7 +873,7 @@ body.mate-dm-active #layout.sidebar-collapsed .mate-collapsed-info {
873
873
  background: rgba(0,0,0,0.45);
874
874
  }
875
875
  .mate-onboarding-card {
876
- background: var(--bg-primary, #fff);
876
+ background: var(--bg);
877
877
  border: 1px solid var(--border-subtle);
878
878
  border-radius: 16px;
879
879
  padding: 32px 36px;
@@ -206,10 +206,6 @@
206
206
  color: var(--danger, #e74c3c);
207
207
  border-color: var(--danger, #e74c3c);
208
208
  }
209
- .mention-stop-btn svg {
210
- width: 12px;
211
- height: 12px;
212
- }
213
209
 
214
210
  .mention-content {
215
211
  font-size: 15px;
@@ -150,6 +150,21 @@
150
150
  text-align: start;
151
151
  }
152
152
 
153
+ /* Other user's message: left-aligned with flipped bubble */
154
+ .msg-user-other {
155
+ align-items: flex-start;
156
+ }
157
+ .msg-user-other .bubble {
158
+ background: var(--bg-elevated, var(--bg-hover));
159
+ border-radius: 20px 20px 20px 4px;
160
+ }
161
+ .msg-user-other .dm-bubble-avatar-other {
162
+ display: block;
163
+ }
164
+ .msg-user-other .dm-bubble-header {
165
+ display: flex;
166
+ }
167
+
153
168
  /* --- User message action bar --- */
154
169
  .msg-actions {
155
170
  display: flex;
@@ -335,7 +335,7 @@ export function handleMentionStart(msg) {
335
335
  var stopBtn = document.createElement("button");
336
336
  stopBtn.className = "mention-stop-btn";
337
337
  stopBtn.title = "Stop";
338
- stopBtn.innerHTML = iconHtml("square");
338
+ stopBtn.textContent = "Stop";
339
339
  stopBtn.addEventListener("click", function () {
340
340
  if (ctx.ws && ctx.connected) {
341
341
  ctx.ws.send(JSON.stringify({ type: "mention_stop", mateId: msg.mateId }));
@@ -383,7 +383,7 @@ export function handleMentionStart(msg) {
383
383
  var stopBtn = document.createElement("button");
384
384
  stopBtn.className = "mention-stop-btn";
385
385
  stopBtn.title = "Stop";
386
- stopBtn.innerHTML = iconHtml("square");
386
+ stopBtn.textContent = "Stop";
387
387
  stopBtn.addEventListener("click", function () {
388
388
  if (ctx.ws && ctx.connected) {
389
389
  ctx.ws.send(JSON.stringify({ type: "mention_stop", mateId: msg.mateId }));
package/lib/public/sw.js CHANGED
@@ -110,6 +110,8 @@ self.addEventListener("push", function (event) {
110
110
  } else if (data.type === "error") {
111
111
  options.requireInteraction = true;
112
112
  options.tag = "claude-error";
113
+ } else if (data.type === "dm") {
114
+ options.tag = data.tag || "dm";
113
115
  }
114
116
 
115
117
  event.waitUntil(
package/lib/push.js CHANGED
@@ -80,12 +80,14 @@ function initPush() {
80
80
  })(startupEndpoints[si]);
81
81
  }
82
82
 
83
- function addSubscription(sub, replaceEndpoint) {
83
+ function addSubscription(sub, replaceEndpoint, userId) {
84
84
  if (!sub || !sub.endpoint) return;
85
85
  // Remove previous subscription from the same client if endpoint changed
86
86
  if (replaceEndpoint && replaceEndpoint !== sub.endpoint) {
87
87
  subscriptions.delete(replaceEndpoint);
88
88
  }
89
+ // Attach userId for per-user targeting (backward-compatible: old subs without userId still work for broadcast)
90
+ if (userId) sub._userId = userId;
89
91
  // Store immediately, then validate async. Invalid subs get cleaned on first sendPush.
90
92
  subscriptions.set(sub.endpoint, sub);
91
93
  save();
@@ -119,11 +121,28 @@ function initPush() {
119
121
  });
120
122
  }
121
123
 
124
+ function sendPushToUser(userId, payload) {
125
+ if (!userId) return;
126
+ var json = JSON.stringify(payload);
127
+ subscriptions.forEach(function (sub, endpoint) {
128
+ if (sub._userId !== userId) return;
129
+ webpush.sendNotification(sub, json, { vapidDetails: vapidDetails })
130
+ .then(function () {})
131
+ .catch(function (err) {
132
+ if (err.statusCode === 410 || err.statusCode === 404 || err.statusCode === 403) {
133
+ subscriptions.delete(endpoint);
134
+ save();
135
+ }
136
+ });
137
+ });
138
+ }
139
+
122
140
  return {
123
141
  publicKey: keys.publicKey,
124
142
  addSubscription: addSubscription,
125
143
  removeSubscription: removeSubscription,
126
144
  sendPush: sendPush,
145
+ sendPushToUser: sendPushToUser,
127
146
  };
128
147
  }
129
148
 
package/lib/sdk-bridge.js CHANGED
@@ -955,6 +955,7 @@ function createSDKBridge(opts) {
955
955
 
956
956
  function cleanupWorker(worker) {
957
957
  console.log("[sdk-bridge] cleanupWorker() called, pid=" + (worker.process ? worker.process.pid : "?") + " stack=" + new Error().stack.split("\n").slice(1, 4).join(" | "));
958
+ if (worker._abortTimeout) { clearTimeout(worker._abortTimeout); worker._abortTimeout = null; }
958
959
  if (worker.connection && !worker.connection.destroyed) {
959
960
  try { worker.connection.end(); } catch (e) {}
960
961
  }
@@ -1069,7 +1070,20 @@ function createSDKBridge(opts) {
1069
1070
  session.pendingElicitations = {};
1070
1071
  session.streamedText = false;
1071
1072
  session.responsePreview = "";
1072
- session.abortController = { abort: function() { console.log("[sdk-bridge] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?")); worker._abortSent = true; worker.send({ type: "abort" }); } };
1073
+ session.abortController = { abort: function() {
1074
+ console.log("[sdk-bridge] ABORT sent to worker pid=" + (worker.process ? worker.process.pid : "?"));
1075
+ worker._abortSent = true;
1076
+ try { worker.send({ type: "abort" }); } catch (e) {}
1077
+ // If the worker doesn't finish within 5s (e.g. subagent stuck), force-kill it.
1078
+ // The worker exit handler will dispatch a fallback query_error and send done.
1079
+ if (worker._abortTimeout) clearTimeout(worker._abortTimeout);
1080
+ worker._abortTimeout = setTimeout(function() {
1081
+ if (worker.process && !worker.process.killed && session.isProcessing) {
1082
+ console.log("[sdk-bridge] Abort timeout: force-killing worker pid=" + (worker.process ? worker.process.pid : "?"));
1083
+ try { worker.process.kill("SIGKILL"); } catch (e) {}
1084
+ }
1085
+ }, 5000);
1086
+ } };
1073
1087
 
1074
1088
  // Build initial user message content
1075
1089
  var content = [];
package/lib/server.js CHANGED
@@ -1072,7 +1072,8 @@ function createServer(opts) {
1072
1072
  try {
1073
1073
  var parsed = JSON.parse(body);
1074
1074
  var sub = parsed.subscription || parsed;
1075
- pushModule.addSubscription(sub, parsed.replaceEndpoint);
1075
+ var _httpPushUser = getMultiUserFromReq(req);
1076
+ pushModule.addSubscription(sub, parsed.replaceEndpoint, _httpPushUser ? _httpPushUser.id : null);
1076
1077
  res.writeHead(200, { "Content-Type": "application/json" });
1077
1078
  res.end('{"ok":true}');
1078
1079
  } catch (e) {
@@ -3161,6 +3162,18 @@ function createServer(opts) {
3161
3162
  otherWs.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
3162
3163
  });
3163
3164
  });
3165
+ // Send push notification to target user
3166
+ if (pushModule && pushModule.sendPushToUser) {
3167
+ var senderName = ws._clayUser ? (ws._clayUser.displayName || ws._clayUser.username || "Someone") : "Someone";
3168
+ var preview = (msg.text || "").substring(0, 140);
3169
+ pushModule.sendPushToUser(targetId, {
3170
+ type: "dm",
3171
+ title: senderName,
3172
+ body: preview,
3173
+ tag: "dm-" + msg.dmKey,
3174
+ dmKey: msg.dmKey,
3175
+ });
3176
+ }
3164
3177
  return;
3165
3178
  }
3166
3179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.24.4-beta.1",
3
+ "version": "2.25.0-beta.1",
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",