clay-server 2.26.0-beta.1 → 2.26.0-beta.11

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.
Files changed (42) hide show
  1. package/bin/cli.js +5 -9
  2. package/lib/browser-mcp-server.js +496 -0
  3. package/lib/daemon.js +1 -1
  4. package/lib/os-users.js +23 -0
  5. package/lib/project-debate.js +243 -95
  6. package/lib/project-mate-interaction.js +766 -0
  7. package/lib/project-memory.js +677 -0
  8. package/lib/project.js +546 -1361
  9. package/lib/public/app.js +817 -175
  10. package/lib/public/css/debate.css +224 -2
  11. package/lib/public/css/icon-strip.css +10 -10
  12. package/lib/public/css/input.css +296 -83
  13. package/lib/public/css/mates.css +56 -57
  14. package/lib/public/css/mention.css +7 -4
  15. package/lib/public/css/menus.css +7 -0
  16. package/lib/public/css/messages.css +17 -0
  17. package/lib/public/css/mobile-nav.css +3 -1
  18. package/lib/public/css/overlays.css +181 -0
  19. package/lib/public/css/rewind.css +79 -0
  20. package/lib/public/css/server-settings.css +1 -0
  21. package/lib/public/css/sidebar.css +10 -0
  22. package/lib/public/css/title-bar.css +189 -3
  23. package/lib/public/index.html +53 -16
  24. package/lib/public/modules/context-sources.js +328 -0
  25. package/lib/public/modules/debate.js +184 -97
  26. package/lib/public/modules/input.js +18 -1
  27. package/lib/public/modules/mate-knowledge.js +11 -11
  28. package/lib/public/modules/mate-memory.js +5 -5
  29. package/lib/public/modules/mate-sidebar.js +13 -9
  30. package/lib/public/modules/mention.js +40 -2
  31. package/lib/public/modules/notifications.js +109 -1
  32. package/lib/public/modules/rewind.js +36 -0
  33. package/lib/public/modules/sidebar.js +107 -28
  34. package/lib/public/modules/terminal.js +8 -0
  35. package/lib/public/modules/theme.js +2 -1
  36. package/lib/public/modules/tools.js +69 -24
  37. package/lib/sdk-bridge.js +81 -7
  38. package/lib/sdk-worker.js +13 -1
  39. package/lib/server.js +42 -0
  40. package/lib/sessions.js +39 -7
  41. package/lib/terminal-manager.js +36 -6
  42. package/package.json +2 -2
@@ -1,9 +1,10 @@
1
1
  import { escapeHtml, copyToClipboard } from './utils.js';
2
- import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
2
+ import { iconHtml, refreshIcons } 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
6
  import { mateAvatarUrl } from './avatar.js';
7
+ import { getChatLayout } from './theme.js';
7
8
 
8
9
  var ctx;
9
10
 
@@ -496,12 +497,17 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
496
497
  return;
497
498
  }
498
499
 
499
- // Mate DM: render as conversational chat bubble instead of formal dialog
500
- if (ctx.isMateDm && ctx.isMateDm()) {
501
- renderMatePermission(requestId, toolName, toolInput, mateId);
500
+ // Channel layout or Mate DM: conversational "Can I ...?" style
501
+ if ((ctx.isMateDm && ctx.isMateDm()) || getChatLayout() === "channel") {
502
+ renderConversationalPermission(requestId, toolName, toolInput, mateId);
502
503
  return;
503
504
  }
504
505
 
506
+ // Bubble layout: formal "Permission Required" dialog
507
+ renderFormalPermission(requestId, toolName, toolInput, decisionReason);
508
+ }
509
+
510
+ function renderFormalPermission(requestId, toolName, toolInput, decisionReason) {
505
511
  var container = document.createElement("div");
506
512
  container.className = "permission-container";
507
513
  container.dataset.requestId = requestId;
@@ -755,18 +761,40 @@ function matePermissionInfo(toolName, toolInput) {
755
761
  return { verb: verb, target: target };
756
762
  }
757
763
 
758
- function renderMatePermission(requestId, toolName, toolInput, mateId) {
759
- var mateName = ctx.getMateName();
760
- var mateAvatar = ctx.getMateAvatarUrl();
761
-
762
- // If mateId provided (e.g. @mention in DM), use that mate's info instead of DM target
764
+ function resolvePermissionIdentity(mateId) {
765
+ // Mate DM: use DM target mate info
766
+ if (ctx.isMateDm && ctx.isMateDm()) {
767
+ var name = ctx.getMateName();
768
+ var avatar = ctx.getMateAvatarUrl();
769
+ // Override if specific mateId provided (e.g. @mention)
770
+ if (mateId && ctx.getMateById) {
771
+ var mentionMate = ctx.getMateById(mateId);
772
+ if (mentionMate) {
773
+ name = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || name;
774
+ avatar = mateAvatarUrl(mentionMate, 36);
775
+ }
776
+ }
777
+ return { name: name, avatar: avatar };
778
+ }
779
+ // Channel with Mate mention
763
780
  if (mateId && ctx.getMateById) {
764
- var mentionMate = ctx.getMateById(mateId);
765
- if (mentionMate) {
766
- mateName = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || mateName;
767
- mateAvatar = mateAvatarUrl(mentionMate, 36);
781
+ var mate = ctx.getMateById(mateId);
782
+ if (mate) {
783
+ return {
784
+ name: (mate.profile && mate.profile.displayName) || mate.displayName || mate.name || "Mate",
785
+ avatar: mateAvatarUrl(mate, 36)
786
+ };
768
787
  }
769
788
  }
789
+ // Project chat (Claude Code)
790
+ return {
791
+ name: "Claude Code",
792
+ avatar: ctx.getClaudeAvatar ? ctx.getClaudeAvatar() : ""
793
+ };
794
+ }
795
+
796
+ function renderConversationalPermission(requestId, toolName, toolInput, mateId) {
797
+ var identity = resolvePermissionIdentity(mateId);
770
798
  var info = matePermissionInfo(toolName, toolInput);
771
799
  var askMsg = "Can I " + info.verb + (info.target ? " " + info.target : "") + "?";
772
800
 
@@ -777,7 +805,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
777
805
  // Avatar (left column)
778
806
  var avi = document.createElement("img");
779
807
  avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
780
- avi.src = mateAvatar;
808
+ avi.src = identity.avatar;
781
809
  avi.alt = "";
782
810
  container.appendChild(avi);
783
811
 
@@ -789,7 +817,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
789
817
  var headerRow = document.createElement("div");
790
818
  headerRow.className = "dm-bubble-header";
791
819
  headerRow.innerHTML =
792
- '<span class="dm-bubble-name">' + escapeHtml(mateName) + '</span>' +
820
+ '<span class="dm-bubble-name">' + escapeHtml(identity.name) + '</span>' +
793
821
  '<span class="dm-bubble-time">' + String(new Date().getHours()).padStart(2, "0") + ":" + String(new Date().getMinutes()).padStart(2, "0") + '</span>';
794
822
  content.appendChild(headerRow);
795
823
 
@@ -1409,12 +1437,11 @@ export function startThinking() {
1409
1437
  var el = thinkingGroup.el;
1410
1438
  el.classList.remove("done");
1411
1439
  el.querySelector(".thinking-content").textContent = "";
1412
- // Mate mode: restore sparkle activity row, hide thinking header
1440
+ // Mate mode: restore dots activity row, hide thinking header
1413
1441
  if (el.classList.contains("mate-thinking")) {
1414
1442
  var actRow = el.querySelector(".mate-thinking-activity");
1415
1443
  if (actRow) {
1416
1444
  actRow.style.display = "";
1417
- actRow.querySelector(".activity-text").textContent = randomThinkingVerb() + "...";
1418
1445
  }
1419
1446
  var header = el.querySelector(".thinking-header");
1420
1447
  if (header) header.style.display = "none";
@@ -1423,7 +1450,7 @@ export function startThinking() {
1423
1450
  refreshIcons();
1424
1451
  ctx.scrollToBottom();
1425
1452
  if (!el.classList.contains("mate-thinking")) {
1426
- ctx.setActivity(randomThinkingVerb() + "...");
1453
+ ctx.setActivity("thinking");
1427
1454
  }
1428
1455
  return;
1429
1456
  }
@@ -1439,10 +1466,7 @@ export function startThinking() {
1439
1466
  '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
1440
1467
  '<div class="dm-bubble-content">' +
1441
1468
  '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
1442
- '<div class="activity-inline mate-thinking-activity">' +
1443
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
1444
- '<span class="activity-text">' + randomThinkingVerb() + '...</span>' +
1445
- '</div>' +
1469
+ '<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
1446
1470
  '<div class="thinking-header" style="display:none">' +
1447
1471
  '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1448
1472
  '<span class="thinking-label">Thinking</span>' +
@@ -1472,7 +1496,7 @@ export function startThinking() {
1472
1496
  thinkingGroup = { el: el, count: 0, totalDuration: 0 };
1473
1497
  currentThinking = { el: el, fullText: "", startTime: Date.now() };
1474
1498
  if (!ctx.isMateDm()) {
1475
- ctx.setActivity(randomThinkingVerb() + "...");
1499
+ ctx.setActivity("thinking");
1476
1500
  }
1477
1501
  }
1478
1502
 
@@ -2063,13 +2087,29 @@ export function markSubagentDone(parentToolId, status, summary, usage) {
2063
2087
  if (usage) updateSubagentProgress(parentToolId, usage, null);
2064
2088
  }
2065
2089
 
2090
+ var _lastCumulativeCost = 0;
2091
+
2092
+ export function resetTurnMetaCost() {
2093
+ _lastCumulativeCost = 0;
2094
+ }
2095
+
2066
2096
  export function addTurnMeta(cost, duration) {
2067
2097
  closeToolGroup();
2068
2098
  var div = document.createElement("div");
2069
2099
  div.className = "turn-meta";
2070
2100
  div.dataset.turn = ctx.turnCounter;
2071
2101
  var parts = [];
2072
- if (cost != null) parts.push("$" + cost.toFixed(4));
2102
+ if (cost != null) {
2103
+ // cost is cumulative total_cost_usd from the SDK.
2104
+ // When the SDK session restarts, total_cost_usd resets to 0 so cost
2105
+ // can drop below _lastCumulativeCost. In that case the entire cost
2106
+ // value IS the delta for this turn (fresh SDK session).
2107
+ var delta = cost - _lastCumulativeCost;
2108
+ if (delta < 0) delta = cost;
2109
+ _lastCumulativeCost = cost;
2110
+ var deltaStr = delta > 0 ? "+$" + delta.toFixed(4) : "$0.0000";
2111
+ parts.push(deltaStr + " \u2192 $" + cost.toFixed(4));
2112
+ }
2073
2113
  if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
2074
2114
  if (parts.length) {
2075
2115
  div.textContent = parts.join(" \u00b7 ");
@@ -2114,6 +2154,7 @@ export function saveToolState() {
2114
2154
  currentToolGroup: currentToolGroup,
2115
2155
  toolGroupCounter: toolGroupCounter,
2116
2156
  toolGroups: toolGroups,
2157
+ lastCumulativeCost: _lastCumulativeCost,
2117
2158
  };
2118
2159
  }
2119
2160
 
@@ -2126,6 +2167,7 @@ export function restoreToolState(saved) {
2126
2167
  currentToolGroup = saved.currentToolGroup;
2127
2168
  toolGroupCounter = saved.toolGroupCounter;
2128
2169
  toolGroups = saved.toolGroups;
2170
+ _lastCumulativeCost = saved.lastCumulativeCost || 0;
2129
2171
  if (todoWidgetEl) {
2130
2172
  setupTodoObserver();
2131
2173
  }
@@ -2146,6 +2188,9 @@ export function resetToolState() {
2146
2188
  currentToolGroup = null;
2147
2189
  toolGroupCounter = 0;
2148
2190
  toolGroups = {};
2191
+ // NOTE: do NOT reset _lastCumulativeCost here — it must persist across
2192
+ // turns so addTurnMeta can compute per-turn deltas. It is only cleared
2193
+ // on new conversation via resetTurnMetaCost().
2149
2194
  var stickyEl = document.getElementById("todo-sticky");
2150
2195
  if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
2151
2196
  }
package/lib/sdk-bridge.js CHANGED
@@ -134,6 +134,7 @@ function createSDKBridge(opts) {
134
134
  var mateDisplayName = opts.mateDisplayName || "";
135
135
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
136
136
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
137
+ var mcpServers = opts.mcpServers || null;
137
138
  var onProcessingChanged = opts.onProcessingChanged || function () {};
138
139
  var onTurnDone = opts.onTurnDone || null;
139
140
 
@@ -188,6 +189,10 @@ function createSDKBridge(opts) {
188
189
  sm.sendAndRecord(session, obj);
189
190
  }
190
191
 
192
+ function sendToSession(session, obj) {
193
+ sm.sendToSession(session, obj);
194
+ }
195
+
191
196
  function processSDKMessage(session, parsed) {
192
197
  // Timing: log key SDK milestones relative to query start
193
198
  if (session._queryStartTs) {
@@ -435,6 +440,15 @@ function createSDKBridge(opts) {
435
440
  session.isProcessing = false;
436
441
  session.rateLimitResetsAt = null; // clear on success
437
442
  onProcessingChanged();
443
+ // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
444
+ if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
445
+ session.queryInstance.getContextUsage().then(function(ctxUsage) {
446
+ session.lastContextUsage = ctxUsage;
447
+ sendToSession(session, { type: "context_usage", data: ctxUsage });
448
+ }).catch(function(e) {
449
+ console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
450
+ });
451
+ }
438
452
  var lastStreamInput = session.lastStreamInputTokens || null;
439
453
  session.lastStreamInputTokens = null;
440
454
  sendAndRecord(session, {
@@ -581,8 +595,26 @@ function createSDKBridge(opts) {
581
595
  isUsingOverage: info.isUsingOverage || false,
582
596
  });
583
597
  // Track rejection for auto-continue / scheduled message support
584
- if (info.status === "rejected" && info.resetsAt) {
598
+ // Skip if using overage (extra usage) user can continue immediately
599
+ if (info.status === "rejected" && info.resetsAt && !info.isUsingOverage) {
585
600
  session.rateLimitResetsAt = info.resetsAt * 1000;
601
+
602
+ // Defensive: if query already completed before this event arrived,
603
+ // schedule auto-continue now (handles race condition where
604
+ // rate_limit_event arrives after the result/done event).
605
+ if (!session.isProcessing && !session.scheduledMessage && !session.destroying) {
606
+ var lateACEnabled = session.onQueryComplete ||
607
+ (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
608
+ if (lateACEnabled) {
609
+ var lateResetsAt = session.rateLimitResetsAt;
610
+ session.rateLimitResetsAt = null;
611
+ session.rateLimitAutoContinuePending = true;
612
+ console.log("[sdk-bridge] Rate limit event arrived late, scheduling auto-continue for session " + session.localId);
613
+ if (typeof opts.scheduleMessage === "function") {
614
+ opts.scheduleMessage(session, "continue", lateResetsAt);
615
+ }
616
+ }
617
+ }
586
618
  }
587
619
  }
588
620
 
@@ -1115,6 +1147,7 @@ function createSDKBridge(opts) {
1115
1147
  agentProgressSummaries: true,
1116
1148
  };
1117
1149
 
1150
+ if (mcpServers) queryOptions.mcpServers = mcpServers;
1118
1151
  if (sm.currentModel) queryOptions.model = sm.currentModel;
1119
1152
  if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
1120
1153
  if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
@@ -1138,6 +1171,8 @@ function createSDKBridge(opts) {
1138
1171
  if (session.lastRewindUuid) {
1139
1172
  queryOptions.resumeSessionAt = session.lastRewindUuid;
1140
1173
  delete session.lastRewindUuid;
1174
+ // Persist the deletion so server restarts don't re-use a stale UUID
1175
+ sm.saveSessionFile(session);
1141
1176
  }
1142
1177
  }
1143
1178
 
@@ -1194,6 +1229,11 @@ function createSDKBridge(opts) {
1194
1229
  });
1195
1230
  break;
1196
1231
 
1232
+ case "context_usage":
1233
+ session.lastContextUsage = msg.data;
1234
+ sendToSession(session, { type: "context_usage", data: msg.data });
1235
+ break;
1236
+
1197
1237
  case "query_done":
1198
1238
  console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
1199
1239
  // Mark that we received a proper IPC completion, so the exit
@@ -1299,6 +1339,8 @@ function createSDKBridge(opts) {
1299
1339
  sm.broadcastSessionList();
1300
1340
  }
1301
1341
  cleanupSessionWorker(session, worker);
1342
+ // Mark session as done so late rate_limit_event can detect race condition
1343
+ session.isProcessing = false;
1302
1344
  // Auto-continue on rate limit (scheduler sessions, or user setting)
1303
1345
  var workerDidScheduleAC = false;
1304
1346
  var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
@@ -1493,6 +1535,18 @@ function createSDKBridge(opts) {
1493
1535
  return Promise.resolve({ behavior: "allow", updatedInput: input });
1494
1536
  }
1495
1537
 
1538
+ // Auto-approve safe browser MCP tools.
1539
+ // Only watch/unwatch: user explicitly chose which tab to share.
1540
+ // Everything else (screenshot, read_page, list_tabs, etc.) can expose
1541
+ // content from tabs the user didn't intend to share, so require approval.
1542
+ var safeBrowserTools = { browser_watch_tab: true, browser_unwatch_tab: true };
1543
+ if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
1544
+ var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
1545
+ if (safeBrowserTools[mcpToolName]) {
1546
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1547
+ }
1548
+ }
1549
+
1496
1550
  // Auto-approve safe Bash commands (read-only, non-destructive)
1497
1551
  // Applies to ALL sessions (mates and regular projects alike).
1498
1552
  // These are purely read-only commands that cannot modify files, install
@@ -1687,10 +1741,16 @@ function createSDKBridge(opts) {
1687
1741
  }
1688
1742
 
1689
1743
  async function processQueryStream(session) {
1744
+ // Capture references at start so we only clean up OUR resources in finally,
1745
+ // not resources from a newer query that may have been created after an abort.
1746
+ var myQueryInstance = session.queryInstance;
1747
+ var myMessageQueue = session.messageQueue;
1748
+ var myAbortController = session.abortController;
1690
1749
  try {
1691
- for await (var msg of session.queryInstance) {
1750
+ for await (var msg of myQueryInstance) {
1692
1751
  processSDKMessage(session, msg);
1693
1752
  }
1753
+ // (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
1694
1754
  // Stream ended normally after a task stop — no "result" message was sent,
1695
1755
  // so the session is still marked as processing. Send interrupted feedback.
1696
1756
  if (session.isProcessing && session.taskStopRequested) {
@@ -1704,7 +1764,7 @@ function createSDKBridge(opts) {
1704
1764
  if (session.isProcessing) {
1705
1765
  session.isProcessing = false;
1706
1766
  onProcessingChanged();
1707
- if (err.name === "AbortError" || (session.abortController && session.abortController.signal.aborted) || session.taskStopRequested) {
1767
+ if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
1708
1768
  if (!session.destroying) {
1709
1769
  sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
1710
1770
  sendAndRecord(session, { type: "done", code: 0 });
@@ -1777,22 +1837,29 @@ function createSDKBridge(opts) {
1777
1837
  } finally {
1778
1838
  // Close the SDK query to terminate the underlying claude child process.
1779
1839
  // Without this, the process stays alive indefinitely (single-user mode).
1780
- if (session.queryInstance) {
1840
+ // Only clean up if the session still references OUR resources.
1841
+ // A rewind + new startQuery may have already replaced these with
1842
+ // a newer query — clobbering them would kill the new query.
1843
+ if (session.queryInstance === myQueryInstance) {
1781
1844
  try {
1782
1845
  if (typeof session.queryInstance.close === "function") {
1783
1846
  session.queryInstance.close();
1784
1847
  }
1785
1848
  } catch (e) {}
1849
+ session.queryInstance = null;
1786
1850
  }
1787
- session.queryInstance = null;
1788
- session.messageQueue = null;
1789
- session.abortController = null;
1851
+ if (session.messageQueue === myMessageQueue) session.messageQueue = null;
1852
+ if (session.abortController === myAbortController) session.abortController = null;
1790
1853
  session.taskStopRequested = false;
1791
1854
  session.pendingPermissions = {};
1792
1855
  session.pendingAskUser = {};
1793
1856
  session.pendingElicitations = {};
1794
1857
 
1795
1858
  // Auto-continue on rate limit (scheduler sessions, or user setting)
1859
+ // Mark session as done processing so the late rate_limit_event handler
1860
+ // can detect the race condition and schedule auto-continue itself.
1861
+ session.isProcessing = false;
1862
+
1796
1863
  var didScheduleAutoContinue = false;
1797
1864
  var acEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
1798
1865
  if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
@@ -1805,6 +1872,10 @@ function createSDKBridge(opts) {
1805
1872
  if (typeof opts.scheduleMessage === "function") {
1806
1873
  opts.scheduleMessage(session, "continue", acResetsAt);
1807
1874
  }
1875
+ } else if (acEnabled && !session.destroying) {
1876
+ // Log why auto-continue was not scheduled (for debugging)
1877
+ console.log("[sdk-bridge] Query done, auto-continue enabled but not scheduled: rateLimitResetsAt=" +
1878
+ session.rateLimitResetsAt + " (will rely on late rate_limit_event handler)");
1808
1879
  }
1809
1880
 
1810
1881
  // Ralph Loop: notify completion so loop orchestrator can proceed
@@ -1911,6 +1982,7 @@ function createSDKBridge(opts) {
1911
1982
  abortController: session.abortController,
1912
1983
  promptSuggestions: true,
1913
1984
  agentProgressSummaries: true,
1985
+ mcpServers: mcpServers || undefined,
1914
1986
  canUseTool: function(toolName, input, toolOpts) {
1915
1987
  return handleCanUseTool(session, toolName, input, toolOpts);
1916
1988
  },
@@ -1952,6 +2024,8 @@ function createSDKBridge(opts) {
1952
2024
  if (session.lastRewindUuid) {
1953
2025
  queryOptions.resumeSessionAt = session.lastRewindUuid;
1954
2026
  delete session.lastRewindUuid;
2027
+ // Persist the deletion so server restarts don't re-use a stale UUID
2028
+ sm.saveSessionFile(session);
1955
2029
  }
1956
2030
  }
1957
2031
 
package/lib/sdk-worker.js CHANGED
@@ -351,7 +351,19 @@ async function handleQueryStart(msg) {
351
351
  }
352
352
  sendToDaemon({ type: "sdk_event", event: event });
353
353
  }
354
- perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), sending query_done");
354
+ perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), fetching context usage");
355
+ // Fetch context usage breakdown before queryInstance is cleared
356
+ try {
357
+ if (queryInstance && typeof queryInstance.getContextUsage === "function") {
358
+ var ctxUsage = await queryInstance.getContextUsage();
359
+ sendToDaemon({ type: "context_usage", data: ctxUsage });
360
+ perf("context usage sent");
361
+ }
362
+ } catch (e) {
363
+ // Non-fatal: SDK may have already shut down
364
+ console.error("[sdk-worker] getContextUsage failed (non-fatal):", e.message);
365
+ }
366
+ perf("sending query_done");
355
367
  sendToDaemon({ type: "query_done" });
356
368
  } catch (err) {
357
369
  var errMsg = err.message || String(err);
package/lib/server.js CHANGED
@@ -38,6 +38,24 @@ function httpGet(url) {
38
38
  });
39
39
  }
40
40
 
41
+ function httpGetBinary(url) {
42
+ return new Promise(function (resolve, reject) {
43
+ var mod = url.startsWith("https") ? https : http;
44
+ mod.get(url, { headers: { "User-Agent": "Clay/1.0" } }, function (resp) {
45
+ if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
46
+ return httpGetBinary(resp.headers.location).then(resolve, reject);
47
+ }
48
+ if (resp.statusCode !== 200) {
49
+ return reject(new Error("HTTP " + resp.statusCode));
50
+ }
51
+ var chunks = [];
52
+ resp.on("data", function (c) { chunks.push(c); });
53
+ resp.on("end", function () { resolve(Buffer.concat(chunks)); });
54
+ resp.on("error", reject);
55
+ }).on("error", reject);
56
+ });
57
+ }
58
+
41
59
  function fetchSkillsPage(url) {
42
60
  return httpGet(url).then(function (html) {
43
61
  // Data is inside self.__next_f.push() with escaped quotes: \"initialSkills\":[{\"source\":...}]
@@ -1018,6 +1036,28 @@ function createServer(opts) {
1018
1036
  return;
1019
1037
  }
1020
1038
 
1039
+ // Chrome extension download (proxy from GitHub)
1040
+ if (fullUrl === "/api/extension/download" && req.method === "GET") {
1041
+ if (!isRequestAuthed(req)) {
1042
+ res.writeHead(401, { "Content-Type": "application/json" });
1043
+ res.end(JSON.stringify({ error: "Unauthorized" }));
1044
+ return;
1045
+ }
1046
+ var archiveUrl = "https://github.com/chadbyte/clay-chrome/archive/refs/heads/main.zip";
1047
+ httpGetBinary(archiveUrl).then(function (buf) {
1048
+ res.writeHead(200, {
1049
+ "Content-Type": "application/zip",
1050
+ "Content-Disposition": 'attachment; filename="clay-chrome-extension.zip"',
1051
+ "Content-Length": buf.length,
1052
+ });
1053
+ res.end(buf);
1054
+ }).catch(function (err) {
1055
+ res.writeHead(502, { "Content-Type": "application/json" });
1056
+ res.end(JSON.stringify({ error: "Failed to download extension: " + (err.message || "unknown error") }));
1057
+ });
1058
+ return;
1059
+ }
1060
+
1021
1061
  // CORS preflight for cross-origin requests (HTTP onboarding → HTTPS)
1022
1062
  if (req.method === "OPTIONS") {
1023
1063
  res.writeHead(204, {
@@ -2889,6 +2929,8 @@ function createServer(opts) {
2889
2929
  osUsers: osUsers,
2890
2930
  currentVersion: currentVersion,
2891
2931
  lanHost: lanHost,
2932
+ port: portNum,
2933
+ tls: !!tlsOptions,
2892
2934
  getProjectCount: function () { return projects.size; },
2893
2935
  getProjectList: function (userId) {
2894
2936
  var list = [];
package/lib/sessions.js CHANGED
@@ -1,5 +1,6 @@
1
1
  var fs = require("fs");
2
2
  var path = require("path");
3
+ var crypto = require("crypto");
3
4
  var config = require("./config");
4
5
  var utils = require("./utils");
5
6
  var users = require("./users");
@@ -75,7 +76,9 @@ function createSessionManager(opts) {
75
76
  }
76
77
 
77
78
  function saveSessionFile(session) {
78
- if (!session.cliSessionId) return;
79
+ if (!session.cliSessionId) {
80
+ session.cliSessionId = crypto.randomUUID();
81
+ }
79
82
  try {
80
83
  var metaObj = {
81
84
  type: "meta",
@@ -96,17 +99,24 @@ function createSessionManager(opts) {
96
99
  lines.push(JSON.stringify(session.history[i]));
97
100
  }
98
101
  var sfPath = sessionFilePath(session.cliSessionId);
99
- fs.writeFileSync(sfPath, lines.join("\n") + "\n");
102
+ // Atomic write: write to temp file then rename, so a crash mid-write
103
+ // cannot leave a truncated/corrupted session file.
104
+ var tmpPath = sfPath + ".tmp." + process.pid;
105
+ fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
100
106
  if (process.platform !== "win32") {
101
- try { fs.chmodSync(sfPath, 0o600); } catch (chmodErr) {}
107
+ try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
102
108
  }
109
+ fs.renameSync(tmpPath, sfPath);
103
110
  } catch(e) {
104
111
  console.error("[session] Failed to save session file:", e.message);
105
112
  }
106
113
  }
107
114
 
108
115
  function appendToSessionFile(session, obj) {
109
- if (!session.cliSessionId) return;
116
+ if (!session.cliSessionId) {
117
+ session.cliSessionId = crypto.randomUUID();
118
+ saveSessionFile(session);
119
+ }
110
120
  session.lastActivity = Date.now();
111
121
  try {
112
122
  var afPath = sessionFilePath(session.cliSessionId);
@@ -123,6 +133,13 @@ function createSessionManager(opts) {
123
133
  var files;
124
134
  try { files = fs.readdirSync(sessionsDir); } catch { return; }
125
135
 
136
+ // Clean up stale temp files from interrupted atomic writes
137
+ for (var ti = 0; ti < files.length; ti++) {
138
+ if (files[ti].indexOf(".tmp.") !== -1) {
139
+ try { fs.unlinkSync(path.join(sessionsDir, files[ti])); } catch (e) {}
140
+ }
141
+ }
142
+
126
143
  var loaded = [];
127
144
  for (var i = 0; i < files.length; i++) {
128
145
  if (!files[i].endsWith(".jsonl")) continue;
@@ -226,10 +243,8 @@ function createSessionManager(opts) {
226
243
  return [...sessions.values()].filter(function (s) {
227
244
  if (s.hidden) return false;
228
245
  if (!multiUser) {
229
- // Single-user mode: only show sessions without ownerId
230
246
  return !s.ownerId;
231
247
  }
232
- // Multi-user mode: include all sessions (per-user filtering done by canAccessSession)
233
248
  return true;
234
249
  });
235
250
  }
@@ -355,7 +370,7 @@ function createSessionManager(opts) {
355
370
  }
356
371
  }
357
372
 
358
- _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
373
+ _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, contextUsage: session.lastContextUsage || null });
359
374
  }
360
375
 
361
376
  function switchSession(localId, targetWs, transform) {
@@ -481,7 +496,23 @@ function createSessionManager(opts) {
481
496
  sessions.delete(localId);
482
497
  }
483
498
 
499
+ function doSendToSession(session, obj) {
500
+ // Send to active clients without recording to history/disk (ephemeral data)
501
+ if (sendEach) {
502
+ var data = JSON.stringify(obj);
503
+ sendEach(function (ws) {
504
+ if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
505
+ ws.send(data);
506
+ }
507
+ });
508
+ } else if (session.localId === activeSessionId) {
509
+ send(obj);
510
+ }
511
+ }
512
+
484
513
  function doSendAndRecord(session, obj) {
514
+ // Stamp every recorded message so history replay preserves original times
515
+ if (!obj._ts) obj._ts = Date.now();
485
516
  session.history.push(obj);
486
517
  appendToSessionFile(session, obj);
487
518
  if (sendEach) {
@@ -726,6 +757,7 @@ function createSessionManager(opts) {
726
757
  saveSessionFile: saveSessionFile,
727
758
  appendToSessionFile: appendToSessionFile,
728
759
  sendAndRecord: doSendAndRecord,
760
+ sendToSession: doSendToSession,
729
761
  findTurnBoundary: findTurnBoundary,
730
762
  replayHistory: replayHistory,
731
763
  searchSessions: searchSessions,