clay-server 2.32.0-beta.2 → 2.32.0-beta.4

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.
@@ -601,7 +601,9 @@ function attachDebate(ctx) {
601
601
  var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
602
602
 
603
603
  var briefText = "";
604
+ var _modMate = matesModule.getMate(mateCtx, debate.moderatorId);
604
605
  ctx.sdk.createMentionSession({
606
+ vendor: _modMate ? _modMate.vendor : null,
605
607
  claudeMd: claudeMd,
606
608
  initialContext: digests,
607
609
  initialMessage: quickBriefPrompt,
@@ -794,7 +796,9 @@ function attachDebate(ctx) {
794
796
  var digests = ctx.loadMateDigests(mateCtx, debate.moderatorId, debate.topic);
795
797
  var moderatorContext = buildModeratorContext(debate) + digests;
796
798
 
799
+ var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
797
800
  ctx.sdk.createMentionSession({
801
+ vendor: _modMate2 ? _modMate2.vendor : null,
798
802
  claudeMd: claudeMd,
799
803
  initialContext: moderatorContext,
800
804
  initialMessage: "Begin the debate on: " + debate.topic,
@@ -987,7 +991,9 @@ function attachDebate(ctx) {
987
991
  historyContext += "---";
988
992
  }
989
993
 
994
+ var _panMate = matesModule.getMate(debate.mateCtx, mateId);
990
995
  ctx.sdk.createMentionSession({
996
+ vendor: _panMate ? _panMate.vendor : null,
991
997
  claudeMd: claudeMd,
992
998
  initialContext: panelistContext + historyContext,
993
999
  initialMessage: "The moderator addresses you:\n\n" + moderatorText,
@@ -1521,7 +1527,9 @@ function attachDebate(ctx) {
1521
1527
  }
1522
1528
  moderatorContext += "---\n";
1523
1529
 
1530
+ var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
1524
1531
  ctx.sdk.createMentionSession({
1532
+ vendor: _modMate3 ? _modMate3.vendor : null,
1525
1533
  claudeMd: claudeMd,
1526
1534
  initialContext: moderatorContext,
1527
1535
  initialMessage: resumePrompt,
@@ -560,6 +560,7 @@ function attachMateInteraction(ctx) {
560
560
 
561
561
  // Create new persistent mention session
562
562
  sdk.createMentionSession({
563
+ vendor: mate.vendor || null,
563
564
  claudeMd: claudeMd,
564
565
  initialContext: mentionContext,
565
566
  initialMessage: mentionFullInput,
@@ -280,6 +280,7 @@ function attachSessions(ctx) {
280
280
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
281
281
  var s = sm.sessions.get(msg.id);
282
282
  s.title = String(msg.title).substring(0, 100);
283
+ s.titleManuallySet = true;
283
284
  sm.saveSessionFile(s);
284
285
  sm.broadcastSessionList();
285
286
  // Sync title to SDK session
package/lib/public/app.js CHANGED
@@ -386,7 +386,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
386
386
  openDm: function (userId) { openDm(userId); },
387
387
  openMateWizard: function () { requireClayMateInterview(function () { openMateWizard(); }); },
388
388
  openAddProjectModal: function () { openAddProjectModal(); },
389
- sendWs: function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
389
+ sendWs: function (msg) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(msg)); },
390
390
  onDmRemoveUser: function (userId) { var dr = Object.assign({}, store.get('dmRemovedUsers')); dr[userId] = true; store.set({ dmRemovedUsers: dr }); },
391
391
  getHistoryFrom: function () { return store.get('historyFrom'); },
392
392
  get permissions() { return store.get('permissions'); },
@@ -399,7 +399,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
399
399
  initMateKnowledge(wsGetter);
400
400
  initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
401
401
  initMateWizard(
402
- function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
402
+ function (msg) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(msg)); },
403
403
  function (mate) { handleMateCreatedInApp(mate); }
404
404
  );
405
405
 
@@ -420,8 +420,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
420
420
  var stickyBtn = document.getElementById("sticky-notes-sidebar-btn");
421
421
  if (stickyBtn) stickyBtn.click();
422
422
  }
423
- if (ws && ws.readyState === 1) {
424
- ws.send(JSON.stringify({ type: "switch_session", id: id }));
423
+ var _ws = _getWsRef();
424
+ if (_ws && _ws.readyState === 1) {
425
+ _ws.send(JSON.stringify({ type: "switch_session", id: id }));
425
426
  }
426
427
  },
427
428
  openDm: function (userId) { openDm(userId); },
@@ -700,7 +701,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
700
701
  isDebateFloorMode: function () { return store.get('debateFloorMode'); },
701
702
  handleDebateFloorSend: function () { handleDebateFloorSend(); },
702
703
  isMateDm: function () { return store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate; },
703
- getDmMateId: function () { return (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) ? dmTargetUser.id : null; },
704
+ getDmMateId: function () { var _dmt = store.get('dmTargetUser'); return (store.get('dmMode') && _dmt && _dmt.isMate) ? _dmt.id : null; },
704
705
  getMateName: function () { return store.get('dmTargetUser') ? (store.get('dmTargetUser').displayName || "Mate") : "Mate"; },
705
706
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
706
707
  showMatePreThinking: function () { showMatePreThinking(); },
@@ -726,7 +727,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
726
727
  // --- Debate module ---
727
728
  initDebate({
728
729
  get ws() { return _getWsRef(); },
729
- sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
730
+ sendWs: function (obj) { var _ws = _getWsRef(); if (_ws && _ws.readyState === 1) _ws.send(JSON.stringify(obj)); },
730
731
  messagesEl: messagesEl,
731
732
  addToMessages: function (el) { addToMessages(el); },
732
733
  scrollToBottom: scrollToBottom,
@@ -23,7 +23,11 @@ export function updateMermaidTheme(vars) {
23
23
  var mermaidIdCounter = 0;
24
24
 
25
25
  export function renderMarkdown(text) {
26
- return DOMPurify.sanitize(marked.parse(text));
26
+ // Normalize smart quotes so bold/italic delimiters flanking them parse correctly
27
+ var normalized = text
28
+ .replace(/[\u201C\u201D]/g, '"')
29
+ .replace(/[\u2018\u2019]/g, "'");
30
+ return DOMPurify.sanitize(marked.parse(normalized));
27
31
  }
28
32
 
29
33
  /**
package/lib/sdk-bridge.js CHANGED
@@ -171,10 +171,50 @@ function createSDKBridge(opts) {
171
171
  var mergeSkills = skills.mergeSkills;
172
172
 
173
173
  // --- Message processing (extracted to sdk-message-processor.js) ---
174
+ // Auto-generate a session title via YOKE adapter.generateTitle().
175
+ // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
176
+ function autoGenerateTitle(session) {
177
+ if (typeof adapter.generateTitle !== "function") {
178
+ console.log("[auto-title] adapter.generateTitle not available for vendor=" + adapter.vendor);
179
+ return;
180
+ }
181
+ var userMessages = [];
182
+ for (var i = 0; i < session.history.length; i++) {
183
+ var entry = session.history[i];
184
+ if (entry.type === "user_message" && entry.text) {
185
+ userMessages.push(entry.text.substring(0, 200));
186
+ if (userMessages.length >= 5) break;
187
+ }
188
+ }
189
+ if (userMessages.length === 0) {
190
+ console.log("[auto-title] No user messages found in session " + session.localId);
191
+ return;
192
+ }
193
+ console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
194
+
195
+ adapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
196
+ if (!title || title.length < 2) return;
197
+ title = title.substring(0, 100);
198
+ if (!session.titleManuallySet) {
199
+ session.title = title;
200
+ session.titleAutoGenerated = true;
201
+ sm.saveSessionFile(session);
202
+ sm.broadcastSessionList();
203
+ if (session.cliSessionId && typeof adapter.renameSession === "function") {
204
+ adapter.renameSession(session.cliSessionId, title, { dir: cwd }).catch(function () {});
205
+ }
206
+ console.log("[auto-title] Generated title for session " + session.localId + ": " + title);
207
+ }
208
+ }).catch(function(e) {
209
+ console.error("[auto-title] Failed:", e.message || e);
210
+ });
211
+ }
212
+
174
213
  var msgProcessor = attachMessageProcessor({
175
214
  sm: sm,
176
215
  send: send,
177
216
  slug: slug,
217
+ cwd: cwd,
178
218
  isMate: isMate,
179
219
  mateDisplayName: mateDisplayName,
180
220
  pushModule: pushModule,
@@ -182,6 +222,7 @@ function createSDKBridge(opts) {
182
222
  adapter: adapter,
183
223
  onProcessingChanged: onProcessingChanged,
184
224
  onTurnDone: onTurnDone,
225
+ onAutoTitle: function (session) { autoGenerateTitle(session); },
185
226
  opts: opts,
186
227
  discoverSkillDirs: discoverSkillDirs,
187
228
  mergeSkills: mergeSkills,
@@ -586,7 +627,9 @@ function createSDKBridge(opts) {
586
627
  console.log("[sdk-bridge] processQueryStream: starting for-await loop, vendor=" + (session.vendor || adapter.vendor));
587
628
  try {
588
629
  for await (var msg of myQueryInstance) {
589
- console.log("[sdk-bridge] processQueryStream: received event yokeType=" + (msg && msg.yokeType) + " type=" + (msg && msg.type) + (msg && msg.text ? " text=" + msg.text.substring(0, 200) : ""));
630
+ if (msg && msg.yokeType !== "text_delta" && msg.yokeType !== "thinking_delta" && msg.yokeType !== "tool_input_delta") {
631
+ console.log("[sdk-bridge] processQueryStream: received event yokeType=" + msg.yokeType);
632
+ }
590
633
  // Handle worker meta events (context_usage, model_changed, etc.)
591
634
  if (msg && msg.type === "_worker_meta") {
592
635
  var metaData = msg.data || {};
@@ -1317,7 +1360,7 @@ function createSDKBridge(opts) {
1317
1360
  // Creates a mention session that can be reused across multiple mentions
1318
1361
  // within a conversation flow (session continuity).
1319
1362
  async function createMentionSession(opts) {
1320
- // opts: { claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
1363
+ // opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
1321
1364
  var abortController = new AbortController();
1322
1365
 
1323
1366
  // Current response callbacks (swapped on each pushMessage)
@@ -1330,9 +1373,12 @@ function createSDKBridge(opts) {
1330
1373
  var mentionBlocks = {};
1331
1374
  var alive = true;
1332
1375
 
1376
+ // Use the mate's vendor adapter if specified, otherwise default
1377
+ var mentionAdapter = (opts.vendor && adapters[opts.vendor]) || adapter;
1378
+
1333
1379
  var handle;
1334
1380
  try {
1335
- handle = await adapter.createQuery({
1381
+ handle = await mentionAdapter.createQuery({
1336
1382
  cwd: cwd,
1337
1383
  systemPrompt: opts.claudeMd,
1338
1384
  model: opts.model || undefined,
@@ -9,13 +9,17 @@ function attachMessageProcessor(ctx) {
9
9
  var pushModule = ctx.pushModule;
10
10
  var getNotificationsModule = ctx.getNotificationsModule || function () { return null; };
11
11
  var getSDK = ctx.getSDK;
12
- var adapter = ctx.adapter; // YOKE adapter (unused currently, available for future use)
12
+ var adapter = ctx.adapter;
13
+ var cwd = ctx.cwd;
13
14
  var onProcessingChanged = ctx.onProcessingChanged;
14
15
  var onTurnDone = ctx.onTurnDone;
16
+ var onAutoTitle = ctx.onAutoTitle;
15
17
  var opts = ctx.opts;
16
18
  var discoverSkillDirs = ctx.discoverSkillDirs;
17
19
  var mergeSkills = ctx.mergeSkills;
18
20
 
21
+ var AUTO_TITLE_TURN_THRESHOLD = 3;
22
+
19
23
  function sendAndRecord(session, obj) {
20
24
  sm.sendAndRecord(session, obj);
21
25
  }
@@ -405,10 +409,23 @@ function attachMessageProcessor(ctx) {
405
409
  }
406
410
  // Reset for next turn in the same query
407
411
  session.lastActivityAt = Date.now();
412
+ session.turnCount = (session.turnCount || 0) + 1;
408
413
  var donePreview = session.responsePreview || "";
409
414
  session.responsePreview = "";
410
415
  session.streamedText = false;
411
416
  sm.broadcastSessionList();
417
+
418
+ // Auto-generate title after N turns (skip if loop or already auto-generated)
419
+ if (session.turnCount === AUTO_TITLE_TURN_THRESHOLD
420
+ && !session.titleAutoGenerated
421
+ && !session.titleManuallySet
422
+ && !session.loop
423
+ && onAutoTitle) {
424
+ try { onAutoTitle(session); } catch (e) {
425
+ console.error("[auto-title] onAutoTitle threw:", e.message || e);
426
+ }
427
+ }
428
+
412
429
  if (onTurnDone) {
413
430
  try { onTurnDone(session, donePreview); } catch (e) {}
414
431
  }
package/lib/sessions.js CHANGED
@@ -286,6 +286,8 @@ function createSessionManager(opts) {
286
286
  allowedTools: {},
287
287
  isProcessing: false,
288
288
  title: "",
289
+ titleAutoGenerated: false,
290
+ turnCount: 0,
289
291
  createdAt: Date.now(),
290
292
  lastActivity: Date.now(),
291
293
  history: [],
@@ -314,6 +316,8 @@ function createSessionManager(opts) {
314
316
  allowedTools: {},
315
317
  isProcessing: false,
316
318
  title: "",
319
+ titleAutoGenerated: false,
320
+ turnCount: 0,
317
321
  createdAt: Date.now(),
318
322
  lastActivity: Date.now(),
319
323
  history: [],
@@ -1096,6 +1096,58 @@ function createClaudeAdapter(opts) {
1096
1096
  return createQueryHandle(rawQuery, mq, ac);
1097
1097
  },
1098
1098
 
1099
+ // --- Title generation ---
1100
+ generateTitle: async function(messages, opts) {
1101
+ console.log("[auto-title/claude] generateTitle called with " + messages.length + " messages");
1102
+ var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
1103
+ var prompt = "Below is a conversation between a user and an AI assistant. Generate a short, descriptive title (3-8 words) that captures the main topic. Reply with ONLY the title, nothing else.\n\n";
1104
+ for (var i = 0; i < messages.length; i++) {
1105
+ prompt += "User message " + (i + 1) + ": " + messages[i] + "\n";
1106
+ }
1107
+ var ac = new AbortController();
1108
+ console.log("[auto-title/claude] Creating query with model=haiku...");
1109
+ var handle = await adapter.createQuery({
1110
+ cwd: (opts && opts.cwd) || _cwd,
1111
+ systemPrompt: systemPrompt,
1112
+ model: "haiku",
1113
+ adapterOptions: {
1114
+ CLAUDE: {
1115
+ settingSources: ["user"],
1116
+ permissionMode: "bypassPermissions",
1117
+ }
1118
+ },
1119
+ abortController: ac,
1120
+ });
1121
+ console.log("[auto-title/claude] Query created, pushing message...");
1122
+ handle.pushMessage(prompt);
1123
+ var title = "";
1124
+ var streamed = false;
1125
+ try {
1126
+ for await (var msg of handle) {
1127
+ if (msg.yokeType === "text_delta" && msg.text) {
1128
+ streamed = true;
1129
+ title += msg.text;
1130
+ } else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !streamed && msg.content) {
1131
+ // Fallback: extract text from non-streamed message content
1132
+ var content = msg.content;
1133
+ if (Array.isArray(content)) {
1134
+ for (var ci = 0; ci < content.length; ci++) {
1135
+ if (content[ci].type === "text" && content[ci].text) {
1136
+ title += content[ci].text;
1137
+ }
1138
+ }
1139
+ }
1140
+ } else if (msg.yokeType === "result") {
1141
+ break;
1142
+ }
1143
+ }
1144
+ } finally {
1145
+ handle.close();
1146
+ }
1147
+ console.log("[auto-title/claude] Generated: " + title.substring(0, 80));
1148
+ return title.replace(/[\r\n]+/g, " ").replace(/^["'\s]+|["'\s.]+$/g, "").trim();
1149
+ },
1150
+
1099
1151
  // --- Session management ---
1100
1152
  // These delegate to SDK module-level functions.
1101
1153
 
@@ -932,6 +932,48 @@ function createCodexAdapter(opts) {
932
932
  return handle;
933
933
  },
934
934
 
935
+ // --- Title generation ---
936
+ generateTitle: async function(messages, opts) {
937
+ var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
938
+ var prompt = "Below is a conversation between a user and an AI assistant. Generate a short, descriptive title (3-8 words) that captures the main topic. Reply with ONLY the title, nothing else.\n\n";
939
+ for (var i = 0; i < messages.length; i++) {
940
+ prompt += "User message " + (i + 1) + ": " + messages[i] + "\n";
941
+ }
942
+ var ac = new AbortController();
943
+ var handle = await adapter.createQuery({
944
+ cwd: (opts && opts.cwd) || _cwd,
945
+ systemPrompt: systemPrompt,
946
+ model: "gpt-5.4-mini",
947
+ abortController: ac,
948
+ canUseTool: function() { return Promise.resolve({ behavior: "deny", message: "No tools." }); },
949
+ });
950
+ handle.pushMessage(prompt);
951
+ var title = "";
952
+ var streamed = false;
953
+ try {
954
+ for await (var msg of handle) {
955
+ if (msg.yokeType === "text_delta" && msg.text) {
956
+ streamed = true;
957
+ title += msg.text;
958
+ } else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !streamed && msg.content) {
959
+ var content = msg.content;
960
+ if (Array.isArray(content)) {
961
+ for (var ci = 0; ci < content.length; ci++) {
962
+ if (content[ci].type === "text" && content[ci].text) {
963
+ title += content[ci].text;
964
+ }
965
+ }
966
+ }
967
+ } else if (msg.yokeType === "result") {
968
+ break;
969
+ }
970
+ }
971
+ } finally {
972
+ handle.close();
973
+ }
974
+ return title.replace(/[\r\n]+/g, " ").replace(/^["'\s]+|["'\s.]+$/g, "").trim();
975
+ },
976
+
935
977
  // Codex has session persistence via thread IDs
936
978
  getSessionInfo: function(sessionId) {
937
979
  return Promise.resolve(null);
@@ -653,6 +653,47 @@ function createGeminiAdapter(opts) {
653
653
  return handle;
654
654
  },
655
655
 
656
+ // --- Title generation ---
657
+ generateTitle: async function(messages, opts) {
658
+ var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
659
+ var prompt = "Below is a conversation between a user and an AI assistant. Generate a short, descriptive title (3-8 words) that captures the main topic. Reply with ONLY the title, nothing else.\n\n";
660
+ for (var i = 0; i < messages.length; i++) {
661
+ prompt += "User message " + (i + 1) + ": " + messages[i] + "\n";
662
+ }
663
+ var ac = new AbortController();
664
+ var handle = await adapter.createQuery({
665
+ cwd: (opts && opts.cwd) || undefined,
666
+ systemPrompt: systemPrompt,
667
+ model: "gemini-2.5-flash",
668
+ abortController: ac,
669
+ });
670
+ handle.pushMessage(prompt);
671
+ var title = "";
672
+ var streamed = false;
673
+ try {
674
+ for await (var msg of handle) {
675
+ if (msg.yokeType === "text_delta" && msg.text) {
676
+ streamed = true;
677
+ title += msg.text;
678
+ } else if (msg.yokeType === "message" && msg.messageRole === "assistant" && !streamed && msg.content) {
679
+ var content = msg.content;
680
+ if (Array.isArray(content)) {
681
+ for (var ci = 0; ci < content.length; ci++) {
682
+ if (content[ci].type === "text" && content[ci].text) {
683
+ title += content[ci].text;
684
+ }
685
+ }
686
+ }
687
+ } else if (msg.yokeType === "result") {
688
+ break;
689
+ }
690
+ }
691
+ } finally {
692
+ handle.close();
693
+ }
694
+ return title.replace(/[\r\n]+/g, " ").replace(/^["'\s]+|["'\s.]+$/g, "").trim();
695
+ },
696
+
656
697
  // Gemini has no session management (stateless chat)
657
698
  getSessionInfo: function() { return Promise.resolve(null); },
658
699
  listSessions: function() { return Promise.resolve([]); },
@@ -21,6 +21,12 @@ var TOOL_POLICIES = ["ask", "allow-all"];
21
21
  * .createToolServer(def): ToolServer (opaque)
22
22
  * .createQuery(opts): QueryHandle
23
23
  *
24
+ * Lightweight utilities:
25
+ * .generateTitle(messages, opts) : Promise<string> - generate a short session title
26
+ * messages: string[] - user messages to derive the title from
27
+ * opts: { cwd }
28
+ * Returns a short (3-8 word) title string.
29
+ *
24
30
  * Additional session management (Claude SDK specific, may vary per adapter):
25
31
  * .getSessionInfo(sessionId, opts): Promise<object|null>
26
32
  * .listSessions(opts) : Promise<Array>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.32.0-beta.2",
3
+ "version": "2.32.0-beta.4",
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",