clay-server 2.17.0 → 2.18.0-beta.2

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/sdk-bridge.js CHANGED
@@ -1115,6 +1115,14 @@ function createSDKBridge(opts) {
1115
1115
  return Promise.resolve({ behavior: "allow", updatedInput: input });
1116
1116
  }
1117
1117
 
1118
+ // Mate sessions (DM): auto-approve read-only tools
1119
+ if (mateDisplayName) {
1120
+ var mateReadTools = { Read: true, Glob: true, Grep: true };
1121
+ if (mateReadTools[toolName]) {
1122
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1123
+ }
1124
+ }
1125
+
1118
1126
  // AskUserQuestion: wait for user answers via WebSocket
1119
1127
  if (toolName === "AskUserQuestion") {
1120
1128
  return new Promise(function(resolve) {
@@ -1729,6 +1737,182 @@ function createSDKBridge(opts) {
1729
1737
  }
1730
1738
  }
1731
1739
 
1740
+ // --- @Mention: persistent read-only session for a mentioned Mate ---
1741
+ // Creates a mention session that can be reused across multiple mentions
1742
+ // within a conversation flow (session continuity).
1743
+ async function createMentionSession(opts) {
1744
+ // opts: { claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
1745
+ var sdk;
1746
+ try {
1747
+ sdk = await getSDK();
1748
+ } catch (e) {
1749
+ opts.onError("Failed to load Claude SDK: " + (e.message || e));
1750
+ return null;
1751
+ }
1752
+
1753
+ var mq = createMessageQueue();
1754
+ var abortController = new AbortController();
1755
+
1756
+ // Current response callbacks (swapped on each pushMessage)
1757
+ var currentOnDelta = opts.onDelta;
1758
+ var currentOnDone = opts.onDone;
1759
+ var currentOnError = opts.onError;
1760
+ var currentOnActivity = opts.onActivity || null;
1761
+ var responseFullText = "";
1762
+ var responseStreamedText = false;
1763
+ var mentionBlocks = {};
1764
+ var alive = true;
1765
+
1766
+ var query;
1767
+ try {
1768
+ query = sdk.query({
1769
+ prompt: mq,
1770
+ options: {
1771
+ cwd: cwd,
1772
+ systemPrompt: opts.claudeMd,
1773
+ settingSources: ["user"],
1774
+ includePartialMessages: true,
1775
+ abortController: abortController,
1776
+ canUseTool: function (toolName, input) {
1777
+ var allowed = { Read: true, Glob: true, Grep: true };
1778
+ if (allowed[toolName]) {
1779
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1780
+ }
1781
+ return Promise.resolve({
1782
+ behavior: "deny",
1783
+ message: "Read-only access. You cannot make changes via @mention.",
1784
+ });
1785
+ },
1786
+ },
1787
+ });
1788
+ } catch (e) {
1789
+ opts.onError("Failed to create mention query: " + (e.message || e));
1790
+ return null;
1791
+ }
1792
+
1793
+ // Push the initial message (context + question)
1794
+ var initialPrompt = opts.initialContext + "\n\n" + opts.initialMessage;
1795
+ mq.push({
1796
+ type: "user",
1797
+ message: { role: "user", content: [{ type: "text", text: initialPrompt }] },
1798
+ });
1799
+
1800
+ // Background stream processing loop
1801
+ (async function () {
1802
+ try {
1803
+ for await (var sdkMsg of query) {
1804
+ if (sdkMsg.type === "stream_event" && sdkMsg.event) {
1805
+ var evt = sdkMsg.event;
1806
+
1807
+ // Track content blocks for activity reporting
1808
+ if (evt.type === "content_block_start") {
1809
+ var block = evt.content_block;
1810
+ var idx = evt.index;
1811
+ if (block.type === "thinking") {
1812
+ mentionBlocks[idx] = { type: "thinking" };
1813
+ if (currentOnActivity) currentOnActivity("thinking");
1814
+ } else if (block.type === "tool_use") {
1815
+ mentionBlocks[idx] = { type: "tool_use", name: block.name, inputJson: "" };
1816
+ var toolLabel = block.name;
1817
+ if (toolLabel === "Read") toolLabel = "Reading file...";
1818
+ else if (toolLabel === "Grep") toolLabel = "Searching code...";
1819
+ else if (toolLabel === "Glob") toolLabel = "Finding files...";
1820
+ if (currentOnActivity) currentOnActivity(toolLabel);
1821
+ } else if (block.type === "text") {
1822
+ mentionBlocks[idx] = { type: "text" };
1823
+ }
1824
+ }
1825
+
1826
+ if (evt.type === "content_block_delta" && evt.delta) {
1827
+ if (evt.delta.type === "text_delta" && typeof evt.delta.text === "string") {
1828
+ responseStreamedText = true;
1829
+ responseFullText += evt.delta.text;
1830
+ if (currentOnActivity) currentOnActivity(null); // clear activity on text
1831
+ if (currentOnDelta) currentOnDelta(evt.delta.text);
1832
+ } else if (evt.delta.type === "input_json_delta" && mentionBlocks[evt.index]) {
1833
+ mentionBlocks[evt.index].inputJson += evt.delta.partial_json;
1834
+ }
1835
+ }
1836
+
1837
+ if (evt.type === "content_block_stop") {
1838
+ var blk = mentionBlocks[evt.index];
1839
+ if (blk && blk.type === "tool_use") {
1840
+ // Show what file is being read
1841
+ var toolInput = {};
1842
+ try { toolInput = JSON.parse(blk.inputJson); } catch (e) {}
1843
+ if (blk.name === "Read" && toolInput.file_path) {
1844
+ var fname = toolInput.file_path.split(/[/\\]/).pop();
1845
+ if (currentOnActivity) currentOnActivity("Reading " + fname + "...");
1846
+ } else if (blk.name === "Grep" && toolInput.pattern) {
1847
+ if (currentOnActivity) currentOnActivity("Searching: " + toolInput.pattern.substring(0, 30) + "...");
1848
+ } else if (blk.name === "Glob" && toolInput.pattern) {
1849
+ if (currentOnActivity) currentOnActivity("Finding: " + toolInput.pattern.substring(0, 30) + "...");
1850
+ }
1851
+ }
1852
+ delete mentionBlocks[evt.index];
1853
+ }
1854
+
1855
+ } else if (sdkMsg.type === "assistant" && !responseStreamedText && sdkMsg.message && sdkMsg.message.content) {
1856
+ // Fallback: if text was not streamed via deltas, extract from assistant message
1857
+ var content = sdkMsg.message.content;
1858
+ if (Array.isArray(content)) {
1859
+ for (var ci = 0; ci < content.length; ci++) {
1860
+ if (content[ci].type === "text" && content[ci].text) {
1861
+ responseFullText += content[ci].text;
1862
+ if (currentOnDelta) currentOnDelta(content[ci].text);
1863
+ }
1864
+ }
1865
+ }
1866
+ } else if (sdkMsg.type === "result") {
1867
+ // One response complete. Signal done and reset for next message.
1868
+ if (currentOnActivity) currentOnActivity(null);
1869
+ if (currentOnDone) {
1870
+ currentOnDone(responseFullText);
1871
+ }
1872
+ currentOnDelta = null;
1873
+ currentOnDone = null;
1874
+ currentOnError = null;
1875
+ currentOnActivity = null;
1876
+ mentionBlocks = {};
1877
+ responseFullText = "";
1878
+ responseStreamedText = false;
1879
+ }
1880
+ }
1881
+ } catch (err) {
1882
+ if (currentOnError) {
1883
+ if (err.name === "AbortError" || (abortController && abortController.signal.aborted)) {
1884
+ currentOnError("Mention query was cancelled.");
1885
+ } else {
1886
+ currentOnError(err.message || String(err));
1887
+ }
1888
+ }
1889
+ }
1890
+ alive = false;
1891
+ })();
1892
+
1893
+ return {
1894
+ // Push a follow-up message to the existing mention session
1895
+ pushMessage: function (text, callbacks) {
1896
+ currentOnDelta = callbacks.onDelta;
1897
+ currentOnDone = callbacks.onDone;
1898
+ currentOnError = callbacks.onError;
1899
+ currentOnActivity = callbacks.onActivity || null;
1900
+ mentionBlocks = {};
1901
+ responseFullText = "";
1902
+ responseStreamedText = false;
1903
+ mq.push({
1904
+ type: "user",
1905
+ message: { role: "user", content: [{ type: "text", text: text }] },
1906
+ });
1907
+ },
1908
+ close: function () {
1909
+ alive = false;
1910
+ try { mq.end(); } catch (e) {}
1911
+ },
1912
+ isAlive: function () { return alive; },
1913
+ };
1914
+ }
1915
+
1732
1916
  return {
1733
1917
  createMessageQueue: createMessageQueue,
1734
1918
  processSDKMessage: processSDKMessage,
@@ -1746,6 +1930,7 @@ function createSDKBridge(opts) {
1746
1930
  permissionPushBody: permissionPushBody,
1747
1931
  warmup: warmup,
1748
1932
  stopTask: stopTask,
1933
+ createMentionSession: createMentionSession,
1749
1934
  };
1750
1935
  }
1751
1936
 
package/lib/sessions.js CHANGED
@@ -395,6 +395,16 @@ function createSessionManager(opts) {
395
395
  }
396
396
  }
397
397
 
398
+ function cleanupMentionSessions(session) {
399
+ if (session._mentionSessions) {
400
+ var mateIds = Object.keys(session._mentionSessions);
401
+ for (var mi = 0; mi < mateIds.length; mi++) {
402
+ try { session._mentionSessions[mateIds[mi]].close(); } catch (e) {}
403
+ }
404
+ session._mentionSessions = {};
405
+ }
406
+ }
407
+
398
408
  function deleteSession(localId, targetWs) {
399
409
  var session = sessions.get(localId);
400
410
  if (!session) return;
@@ -402,6 +412,8 @@ function createSessionManager(opts) {
402
412
  // Clean up unread tracking
403
413
  delete singleUserUnread[localId];
404
414
 
415
+ cleanupMentionSessions(session);
416
+
405
417
  if (session.abortController) {
406
418
  try { session.abortController.abort(); } catch(e) {}
407
419
  }
@@ -431,6 +443,7 @@ function createSessionManager(opts) {
431
443
  var session = sessions.get(localId);
432
444
  if (!session) return;
433
445
  delete singleUserUnread[localId];
446
+ cleanupMentionSessions(session);
434
447
  if (session.abortController) {
435
448
  try { session.abortController.abort(); } catch(e) {}
436
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.17.0",
3
+ "version": "2.18.0-beta.2",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",