clay-server 3.3.2-beta.2 → 3.4.0-beta.1
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-file-watch.js +77 -16
- package/lib/project.js +28 -41
- package/lib/public/css/input.css +4 -0
- package/lib/public/css/mates.css +1 -0
- package/lib/public/gemini-avatar.svg +11 -0
- package/lib/public/index.html +8 -0
- package/lib/public/modules/app-panels.js +13 -1
- package/lib/public/modules/app-rendering.js +7 -1
- package/lib/public/modules/mate-sidebar.js +9 -3
- package/lib/public/modules/sidebar-mates.js +7 -1
- package/lib/public/modules/tools.js +7 -1
- package/lib/public/opencode-avatar.svg +4 -0
- package/lib/sdk-bridge.js +18 -2
- package/lib/yoke/acp-agent-profiles.js +188 -0
- package/lib/yoke/acp-driver-runtime.js +50 -0
- package/lib/yoke/acp-event-normalizer.js +179 -0
- package/lib/yoke/acp-process-manager.js +264 -0
- package/lib/yoke/acp-query-handle.js +487 -0
- package/lib/yoke/adapters/acp.js +317 -0
- package/lib/yoke/adapters/gemini.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -4
- package/lib/yoke/adapters/opencode.js +7 -0
- package/lib/yoke/index.js +45 -11
- package/lib/yoke/interface.js +2 -0
- package/lib/yoke/kiro-acp-server.js +30 -276
- package/lib/yoke/vendor-registry.js +22 -0
- 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
|
@@ -21,12 +21,22 @@ function attachFileWatch(ctx) {
|
|
|
21
21
|
// one browser tab silently replace another tab's live preview subscription.
|
|
22
22
|
var fileWatchers = new Map();
|
|
23
23
|
|
|
24
|
+
function settleFileWatchReady(entry, ready) {
|
|
25
|
+
if (!entry || !entry.resolveReady) return;
|
|
26
|
+
var resolve = entry.resolveReady;
|
|
27
|
+
entry.resolveReady = null;
|
|
28
|
+
resolve(ready);
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
function closeFileWatch(key) {
|
|
25
32
|
var entry = fileWatchers.get(key);
|
|
26
33
|
if (!entry) return;
|
|
27
34
|
clearTimeout(entry.debounce);
|
|
35
|
+
if (entry.reconcile) clearImmediate(entry.reconcile);
|
|
36
|
+
if (entry.pollTimer) clearInterval(entry.pollTimer);
|
|
28
37
|
try { entry.watcher.close(); } catch (e) {}
|
|
29
38
|
fileWatchers.delete(key);
|
|
39
|
+
settleFileWatchReady(entry, false);
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
function sendFileChanged(client, message) {
|
|
@@ -37,6 +47,36 @@ function attachFileWatch(ctx) {
|
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
function readFileSnapshot(absPath) {
|
|
51
|
+
var stat = fs.statSync(absPath);
|
|
52
|
+
var ext = path.extname(absPath).toLowerCase();
|
|
53
|
+
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
|
|
54
|
+
return { content: fs.readFileSync(absPath, "utf8"), size: stat.size };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function publishFileChanged(key, client, relPath, absPath) {
|
|
58
|
+
var latest = fileWatchers.get(key);
|
|
59
|
+
if (!latest || latest.relPath !== relPath) return;
|
|
60
|
+
try {
|
|
61
|
+
var snapshot = readFileSnapshot(absPath);
|
|
62
|
+
if (!snapshot) return;
|
|
63
|
+
if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
|
|
64
|
+
latest.hasSnapshot = true;
|
|
65
|
+
latest.content = snapshot.content;
|
|
66
|
+
latest.size = snapshot.size;
|
|
67
|
+
sendFileChanged(client, {
|
|
68
|
+
type: "fs_file_changed",
|
|
69
|
+
path: relPath,
|
|
70
|
+
content: snapshot.content,
|
|
71
|
+
size: snapshot.size,
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Atomic saves can briefly remove the destination path between rename
|
|
75
|
+
// events. Keep the parent watcher alive for the next event.
|
|
76
|
+
if (e.code !== "ENOENT") closeFileWatch(key);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
function startFileWatch(client, relPath) {
|
|
41
81
|
// Preserve the old single-argument API for callers outside the websocket
|
|
42
82
|
// file browser. They share one legacy subscription.
|
|
@@ -45,10 +85,10 @@ function attachFileWatch(ctx) {
|
|
|
45
85
|
client = null;
|
|
46
86
|
}
|
|
47
87
|
var absPath = safePath(cwd, relPath);
|
|
48
|
-
if (!absPath) return;
|
|
88
|
+
if (!absPath) return Promise.resolve(false);
|
|
49
89
|
var key = client || "_legacy";
|
|
50
90
|
var existing = fileWatchers.get(key);
|
|
51
|
-
if (existing && existing.relPath === relPath) return;
|
|
91
|
+
if (existing && existing.relPath === relPath) return existing.ready;
|
|
52
92
|
closeFileWatch(key);
|
|
53
93
|
|
|
54
94
|
// Watch the parent directory rather than the file inode. Editors and agent
|
|
@@ -56,6 +96,8 @@ function attachFileWatch(ctx) {
|
|
|
56
96
|
// misses later edits even though the path still exists.
|
|
57
97
|
var parentPath = path.dirname(absPath);
|
|
58
98
|
var baseName = path.basename(absPath);
|
|
99
|
+
var initialSnapshot = null;
|
|
100
|
+
try { initialSnapshot = readFileSnapshot(absPath); } catch (e) {}
|
|
59
101
|
try {
|
|
60
102
|
var watcher = fs.watch(parentPath, function (eventType, filename) {
|
|
61
103
|
if (filename && String(filename) !== baseName) return;
|
|
@@ -63,25 +105,44 @@ function attachFileWatch(ctx) {
|
|
|
63
105
|
if (!active || active.relPath !== relPath) return;
|
|
64
106
|
clearTimeout(active.debounce);
|
|
65
107
|
active.debounce = setTimeout(function () {
|
|
66
|
-
|
|
67
|
-
if (!latest || latest.relPath !== relPath) return;
|
|
68
|
-
try {
|
|
69
|
-
var stat = fs.statSync(absPath);
|
|
70
|
-
var ext = path.extname(absPath).toLowerCase();
|
|
71
|
-
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
|
|
72
|
-
var content = fs.readFileSync(absPath, "utf8");
|
|
73
|
-
sendFileChanged(client, { type: "fs_file_changed", path: relPath, content: content, size: stat.size });
|
|
74
|
-
} catch (e) {
|
|
75
|
-
// Atomic saves can briefly remove the destination path between
|
|
76
|
-
// rename events. Keep the parent watcher alive for the next event.
|
|
77
|
-
if (e.code !== "ENOENT") closeFileWatch(key);
|
|
78
|
-
}
|
|
108
|
+
publishFileChanged(key, client, relPath, absPath);
|
|
79
109
|
}, 200);
|
|
80
110
|
});
|
|
81
|
-
|
|
111
|
+
var resolveReady = null;
|
|
112
|
+
var ready = new Promise(function (resolve) { resolveReady = resolve; });
|
|
113
|
+
var entry = {
|
|
114
|
+
watcher: watcher,
|
|
115
|
+
relPath: relPath,
|
|
116
|
+
debounce: null,
|
|
117
|
+
reconcile: null,
|
|
118
|
+
pollTimer: null,
|
|
119
|
+
ready: ready,
|
|
120
|
+
resolveReady: resolveReady,
|
|
121
|
+
hasSnapshot: !!initialSnapshot,
|
|
122
|
+
content: initialSnapshot ? initialSnapshot.content : null,
|
|
123
|
+
size: initialSnapshot ? initialSnapshot.size : null,
|
|
124
|
+
};
|
|
125
|
+
fileWatchers.set(key, entry);
|
|
126
|
+
// Directory events are the low-latency path, but macOS can coalesce or
|
|
127
|
+
// drop them under load. Periodic content reconciliation is the source of
|
|
128
|
+
// truth and also survives atomic replacements with identical metadata.
|
|
129
|
+
entry.pollTimer = setInterval(function () {
|
|
130
|
+
publishFileChanged(key, client, relPath, absPath);
|
|
131
|
+
}, 1000);
|
|
132
|
+
// fs.watch has no readiness event. Reconcile once on the next event-loop
|
|
133
|
+
// turn so a change between the initial read and native watcher activation
|
|
134
|
+
// cannot leave the browser showing stale content.
|
|
135
|
+
entry.reconcile = setImmediate(function () {
|
|
136
|
+
var active = fileWatchers.get(key);
|
|
137
|
+
if (active) active.reconcile = null;
|
|
138
|
+
publishFileChanged(key, client, relPath, absPath);
|
|
139
|
+
if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
|
|
140
|
+
});
|
|
82
141
|
watcher.on("error", function () { closeFileWatch(key); });
|
|
142
|
+
return ready;
|
|
83
143
|
} catch (e) {
|
|
84
144
|
closeFileWatch(key);
|
|
145
|
+
return Promise.resolve(false);
|
|
85
146
|
}
|
|
86
147
|
}
|
|
87
148
|
|
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/lib/public/css/input.css
CHANGED
|
@@ -799,6 +799,10 @@
|
|
|
799
799
|
.vendor-toggle-label {
|
|
800
800
|
pointer-events: none;
|
|
801
801
|
}
|
|
802
|
+
@media (min-width: 901px) {
|
|
803
|
+
.vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
|
|
804
|
+
.vendor-toggle-btn:not(.active) { padding: 0 8px; }
|
|
805
|
+
}
|
|
802
806
|
@media (max-width: 900px) {
|
|
803
807
|
.vendor-toggle-label { display: none; }
|
|
804
808
|
.vendor-toggle-btn { padding: 0 8px; }
|
package/lib/public/css/mates.css
CHANGED
|
@@ -682,6 +682,7 @@
|
|
|
682
682
|
.mate-vendor-label {
|
|
683
683
|
pointer-events: none;
|
|
684
684
|
}
|
|
685
|
+
.mate-vendor-btn:not(.active) .mate-vendor-label { display: none; }
|
|
685
686
|
|
|
686
687
|
/* Collapse button inside mate header: white to match header style */
|
|
687
688
|
.mate-sidebar-header .sidebar-collapse-btn {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Gemini CLI">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="8" y1="56" x2="56" y2="8" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#4f7cff"/>
|
|
5
|
+
<stop offset="0.5" stop-color="#9b5cff"/>
|
|
6
|
+
<stop offset="1" stop-color="#ff65b3"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect width="64" height="64" rx="16" fill="#10131d"/>
|
|
10
|
+
<path d="M32 8c2.3 14.1 9.9 21.7 24 24-14.1 2.3-21.7 9.9-24 24-2.3-14.1-9.9-21.7-24-24 14.1-2.3 21.7-9.9 24-24Z" fill="url(#g)"/>
|
|
11
|
+
</svg>
|
package/lib/public/index.html
CHANGED
|
@@ -527,6 +527,14 @@
|
|
|
527
527
|
<img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
|
|
528
528
|
<span class="vendor-toggle-label">Codex</span>
|
|
529
529
|
</button>
|
|
530
|
+
<button id="vendor-btn-gemini" class="vendor-toggle-btn" data-vendor="gemini">
|
|
531
|
+
<img src="/gemini-avatar.svg" class="vendor-toggle-icon" alt="Gemini">
|
|
532
|
+
<span class="vendor-toggle-label">Gemini CLI</span>
|
|
533
|
+
</button>
|
|
534
|
+
<button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
|
|
535
|
+
<img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
|
|
536
|
+
<span class="vendor-toggle-label">OpenCode</span>
|
|
537
|
+
</button>
|
|
530
538
|
<button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
|
|
531
539
|
<img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
|
|
532
540
|
<span class="vendor-toggle-label">Kiro CLI</span>
|
|
@@ -89,6 +89,8 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
|
89
89
|
var EFFORT_LEVELS_BY_VENDOR = {
|
|
90
90
|
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
91
91
|
codex: ["minimal", "low", "medium", "high", "xhigh"],
|
|
92
|
+
gemini: [],
|
|
93
|
+
opencode: [],
|
|
92
94
|
kiro: ["low", "medium", "high", "xhigh", "max"],
|
|
93
95
|
};
|
|
94
96
|
var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
|
|
@@ -441,8 +443,16 @@ export function initPanels() {
|
|
|
441
443
|
var vendorToggleWrap = $("vendor-toggle-wrap");
|
|
442
444
|
var vendorBtnClaude = $("vendor-btn-claude");
|
|
443
445
|
var vendorBtnCodex = $("vendor-btn-codex");
|
|
446
|
+
var vendorBtnGemini = $("vendor-btn-gemini");
|
|
447
|
+
var vendorBtnOpenCode = $("vendor-btn-opencode");
|
|
444
448
|
var vendorBtnKiro = $("vendor-btn-kiro");
|
|
445
|
-
var vendorBtns = {
|
|
449
|
+
var vendorBtns = {
|
|
450
|
+
claude: vendorBtnClaude,
|
|
451
|
+
codex: vendorBtnCodex,
|
|
452
|
+
gemini: vendorBtnGemini,
|
|
453
|
+
opencode: vendorBtnOpenCode,
|
|
454
|
+
kiro: vendorBtnKiro,
|
|
455
|
+
};
|
|
446
456
|
|
|
447
457
|
function updateVendorToggle() {
|
|
448
458
|
var installed = store.get('installedVendors') || [];
|
|
@@ -471,6 +481,8 @@ export function initPanels() {
|
|
|
471
481
|
|
|
472
482
|
if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
|
|
473
483
|
if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
|
|
484
|
+
if (vendorBtnGemini) vendorBtnGemini.addEventListener("click", function() { onVendorClick("gemini"); });
|
|
485
|
+
if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
|
|
474
486
|
if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
|
|
475
487
|
|
|
476
488
|
// --- Reactive UI sync ---
|
|
@@ -19,21 +19,27 @@ import { getScheduledMsgEl } from './app-rate-limit.js';
|
|
|
19
19
|
export var VENDOR_AVATARS = {
|
|
20
20
|
claude: "/claude-code-avatar.png",
|
|
21
21
|
codex: "/codex-avatar.png",
|
|
22
|
+
gemini: "/gemini-avatar.svg",
|
|
23
|
+
opencode: "/opencode-avatar.svg",
|
|
22
24
|
kiro: "/kiro-avatar.svg",
|
|
23
25
|
};
|
|
24
26
|
export var VENDOR_NAMES = {
|
|
25
27
|
claude: "Claude Code",
|
|
26
28
|
codex: "Codex",
|
|
29
|
+
gemini: "Gemini CLI",
|
|
30
|
+
opencode: "OpenCode",
|
|
27
31
|
kiro: "Kiro CLI",
|
|
28
32
|
};
|
|
29
33
|
// Display order for every vendor Clay knows about, installed or not. Pickers
|
|
30
34
|
// render the full list so a missing CLI reads as "not installed yet" rather
|
|
31
35
|
// than "Clay doesn't support it".
|
|
32
|
-
export var VENDOR_ORDER = ["claude", "codex", "kiro"];
|
|
36
|
+
export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
|
|
33
37
|
// Where to send the user when they pick a vendor whose CLI isn't installed.
|
|
34
38
|
export var VENDOR_HOMEPAGES = {
|
|
35
39
|
claude: "https://claude.com/product/claude-code",
|
|
36
40
|
codex: "https://openai.com/codex/",
|
|
41
|
+
gemini: "https://github.com/google-gemini/gemini-cli",
|
|
42
|
+
opencode: "https://opencode.ai/",
|
|
37
43
|
kiro: "https://kiro.dev/",
|
|
38
44
|
};
|
|
39
45
|
var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
|
|
@@ -149,9 +149,15 @@ export function showMateSidebar(mateId, mateData) {
|
|
|
149
149
|
if (mateVendorWrap) {
|
|
150
150
|
var available = store.get('availableVendors') || [];
|
|
151
151
|
var mateVendor = mateData.vendor || "claude";
|
|
152
|
-
var vendorIcons = {
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
var vendorIcons = {
|
|
153
|
+
claude: "/claude-code-avatar.png",
|
|
154
|
+
codex: "/codex-avatar.png",
|
|
155
|
+
gemini: "/gemini-avatar.svg",
|
|
156
|
+
opencode: "/opencode-avatar.svg",
|
|
157
|
+
kiro: "/kiro-avatar.svg",
|
|
158
|
+
};
|
|
159
|
+
var vendorNames = { claude: "Claude Code", codex: "Codex", gemini: "Gemini CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
|
|
160
|
+
var vendorKeys = ["claude", "codex", "gemini", "opencode", "kiro"];
|
|
155
161
|
mateVendorWrap.innerHTML = "";
|
|
156
162
|
for (var vi = 0; vi < vendorKeys.length; vi++) {
|
|
157
163
|
var vk = vendorKeys[vi];
|
|
@@ -457,7 +457,13 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
|
|
|
457
457
|
// Tooltip
|
|
458
458
|
var displayName = mp.displayName || mate.name || "New Mate";
|
|
459
459
|
var mateVendor = mate.vendor || "claude";
|
|
460
|
-
var vendorLabels = {
|
|
460
|
+
var vendorLabels = {
|
|
461
|
+
claude: "Claude Code",
|
|
462
|
+
codex: "OpenAI Codex",
|
|
463
|
+
gemini: "Gemini CLI",
|
|
464
|
+
opencode: "OpenCode",
|
|
465
|
+
kiro: "Kiro CLI",
|
|
466
|
+
};
|
|
461
467
|
el.addEventListener("mouseenter", function () {
|
|
462
468
|
var html = '<div style="font-weight:600">' + escapeHtml(displayName);
|
|
463
469
|
if (mate.primary) {
|
|
@@ -814,7 +814,13 @@ function resolvePermissionIdentity(mateId, vendor) {
|
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
// Project chat: use vendor name and avatar
|
|
817
|
-
var vendorAvatars = {
|
|
817
|
+
var vendorAvatars = {
|
|
818
|
+
claude: "/claude-code-avatar.png",
|
|
819
|
+
codex: "/codex-avatar.png",
|
|
820
|
+
gemini: "/gemini-avatar.svg",
|
|
821
|
+
opencode: "/opencode-avatar.svg",
|
|
822
|
+
kiro: "/kiro-avatar.svg",
|
|
823
|
+
};
|
|
818
824
|
var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
|
|
819
825
|
return {
|
|
820
826
|
name: vendorName,
|