clay-server 3.3.2-beta.2 → 3.3.2-beta.3

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.
@@ -42,6 +42,7 @@ function getToolDefs(onPropose) {
42
42
  format: { type: "string", description: "Debate format, e.g. free_discussion (default)" },
43
43
  context: { type: "string", description: "Key context from the conversation that panelists should know" },
44
44
  specialRequests: { type: "string", description: "Special instructions for the debate, or empty" },
45
+ moderatorId: { type: "string", description: "Mate ID for the moderator. Required when proposing from a normal project; Mate sessions moderate their own proposals." },
45
46
  panelists: { type: "string", description: "JSON array of panelist objects: [{\"mateId\": \"<UUID>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]" },
46
47
  }, ["topic", "panelists"]),
47
48
  handler: function (args) {
@@ -51,6 +52,13 @@ function getToolDefs(onPropose) {
51
52
  } catch (e) {
52
53
  return Promise.resolve({
53
54
  content: [{ type: "text", text: "Error: panelists must be a valid JSON array. Got: " + (args.panelists || "").substring(0, 100) }],
55
+ isError: true,
56
+ });
57
+ }
58
+ if (!Array.isArray(panelists)) {
59
+ return Promise.resolve({
60
+ content: [{ type: "text", text: "Error: panelists must be a JSON array." }],
61
+ isError: true,
54
62
  });
55
63
  }
56
64
 
@@ -59,6 +67,7 @@ function getToolDefs(onPropose) {
59
67
  format: args.format || "free_discussion",
60
68
  context: args.context || "",
61
69
  specialRequests: args.specialRequests || null,
70
+ moderatorId: args.moderatorId || null,
62
71
  panelists: panelists,
63
72
  };
64
73
 
@@ -66,6 +75,12 @@ function getToolDefs(onPropose) {
66
75
  if (result && result.action === "start") {
67
76
  return { content: [{ type: "text", text: "Debate approved and started. Topic: " + briefData.topic }] };
68
77
  }
78
+ if (result && result.action === "error") {
79
+ return {
80
+ content: [{ type: "text", text: "Error: " + (result.error || "The debate could not be started.") }],
81
+ isError: true,
82
+ };
83
+ }
69
84
  return { content: [{ type: "text", text: "Debate proposal was cancelled by the user." }] };
70
85
  });
71
86
  }
@@ -6,6 +6,22 @@ var emailAccounts = require("./email-accounts");
6
6
  var { getCodexConfig } = require("./codex-defaults");
7
7
  var yoke = require("./yoke");
8
8
 
9
+ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
10
+ try {
11
+ handleMessage(ws, msg);
12
+ return true;
13
+ } catch (err) {
14
+ var detail = err && err.stack ? err.stack : (err && err.message ? err.message : String(err));
15
+ console.error("[project-connection] Message handler failed for " + slug + "/" + (msg.type || "unknown") + ":", detail);
16
+ try {
17
+ sendTo(ws, { type: "error", text: "The request could not be completed. Check the server logs for details." });
18
+ } catch (sendErr) {
19
+ console.error("[project-connection] Failed to send message error for " + slug + ":", sendErr && sendErr.message ? sendErr.message : sendErr);
20
+ }
21
+ return false;
22
+ }
23
+ }
24
+
9
25
  /**
10
26
  * Attach connection/disconnection handlers to a project context.
11
27
  *
@@ -286,7 +302,7 @@ function attachConnection(ctx) {
286
302
  ws.on("message", function (raw) {
287
303
  var msg;
288
304
  try { msg = JSON.parse(raw.toString()); } catch (e) { return; }
289
- handleMessage(ws, msg);
305
+ dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg);
290
306
  });
291
307
 
292
308
  ws.on("close", function () {
@@ -317,4 +333,4 @@ function attachConnection(ctx) {
317
333
  };
318
334
  }
319
335
 
320
- module.exports = { attachConnection: attachConnection };
336
+ module.exports = { attachConnection: attachConnection, dispatchMessageSafely: dispatchMessageSafely };
@@ -0,0 +1,138 @@
1
+ var path = require("path");
2
+ var debateMcp = require("./debate-mcp-server");
3
+
4
+ function attachDebateProposal(ctx) {
5
+ var pendingProposals = {};
6
+
7
+ function createMcpServer(adapter, boundSession) {
8
+ var toolDefs = debateMcp.getToolDefs(function onPropose(briefData) {
9
+ if (!boundSession) {
10
+ return Promise.resolve({
11
+ action: "error",
12
+ error: "Debate proposals require an active Clay session.",
13
+ });
14
+ }
15
+
16
+ return new Promise(function (resolve) {
17
+ var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
18
+ briefData.proposalId = proposalId;
19
+ pendingProposals[proposalId] = {
20
+ resolve: resolve,
21
+ briefData: briefData,
22
+ session: boundSession,
23
+ };
24
+ });
25
+ });
26
+
27
+ return adapter.createToolServer({
28
+ name: "clay-debate",
29
+ version: "1.0.0",
30
+ tools: toolDefs,
31
+ });
32
+ }
33
+
34
+ function findPendingProposal(ws, msg) {
35
+ var keys = Object.keys(pendingProposals);
36
+ if (keys.length === 0) return null;
37
+ var key = msg.proposalId || null;
38
+ if (!key && ws && Number.isInteger(ws._clayActiveSession)) {
39
+ for (var i = keys.length - 1; i >= 0; i--) {
40
+ var candidate = pendingProposals[keys[i]];
41
+ if (candidate.session && candidate.session.localId === ws._clayActiveSession) {
42
+ key = keys[i];
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ if (!key) key = keys[keys.length - 1];
48
+ var pending = pendingProposals[key];
49
+ if (!pending) return null;
50
+ delete pendingProposals[key];
51
+ return pending;
52
+ }
53
+
54
+ function rejectProposal(ws, pending, error) {
55
+ try {
56
+ ctx.sendTo(ws, { type: "debate_error", error: error });
57
+ } catch (sendErr) {
58
+ console.error("[debate] Failed to send proposal error:", sendErr && sendErr.message ? sendErr.message : sendErr);
59
+ }
60
+ pending.resolve({ action: "error", error: error });
61
+ }
62
+
63
+ function validateProposal(ws, pending) {
64
+ var briefData = pending.briefData;
65
+ var moderatorId = ctx.isMate ? path.basename(ctx.cwd) : briefData.moderatorId;
66
+ if (!moderatorId || typeof moderatorId !== "string") {
67
+ return { error: "A valid Mate moderator is required to start this debate." };
68
+ }
69
+
70
+ var userId = ws && ws._clayUser
71
+ ? ws._clayUser.id
72
+ : (pending.session.ownerId || ctx.getProjectOwnerId() || null);
73
+ var mateCtx = ctx.buildMateCtx(userId);
74
+ if (!ctx.getMate(mateCtx, moderatorId)) {
75
+ return { error: "The selected debate moderator is unavailable." };
76
+ }
77
+
78
+ var panelists = briefData.panelists;
79
+ if (!Array.isArray(panelists) || panelists.length === 0) {
80
+ return { error: "At least one valid panelist is required to start this debate." };
81
+ }
82
+
83
+ var seen = {};
84
+ for (var i = 0; i < panelists.length; i++) {
85
+ var panelistId = panelists[i] && panelists[i].mateId;
86
+ if (!panelistId || typeof panelistId !== "string" || !ctx.getMate(mateCtx, panelistId)) {
87
+ return { error: "One or more selected debate panelists are unavailable." };
88
+ }
89
+ if (panelistId === moderatorId) {
90
+ return { error: "The debate moderator cannot also be a panelist." };
91
+ }
92
+ if (seen[panelistId]) {
93
+ return { error: "The same Mate cannot be added to a debate more than once." };
94
+ }
95
+ seen[panelistId] = true;
96
+ }
97
+
98
+ return { moderatorId: moderatorId };
99
+ }
100
+
101
+ function handleMessage(ws, msg) {
102
+ if (msg.type !== "debate_proposal_response") return false;
103
+
104
+ var pending = findPendingProposal(ws, msg);
105
+ if (!pending) return true;
106
+ if (msg.action !== "start") {
107
+ pending.resolve({ action: "cancel" });
108
+ return true;
109
+ }
110
+
111
+ var validated = validateProposal(ws, pending);
112
+ if (validated.error) {
113
+ rejectProposal(ws, pending, validated.error);
114
+ return true;
115
+ }
116
+
117
+ try {
118
+ var result = ctx.startDebate(pending.session, pending.briefData, validated.moderatorId, ws);
119
+ if (result && result.error) {
120
+ rejectProposal(ws, pending, result.error);
121
+ return true;
122
+ }
123
+ pending.resolve({ action: "start" });
124
+ } catch (err) {
125
+ var detail = err && err.message ? err.message : String(err);
126
+ console.error("[debate] Failed to start approved proposal:", detail);
127
+ rejectProposal(ws, pending, "The debate could not be started. Check the server logs for details.");
128
+ }
129
+ return true;
130
+ }
131
+
132
+ return {
133
+ createMcpServer: createMcpServer,
134
+ handleMessage: handleMessage,
135
+ };
136
+ }
137
+
138
+ module.exports = { attachDebateProposal: attachDebateProposal };
@@ -1742,6 +1742,7 @@ function attachDebate(ctx) {
1742
1742
 
1743
1743
  console.log("[debate] MCP debate approved. Topic:", debate.topic, "debateId:", debateId);
1744
1744
  startDebateLive(session);
1745
+ return { ok: true };
1745
1746
  }
1746
1747
 
1747
1748
  // --- Public API ---
package/lib/project.js CHANGED
@@ -16,6 +16,7 @@ var matesModule = require("./mates");
16
16
  var sessionSearch = require("./session-search");
17
17
  var userPresence = require("./user-presence");
18
18
  var { attachDebate } = require("./project-debate");
19
+ var { attachDebateProposal } = require("./project-debate-proposal");
19
20
  var { attachMemory } = require("./project-memory");
20
21
  var { attachMateInteraction } = require("./project-mate-interaction");
21
22
  var { attachUserMention } = require("./project-user-mention");
@@ -247,7 +248,6 @@ function createProjectContext(opts) {
247
248
  var clients = new Set();
248
249
 
249
250
  // --- Browser extension state (shared mutable object) ---
250
- var _pendingDebateProposals = {}; // proposalId -> { resolve, briefData }
251
251
  var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
252
252
  var browserState = {
253
253
  _browserTabList: {},
@@ -568,6 +568,19 @@ function createProjectContext(opts) {
568
568
  getLinuxUserForSession: getLinuxUserForSession,
569
569
  getPairToolDefs: function (boundSession) { return _sessionPair.getToolDefs(boundSession); },
570
570
  });
571
+ var _debate = null;
572
+ var _debateProposal = attachDebateProposal({
573
+ cwd: cwd,
574
+ isMate: isMate,
575
+ sendTo: sendTo,
576
+ buildMateCtx: matesModule.buildMateCtx,
577
+ getMate: matesModule.getMate,
578
+ getProjectOwnerId: function () { return projectOwnerId; },
579
+ startDebate: function (session, briefData, moderatorId, ws) {
580
+ if (!_debate) return { error: "The debate engine is unavailable." };
581
+ return _debate.handleMcpDebateApproval(session, briefData, moderatorId, ws);
582
+ },
583
+ });
571
584
 
572
585
  // --- MCP tool servers (created via YOKE adapter) ---
573
586
  var mcpServers = (function () {
@@ -605,20 +618,7 @@ function createProjectContext(opts) {
605
618
 
606
619
  // Debate MCP server (available to both mates and main project)
607
620
  try {
608
- var debateMcp = require("./debate-mcp-server");
609
- var debateToolDefs = debateMcp.getToolDefs(function onPropose(briefData) {
610
- return new Promise(function (resolve) {
611
- var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
612
- briefData.proposalId = proposalId;
613
- _pendingDebateProposals[proposalId] = {
614
- resolve: resolve,
615
- briefData: briefData,
616
- };
617
- // The SDK sends tool_executing with briefData as input.
618
- // Client renders the debate brief card when it sees propose_debate.
619
- });
620
- });
621
- var debateMcpConfig = adapter.createToolServer({ name: "clay-debate", version: "1.0.0", tools: debateToolDefs });
621
+ var debateMcpConfig = _debateProposal.createMcpServer(adapter, null);
622
622
  if (debateMcpConfig) servers[debateMcpConfig.name || "clay-debate"] = debateMcpConfig;
623
623
  } catch (e) {
624
624
  console.error("[project] Failed to create debate MCP server:", e.message);
@@ -807,6 +807,15 @@ function createProjectContext(opts) {
807
807
  }
808
808
  continue;
809
809
  }
810
+ if (name === "clay-debate" && forSession) {
811
+ try {
812
+ var boundDebate = _debateProposal.createMcpServer(adapter, forSession);
813
+ if (boundDebate) { filtered[name] = boundDebate; hasAny = true; }
814
+ } catch (e) {
815
+ console.error("[project] Failed to bind debate MCP server:", e.message);
816
+ }
817
+ continue;
818
+ }
810
819
  filtered[name] = mcpServers[name];
811
820
  hasAny = true;
812
821
  }
@@ -1139,28 +1148,7 @@ function createProjectContext(opts) {
1139
1148
  handleDebateConfirmBrief(ws);
1140
1149
  return;
1141
1150
  }
1142
- if (msg.type === "debate_proposal_response") {
1143
- // Match the most recent pending proposal (proposalId may not be
1144
- // available on the client since it's not part of the tool input)
1145
- var _dpKeys = Object.keys(_pendingDebateProposals);
1146
- if (_dpKeys.length === 0) return;
1147
- var _dpKey = msg.proposalId || _dpKeys[_dpKeys.length - 1];
1148
- var pending = _pendingDebateProposals[_dpKey];
1149
- if (!pending) return;
1150
- delete _pendingDebateProposals[_dpKey];
1151
- if (msg.action === "start") {
1152
- // Set up debate state on the session, then transition to live
1153
- var _dpSession = getSessionForWs(ws);
1154
- if (_dpSession) {
1155
- var _dpMateId = isMate ? path.basename(cwd) : null;
1156
- handleMcpDebateApproval(_dpSession, pending.briefData, _dpMateId, ws);
1157
- }
1158
- pending.resolve({ action: "start" });
1159
- } else {
1160
- pending.resolve({ action: "cancel" });
1161
- }
1162
- return;
1163
- }
1151
+ if (_debateProposal.handleMessage(ws, msg)) return;
1164
1152
  if (msg.type === "debate_user_floor_response") {
1165
1153
  handleDebateUserFloorResponse(ws, msg);
1166
1154
  return;
@@ -1268,7 +1256,7 @@ function createProjectContext(opts) {
1268
1256
  var handleUserMention = _userMention.handleUserMention;
1269
1257
 
1270
1258
  // --- Debate engine (delegated to project-debate.js) ---
1271
- var _debate = attachDebate({
1259
+ _debate = attachDebate({
1272
1260
  cwd: cwd,
1273
1261
  slug: slug,
1274
1262
  isMate: isMate,
@@ -1297,7 +1285,6 @@ function createProjectContext(opts) {
1297
1285
  var handleDebateUserFloorResponse = _debate.handleDebateUserFloorResponse;
1298
1286
  var restoreDebateState = _debate.restoreDebateState;
1299
1287
  var checkForDmDebateBrief = _debate.checkForDmDebateBrief;
1300
- var handleMcpDebateApproval = _debate.handleMcpDebateApproval;
1301
1288
 
1302
1289
  // --- Session presence (who is viewing which session) ---
1303
1290
  function broadcastPresence() {
@@ -1547,7 +1534,7 @@ function createProjectContext(opts) {
1547
1534
  // In-app MCP servers (debate, browser, email).
1548
1535
  // Use getLocalMcpServers() so clay-browser is hidden unless the
1549
1536
  // Chrome extension is currently connected (see issue #325).
1550
- var localMcp = getLocalMcpServers();
1537
+ var localMcp = getLocalMcpServers(boundSession);
1551
1538
  if (localMcp) {
1552
1539
  var inAppNames = Object.keys(localMcp);
1553
1540
  for (var i = 0; i < inAppNames.length; i++) {
@@ -1599,7 +1586,7 @@ function createProjectContext(opts) {
1599
1586
  }
1600
1587
  if (sessionOnly) return Promise.reject(new Error("Session tool not found: " + serverName + "/" + toolName));
1601
1588
  // Try in-app servers first (gated by extension connectivity for clay-browser).
1602
- var localMcp = getLocalMcpServers();
1589
+ var localMcp = getLocalMcpServers(boundSession);
1603
1590
  if (localMcp && localMcp[serverName]) {
1604
1591
  var server = localMcp[serverName];
1605
1592
  if (server.instance && server.instance._registeredTools && server.instance._registeredTools[toolName]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.3.2-beta.2",
3
+ "version": "3.3.2-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -49,7 +49,7 @@
49
49
  "homepage": "https://github.com/chadbyte/clay#readme",
50
50
  "author": "Chad",
51
51
  "dependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "^0.3.233",
52
+ "@anthropic-ai/claude-agent-sdk": "^0.3.241",
53
53
  "@lydell/node-pty": "^1.2.0-beta.3",
54
54
  "@openai/codex": "^0.147.0",
55
55
  "imapflow": "^1.3.1",