clay-server 2.18.0-beta.9 → 2.18.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.
@@ -19,6 +19,7 @@ var searchMatchIds = null; // null = no search, Set of matched session IDs
19
19
  var searchDebounce = null;
20
20
  var cachedSessions = [];
21
21
  var expandedLoopGroups = new Set();
22
+ var expandedLoopRuns = new Set();
22
23
 
23
24
  // --- Cached project data for mobile sheet ---
24
25
  var cachedProjectList = [];
@@ -309,39 +310,52 @@ function renderLoopChild(s) {
309
310
 
310
311
  function renderLoopGroup(loopId, children, groupKey) {
311
312
  var gk = groupKey || loopId;
312
- // Sort children by iteration then role (coder before judge)
313
- children.sort(function (a, b) {
314
- var ai = (a.loop && a.loop.iteration) || 0;
315
- var bi = (b.loop && b.loop.iteration) || 0;
316
- if (ai !== bi) return ai - bi;
317
- // coder before judge within same iteration
318
- var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
319
- var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
320
- return ar - br;
321
- });
313
+
314
+ // Sub-group children by startedAt (each run)
315
+ var runMap = {};
316
+ for (var i = 0; i < children.length; i++) {
317
+ var runKey = String(children[i].loop && children[i].loop.startedAt || 0);
318
+ if (!runMap[runKey]) runMap[runKey] = [];
319
+ runMap[runKey].push(children[i]);
320
+ }
321
+ var runKeys = Object.keys(runMap);
322
+
323
+ // Sort each run's children by iteration then role
324
+ for (var ri = 0; ri < runKeys.length; ri++) {
325
+ runMap[runKeys[ri]].sort(function (a, b) {
326
+ var ai = (a.loop && a.loop.iteration) || 0;
327
+ var bi = (b.loop && b.loop.iteration) || 0;
328
+ if (ai !== bi) return ai - bi;
329
+ var ar = (a.loop && a.loop.role === "judge") ? 1 : 0;
330
+ var br = (b.loop && b.loop.role === "judge") ? 1 : 0;
331
+ return ar - br;
332
+ });
333
+ }
334
+
335
+ // Sort runs by startedAt descending (newest first)
336
+ runKeys.sort(function (a, b) { return Number(b) - Number(a); });
322
337
 
323
338
  var expanded = expandedLoopGroups.has(gk);
324
339
  var hasActive = false;
325
340
  var anyProcessing = false;
326
341
  var latestSession = children[0];
327
- for (var i = 0; i < children.length; i++) {
328
- if (children[i].active) hasActive = true;
329
- if (children[i].isProcessing) anyProcessing = true;
330
- if ((children[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
331
- latestSession = children[i];
342
+ for (var ci = 0; ci < children.length; ci++) {
343
+ if (children[ci].active) hasActive = true;
344
+ if (children[ci].isProcessing) anyProcessing = true;
345
+ if ((children[ci].lastActivity || 0) > (latestSession.lastActivity || 0)) {
346
+ latestSession = children[ci];
332
347
  }
333
348
  }
334
349
 
335
350
  var loopName = (children[0].loop && children[0].loop.name) || "Ralph Loop";
336
351
  var isRalph = children[0].loop && children[0].loop.source === "ralph";
337
352
  var isCrafting = false;
338
- var maxIter = 0;
339
353
  for (var j = 0; j < children.length; j++) {
340
- var iter = (children[j].loop && children[j].loop.iteration) || 0;
341
- if (iter > maxIter) maxIter = iter;
342
354
  if (children[j].loop && children[j].loop.role === "crafting") isCrafting = true;
343
355
  }
344
356
 
357
+ var runCount = runKeys.length;
358
+
345
359
  var wrapper = document.createElement("div");
346
360
  wrapper.className = "session-loop-wrapper";
347
361
 
@@ -378,7 +392,8 @@ function renderLoopGroup(loopId, children, groupKey) {
378
392
  if (isCrafting && children.length === 1) {
379
393
  textHtml += '<span class="session-loop-badge crafting">Crafting</span>';
380
394
  } else {
381
- textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + children.length + '</span>';
395
+ var countLabel = runCount === 1 ? children.length : runCount + (runCount === 1 ? " run" : " runs");
396
+ textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + countLabel + '</span>';
382
397
  }
383
398
  textSpan.innerHTML = textHtml;
384
399
  el.appendChild(textSpan);
@@ -396,7 +411,7 @@ function renderLoopGroup(loopId, children, groupKey) {
396
411
  })(loopId, loopName, children.length, moreBtn));
397
412
  el.appendChild(moreBtn);
398
413
 
399
- // Click row (not chevron/more) switch to latest session
414
+ // Click row (not chevron/more) -> switch to latest session
400
415
  el.addEventListener("click", (function (id) {
401
416
  return function () {
402
417
  if (ctx.ws && ctx.connected) {
@@ -409,12 +424,98 @@ function renderLoopGroup(loopId, children, groupKey) {
409
424
 
410
425
  wrapper.appendChild(el);
411
426
 
412
- // Expanded children
427
+ // Expanded: show runs as sub-groups
413
428
  if (expanded) {
414
429
  var childContainer = document.createElement("div");
415
430
  childContainer.className = "session-loop-children";
416
- for (var k = 0; k < children.length; k++) {
417
- childContainer.appendChild(renderLoopChild(children[k]));
431
+
432
+ if (runCount === 1) {
433
+ // Single run: show sessions directly (no extra nesting)
434
+ var singleRun = runMap[runKeys[0]];
435
+ for (var sk = 0; sk < singleRun.length; sk++) {
436
+ childContainer.appendChild(renderLoopChild(singleRun[sk]));
437
+ }
438
+ } else {
439
+ // Multiple runs: render each run as a collapsible sub-group
440
+ for (var rk = 0; rk < runKeys.length; rk++) {
441
+ childContainer.appendChild(renderLoopRun(gk, runKeys[rk], runMap[runKeys[rk]], isRalph));
442
+ }
443
+ }
444
+
445
+ wrapper.appendChild(childContainer);
446
+ }
447
+
448
+ return wrapper;
449
+ }
450
+
451
+ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
452
+ var runGk = parentGk + ":" + startedAtKey;
453
+ var expanded = expandedLoopRuns.has(runGk);
454
+ var startedAt = Number(startedAtKey);
455
+ var timeLabel = startedAt ? new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "Unknown";
456
+
457
+ var hasActive = false;
458
+ var anyProcessing = false;
459
+ var latestSession = sessions[0];
460
+ for (var i = 0; i < sessions.length; i++) {
461
+ if (sessions[i].active) hasActive = true;
462
+ if (sessions[i].isProcessing) anyProcessing = true;
463
+ if ((sessions[i].lastActivity || 0) > (latestSession.lastActivity || 0)) {
464
+ latestSession = sessions[i];
465
+ }
466
+ }
467
+
468
+ var wrapper = document.createElement("div");
469
+ wrapper.className = "session-loop-run-wrapper";
470
+
471
+ var el = document.createElement("div");
472
+ el.className = "session-loop-run" + (hasActive ? " active" : "") + (expanded ? " expanded" : "") + (isRalph ? "" : " scheduled");
473
+
474
+ var chevron = document.createElement("button");
475
+ chevron.className = "session-loop-chevron";
476
+ chevron.innerHTML = iconHtml("chevron-right");
477
+ chevron.addEventListener("click", (function (rk) {
478
+ return function (e) {
479
+ e.stopPropagation();
480
+ if (expandedLoopRuns.has(rk)) {
481
+ expandedLoopRuns.delete(rk);
482
+ } else {
483
+ expandedLoopRuns.add(rk);
484
+ }
485
+ renderSessionList(null);
486
+ };
487
+ })(runGk));
488
+ el.appendChild(chevron);
489
+
490
+ var textSpan = document.createElement("span");
491
+ textSpan.className = "session-item-text";
492
+ var textHtml = "";
493
+ if (anyProcessing) {
494
+ textHtml += '<span class="session-processing"></span>';
495
+ }
496
+ textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
497
+ textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
498
+ textSpan.innerHTML = textHtml;
499
+ el.appendChild(textSpan);
500
+
501
+ // Click row -> switch to latest session of this run
502
+ el.addEventListener("click", (function (id) {
503
+ return function () {
504
+ if (ctx.ws && ctx.connected) {
505
+ ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
506
+ dismissOverlayPanels();
507
+ closeSidebar();
508
+ }
509
+ };
510
+ })(latestSession.id));
511
+
512
+ wrapper.appendChild(el);
513
+
514
+ if (expanded) {
515
+ var childContainer = document.createElement("div");
516
+ childContainer.className = "session-loop-children";
517
+ for (var k = 0; k < sessions.length; k++) {
518
+ childContainer.appendChild(renderLoopChild(sessions[k]));
418
519
  }
419
520
  wrapper.appendChild(childContainer);
420
521
  }
@@ -493,7 +594,7 @@ export function renderSessionList(sessions) {
493
594
  ctx.sessionListEl.innerHTML = "";
494
595
 
495
596
  // Partition: loop sessions vs normal sessions
496
- // Group by loopId + startedAt so different runs of the same task are separate groups
597
+ // Group by loopId + date so all runs of the same task on the same day are merged
497
598
  var loopGroups = {}; // groupKey -> [sessions]
498
599
  var normalSessions = [];
499
600
  for (var i = 0; i < cachedSessions.length; i++) {
@@ -502,7 +603,9 @@ export function renderSessionList(sessions) {
502
603
  // Task crafting sessions live in the scheduler calendar, not the main list
503
604
  continue;
504
605
  } else if (s.loop && s.loop.loopId) {
505
- var groupKey = s.loop.loopId + ":" + (s.loop.startedAt || 0);
606
+ var startedAt = s.loop.startedAt || 0;
607
+ var dateStr = startedAt ? new Date(startedAt).toISOString().slice(0, 10) : "unknown";
608
+ var groupKey = s.loop.loopId + ":" + dateStr;
506
609
  if (!loopGroups[groupKey]) loopGroups[groupKey] = [];
507
610
  loopGroups[groupKey].push(s);
508
611
  } else {
@@ -1665,8 +1768,12 @@ export function initSidebar(_ctx) {
1665
1768
  var sidebarColumn = document.getElementById("sidebar-column");
1666
1769
 
1667
1770
  function syncUserIslandWidth() {
1668
- if (!userIsland || !sidebarColumn) return;
1669
- var rect = sidebarColumn.getBoundingClientRect();
1771
+ if (!userIsland) return;
1772
+ var mateSidebarColumn = document.getElementById("mate-sidebar-column");
1773
+ var isMateDM = document.body.classList.contains("mate-dm-active");
1774
+ var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
1775
+ if (!col) return;
1776
+ var rect = col.getBoundingClientRect();
1670
1777
  userIsland.style.width = (rect.right - 8 - 8) + "px";
1671
1778
  }
1672
1779
 
@@ -1919,6 +2026,23 @@ function showIconTooltip(el, text) {
1919
2026
  });
1920
2027
  }
1921
2028
 
2029
+ function showIconTooltipHtml(el, html) {
2030
+ hideIconTooltip();
2031
+ var tip = document.createElement("div");
2032
+ tip.className = "icon-strip-tooltip";
2033
+ tip.style.whiteSpace = "normal";
2034
+ tip.style.maxWidth = "260px";
2035
+ tip.innerHTML = html;
2036
+ document.body.appendChild(tip);
2037
+ iconStripTooltip = tip;
2038
+
2039
+ requestAnimationFrame(function () {
2040
+ var rect = el.getBoundingClientRect();
2041
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
2042
+ tip.classList.add("visible");
2043
+ });
2044
+ }
2045
+
1922
2046
  function hideIconTooltip() {
1923
2047
  if (iconStripTooltip) {
1924
2048
  iconStripTooltip.remove();
@@ -3452,7 +3576,13 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
3452
3576
 
3453
3577
  // Tooltip
3454
3578
  var displayName = mp.displayName || mate.name || "New Mate";
3455
- el.addEventListener("mouseenter", function () { showIconTooltip(el, displayName); });
3579
+ el.addEventListener("mouseenter", function () {
3580
+ if (mate.bio) {
3581
+ showIconTooltipHtml(el, '<div style="font-weight:600">' + escapeHtml(displayName) + '</div><div style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-top:2px">' + escapeHtml(mate.bio) + '</div>');
3582
+ } else {
3583
+ showIconTooltip(el, displayName);
3584
+ }
3585
+ });
3456
3586
  el.addEventListener("mouseleave", hideIconTooltip);
3457
3587
 
3458
3588
  // Click: open DM with mate
@@ -3,6 +3,7 @@ import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
4
  import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
5
5
  import { openFile } from './filebrowser.js';
6
+ import { mateAvatarUrl } from './avatar.js';
6
7
 
7
8
  var ctx;
8
9
 
@@ -419,7 +420,7 @@ function permissionInputSummary(toolName, input) {
419
420
  }
420
421
  }
421
422
 
422
- export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason) {
423
+ export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason, mateId) {
423
424
  if (pendingPermissions[requestId]) return;
424
425
  ctx.finalizeAssistantBlock();
425
426
  stopThinking();
@@ -433,7 +434,7 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
433
434
 
434
435
  // Mate DM: render as conversational chat bubble instead of formal dialog
435
436
  if (ctx.isMateDm && ctx.isMateDm()) {
436
- renderMatePermission(requestId, toolName, toolInput);
437
+ renderMatePermission(requestId, toolName, toolInput, mateId);
437
438
  return;
438
439
  }
439
440
 
@@ -690,9 +691,18 @@ function matePermissionInfo(toolName, toolInput) {
690
691
  return { verb: verb, target: target };
691
692
  }
692
693
 
693
- function renderMatePermission(requestId, toolName, toolInput) {
694
+ function renderMatePermission(requestId, toolName, toolInput, mateId) {
694
695
  var mateName = ctx.getMateName();
695
696
  var mateAvatar = ctx.getMateAvatarUrl();
697
+
698
+ // If mateId provided (e.g. @mention in DM), use that mate's info instead of DM target
699
+ if (mateId && ctx.getMateById) {
700
+ var mentionMate = ctx.getMateById(mateId);
701
+ if (mentionMate) {
702
+ mateName = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || mateName;
703
+ mateAvatar = mateAvatarUrl(mentionMate, 36);
704
+ }
705
+ }
696
706
  var info = matePermissionInfo(toolName, toolInput);
697
707
  var askMsg = "Can I " + info.verb + (info.target ? " " + info.target : "") + "?";
698
708
 
@@ -46,6 +46,8 @@ function convertTitles() {
46
46
  "#top-bar [title]",
47
47
  ".title-bar-content [title]",
48
48
  "#input-area [title]",
49
+ ".mate-sidebar-actions [title]",
50
+ "#mate-sidebar-header [title]",
49
51
  ];
50
52
  var els = document.querySelectorAll(selectors.join(", "));
51
53
  for (var i = 0; i < els.length; i++) {
@@ -27,3 +27,4 @@
27
27
  @import url("css/mates.css");
28
28
  @import url("css/command-palette.css");
29
29
  @import url("css/mention.css");
30
+ @import url("css/debate.css");
package/lib/scheduler.js CHANGED
@@ -217,6 +217,62 @@ function createLoopRegistry(opts) {
217
217
  }
218
218
  }
219
219
 
220
+ // Check recurrenceEnd conditions before triggering
221
+ if (rec.recurrenceEnd) {
222
+ var shouldDisable = false;
223
+ if (rec.recurrenceEnd.type === "until" && rec.recurrenceEnd.date) {
224
+ var reParts = rec.recurrenceEnd.date.split("-");
225
+ var endDate = new Date(parseInt(reParts[0], 10), parseInt(reParts[1], 10) - 1, parseInt(reParts[2], 10), 23, 59, 59, 999);
226
+ if (now > endDate.getTime()) {
227
+ shouldDisable = true;
228
+ }
229
+ } else if (rec.recurrenceEnd.type === "after" && rec.recurrenceEnd.count > 0) {
230
+ if ((rec.runs || []).length >= rec.recurrenceEnd.count) {
231
+ shouldDisable = true;
232
+ }
233
+ }
234
+ if (shouldDisable) {
235
+ rec.enabled = false;
236
+ rec.nextRunAt = null;
237
+ save();
238
+ if (onChange) onChange(records);
239
+ continue;
240
+ }
241
+ }
242
+
243
+ // Check intervalEnd conditions before triggering
244
+ if (rec.intervalEnd) {
245
+ var skipTrigger = false;
246
+ if (rec.intervalEnd.type === "until" && rec.intervalEnd.time) {
247
+ // Stop at a specific time of day
248
+ var nowDate = new Date(now);
249
+ var ieParts = rec.intervalEnd.time.split(":");
250
+ var stopH = parseInt(ieParts[0], 10);
251
+ var stopM = parseInt(ieParts[1], 10) || 0;
252
+ var nowMinOfDay = nowDate.getHours() * 60 + nowDate.getMinutes();
253
+ if (nowMinOfDay >= stopH * 60 + stopM) {
254
+ skipTrigger = true;
255
+ }
256
+ } else if (rec.intervalEnd.type === "after" && rec.intervalEnd.count > 0) {
257
+ // Stop after N runs today
258
+ var todayStart = new Date(now);
259
+ todayStart.setHours(0, 0, 0, 0);
260
+ var todayStartMs = todayStart.getTime();
261
+ var todayRuns = (rec.runs || []).filter(function (r) { return r.startedAt >= todayStartMs; });
262
+ if (todayRuns.length >= rec.intervalEnd.count) {
263
+ skipTrigger = true;
264
+ }
265
+ }
266
+ if (skipTrigger) {
267
+ // Advance nextRunAt without triggering
268
+ if (rec.cron) {
269
+ rec.nextRunAt = nextRunTime(rec.cron, now);
270
+ }
271
+ save();
272
+ continue;
273
+ }
274
+ }
275
+
220
276
  // Update nextRunAt
221
277
  rec.lastRunAt = now;
222
278
  if (rec.cron) {
@@ -247,7 +303,7 @@ function createLoopRegistry(opts) {
247
303
  task: data.task || "",
248
304
  cron: data.cron || null,
249
305
  enabled: data.cron ? (data.enabled !== false) : false,
250
- maxIterations: data.maxIterations || 20,
306
+ maxIterations: (data.maxIterations >= 1) ? data.maxIterations : 20,
251
307
  createdAt: Date.now(),
252
308
  updatedAt: Date.now(),
253
309
  lastRunAt: null,
@@ -265,6 +321,7 @@ function createLoopRegistry(opts) {
265
321
  mode: data.mode || "loop",
266
322
  prompt: data.prompt || null,
267
323
  skipIfRunning: data.skipIfRunning !== undefined ? data.skipIfRunning : true,
324
+ intervalEnd: data.intervalEnd || null,
268
325
  runs: [],
269
326
  };
270
327
  if (rec.cron && rec.enabled) {
@@ -301,6 +358,7 @@ function createLoopRegistry(opts) {
301
358
  if (data.allDay !== undefined) rec.allDay = data.allDay;
302
359
  if (data.linkedTaskId !== undefined) rec.linkedTaskId = data.linkedTaskId;
303
360
  if (data.skipIfRunning !== undefined) rec.skipIfRunning = data.skipIfRunning;
361
+ if (data.intervalEnd !== undefined) rec.intervalEnd = data.intervalEnd;
304
362
  rec.updatedAt = Date.now();
305
363
  if (rec.cron && rec.enabled) {
306
364
  rec.nextRunAt = nextRunTime(rec.cron);
package/lib/sessions.js CHANGED
@@ -76,7 +76,6 @@ function createSessionManager(opts) {
76
76
 
77
77
  function saveSessionFile(session) {
78
78
  if (!session.cliSessionId) return;
79
- session.lastActivity = Date.now();
80
79
  try {
81
80
  var metaObj = {
82
81
  type: "meta",
@@ -89,6 +88,8 @@ function createSessionManager(opts) {
89
88
  if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
90
89
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
91
90
  if (session.loop) metaObj.loop = session.loop;
91
+ if (session.debateState) metaObj.debateState = session.debateState;
92
+ if (session.debateSetupMode) metaObj.debateSetupMode = true;
92
93
  var meta = JSON.stringify(metaObj);
93
94
  var lines = [meta];
94
95
  for (var i = 0; i < session.history.length; i++) {
@@ -174,6 +175,8 @@ function createSessionManager(opts) {
174
175
  lastRewindUuid: m.lastRewindUuid || null,
175
176
  };
176
177
  if (m.loop) session.loop = m.loop;
178
+ if (m.debateState) session.debateState = m.debateState;
179
+ if (m.debateSetupMode) session.debateSetupMode = true;
177
180
  if (m.ownerId) session.ownerId = m.ownerId;
178
181
  session.sessionVisibility = m.sessionVisibility || "shared";
179
182
  sessions.set(localId, session);
@@ -329,7 +332,11 @@ function createSessionManager(opts) {
329
332
  _send({ type: "history_meta", total: total, from: fromIndex });
330
333
 
331
334
  for (var i = fromIndex; i < total; i++) {
332
- _send(transform ? transform(session.history[i]) : session.history[i]);
335
+ var _item = session.history[i];
336
+ if (_item && (_item.type === "mention_user" || _item.type === "mention_response")) {
337
+ console.log("[DEBUG replayHistory] sending mention at index=" + i + " from=" + fromIndex + " total=" + total + " type=" + _item.type + " mate=" + (_item.mateName || ""));
338
+ }
339
+ _send(transform ? transform(_item) : _item);
333
340
  }
334
341
 
335
342
  // Find the last result message in the full history for accurate context data
@@ -0,0 +1,92 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var config = require("./config");
4
+
5
+ // In-memory store: { slug: { userId: { sessionId, mateDm } } }
6
+ var store = {};
7
+ var presencePath = path.join(config.CONFIG_DIR, "user-presence.json");
8
+ var saveTimer = null;
9
+
10
+ // Load from disk on startup
11
+ function load() {
12
+ try {
13
+ if (fs.existsSync(presencePath)) {
14
+ var raw = fs.readFileSync(presencePath, "utf8");
15
+ store = JSON.parse(raw);
16
+ }
17
+ } catch (e) {
18
+ store = {};
19
+ }
20
+ }
21
+
22
+ // Debounced save to disk (200ms)
23
+ function scheduleSave() {
24
+ if (saveTimer) return;
25
+ saveTimer = setTimeout(function () {
26
+ saveTimer = null;
27
+ try {
28
+ config.ensureConfigDir();
29
+ fs.writeFileSync(presencePath, JSON.stringify(store, null, 2), "utf8");
30
+ } catch (e) {
31
+ // Silently ignore write errors
32
+ }
33
+ }, 200);
34
+ }
35
+
36
+ function setPresence(slug, userId, sessionId, mateDm) {
37
+ if (!slug || !userId) return;
38
+ if (!store[slug]) store[slug] = {};
39
+ store[slug][userId] = {
40
+ sessionId: sessionId || null,
41
+ mateDm: mateDm !== undefined ? mateDm : null,
42
+ };
43
+ scheduleSave();
44
+ }
45
+
46
+ function getPresence(slug, userId) {
47
+ if (!slug || !userId) return null;
48
+ if (!store[slug]) return null;
49
+ return store[slug][userId] || null;
50
+ }
51
+
52
+ function setMateDm(slug, userId, mateDm) {
53
+ if (!slug || !userId) return;
54
+ if (!store[slug]) store[slug] = {};
55
+ var existing = store[slug][userId] || {};
56
+ store[slug][userId] = {
57
+ sessionId: existing.sessionId || null,
58
+ mateDm: mateDm !== undefined ? mateDm : null,
59
+ };
60
+ scheduleSave();
61
+ }
62
+
63
+ function clearPresence(slug, userId) {
64
+ if (!slug || !userId) return;
65
+ if (store[slug]) {
66
+ delete store[slug][userId];
67
+ if (Object.keys(store[slug]).length === 0) delete store[slug];
68
+ scheduleSave();
69
+ }
70
+ }
71
+
72
+ // Remove all presence entries referencing a deleted session
73
+ function clearSession(slug, sessionId) {
74
+ if (!slug || !store[slug]) return;
75
+ var keys = Object.keys(store[slug]);
76
+ for (var i = 0; i < keys.length; i++) {
77
+ if (store[slug][keys[i]].sessionId === sessionId) {
78
+ store[slug][keys[i]].sessionId = null;
79
+ }
80
+ }
81
+ scheduleSave();
82
+ }
83
+
84
+ load();
85
+
86
+ module.exports = {
87
+ setPresence: setPresence,
88
+ getPresence: getPresence,
89
+ setMateDm: setMateDm,
90
+ clearPresence: clearPresence,
91
+ clearSession: clearSession,
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.18.0-beta.9",
3
+ "version": "2.18.0",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",