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.
- package/lib/debate-mcp-server.js +15 -0
- package/lib/project-connection.js +18 -2
- package/lib/project-debate-proposal.js +138 -0
- package/lib/project-debate.js +1 -0
- package/lib/project.js +28 -41
- package/package.json +2 -2
package/lib/debate-mcp-server.js
CHANGED
|
@@ -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
|
|
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 };
|
package/lib/project-debate.js
CHANGED
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
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|