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.
@@ -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 that a topic would benefit from a multi-perspective debate " +
637
- "(e.g., \"let's debate this\", \"I want to hear different viewpoints\"), you can propose " +
638
- "a structured debate by writing a brief file.\n\n" +
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
- "1. Generate a unique ID: `debate_` followed by the current timestamp in milliseconds\n" +
641
- "2. Write the brief as JSON to: `.clay/debates/<debate_id>/brief.json` (relative to the project root)\n" +
642
- "3. The system will detect the file and show the user an inline card with your proposal\n" +
643
- "4. The user can then approve or cancel the debate\n\n" +
644
- "**Brief JSON schema:**\n" +
645
- "```json\n" +
646
- "{\n" +
647
- " \"topic\": \"The refined debate topic\",\n" +
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
- "- Make sure the directory exists before writing (use mkdir -p or equivalent).\n";
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;
@@ -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
- // If debate was started from DM (no setupSessionId), go to reviewing phase
274
- if (!debate.setupSessionId) {
275
- console.log("[debate] Brief picked up from DM, entering review phase. Topic:", debate.topic);
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
- var userId = ws._clayUser ? ws._clayUser.id : null;
327
- var mateCtx = matesModule.buildMateCtx(userId);
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
- // Already restored
331
- if (session._debate) return;
332
-
333
- // Has persisted debate state?
334
- if (!session.debateState) return;
335
-
336
- var phase = session.debateState.phase;
337
- if (phase !== "preparing" && phase !== "reviewing" && phase !== "live") return;
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
- var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
573
- if (autoAllow[toolName]) {
574
- return Promise.resolve({ behavior: "allow", updatedInput: input });
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: isMate ? undefined : (function () {
561
+ mcpServers: (function () {
562
+ var servers = {};
563
+
564
+ // Debate MCP server (available to both mates and main project)
561
565
  try {
562
- var browserMcp = require("./browser-mcp-server");
563
- var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
564
- return Object.values(_browserTabList || {});
565
- }, {
566
- watchTab: function (tabId) {
567
- var key = "tab:" + tabId;
568
- var active = loadContextSources(slug);
569
- if (active.indexOf(key) === -1) {
570
- active.push(key);
571
- saveContextSources(slug, active);
572
- var msg = JSON.stringify({ type: "context_sources_state", active: active });
573
- for (var c of clients) { if (c.readyState === 1) c.send(msg); }
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 (!mcpConfig) return undefined;
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 browser MCP server:", e.message);
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) { digestDmTurn(session, preview); } : null,
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 === "AskUserQuestion" && msg.input && msg.input.questions) {
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
- function handleCanUseTool(session, toolName, input, opts) {
1571
- // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1572
- // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1573
- if (session.loop && session.loop.active && session.loop.role !== "crafting") {
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 Promise.resolve({ behavior: "allow", updatedInput: input });
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 Promise.resolve({ behavior: "allow", updatedInput: input });
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 Promise.resolve({ behavior: "allow", updatedInput: input });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.17",
3
+ "version": "2.26.0-beta.18",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",