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

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 +206 -88
  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 +263 -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 +313 -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
@@ -156,6 +156,7 @@ function attachDebate(ctx) {
156
156
  setupStartedAt: d.setupStartedAt || null,
157
157
  round: d.round || 1,
158
158
  awaitingConcludeConfirm: !!d.awaitingConcludeConfirm,
159
+ awaitingUserFloor: !!d.awaitingUserFloor,
159
160
  };
160
161
  ctx.sm.saveSessionFile(session);
161
162
  }
@@ -186,6 +187,7 @@ function attachDebate(ctx) {
186
187
  setupStartedAt: ds.setupStartedAt || null,
187
188
  briefPath: ds.briefPath || null,
188
189
  awaitingConcludeConfirm: !!ds.awaitingConcludeConfirm,
190
+ awaitingUserFloor: !!ds.awaitingUserFloor,
189
191
  };
190
192
 
191
193
  // Fallback: if awaitingConcludeConfirm was not persisted, detect from history
@@ -255,17 +257,11 @@ function attachDebate(ctx) {
255
257
  debate.context = brief.context || "";
256
258
  debate.specialRequests = brief.specialRequests || null;
257
259
 
258
- // Update panelists with roles from the brief
260
+ // Replace panelists with those selected in the brief
259
261
  if (brief.panelists && brief.panelists.length) {
260
- for (var i = 0; i < brief.panelists.length; i++) {
261
- var bp = brief.panelists[i];
262
- for (var j = 0; j < debate.panelists.length; j++) {
263
- if (debate.panelists[j].mateId === bp.mateId) {
264
- debate.panelists[j].role = bp.role || "";
265
- debate.panelists[j].brief = bp.brief || "";
266
- }
267
- }
268
- }
262
+ debate.panelists = brief.panelists.map(function (bp) {
263
+ return { mateId: bp.mateId, role: bp.role || "", brief: bp.brief || "" };
264
+ });
269
265
  }
270
266
 
271
267
  // Rebuild name map with updated roles
@@ -358,20 +354,17 @@ function attachDebate(ctx) {
358
354
  console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
359
355
  startDebateBriefWatcher(session, debate, briefPath);
360
356
 
361
- // Only show preparing indicator for quick start (standard setup shows skill in real-time)
362
- if (debate.quickStart) {
363
- ctx.sendTo(ws, {
364
- type: "debate_preparing",
365
- topic: debate.topic,
366
- moderatorId: debate.moderatorId,
367
- moderatorName: moderatorProfile.name,
368
- setupSessionId: debate.setupSessionId,
369
- panelists: debate.panelists.map(function (p) {
370
- var prof = ctx.getMateProfile(mateCtx, p.mateId);
371
- return { mateId: p.mateId, name: prof.name };
372
- }),
373
- });
374
- }
357
+ ctx.sendTo(ws, {
358
+ type: "debate_preparing",
359
+ topic: debate.topic,
360
+ moderatorId: debate.moderatorId,
361
+ moderatorName: moderatorProfile.name,
362
+ setupSessionId: debate.setupSessionId,
363
+ panelists: debate.panelists.map(function (p) {
364
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
365
+ return { mateId: p.mateId, name: prof.name };
366
+ }),
367
+ });
375
368
  } else if (phase === "reviewing") {
376
369
  console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
377
370
  ctx.sendTo(ws, {
@@ -500,8 +493,27 @@ function attachDebate(ctx) {
500
493
  var session = ctx.getSessionForWs(ws);
501
494
  if (!session) return;
502
495
 
503
- if (!msg.moderatorId || !msg.topic || !msg.panelists || !msg.panelists.length) {
504
- ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic, panelists." });
496
+ if (!msg.moderatorId || !msg.topic) {
497
+ ctx.sendTo(ws, { type: "debate_error", error: "Missing required fields: moderatorId, topic." });
498
+ return;
499
+ }
500
+
501
+ // delegatePanelists: moderator picks panelists, populate all available mates
502
+ if (msg.delegatePanelists) {
503
+ var userId = ws._clayUser ? ws._clayUser.id : null;
504
+ var tmpCtx = matesModule.buildMateCtx(userId);
505
+ var matesData = matesModule.loadMates(tmpCtx);
506
+ var allMates = matesData.mates || [];
507
+ msg.panelists = [];
508
+ for (var mi = 0; mi < allMates.length; mi++) {
509
+ if (allMates[mi].id !== msg.moderatorId && allMates[mi].status !== "interviewing") {
510
+ msg.panelists.push({ mateId: allMates[mi].id, role: "", brief: "" });
511
+ }
512
+ }
513
+ }
514
+
515
+ if (!msg.panelists || !msg.panelists.length) {
516
+ ctx.sendTo(ws, { type: "debate_error", error: "No panelists available." });
505
517
  return;
506
518
  }
507
519
 
@@ -597,7 +609,9 @@ function attachDebate(ctx) {
597
609
  "",
598
610
  "## Your Task",
599
611
  "Based on the conversation context, create a debate brief. You know the topic well because you were just discussing it.",
600
- "Assign each panelist a role and perspective that will create the most productive debate.",
612
+ msg.delegatePanelists
613
+ ? "IMPORTANT: Select only 2-4 panelists who are most relevant to this specific topic. Do NOT include all of them. Be selective. Only pick mates whose expertise or personality directly contributes to this debate."
614
+ : "The user already selected these panelists. Assign each one a role and perspective that will create the most productive debate.",
601
615
  "",
602
616
  "Output ONLY a valid JSON object (no markdown fences, no extra text):",
603
617
  "{",
@@ -723,13 +737,24 @@ function attachDebate(ctx) {
723
737
  // Watch for brief.json in the debate-specific directory
724
738
  startDebateBriefWatcher(session, debate, briefPath);
725
739
 
726
- // Standard setup: no preparing indicator needed because the user
727
- // sees the skill working in real-time in the setup session.
740
+ // Notify clients that setup is in progress
741
+ var preparingMsg = {
742
+ type: "debate_preparing",
743
+ topic: debate.topic || "(Setting up...)",
744
+ moderatorId: debate.moderatorId,
745
+ moderatorName: moderatorProfile.name,
746
+ setupSessionId: setupSession.localId,
747
+ panelists: debate.panelists.map(function (p) {
748
+ var prof = ctx.getMateProfile(mateCtx, p.mateId);
749
+ return { mateId: p.mateId, name: prof.name };
750
+ }),
751
+ };
752
+ ctx.sendTo(ws, preparingMsg);
753
+ ctx.sendToSession(session.localId, preparingMsg);
728
754
 
729
- // Start the setup skill session
730
- setupSession.history.push({ type: "user_message", text: craftingPrompt });
731
- ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt });
732
- ctx.sendToSession(setupSession.localId, { type: "user_message", text: craftingPrompt });
755
+ // Start the setup skill session (don't send user_message to client — it's an internal prompt)
756
+ setupSession.history.push({ type: "user_message", text: craftingPrompt, _internal: true });
757
+ ctx.sm.appendToSessionFile(setupSession, { type: "user_message", text: craftingPrompt, _internal: true });
733
758
  setupSession.isProcessing = true;
734
759
  ctx.onProcessingChanged();
735
760
  setupSession.sentToolResults = {};
@@ -754,10 +779,6 @@ function attachDebate(ctx) {
754
779
  var debateSession = ctx.sm.createSession();
755
780
  debateSession.title = debate.topic.slice(0, 50);
756
781
  debateSession.loop = { active: true, iteration: 1, role: "debate", loopId: debate.debateId, name: debate.topic.slice(0, 40), source: "debate", startedAt: debate.setupStartedAt || Date.now() };
757
- // Assign cliSessionId manually so saveSessionFile works (no SDK query for debate sessions)
758
- if (!debateSession.cliSessionId) {
759
- debateSession.cliSessionId = crypto.randomUUID();
760
- }
761
782
  ctx.sm.saveSessionFile(debateSession);
762
783
  ctx.sm.switchSession(debateSession.localId, null, ctx.hydrateImageRefs);
763
784
  debate.liveSessionId = debateSession.localId;
@@ -886,6 +907,12 @@ function attachDebate(ctx) {
886
907
  return;
887
908
  }
888
909
 
910
+ // Check if user raised hand
911
+ if (debate.handRaised) {
912
+ yieldFloorToUser(session);
913
+ return;
914
+ }
915
+
889
916
  // Trigger the first mentioned panelist
890
917
  triggerPanelist(session, mentionedIds[0], fullText);
891
918
  }
@@ -1041,12 +1068,18 @@ function attachDebate(ctx) {
1041
1068
  return;
1042
1069
  }
1043
1070
 
1044
- // Check for pending user comment
1071
+ // Check for pending user comment (legacy)
1045
1072
  if (debate.pendingComment) {
1046
1073
  injectUserComment(session);
1047
1074
  return;
1048
1075
  }
1049
1076
 
1077
+ // Check if user raised hand (no comment, just wants the floor)
1078
+ if (debate.handRaised) {
1079
+ yieldFloorToUser(session);
1080
+ return;
1081
+ }
1082
+
1050
1083
  // Feed panelist response back to moderator
1051
1084
  feedBackToModerator(session, mateId, fullText);
1052
1085
  }
@@ -1115,73 +1148,75 @@ function attachDebate(ctx) {
1115
1148
 
1116
1149
  // --- User interaction during debate ---
1117
1150
 
1118
- function handleDebateComment(ws, msg) {
1151
+ function handleDebateHandRaise(ws) {
1119
1152
  var session = ctx.getSessionForWs(ws);
1120
1153
  if (!session) return;
1121
1154
 
1122
1155
  var debate = session._debate;
1123
- if (!debate || debate.phase !== "live") {
1124
- ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1125
- return;
1126
- }
1127
-
1128
- // If awaiting conclude confirmation, re-send the confirm prompt instead
1156
+ if (!debate || debate.phase !== "live") return;
1157
+ if (debate.awaitingUserFloor || debate.handRaised) return;
1129
1158
  if (debate.awaitingConcludeConfirm) {
1130
1159
  ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
1131
1160
  return;
1132
1161
  }
1133
1162
 
1163
+ debate.handRaised = true;
1164
+ ctx.sendToSession(session.localId, { type: "debate_hand_raised" });
1165
+
1166
+ // If no one is speaking, yield floor immediately
1167
+ if (!debate.turnInProgress) {
1168
+ yieldFloorToUser(session);
1169
+ }
1170
+ // Otherwise: current speaker finishes -> handRaised detected -> yieldFloorToUser
1171
+ }
1172
+
1173
+ function handleDebateComment(ws, msg) {
1174
+ // Legacy: kept for compatibility but now hand raise is separate
1175
+ var session = ctx.getSessionForWs(ws);
1176
+ if (!session) return;
1177
+
1178
+ var debate = session._debate;
1179
+ if (!debate || debate.phase !== "live") {
1180
+ ctx.sendTo(ws, { type: "debate_error", error: "No active debate." });
1181
+ return;
1182
+ }
1183
+ if (debate.awaitingUserFloor) return;
1134
1184
  if (!msg.text) return;
1135
1185
 
1136
1186
  debate.pendingComment = { text: msg.text };
1187
+ debate.handRaised = true;
1137
1188
  ctx.sendToSession(session.localId, { type: "debate_comment_queued", text: msg.text });
1138
1189
 
1139
- // If a panelist turn is in progress, abort it and go straight to moderator
1140
- if (debate.turnInProgress && debate._currentTurnMateId && debate._currentTurnMateId !== debate.moderatorId) {
1141
- var abortMateId = debate._currentTurnMateId;
1142
- console.log("[debate] User raised hand during panelist turn, aborting " + abortMateId);
1143
-
1144
- // Close the panelist's mention session to stop generation
1145
- if (debate.panelistSessions[abortMateId]) {
1146
- try { debate.panelistSessions[abortMateId].close(); } catch (e) {}
1147
- delete debate.panelistSessions[abortMateId];
1148
- }
1190
+ if (!debate.turnInProgress) {
1191
+ injectUserComment(session);
1192
+ }
1193
+ }
1149
1194
 
1150
- // Save partial text as interrupted turn
1151
- var partialText = debate._currentTurnText || "(interrupted by audience)";
1152
- var profile = ctx.getMateProfile(debate.mateCtx, abortMateId);
1153
- var panelistInfo = null;
1154
- for (var pi = 0; pi < debate.panelists.length; pi++) {
1155
- if (debate.panelists[pi].mateId === abortMateId) { panelistInfo = debate.panelists[pi]; break; }
1156
- }
1195
+ function yieldFloorToUser(session) {
1196
+ var debate = session._debate;
1197
+ if (!debate || !debate.moderatorSession || debate.phase === "ended") return;
1157
1198
 
1158
- ctx.sendToSession(session.localId, {
1159
- type: "debate_turn_done",
1160
- mateId: abortMateId,
1161
- mateName: profile.name,
1162
- role: panelistInfo ? panelistInfo.role : "",
1163
- text: partialText,
1164
- interrupted: true,
1165
- avatarStyle: profile.avatarStyle,
1166
- avatarSeed: profile.avatarSeed,
1167
- avatarColor: profile.avatarColor,
1168
- });
1199
+ debate.handRaised = false;
1200
+ debate.turnInProgress = true;
1201
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1169
1202
 
1170
- var turnEntry = { type: "debate_turn_done", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", round: debate.round, text: partialText, avatarStyle: profile.avatarStyle, avatarSeed: profile.avatarSeed, avatarColor: profile.avatarColor, interrupted: true };
1171
- session.history.push(turnEntry);
1172
- ctx.sm.appendToSessionFile(session, turnEntry);
1173
- debate.history.push({ speaker: "panelist", mateId: abortMateId, mateName: profile.name, role: panelistInfo ? panelistInfo.role : "", text: partialText });
1203
+ ctx.sendToSession(session.localId, {
1204
+ type: "debate_turn",
1205
+ mateId: debate.moderatorId,
1206
+ mateName: moderatorProfile.name,
1207
+ role: "moderator",
1208
+ round: debate.round,
1209
+ avatarColor: moderatorProfile.avatarColor,
1210
+ avatarStyle: moderatorProfile.avatarStyle,
1211
+ avatarSeed: moderatorProfile.avatarSeed,
1212
+ });
1174
1213
 
1175
- debate.turnInProgress = false;
1176
- debate._currentTurnMateId = null;
1177
- debate._currentTurnText = "";
1178
- }
1214
+ var feedText = "[The user raised their hand to speak.]\n" +
1215
+ "[Acknowledge this briefly and yield the floor to the user. Say something like " +
1216
+ "\"Go ahead\" or \"The floor is yours\". Do NOT call on any panelist (no @mentions). " +
1217
+ "The debate will pause for the user to speak.]";
1179
1218
 
1180
- // Inject to moderator immediately if no turn in progress (or just aborted)
1181
- if (!debate.turnInProgress) {
1182
- injectUserComment(session);
1183
- }
1184
- // If moderator is currently speaking, pendingComment will be picked up after moderator's onDone
1219
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1185
1220
  }
1186
1221
 
1187
1222
  function injectUserComment(session) {
@@ -1190,6 +1225,7 @@ function attachDebate(ctx) {
1190
1225
 
1191
1226
  var comment = debate.pendingComment;
1192
1227
  debate.pendingComment = null;
1228
+ debate.handRaised = false;
1193
1229
 
1194
1230
  // Record in debate history
1195
1231
  debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: comment.text });
@@ -1199,7 +1235,7 @@ function attachDebate(ctx) {
1199
1235
  ctx.sm.appendToSessionFile(session, commentEntry);
1200
1236
  ctx.sendToSession(session.localId, commentEntry);
1201
1237
 
1202
- // Feed to moderator
1238
+ // Feed to moderator: yield the floor to the user
1203
1239
  debate.turnInProgress = true;
1204
1240
  var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1205
1241
 
@@ -1216,9 +1252,89 @@ function attachDebate(ctx) {
1216
1252
 
1217
1253
  var feedText = "[The user raised their hand and said:]\n" +
1218
1254
  comment.text + "\n" +
1219
- "[Please acknowledge this and weave it into the discussion. Then continue the debate.]";
1255
+ "[Acknowledge the user's input. Briefly respond, then YIELD THE FLOOR to the user by saying something like " +
1256
+ "\"The floor is yours\" or \"Go ahead\". Do NOT call on any panelist (no @mentions). " +
1257
+ "The debate will pause for the user to speak.]";
1258
+
1259
+ debate.moderatorSession.pushMessage(feedText, buildModeratorYieldCallbacks(session));
1260
+ }
1261
+
1262
+ function buildModeratorYieldCallbacks(session) {
1263
+ var debate = session._debate;
1264
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1265
+ return {
1266
+ onActivity: function (activity) {
1267
+ if (session._debate && session._debate.phase !== "ended") {
1268
+ ctx.sendToSession(session.localId, { type: "debate_activity", mateId: debate.moderatorId, activity: activity });
1269
+ }
1270
+ },
1271
+ onDelta: function (delta) {
1272
+ if (session._debate && session._debate.phase !== "ended") {
1273
+ ctx.sendToSession(session.localId, { type: "debate_stream", mateId: debate.moderatorId, mateName: moderatorProfile.name, delta: delta });
1274
+ }
1275
+ },
1276
+ onDone: function (fullText) {
1277
+ if (!debate || debate.phase === "ended") return;
1278
+ debate.turnInProgress = false;
1279
+
1280
+ // Record moderator yield turn
1281
+ debate.history.push({ speaker: "moderator", mateId: debate.moderatorId, mateName: moderatorProfile.name, text: fullText });
1282
+ var turnEntry = { type: "debate_turn_done", mateId: debate.moderatorId, mateName: moderatorProfile.name, role: "moderator", round: debate.round, text: fullText, avatarStyle: moderatorProfile.avatarStyle, avatarSeed: moderatorProfile.avatarSeed, avatarColor: moderatorProfile.avatarColor };
1283
+ session.history.push(turnEntry);
1284
+ ctx.sm.appendToSessionFile(session, turnEntry);
1285
+ ctx.sendToSession(session.localId, turnEntry);
1286
+
1287
+ // Enter user floor mode: pause debate and show input
1288
+ debate.awaitingUserFloor = true;
1289
+ persistDebateState(session);
1290
+ ctx.sendToSession(session.localId, { type: "debate_user_floor", topic: debate.topic, round: debate.round });
1291
+ },
1292
+ onError: function (errMsg) {
1293
+ console.error("[debate] Moderator yield error:", errMsg);
1294
+ endDebate(session, "error");
1295
+ },
1296
+ };
1297
+ }
1298
+
1299
+ function handleDebateUserFloorResponse(ws, msg) {
1300
+ var session = ctx.getSessionForWs(ws);
1301
+ if (!session) return;
1302
+
1303
+ var debate = session._debate;
1304
+ if (!debate || !debate.awaitingUserFloor || debate.phase !== "live") return;
1305
+
1306
+ debate.awaitingUserFloor = false;
1307
+ var userText = (msg && msg.text) ? msg.text.trim() : "";
1308
+ if (!userText) return;
1309
+
1310
+ // Record user's floor contribution
1311
+ debate.history.push({ speaker: "user", mateId: null, mateName: "User", text: userText });
1312
+ var floorEntry = { type: "debate_user_floor_done", text: userText };
1313
+ session.history.push(floorEntry);
1314
+ ctx.sm.appendToSessionFile(session, floorEntry);
1315
+ ctx.sendToSession(session.localId, floorEntry);
1316
+
1317
+ // Feed to moderator to resume debate
1318
+ debate.turnInProgress = true;
1319
+ var moderatorProfile = ctx.getMateProfile(debate.mateCtx, debate.moderatorId);
1320
+
1321
+ ctx.sendToSession(session.localId, {
1322
+ type: "debate_turn",
1323
+ mateId: debate.moderatorId,
1324
+ mateName: moderatorProfile.name,
1325
+ role: "moderator",
1326
+ round: debate.round,
1327
+ avatarColor: moderatorProfile.avatarColor,
1328
+ avatarStyle: moderatorProfile.avatarStyle,
1329
+ avatarSeed: moderatorProfile.avatarSeed,
1330
+ });
1331
+
1332
+ var feedText = "[The user took the floor and said:]\n" +
1333
+ userText + "\n" +
1334
+ "[Acknowledge the user's contribution and resume the debate. Call on the next panelist with @TheirName.]";
1220
1335
 
1221
1336
  debate.moderatorSession.pushMessage(feedText, buildModeratorCallbacks(session));
1337
+ persistDebateState(session);
1222
1338
  }
1223
1339
 
1224
1340
  function handleDebateConfirmBrief(ws) {
@@ -1592,10 +1708,12 @@ function attachDebate(ctx) {
1592
1708
 
1593
1709
  return {
1594
1710
  handleDebateStart: handleDebateStart,
1711
+ handleDebateHandRaise: handleDebateHandRaise,
1595
1712
  handleDebateComment: handleDebateComment,
1596
1713
  handleDebateStop: handleDebateStop,
1597
1714
  handleDebateConcludeResponse: handleDebateConcludeResponse,
1598
1715
  handleDebateConfirmBrief: handleDebateConfirmBrief,
1716
+ handleDebateUserFloorResponse: handleDebateUserFloorResponse,
1599
1717
  restoreDebateState: restoreDebateState,
1600
1718
  checkForDmDebateBrief: checkForDmDebateBrief,
1601
1719
  };