clay-server 2.26.0-beta.17 → 2.26.0-beta.18
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 +94 -0
- package/lib/mates.js +12 -24
- package/lib/project-debate.js +64 -81
- package/lib/project-mate-interaction.js +4 -5
- package/lib/project.js +86 -35
- package/lib/public/app.js +17 -2
- package/lib/public/modules/debate.js +114 -0
- package/lib/sdk-bridge.js +37 -16
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Debate MCP Server for Clay (in-process SDK version)
|
|
2
|
+
// Provides the propose_debate tool so mates can propose debates
|
|
3
|
+
// via the SDK tool system instead of writing files to disk.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// var debateMcp = require("./debate-mcp-server");
|
|
7
|
+
// var mcpConfig = debateMcp.create(onPropose);
|
|
8
|
+
// // Pass mcpConfig to sdk-bridge opts.mcpServers
|
|
9
|
+
|
|
10
|
+
var z;
|
|
11
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
12
|
+
|
|
13
|
+
function buildShape(props, required) {
|
|
14
|
+
if (!z) return {};
|
|
15
|
+
var shape = {};
|
|
16
|
+
var keys = Object.keys(props);
|
|
17
|
+
for (var i = 0; i < keys.length; i++) {
|
|
18
|
+
var k = keys[i];
|
|
19
|
+
var p = props[k];
|
|
20
|
+
var field;
|
|
21
|
+
if (p.type === "number") field = z.number();
|
|
22
|
+
else if (p.type === "boolean") field = z.boolean();
|
|
23
|
+
else if (p.enum) field = z.enum(p.enum);
|
|
24
|
+
else field = z.string();
|
|
25
|
+
if (p.description) field = field.describe(p.description);
|
|
26
|
+
if (!required || required.indexOf(k) === -1) field = field.optional();
|
|
27
|
+
shape[k] = field;
|
|
28
|
+
}
|
|
29
|
+
return shape;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// onPropose(briefData) -> Promise<{action: "start"|"cancel"}>
|
|
33
|
+
// The returned Promise blocks the tool until the user approves or cancels.
|
|
34
|
+
function create(onPropose) {
|
|
35
|
+
var sdk;
|
|
36
|
+
try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
|
|
37
|
+
console.error("[debate-mcp] Failed to load SDK:", e.message);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var createSdkMcpServer = sdk.createSdkMcpServer;
|
|
42
|
+
var tool = sdk.tool;
|
|
43
|
+
if (!createSdkMcpServer || !tool) {
|
|
44
|
+
console.error("[debate-mcp] SDK missing createSdkMcpServer or tool helper");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var tools = [];
|
|
49
|
+
|
|
50
|
+
tools.push(tool(
|
|
51
|
+
"propose_debate",
|
|
52
|
+
"Propose a structured debate among Clay Mates. The user will see an inline approval card. The tool blocks until the user approves or cancels.",
|
|
53
|
+
buildShape({
|
|
54
|
+
topic: { type: "string", description: "The debate topic" },
|
|
55
|
+
format: { type: "string", description: "Debate format, e.g. free_discussion (default)" },
|
|
56
|
+
context: { type: "string", description: "Key context from the conversation that panelists should know" },
|
|
57
|
+
specialRequests: { type: "string", description: "Special instructions for the debate, or empty" },
|
|
58
|
+
panelists: { type: "string", description: "JSON array of panelist objects: [{\"mateId\": \"<UUID>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]" },
|
|
59
|
+
}, ["topic", "panelists"]),
|
|
60
|
+
function (args) {
|
|
61
|
+
var panelists;
|
|
62
|
+
try {
|
|
63
|
+
panelists = JSON.parse(args.panelists);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return Promise.resolve({
|
|
66
|
+
content: [{ type: "text", text: "Error: panelists must be a valid JSON array. Got: " + (args.panelists || "").substring(0, 100) }],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var briefData = {
|
|
71
|
+
topic: args.topic || "Untitled debate",
|
|
72
|
+
format: args.format || "free_discussion",
|
|
73
|
+
context: args.context || "",
|
|
74
|
+
specialRequests: args.specialRequests || null,
|
|
75
|
+
panelists: panelists,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return onPropose(briefData).then(function (result) {
|
|
79
|
+
if (result && result.action === "start") {
|
|
80
|
+
return { content: [{ type: "text", text: "Debate approved and started. Topic: " + briefData.topic }] };
|
|
81
|
+
}
|
|
82
|
+
return { content: [{ type: "text", text: "Debate proposal was cancelled by the user." }] };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
return createSdkMcpServer({
|
|
88
|
+
name: "clay-debate",
|
|
89
|
+
version: "1.0.0",
|
|
90
|
+
tools: tools,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { create: create };
|
package/lib/mates.js
CHANGED
|
@@ -633,35 +633,23 @@ var DEBATE_AWARENESS_SECTION =
|
|
|
633
633
|
"\n\n" + DEBATE_AWARENESS_MARKER + "\n" +
|
|
634
634
|
"## Proposing Debates\n\n" +
|
|
635
635
|
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
636
|
-
"When the user suggests
|
|
637
|
-
"
|
|
638
|
-
"
|
|
636
|
+
"When the user suggests a debate, you MUST use the `propose_debate` tool. " +
|
|
637
|
+
"NEVER write debate files to disk. NEVER mkdir for debates. NEVER use Write/Bash for debate setup. " +
|
|
638
|
+
"The ONLY way to propose a debate is the `propose_debate` tool.\n\n" +
|
|
639
639
|
"**How to propose a debate:**\n" +
|
|
640
|
-
"
|
|
641
|
-
"
|
|
642
|
-
"
|
|
643
|
-
"
|
|
644
|
-
"
|
|
645
|
-
"
|
|
646
|
-
"{\n" +
|
|
647
|
-
"
|
|
648
|
-
" \"format\": \"free_discussion\",\n" +
|
|
649
|
-
" \"context\": \"Key context from the conversation that panelists should know\",\n" +
|
|
650
|
-
" \"specialRequests\": \"Any special instructions, or null\",\n" +
|
|
651
|
-
" \"panelists\": [\n" +
|
|
652
|
-
" {\n" +
|
|
653
|
-
" \"mateId\": \"<mate UUID from the team roster above>\",\n" +
|
|
654
|
-
" \"role\": \"The perspective or stance this panelist should take\",\n" +
|
|
655
|
-
" \"brief\": \"Specific guidance for this panelist\"\n" +
|
|
656
|
-
" }\n" +
|
|
657
|
-
" ]\n" +
|
|
658
|
-
"}\n" +
|
|
659
|
-
"```\n\n" +
|
|
640
|
+
"Call the `propose_debate` tool with these parameters:\n" +
|
|
641
|
+
"- `topic` (required): The refined debate topic\n" +
|
|
642
|
+
"- `format`: Debate format, default \"free_discussion\"\n" +
|
|
643
|
+
"- `context`: Key context from the conversation that panelists should know\n" +
|
|
644
|
+
"- `specialRequests`: Any special instructions\n" +
|
|
645
|
+
"- `panelists` (required): A JSON string array of panelist objects:\n" +
|
|
646
|
+
" `[{\"mateId\": \"<mate UUID from team roster>\", \"role\": \"perspective\", \"brief\": \"guidance\"}]`\n\n" +
|
|
647
|
+
"The user will see an inline approval card. The tool blocks until they approve or cancel.\n\n" +
|
|
660
648
|
"**Rules:**\n" +
|
|
661
649
|
"- Choose 2-4 panelists from the team roster. Pick mates whose expertise fits the topic.\n" +
|
|
662
650
|
"- Do NOT include yourself as a panelist. You will moderate the debate.\n" +
|
|
663
651
|
"- Only propose a debate when the user explicitly asks for one.\n" +
|
|
664
|
-
"-
|
|
652
|
+
"- Do NOT write files to disk for debate proposals. Always use the propose_debate tool.\n";
|
|
665
653
|
|
|
666
654
|
function enforceDebateAwareness(filePath) {
|
|
667
655
|
if (!fs.existsSync(filePath)) return false;
|
package/lib/project-debate.js
CHANGED
|
@@ -14,6 +14,13 @@ var matesModule = require("./mates");
|
|
|
14
14
|
*/
|
|
15
15
|
function attachDebate(ctx) {
|
|
16
16
|
|
|
17
|
+
// For mate projects, enforce latest debate awareness prompt in CLAUDE.md
|
|
18
|
+
// so mates use the propose_debate MCP tool instead of writing files.
|
|
19
|
+
if (ctx.isMate) {
|
|
20
|
+
var _debateClaudeMdPath = path.join(ctx.cwd, "CLAUDE.md");
|
|
21
|
+
try { matesModule.enforceDebateAwareness(_debateClaudeMdPath); } catch (e) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
// --- Helpers shared with other modules ---
|
|
18
25
|
|
|
19
26
|
function escapeRegex(str) {
|
|
@@ -270,9 +277,9 @@ function attachDebate(ctx) {
|
|
|
270
277
|
var mateCtx = debate.mateCtx || matesModule.buildMateCtx(null);
|
|
271
278
|
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
272
279
|
|
|
273
|
-
//
|
|
274
|
-
if (!debate.setupSessionId) {
|
|
275
|
-
console.log("[debate] Brief picked up
|
|
280
|
+
// quickStart from DM or no setupSessionId: show brief card for user approval
|
|
281
|
+
if (!debate.setupSessionId || debate.quickStart) {
|
|
282
|
+
console.log("[debate] Brief picked up, entering review phase. Topic:", debate.topic);
|
|
276
283
|
debate.phase = "reviewing";
|
|
277
284
|
persistDebateState(session);
|
|
278
285
|
|
|
@@ -323,85 +330,18 @@ function attachDebate(ctx) {
|
|
|
323
330
|
// --- Restore debate on reconnect ---
|
|
324
331
|
|
|
325
332
|
function restoreDebateState(ws) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
// On server restart, SDK sessions are lost so debates cannot continue.
|
|
334
|
+
// Clear stale debate state instead of restoring dead UI.
|
|
329
335
|
ctx.sm.sessions.forEach(function (session) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Restore _debate from persisted state (pass userId for correct mateCtx)
|
|
340
|
-
var debate = restoreDebateFromState(session, userId);
|
|
341
|
-
if (!debate) return;
|
|
342
|
-
|
|
343
|
-
// Update mateCtx with the connected user's context
|
|
344
|
-
debate.mateCtx = mateCtx;
|
|
345
|
-
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
346
|
-
|
|
347
|
-
var moderatorProfile = ctx.getMateProfile(mateCtx, debate.moderatorId);
|
|
348
|
-
|
|
349
|
-
if (phase === "preparing") {
|
|
350
|
-
var briefPath = debate.briefPath;
|
|
351
|
-
if (!briefPath && debate.debateId) {
|
|
352
|
-
briefPath = path.join(ctx.cwd, ".clay", "debates", debate.debateId, "brief.json");
|
|
353
|
-
}
|
|
354
|
-
if (!briefPath) return;
|
|
355
|
-
|
|
356
|
-
console.log("[debate] Restoring debate (preparing). topic:", debate.topic, "briefPath:", briefPath);
|
|
357
|
-
startDebateBriefWatcher(session, debate, briefPath);
|
|
358
|
-
|
|
359
|
-
ctx.sendTo(ws, {
|
|
360
|
-
type: "debate_preparing",
|
|
361
|
-
topic: debate.topic,
|
|
362
|
-
moderatorId: debate.moderatorId,
|
|
363
|
-
moderatorName: moderatorProfile.name,
|
|
364
|
-
setupSessionId: debate.setupSessionId,
|
|
365
|
-
panelists: debate.panelists.map(function (p) {
|
|
366
|
-
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
367
|
-
return { mateId: p.mateId, name: prof.name };
|
|
368
|
-
}),
|
|
369
|
-
});
|
|
370
|
-
} else if (phase === "reviewing") {
|
|
371
|
-
console.log("[debate] Restoring debate (reviewing). topic:", debate.topic);
|
|
372
|
-
ctx.sendTo(ws, {
|
|
373
|
-
type: "debate_brief_ready",
|
|
374
|
-
debateId: debate.debateId,
|
|
375
|
-
topic: debate.topic,
|
|
376
|
-
format: debate.format || "free_discussion",
|
|
377
|
-
context: debate.context || "",
|
|
378
|
-
specialRequests: debate.specialRequests || null,
|
|
379
|
-
moderatorId: debate.moderatorId,
|
|
380
|
-
moderatorName: moderatorProfile.name,
|
|
381
|
-
panelists: debate.panelists.map(function (p) {
|
|
382
|
-
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
383
|
-
return { mateId: p.mateId, name: prof.name, role: p.role || "", brief: p.brief || "" };
|
|
384
|
-
}),
|
|
385
|
-
});
|
|
386
|
-
} else if (phase === "live") {
|
|
387
|
-
console.log("[debate] Restoring debate (live). topic:", debate.topic, "awaitingConclude:", debate.awaitingConcludeConfirm);
|
|
388
|
-
// Debate was live when server restarted. It can't resume AI turns,
|
|
389
|
-
// but we can show the sticky and let user see history.
|
|
390
|
-
ctx.sendTo(ws, {
|
|
391
|
-
type: "debate_started",
|
|
392
|
-
topic: debate.topic,
|
|
393
|
-
format: debate.format,
|
|
394
|
-
round: debate.round,
|
|
395
|
-
moderatorId: debate.moderatorId,
|
|
396
|
-
moderatorName: moderatorProfile.name,
|
|
397
|
-
panelists: debate.panelists.map(function (p) {
|
|
398
|
-
var prof = ctx.getMateProfile(mateCtx, p.mateId);
|
|
399
|
-
return { mateId: p.mateId, name: prof.name, role: p.role, avatarColor: prof.avatarColor, avatarStyle: prof.avatarStyle, avatarSeed: prof.avatarSeed };
|
|
400
|
-
}),
|
|
401
|
-
});
|
|
402
|
-
// If moderator had concluded, re-send conclude confirm so client shows End/Continue UI
|
|
403
|
-
if (debate.awaitingConcludeConfirm) {
|
|
404
|
-
ctx.sendTo(ws, { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round });
|
|
336
|
+
if (session._debate) {
|
|
337
|
+
delete session._debate;
|
|
338
|
+
}
|
|
339
|
+
if (session.debateState) {
|
|
340
|
+
var phase = session.debateState.phase;
|
|
341
|
+
if (phase === "preparing" || phase === "reviewing" || phase === "live") {
|
|
342
|
+
console.log("[debate] Clearing stale debate state:", session.debateState.topic);
|
|
343
|
+
session.debateState = null;
|
|
344
|
+
ctx.sm.saveSessionFile(session);
|
|
405
345
|
}
|
|
406
346
|
}
|
|
407
347
|
});
|
|
@@ -1741,6 +1681,48 @@ function attachDebate(ctx) {
|
|
|
1741
1681
|
})();
|
|
1742
1682
|
}
|
|
1743
1683
|
|
|
1684
|
+
// --- MCP-based debate proposal approval ---
|
|
1685
|
+
|
|
1686
|
+
function handleMcpDebateApproval(session, briefData, mateId, ws) {
|
|
1687
|
+
if (session._debate && (session._debate.phase === "preparing" || session._debate.phase === "reviewing" || session._debate.phase === "live")) {
|
|
1688
|
+
console.warn("[debate] Cannot start MCP debate: another debate is active on this session");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
var userId = ws && ws._clayUser ? ws._clayUser.id : (session.ownerId || ctx.projectOwnerId || null);
|
|
1693
|
+
var mateCtx = matesModule.buildMateCtx(userId);
|
|
1694
|
+
var debateId = "debate_" + Date.now();
|
|
1695
|
+
|
|
1696
|
+
var debate = {
|
|
1697
|
+
phase: "reviewing",
|
|
1698
|
+
topic: briefData.topic || "Untitled debate",
|
|
1699
|
+
format: briefData.format || "free_discussion",
|
|
1700
|
+
context: briefData.context || "",
|
|
1701
|
+
specialRequests: briefData.specialRequests || null,
|
|
1702
|
+
moderatorId: mateId,
|
|
1703
|
+
panelists: (briefData.panelists || []).map(function (p) {
|
|
1704
|
+
return { mateId: p.mateId, role: p.role || "", brief: p.brief || "" };
|
|
1705
|
+
}),
|
|
1706
|
+
mateCtx: mateCtx,
|
|
1707
|
+
moderatorSession: null,
|
|
1708
|
+
panelistSessions: {},
|
|
1709
|
+
nameMap: null,
|
|
1710
|
+
turnInProgress: false,
|
|
1711
|
+
pendingComment: null,
|
|
1712
|
+
round: 1,
|
|
1713
|
+
history: [],
|
|
1714
|
+
setupSessionId: null,
|
|
1715
|
+
debateId: debateId,
|
|
1716
|
+
briefPath: null,
|
|
1717
|
+
ownerId: userId,
|
|
1718
|
+
};
|
|
1719
|
+
debate.nameMap = buildDebateNameMap(debate.panelists, mateCtx);
|
|
1720
|
+
session._debate = debate;
|
|
1721
|
+
|
|
1722
|
+
console.log("[debate] MCP debate approved. Topic:", debate.topic, "debateId:", debateId);
|
|
1723
|
+
startDebateLive(session);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1744
1726
|
// --- Public API ---
|
|
1745
1727
|
|
|
1746
1728
|
return {
|
|
@@ -1753,6 +1735,7 @@ function attachDebate(ctx) {
|
|
|
1753
1735
|
handleDebateUserFloorResponse: handleDebateUserFloorResponse,
|
|
1754
1736
|
restoreDebateState: restoreDebateState,
|
|
1755
1737
|
checkForDmDebateBrief: checkForDmDebateBrief,
|
|
1738
|
+
handleMcpDebateApproval: handleMcpDebateApproval,
|
|
1756
1739
|
};
|
|
1757
1740
|
}
|
|
1758
1741
|
|
|
@@ -569,11 +569,10 @@ function attachMateInteraction(ctx) {
|
|
|
569
569
|
onDone: mentionCallbacks.onDone,
|
|
570
570
|
onError: mentionCallbacks.onError,
|
|
571
571
|
canUseTool: function (toolName, input, toolOpts) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// Route through the project session's permission system
|
|
572
|
+
// Use the shared whitelist from sdk-bridge (read-only tools + safe bash commands)
|
|
573
|
+
var whitelisted = sdk.checkToolWhitelist(toolName, input);
|
|
574
|
+
if (whitelisted) return Promise.resolve(whitelisted);
|
|
575
|
+
// Not whitelisted: route through the project session's permission system
|
|
577
576
|
return new Promise(function (resolve) {
|
|
578
577
|
var requestId = crypto.randomUUID();
|
|
579
578
|
session.pendingPermissions[requestId] = {
|
package/lib/project.js
CHANGED
|
@@ -293,6 +293,7 @@ function createProjectContext(opts) {
|
|
|
293
293
|
|
|
294
294
|
// --- Browser extension state ---
|
|
295
295
|
var _browserTabList = {}; // tabId -> { id, url, title, favIconUrl }
|
|
296
|
+
var _pendingDebateProposals = {}; // proposalId -> { resolve, briefData }
|
|
296
297
|
var _extensionWs = null; // WebSocket of the client with the Chrome extension
|
|
297
298
|
var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
|
|
298
299
|
var pendingExtensionRequests = {}; // requestId -> { resolve, timer }
|
|
@@ -557,47 +558,72 @@ function createProjectContext(opts) {
|
|
|
557
558
|
mateDisplayName: opts.mateDisplayName || "",
|
|
558
559
|
isMate: isMate,
|
|
559
560
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
560
|
-
mcpServers:
|
|
561
|
+
mcpServers: (function () {
|
|
562
|
+
var servers = {};
|
|
563
|
+
|
|
564
|
+
// Debate MCP server (available to both mates and main project)
|
|
561
565
|
try {
|
|
562
|
-
var
|
|
563
|
-
var
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
return active;
|
|
576
|
-
},
|
|
577
|
-
unwatchTab: function (tabId) {
|
|
578
|
-
var key = "tab:" + tabId;
|
|
579
|
-
var active = loadContextSources(slug);
|
|
580
|
-
var idx = active.indexOf(key);
|
|
581
|
-
if (idx !== -1) {
|
|
582
|
-
active.splice(idx, 1);
|
|
583
|
-
saveContextSources(slug, active);
|
|
584
|
-
var msg = JSON.stringify({ type: "context_sources_state", active: active });
|
|
585
|
-
for (var c of clients) { if (c.readyState === 1) c.send(msg); }
|
|
586
|
-
}
|
|
587
|
-
return active;
|
|
588
|
-
},
|
|
566
|
+
var debateMcp = require("./debate-mcp-server");
|
|
567
|
+
var debateMcpConfig = debateMcp.create(function onPropose(briefData) {
|
|
568
|
+
return new Promise(function (resolve) {
|
|
569
|
+
var proposalId = "dp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
|
|
570
|
+
briefData.proposalId = proposalId;
|
|
571
|
+
_pendingDebateProposals[proposalId] = {
|
|
572
|
+
resolve: resolve,
|
|
573
|
+
briefData: briefData,
|
|
574
|
+
};
|
|
575
|
+
// The SDK sends tool_executing with briefData as input.
|
|
576
|
+
// Client renders the debate brief card when it sees propose_debate.
|
|
577
|
+
});
|
|
589
578
|
});
|
|
590
|
-
if (
|
|
591
|
-
var servers = {};
|
|
592
|
-
servers[mcpConfig.name || "clay-browser"] = mcpConfig;
|
|
593
|
-
return servers;
|
|
579
|
+
if (debateMcpConfig) servers[debateMcpConfig.name || "clay-debate"] = debateMcpConfig;
|
|
594
580
|
} catch (e) {
|
|
595
|
-
console.error("[project] Failed to create
|
|
596
|
-
return undefined;
|
|
581
|
+
console.error("[project] Failed to create debate MCP server:", e.message);
|
|
597
582
|
}
|
|
583
|
+
|
|
584
|
+
// Browser MCP server (main project only, not mates)
|
|
585
|
+
if (!isMate) {
|
|
586
|
+
try {
|
|
587
|
+
var browserMcp = require("./browser-mcp-server");
|
|
588
|
+
var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
|
|
589
|
+
return Object.values(_browserTabList || {});
|
|
590
|
+
}, {
|
|
591
|
+
watchTab: function (tabId) {
|
|
592
|
+
var key = "tab:" + tabId;
|
|
593
|
+
var active = loadContextSources(slug);
|
|
594
|
+
if (active.indexOf(key) === -1) {
|
|
595
|
+
active.push(key);
|
|
596
|
+
saveContextSources(slug, active);
|
|
597
|
+
var _msg = JSON.stringify({ type: "context_sources_state", active: active });
|
|
598
|
+
for (var c of clients) { if (c.readyState === 1) c.send(_msg); }
|
|
599
|
+
}
|
|
600
|
+
return active;
|
|
601
|
+
},
|
|
602
|
+
unwatchTab: function (tabId) {
|
|
603
|
+
var key = "tab:" + tabId;
|
|
604
|
+
var active = loadContextSources(slug);
|
|
605
|
+
var idx = active.indexOf(key);
|
|
606
|
+
if (idx !== -1) {
|
|
607
|
+
active.splice(idx, 1);
|
|
608
|
+
saveContextSources(slug, active);
|
|
609
|
+
var _msg = JSON.stringify({ type: "context_sources_state", active: active });
|
|
610
|
+
for (var c of clients) { if (c.readyState === 1) c.send(_msg); }
|
|
611
|
+
}
|
|
612
|
+
return active;
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
if (mcpConfig) servers[mcpConfig.name || "clay-browser"] = mcpConfig;
|
|
616
|
+
} catch (e) {
|
|
617
|
+
console.error("[project] Failed to create browser MCP server:", e.message);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return Object.keys(servers).length > 0 ? servers : undefined;
|
|
598
622
|
})(),
|
|
599
623
|
onProcessingChanged: onProcessingChanged,
|
|
600
|
-
onTurnDone: isMate ? function (session, preview) {
|
|
624
|
+
onTurnDone: isMate ? function (session, preview) {
|
|
625
|
+
digestDmTurn(session, preview);
|
|
626
|
+
} : null,
|
|
601
627
|
scheduleMessage: function (session, text, resetsAt) {
|
|
602
628
|
scheduleMessage(session, text, resetsAt);
|
|
603
629
|
},
|
|
@@ -1689,6 +1715,28 @@ function createProjectContext(opts) {
|
|
|
1689
1715
|
handleDebateConfirmBrief(ws);
|
|
1690
1716
|
return;
|
|
1691
1717
|
}
|
|
1718
|
+
if (msg.type === "debate_proposal_response") {
|
|
1719
|
+
// Match the most recent pending proposal (proposalId may not be
|
|
1720
|
+
// available on the client since it's not part of the tool input)
|
|
1721
|
+
var _dpKeys = Object.keys(_pendingDebateProposals);
|
|
1722
|
+
if (_dpKeys.length === 0) return;
|
|
1723
|
+
var _dpKey = msg.proposalId || _dpKeys[_dpKeys.length - 1];
|
|
1724
|
+
var pending = _pendingDebateProposals[_dpKey];
|
|
1725
|
+
if (!pending) return;
|
|
1726
|
+
delete _pendingDebateProposals[_dpKey];
|
|
1727
|
+
if (msg.action === "start") {
|
|
1728
|
+
// Set up debate state on the session, then transition to live
|
|
1729
|
+
var _dpSession = getSessionForWs(ws);
|
|
1730
|
+
if (_dpSession) {
|
|
1731
|
+
var _dpMateId = isMate ? path.basename(cwd) : null;
|
|
1732
|
+
handleMcpDebateApproval(_dpSession, pending.briefData, _dpMateId, ws);
|
|
1733
|
+
}
|
|
1734
|
+
pending.resolve({ action: "start" });
|
|
1735
|
+
} else {
|
|
1736
|
+
pending.resolve({ action: "cancel" });
|
|
1737
|
+
}
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1692
1740
|
if (msg.type === "debate_user_floor_response") {
|
|
1693
1741
|
handleDebateUserFloorResponse(ws, msg);
|
|
1694
1742
|
return;
|
|
@@ -4307,6 +4355,8 @@ function createProjectContext(opts) {
|
|
|
4307
4355
|
var _debate = attachDebate({
|
|
4308
4356
|
cwd: cwd,
|
|
4309
4357
|
slug: slug,
|
|
4358
|
+
isMate: isMate,
|
|
4359
|
+
projectOwnerId: projectOwnerId,
|
|
4310
4360
|
send: send,
|
|
4311
4361
|
sendTo: sendTo,
|
|
4312
4362
|
sendToSession: sendToSession,
|
|
@@ -4331,6 +4381,7 @@ function createProjectContext(opts) {
|
|
|
4331
4381
|
var handleDebateUserFloorResponse = _debate.handleDebateUserFloorResponse;
|
|
4332
4382
|
var restoreDebateState = _debate.restoreDebateState;
|
|
4333
4383
|
var checkForDmDebateBrief = _debate.checkForDmDebateBrief;
|
|
4384
|
+
var handleMcpDebateApproval = _debate.handleMcpDebateApproval;
|
|
4334
4385
|
|
|
4335
4386
|
// --- Session presence (who is viewing which session) ---
|
|
4336
4387
|
function broadcastPresence() {
|
package/lib/public/app.js
CHANGED
|
@@ -32,7 +32,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
|
|
|
32
32
|
import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
|
|
33
33
|
import { initLongPress } from './modules/longpress.js';
|
|
34
34
|
import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
|
|
35
|
-
import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf } from './modules/debate.js';
|
|
35
|
+
import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf, renderMcpDebateProposal } from './modules/debate.js';
|
|
36
36
|
|
|
37
37
|
// --- Base path for multi-project routing ---
|
|
38
38
|
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
@@ -4838,6 +4838,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4838
4838
|
}
|
|
4839
4839
|
renderPlanBanner("exit");
|
|
4840
4840
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
4841
|
+
} else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
|
|
4842
|
+
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
4841
4843
|
} else if (getTodoTools()[msg.name]) {
|
|
4842
4844
|
getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
|
|
4843
4845
|
} else {
|
|
@@ -4846,7 +4848,18 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
4846
4848
|
break;
|
|
4847
4849
|
|
|
4848
4850
|
case "tool_executing":
|
|
4849
|
-
if (msg.name === "
|
|
4851
|
+
if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
|
|
4852
|
+
var _dpTool = getTools()[msg.id];
|
|
4853
|
+
if (_dpTool) {
|
|
4854
|
+
if (_dpTool.el) _dpTool.el.style.display = "none";
|
|
4855
|
+
_dpTool.done = true;
|
|
4856
|
+
_dpTool.hidden = true;
|
|
4857
|
+
removeToolFromGroup(msg.id);
|
|
4858
|
+
}
|
|
4859
|
+
finalizeAssistantBlock();
|
|
4860
|
+
renderMcpDebateProposal(msg.id, msg.input);
|
|
4861
|
+
startUrgentBlink();
|
|
4862
|
+
} else if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
|
|
4850
4863
|
var askTool = getTools()[msg.id];
|
|
4851
4864
|
if (askTool) {
|
|
4852
4865
|
if (askTool.el) askTool.el.style.display = "none";
|
|
@@ -5902,7 +5915,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
5902
5915
|
// --- Debate module ---
|
|
5903
5916
|
initDebate({
|
|
5904
5917
|
get ws() { return ws; },
|
|
5918
|
+
sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
|
|
5905
5919
|
messagesEl: messagesEl,
|
|
5920
|
+
addToMessages: function (el) { addToMessages(el); },
|
|
5906
5921
|
scrollToBottom: scrollToBottom,
|
|
5907
5922
|
addCopyHandler: addCopyHandler,
|
|
5908
5923
|
matesList: function () { return cachedMatesList || []; },
|
|
@@ -1301,3 +1301,117 @@ function renderDebateBriefCard(msg, resolved) {
|
|
|
1301
1301
|
refreshIcons();
|
|
1302
1302
|
ctx.scrollToBottom();
|
|
1303
1303
|
}
|
|
1304
|
+
|
|
1305
|
+
// --- MCP-based debate proposal card ---
|
|
1306
|
+
// Renders an inline card from the propose_debate MCP tool input.
|
|
1307
|
+
// The card reuses the same visual structure as renderDebateBriefCard
|
|
1308
|
+
// but sends debate_proposal_response instead of debate_confirm_brief.
|
|
1309
|
+
|
|
1310
|
+
export function renderMcpDebateProposal(toolId, input) {
|
|
1311
|
+
var panelists = [];
|
|
1312
|
+
try {
|
|
1313
|
+
panelists = typeof input.panelists === "string" ? JSON.parse(input.panelists) : (input.panelists || []);
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
panelists = [];
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
var el = document.createElement("div");
|
|
1319
|
+
el.className = "debate-brief-card";
|
|
1320
|
+
|
|
1321
|
+
// Header
|
|
1322
|
+
var header = document.createElement("div");
|
|
1323
|
+
header.className = "debate-brief-card-header";
|
|
1324
|
+
header.innerHTML =
|
|
1325
|
+
'<span class="debate-brief-card-icon">' + iconHtml("message-circle") + '</span>' +
|
|
1326
|
+
'<span class="debate-brief-card-title">Debate Proposal</span>' +
|
|
1327
|
+
'<span class="debate-brief-card-chevron">' + iconHtml("chevron-down") + '</span>';
|
|
1328
|
+
|
|
1329
|
+
// Body
|
|
1330
|
+
var body = document.createElement("div");
|
|
1331
|
+
body.className = "debate-brief-card-body";
|
|
1332
|
+
|
|
1333
|
+
var topicHtml = '<div class="debate-brief-topic">' + escapeHtml(input.topic || "Untitled") + '</div>';
|
|
1334
|
+
|
|
1335
|
+
if (input.context) {
|
|
1336
|
+
topicHtml += '<div class="debate-brief-context">' + escapeHtml(input.context) + '</div>';
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Resolve mate names from matesList
|
|
1340
|
+
var mates = ctx.matesList ? ctx.matesList() : [];
|
|
1341
|
+
var mateMap = {};
|
|
1342
|
+
for (var mi = 0; mi < mates.length; mi++) {
|
|
1343
|
+
mateMap[mates[mi].id] = mates[mi];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
topicHtml += '<div class="debate-brief-panelists-label">' + iconHtml("users") + ' <strong>Panelists:</strong></div>';
|
|
1347
|
+
topicHtml += '<div class="debate-brief-panelists">';
|
|
1348
|
+
for (var i = 0; i < panelists.length; i++) {
|
|
1349
|
+
var p = panelists[i];
|
|
1350
|
+
var mate = mateMap[p.mateId];
|
|
1351
|
+
var mateName = mate ? (mate.displayName || mate.name || p.mateId) : p.mateId;
|
|
1352
|
+
var avatarSrc = mate ? mateAvatarUrl(mate, 24) : "";
|
|
1353
|
+
topicHtml += '<div class="debate-brief-panelist" style="display:flex;align-items:center;gap:8px;">';
|
|
1354
|
+
if (avatarSrc) {
|
|
1355
|
+
topicHtml += '<img src="' + escapeHtml(avatarSrc) + '" width="24" height="24" style="border-radius:50%;flex-shrink:0;">';
|
|
1356
|
+
}
|
|
1357
|
+
topicHtml += '<span class="debate-brief-panelist-name">' + escapeHtml(mateName) + '</span>';
|
|
1358
|
+
if (p.role) {
|
|
1359
|
+
topicHtml += '<span class="debate-brief-panelist-role">' + escapeHtml(p.role) + '</span>';
|
|
1360
|
+
}
|
|
1361
|
+
topicHtml += '</div>';
|
|
1362
|
+
}
|
|
1363
|
+
topicHtml += '</div>';
|
|
1364
|
+
|
|
1365
|
+
if (input.specialRequests) {
|
|
1366
|
+
topicHtml += '<div class="debate-brief-special">' +
|
|
1367
|
+
iconHtml("info") + ' ' + escapeHtml(input.specialRequests) +
|
|
1368
|
+
'</div>';
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
body.innerHTML = topicHtml;
|
|
1372
|
+
|
|
1373
|
+
// Actions
|
|
1374
|
+
var actions = document.createElement("div");
|
|
1375
|
+
actions.className = "debate-brief-actions";
|
|
1376
|
+
|
|
1377
|
+
var startBtn = document.createElement("button");
|
|
1378
|
+
startBtn.className = "debate-brief-start-btn";
|
|
1379
|
+
startBtn.innerHTML = iconHtml("play") + " Start Debate";
|
|
1380
|
+
|
|
1381
|
+
var cancelBtn = document.createElement("button");
|
|
1382
|
+
cancelBtn.className = "debate-brief-cancel-btn";
|
|
1383
|
+
cancelBtn.textContent = "Cancel";
|
|
1384
|
+
|
|
1385
|
+
startBtn.addEventListener("click", function () {
|
|
1386
|
+
if (ctx.sendWs) {
|
|
1387
|
+
ctx.sendWs({ type: "debate_proposal_response", proposalId: input.proposalId, action: "start" });
|
|
1388
|
+
}
|
|
1389
|
+
el.classList.add("resolved");
|
|
1390
|
+
actions.innerHTML = '<span class="debate-brief-resolved-label">' + iconHtml("check") + ' Starting debate...</span>';
|
|
1391
|
+
refreshIcons();
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
cancelBtn.addEventListener("click", function () {
|
|
1395
|
+
if (ctx.sendWs) {
|
|
1396
|
+
ctx.sendWs({ type: "debate_proposal_response", proposalId: input.proposalId, action: "cancel" });
|
|
1397
|
+
}
|
|
1398
|
+
el.classList.add("resolved");
|
|
1399
|
+
actions.innerHTML = '<span class="debate-brief-resolved-label debate-brief-cancelled">' + iconHtml("x") + ' Cancelled</span>';
|
|
1400
|
+
refreshIcons();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
actions.appendChild(startBtn);
|
|
1404
|
+
actions.appendChild(cancelBtn);
|
|
1405
|
+
|
|
1406
|
+
// Collapse toggle
|
|
1407
|
+
header.addEventListener("click", function () {
|
|
1408
|
+
el.classList.toggle("collapsed");
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
el.appendChild(header);
|
|
1412
|
+
el.appendChild(body);
|
|
1413
|
+
el.appendChild(actions);
|
|
1414
|
+
ctx.addToMessages(el);
|
|
1415
|
+
refreshIcons();
|
|
1416
|
+
ctx.scrollToBottom();
|
|
1417
|
+
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1567,24 +1567,15 @@ function createSDKBridge(opts) {
|
|
|
1567
1567
|
|
|
1568
1568
|
// --- SDK query lifecycle ---
|
|
1569
1569
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
if (toolName === "AskUserQuestion") {
|
|
1575
|
-
return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
|
|
1576
|
-
}
|
|
1577
|
-
if (toolName === "EnterPlanMode") {
|
|
1578
|
-
return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
|
|
1579
|
-
}
|
|
1580
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1570
|
+
// Check if a tool should be auto-approved based on whitelist rules.
|
|
1571
|
+
// Returns { behavior: "allow", updatedInput } if whitelisted, or null if not.
|
|
1572
|
+
// Shared by handleCanUseTool and mate mention canUseTool handlers.
|
|
1573
|
+
function checkToolWhitelist(toolName, input) {
|
|
1583
1574
|
// Auto-approve read-only tools for ALL sessions.
|
|
1584
1575
|
// These tools only inspect files and fetch data — no side effects.
|
|
1585
1576
|
var readOnlyTools = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
1586
1577
|
if (readOnlyTools[toolName]) {
|
|
1587
|
-
return
|
|
1578
|
+
return { behavior: "allow", updatedInput: input };
|
|
1588
1579
|
}
|
|
1589
1580
|
|
|
1590
1581
|
// Auto-approve safe browser MCP tools.
|
|
@@ -1595,10 +1586,17 @@ function createSDKBridge(opts) {
|
|
|
1595
1586
|
if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
|
|
1596
1587
|
var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
|
|
1597
1588
|
if (safeBrowserTools[mcpToolName]) {
|
|
1598
|
-
return
|
|
1589
|
+
return { behavior: "allow", updatedInput: input };
|
|
1599
1590
|
}
|
|
1600
1591
|
}
|
|
1601
1592
|
|
|
1593
|
+
// Auto-approve debate MCP tools (propose_debate).
|
|
1594
|
+
// These are user-facing tools that show inline approval cards,
|
|
1595
|
+
// so the permission prompt is redundant.
|
|
1596
|
+
if (toolName.indexOf("mcp__clay-debate__") === 0) {
|
|
1597
|
+
return { behavior: "allow", updatedInput: input };
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1602
1600
|
// Auto-approve safe Bash commands (read-only, non-destructive)
|
|
1603
1601
|
// Applies to ALL sessions (mates and regular projects alike).
|
|
1604
1602
|
// These are purely read-only commands that cannot modify files, install
|
|
@@ -1663,10 +1661,32 @@ function createSDKBridge(opts) {
|
|
|
1663
1661
|
if (!safeBashCommands[firstWord]) { allSafe = false; break; }
|
|
1664
1662
|
}
|
|
1665
1663
|
if (allSafe) {
|
|
1666
|
-
return
|
|
1664
|
+
return { behavior: "allow", updatedInput: input };
|
|
1667
1665
|
}
|
|
1668
1666
|
}
|
|
1669
1667
|
|
|
1668
|
+
return null; // Not whitelisted
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function handleCanUseTool(session, toolName, input, opts) {
|
|
1672
|
+
// Ralph Loop execution: auto-approve all tools, deny interactive ones.
|
|
1673
|
+
// Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
|
|
1674
|
+
if (session.loop && session.loop.active && session.loop.role !== "crafting") {
|
|
1675
|
+
if (toolName === "AskUserQuestion") {
|
|
1676
|
+
return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
|
|
1677
|
+
}
|
|
1678
|
+
if (toolName === "EnterPlanMode") {
|
|
1679
|
+
return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
|
|
1680
|
+
}
|
|
1681
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Check shared whitelist (read-only tools, safe browser tools, safe bash commands)
|
|
1685
|
+
var whitelisted = checkToolWhitelist(toolName, input);
|
|
1686
|
+
if (whitelisted) {
|
|
1687
|
+
return Promise.resolve(whitelisted);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1670
1690
|
// AskUserQuestion: wait for user answers via WebSocket
|
|
1671
1691
|
if (toolName === "AskUserQuestion") {
|
|
1672
1692
|
return new Promise(function(resolve) {
|
|
@@ -2529,6 +2549,7 @@ function createSDKBridge(opts) {
|
|
|
2529
2549
|
return {
|
|
2530
2550
|
createMessageQueue: createMessageQueue,
|
|
2531
2551
|
processSDKMessage: processSDKMessage,
|
|
2552
|
+
checkToolWhitelist: checkToolWhitelist,
|
|
2532
2553
|
handleCanUseTool: handleCanUseTool,
|
|
2533
2554
|
handleElicitation: handleElicitation,
|
|
2534
2555
|
processQueryStream: processQueryStream,
|
package/package.json
CHANGED