clay-server 2.26.0-beta.1 → 2.26.0-beta.11
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/bin/cli.js +5 -9
- package/lib/browser-mcp-server.js +496 -0
- package/lib/daemon.js +1 -1
- package/lib/os-users.js +23 -0
- package/lib/project-debate.js +243 -95
- package/lib/project-mate-interaction.js +766 -0
- package/lib/project-memory.js +677 -0
- package/lib/project.js +546 -1361
- package/lib/public/app.js +817 -175
- package/lib/public/css/debate.css +224 -2
- package/lib/public/css/icon-strip.css +10 -10
- package/lib/public/css/input.css +296 -83
- package/lib/public/css/mates.css +56 -57
- package/lib/public/css/mention.css +7 -4
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/messages.css +17 -0
- package/lib/public/css/mobile-nav.css +3 -1
- package/lib/public/css/overlays.css +181 -0
- package/lib/public/css/rewind.css +79 -0
- package/lib/public/css/server-settings.css +1 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/css/title-bar.css +189 -3
- package/lib/public/index.html +53 -16
- package/lib/public/modules/context-sources.js +328 -0
- package/lib/public/modules/debate.js +184 -97
- package/lib/public/modules/input.js +18 -1
- package/lib/public/modules/mate-knowledge.js +11 -11
- package/lib/public/modules/mate-memory.js +5 -5
- package/lib/public/modules/mate-sidebar.js +13 -9
- package/lib/public/modules/mention.js +40 -2
- package/lib/public/modules/notifications.js +109 -1
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/sidebar.js +107 -28
- package/lib/public/modules/terminal.js +8 -0
- package/lib/public/modules/theme.js +2 -1
- package/lib/public/modules/tools.js +69 -24
- package/lib/sdk-bridge.js +81 -7
- package/lib/sdk-worker.js +13 -1
- package/lib/server.js +42 -0
- package/lib/sessions.js +39 -7
- package/lib/terminal-manager.js +36 -6
- package/package.json +2 -2
package/bin/cli.js
CHANGED
|
@@ -1507,6 +1507,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1507
1507
|
if (prevProjectMap[cwd]) {
|
|
1508
1508
|
if (prevProjectMap[cwd].visibility) cwdEntry.visibility = prevProjectMap[cwd].visibility;
|
|
1509
1509
|
if (prevProjectMap[cwd].allowedUsers) cwdEntry.allowedUsers = prevProjectMap[cwd].allowedUsers;
|
|
1510
|
+
if (prevProjectMap[cwd].ownerId) cwdEntry.ownerId = prevProjectMap[cwd].ownerId;
|
|
1510
1511
|
}
|
|
1511
1512
|
allProjects.push(cwdEntry);
|
|
1512
1513
|
usedSlugs.push(slug);
|
|
@@ -1525,6 +1526,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1525
1526
|
if (prevProjectMap[rp.path]) {
|
|
1526
1527
|
if (prevProjectMap[rp.path].visibility) rpEntry.visibility = prevProjectMap[rp.path].visibility;
|
|
1527
1528
|
if (prevProjectMap[rp.path].allowedUsers) rpEntry.allowedUsers = prevProjectMap[rp.path].allowedUsers;
|
|
1529
|
+
if (prevProjectMap[rp.path].ownerId) rpEntry.ownerId = prevProjectMap[rp.path].ownerId;
|
|
1528
1530
|
}
|
|
1529
1531
|
allProjects.push(rpEntry);
|
|
1530
1532
|
}
|
|
@@ -1879,19 +1881,13 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1879
1881
|
}
|
|
1880
1882
|
clearStaleConfig();
|
|
1881
1883
|
|
|
1882
|
-
// Re-fork with TLS
|
|
1883
|
-
var newConfig = {
|
|
1884
|
+
// Re-fork with TLS (preserve all existing config fields)
|
|
1885
|
+
var newConfig = Object.assign({}, config, {
|
|
1884
1886
|
pid: null,
|
|
1885
|
-
port: config.port,
|
|
1886
|
-
pinHash: config.pinHash || null,
|
|
1887
1887
|
tls: true,
|
|
1888
1888
|
builtinCert: hasBuiltinCert,
|
|
1889
1889
|
mkcertDetected: mkcertDetected,
|
|
1890
|
-
|
|
1891
|
-
keepAwake: config.keepAwake || false,
|
|
1892
|
-
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1893
|
-
projects: config.projects || [],
|
|
1894
|
-
};
|
|
1890
|
+
});
|
|
1895
1891
|
|
|
1896
1892
|
ensureConfigDir();
|
|
1897
1893
|
saveConfig(newConfig);
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// Browser MCP Server for Clay (in-process SDK version)
|
|
2
|
+
// Provides browser automation tools to Claude via createSdkMcpServer.
|
|
3
|
+
// Calls sendExtensionCommand directly instead of HTTP bridge.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// var browserMcp = require("./browser-mcp-server");
|
|
7
|
+
// var mcpConfig = browserMcp.create(sendExtensionCommandAny);
|
|
8
|
+
// // Pass mcpConfig to sdk-bridge opts.mcpServers
|
|
9
|
+
|
|
10
|
+
var z;
|
|
11
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
12
|
+
|
|
13
|
+
// Build a Zod shape from simple property descriptors
|
|
14
|
+
function buildShape(props, required) {
|
|
15
|
+
if (!z) return {};
|
|
16
|
+
var shape = {};
|
|
17
|
+
var keys = Object.keys(props);
|
|
18
|
+
for (var i = 0; i < keys.length; i++) {
|
|
19
|
+
var k = keys[i];
|
|
20
|
+
var p = props[k];
|
|
21
|
+
var field;
|
|
22
|
+
if (p.type === "number") field = z.number();
|
|
23
|
+
else if (p.type === "boolean") field = z.boolean();
|
|
24
|
+
else if (p.enum) field = z.enum(p.enum);
|
|
25
|
+
else field = z.string();
|
|
26
|
+
if (p.description) field = field.describe(p.description);
|
|
27
|
+
if (!required || required.indexOf(k) === -1) field = field.optional();
|
|
28
|
+
shape[k] = field;
|
|
29
|
+
}
|
|
30
|
+
return shape;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function create(sendCommand, getTabList, contextOps) {
|
|
34
|
+
// sendCommand(command, args, timeout) -> Promise<result>
|
|
35
|
+
// getTabList() -> array of { id, url, title, favIconUrl }
|
|
36
|
+
var sdk;
|
|
37
|
+
try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
|
|
38
|
+
console.error("[browser-mcp] Failed to load SDK:", e.message);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var createSdkMcpServer = sdk.createSdkMcpServer;
|
|
43
|
+
var tool = sdk.tool;
|
|
44
|
+
if (!createSdkMcpServer || !tool) {
|
|
45
|
+
console.error("[browser-mcp] SDK missing createSdkMcpServer or tool helper");
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Helper: ensure inject.js loaded (best-effort)
|
|
50
|
+
function ensureInjected(tabId) {
|
|
51
|
+
return sendCommand("tab_inject", { tabId: tabId }).catch(function () {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var tools = [];
|
|
55
|
+
|
|
56
|
+
// --- browser_list_tabs ---
|
|
57
|
+
tools.push(tool(
|
|
58
|
+
"browser_list_tabs",
|
|
59
|
+
"List all open browser tabs with their IDs, URLs, and titles",
|
|
60
|
+
buildShape({}, []),
|
|
61
|
+
function () {
|
|
62
|
+
var tabs = getTabList ? getTabList() : [];
|
|
63
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(tabs, null, 2) }] });
|
|
64
|
+
}
|
|
65
|
+
));
|
|
66
|
+
|
|
67
|
+
// --- browser_open ---
|
|
68
|
+
tools.push(tool(
|
|
69
|
+
"browser_open",
|
|
70
|
+
"Open a new browser tab and return its tab ID",
|
|
71
|
+
buildShape({
|
|
72
|
+
url: { type: "string", description: "URL to open" },
|
|
73
|
+
active: { type: "boolean", description: "Activate the tab (default true)" },
|
|
74
|
+
}, ["url"]),
|
|
75
|
+
function (args) {
|
|
76
|
+
return sendCommand("tab_open", { url: args.url, active: args.active !== false }).then(function (result) {
|
|
77
|
+
return { content: [{ type: "text", text: "Opened tab " + (result.id || "unknown") + ": " + args.url }] };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
));
|
|
81
|
+
|
|
82
|
+
// --- browser_close ---
|
|
83
|
+
tools.push(tool(
|
|
84
|
+
"browser_close",
|
|
85
|
+
"Close a browser tab",
|
|
86
|
+
buildShape({
|
|
87
|
+
tabId: { type: "number", description: "Tab ID to close" },
|
|
88
|
+
}, ["tabId"]),
|
|
89
|
+
function (args) {
|
|
90
|
+
return sendCommand("tab_close", { tabId: args.tabId }).then(function () {
|
|
91
|
+
return { content: [{ type: "text", text: "Closed tab " + args.tabId }] };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
));
|
|
95
|
+
|
|
96
|
+
// --- browser_navigate ---
|
|
97
|
+
tools.push(tool(
|
|
98
|
+
"browser_navigate",
|
|
99
|
+
"Navigate a tab to a new URL",
|
|
100
|
+
buildShape({
|
|
101
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
102
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
103
|
+
}, ["tabId", "url"]),
|
|
104
|
+
function (args) {
|
|
105
|
+
return sendCommand("tab_navigate", { tabId: args.tabId, url: args.url }).then(function () {
|
|
106
|
+
return { content: [{ type: "text", text: "Navigated tab " + args.tabId + " to " + args.url }] };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
));
|
|
110
|
+
|
|
111
|
+
// --- browser_screenshot ---
|
|
112
|
+
tools.push(tool(
|
|
113
|
+
"browser_screenshot",
|
|
114
|
+
"Capture a screenshot of a browser tab (full viewport or a specific element)",
|
|
115
|
+
buildShape({
|
|
116
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
117
|
+
selector: { type: "string", description: "CSS selector to capture a specific element (optional)" },
|
|
118
|
+
}, ["tabId"]),
|
|
119
|
+
function (args) {
|
|
120
|
+
var extArgs = { tabId: args.tabId };
|
|
121
|
+
if (args.selector) extArgs.selector = args.selector;
|
|
122
|
+
return sendCommand("tab_screenshot", extArgs, 10000).then(function (result) {
|
|
123
|
+
if (!result || !result.image) throw new Error("Screenshot failed");
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{ type: "image", data: result.image, mimeType: "image/png" },
|
|
127
|
+
{ type: "text", text: "Screenshot captured" + (args.selector ? " (selector: " + args.selector + ")" : " (full viewport)") },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
));
|
|
133
|
+
|
|
134
|
+
// --- browser_console ---
|
|
135
|
+
tools.push(tool(
|
|
136
|
+
"browser_console",
|
|
137
|
+
"Read captured console logs from a tab (log, warn, error, info)",
|
|
138
|
+
buildShape({
|
|
139
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
140
|
+
}, ["tabId"]),
|
|
141
|
+
function (args) {
|
|
142
|
+
return ensureInjected(args.tabId).then(function () {
|
|
143
|
+
return sendCommand("tab_console", { tabId: args.tabId });
|
|
144
|
+
}).then(function (result) {
|
|
145
|
+
var logs = [];
|
|
146
|
+
try { logs = typeof result.logs === "string" ? JSON.parse(result.logs) : (result.logs || []); } catch (e) {}
|
|
147
|
+
if (logs.length === 0) return { content: [{ type: "text", text: "No console output captured" }] };
|
|
148
|
+
var lines = logs.map(function (entry) {
|
|
149
|
+
var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
|
|
150
|
+
return "[" + ts + " " + (entry.level || "log").toUpperCase() + "] " + (entry.text || "");
|
|
151
|
+
});
|
|
152
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
));
|
|
156
|
+
|
|
157
|
+
// --- browser_network ---
|
|
158
|
+
tools.push(tool(
|
|
159
|
+
"browser_network",
|
|
160
|
+
"Read captured network requests (fetch/XHR) from a tab",
|
|
161
|
+
buildShape({
|
|
162
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
163
|
+
}, ["tabId"]),
|
|
164
|
+
function (args) {
|
|
165
|
+
return ensureInjected(args.tabId).then(function () {
|
|
166
|
+
return sendCommand("tab_network", { tabId: args.tabId });
|
|
167
|
+
}).then(function (result) {
|
|
168
|
+
var reqs = [];
|
|
169
|
+
try { reqs = typeof result.network === "string" ? JSON.parse(result.network) : (result.network || []); } catch (e) {}
|
|
170
|
+
if (reqs.length === 0) return { content: [{ type: "text", text: "No network requests captured" }] };
|
|
171
|
+
var lines = reqs.map(function (r) {
|
|
172
|
+
var line = (r.method || "GET") + " " + (r.url || "") + " " + (r.status || 0) + " " + (r.duration || 0) + "ms";
|
|
173
|
+
if (r.error) line += " [" + r.error + "]";
|
|
174
|
+
return line;
|
|
175
|
+
});
|
|
176
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
));
|
|
180
|
+
|
|
181
|
+
// --- browser_read_page ---
|
|
182
|
+
tools.push(tool(
|
|
183
|
+
"browser_read_page",
|
|
184
|
+
"Read page text content (innerText). Optionally read only a specific element.",
|
|
185
|
+
buildShape({
|
|
186
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
187
|
+
selector: { type: "string", description: "CSS selector to read specific element (optional)" },
|
|
188
|
+
}, ["tabId"]),
|
|
189
|
+
function (args) {
|
|
190
|
+
if (args.selector) {
|
|
191
|
+
var script = "(function() { var el = document.querySelector(" + JSON.stringify(args.selector) + "); if (!el) return ''; var t = el.innerText; return t.length > 32768 ? t.substring(0, 32768) : t; })()";
|
|
192
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
193
|
+
var text = result.value || "";
|
|
194
|
+
if (!text) return { content: [{ type: "text", text: "Element not found or empty: " + args.selector }] };
|
|
195
|
+
return { content: [{ type: "text", text: text }] };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return sendCommand("tab_page_text", { tabId: args.tabId }).then(function (result) {
|
|
199
|
+
var text = result.text || "";
|
|
200
|
+
if (!text) return { content: [{ type: "text", text: "Page has no text content" }] };
|
|
201
|
+
return { content: [{ type: "text", text: text }] };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
));
|
|
205
|
+
|
|
206
|
+
// --- browser_dom ---
|
|
207
|
+
tools.push(tool(
|
|
208
|
+
"browser_dom",
|
|
209
|
+
"Get a simplified DOM tree (tag, id, class, children) for structural analysis",
|
|
210
|
+
buildShape({
|
|
211
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
212
|
+
selector: { type: "string", description: "CSS selector for root element (default body)" },
|
|
213
|
+
depth: { type: "number", description: "Max tree depth (default 3)" },
|
|
214
|
+
}, ["tabId"]),
|
|
215
|
+
function (args) {
|
|
216
|
+
var selector = args.selector ? JSON.stringify(args.selector) : '"body"';
|
|
217
|
+
var depth = args.depth || 3;
|
|
218
|
+
var script = "(function() {" +
|
|
219
|
+
"function walk(el, d, max) {" +
|
|
220
|
+
" if (!el || d > max) return null;" +
|
|
221
|
+
" var n = { tag: el.tagName.toLowerCase() };" +
|
|
222
|
+
" if (el.id) n.id = el.id;" +
|
|
223
|
+
" if (el.className && typeof el.className === 'string') { var c = el.className.trim(); if (c) n.class = c; }" +
|
|
224
|
+
" if (el.children.length > 0 && d < max) {" +
|
|
225
|
+
" n.children = [];" +
|
|
226
|
+
" for (var i = 0; i < el.children.length; i++) {" +
|
|
227
|
+
" var child = walk(el.children[i], d + 1, max);" +
|
|
228
|
+
" if (child) n.children.push(child);" +
|
|
229
|
+
" }" +
|
|
230
|
+
" } else if (el.children.length > 0) {" +
|
|
231
|
+
" n.childCount = el.children.length;" +
|
|
232
|
+
" }" +
|
|
233
|
+
" if (el.children.length === 0 && el.textContent) {" +
|
|
234
|
+
" var t = el.textContent.trim();" +
|
|
235
|
+
" if (t.length > 100) t = t.substring(0, 100) + '...';" +
|
|
236
|
+
" if (t) n.text = t;" +
|
|
237
|
+
" }" +
|
|
238
|
+
" return n;" +
|
|
239
|
+
"}" +
|
|
240
|
+
"var root = document.querySelector(" + selector + ") || document.body;" +
|
|
241
|
+
"return JSON.stringify(walk(root, 0, " + depth + "), null, 2);" +
|
|
242
|
+
"})()";
|
|
243
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
244
|
+
return { content: [{ type: "text", text: result.value || "null" }] };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
));
|
|
248
|
+
|
|
249
|
+
// --- browser_styles ---
|
|
250
|
+
tools.push(tool(
|
|
251
|
+
"browser_styles",
|
|
252
|
+
"Get computed styles of an element (display, position, size, colors, etc.)",
|
|
253
|
+
buildShape({
|
|
254
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
255
|
+
selector: { type: "string", description: "CSS selector" },
|
|
256
|
+
}, ["tabId", "selector"]),
|
|
257
|
+
function (args) {
|
|
258
|
+
var script = "(function() {" +
|
|
259
|
+
"var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
|
|
260
|
+
"if (!el) return JSON.stringify({ error: 'Element not found' });" +
|
|
261
|
+
"var cs = window.getComputedStyle(el);" +
|
|
262
|
+
"var props = ['display','visibility','opacity','position','top','right','bottom','left'," +
|
|
263
|
+
"'width','height','minWidth','minHeight','maxWidth','maxHeight'," +
|
|
264
|
+
"'margin','padding','border','borderRadius'," +
|
|
265
|
+
"'color','backgroundColor','fontSize','fontFamily','fontWeight'," +
|
|
266
|
+
"'overflow','zIndex','transform','transition','boxShadow','cursor'];" +
|
|
267
|
+
"var result = {};" +
|
|
268
|
+
"for (var i = 0; i < props.length; i++) { result[props[i]] = cs[props[i]]; }" +
|
|
269
|
+
"result.boundingRect = el.getBoundingClientRect().toJSON();" +
|
|
270
|
+
"return JSON.stringify(result, null, 2);" +
|
|
271
|
+
"})()";
|
|
272
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
273
|
+
return { content: [{ type: "text", text: result.value || "null" }] };
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
));
|
|
277
|
+
|
|
278
|
+
// --- browser_storage ---
|
|
279
|
+
tools.push(tool(
|
|
280
|
+
"browser_storage",
|
|
281
|
+
"Read browser storage (localStorage, sessionStorage, or cookies)",
|
|
282
|
+
buildShape({
|
|
283
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
284
|
+
type: { type: "string", enum: ["local", "session", "cookie"], description: "Storage type (default local)" },
|
|
285
|
+
}, ["tabId"]),
|
|
286
|
+
function (args) {
|
|
287
|
+
var storageType = args.type || "local";
|
|
288
|
+
var script;
|
|
289
|
+
if (storageType === "cookie") {
|
|
290
|
+
script = "JSON.stringify(document.cookie.split('; ').reduce(function(o, c) { var p = c.split('='); o[p[0]] = decodeURIComponent(p.slice(1).join('=')); return o; }, {}), null, 2)";
|
|
291
|
+
} else if (storageType === "session") {
|
|
292
|
+
script = "(function() { var o = {}; for (var i = 0; i < sessionStorage.length; i++) { var k = sessionStorage.key(i); o[k] = sessionStorage.getItem(k); } return JSON.stringify(o, null, 2); })()";
|
|
293
|
+
} else {
|
|
294
|
+
script = "(function() { var o = {}; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); o[k] = localStorage.getItem(k); } return JSON.stringify(o, null, 2); })()";
|
|
295
|
+
}
|
|
296
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
297
|
+
return { content: [{ type: "text", text: result.value || "{}" }] };
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
));
|
|
301
|
+
|
|
302
|
+
// --- browser_evaluate ---
|
|
303
|
+
tools.push(tool(
|
|
304
|
+
"browser_evaluate",
|
|
305
|
+
"Execute arbitrary JavaScript in the page context and return the result",
|
|
306
|
+
buildShape({
|
|
307
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
308
|
+
script: { type: "string", description: "JavaScript expression or IIFE to evaluate" },
|
|
309
|
+
}, ["tabId", "script"]),
|
|
310
|
+
function (args) {
|
|
311
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: args.script }).then(function (result) {
|
|
312
|
+
if (result.error) throw new Error(result.error);
|
|
313
|
+
var text = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
|
|
314
|
+
return { content: [{ type: "text", text: text || "(undefined)" }] };
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
));
|
|
318
|
+
|
|
319
|
+
// --- browser_click ---
|
|
320
|
+
tools.push(tool(
|
|
321
|
+
"browser_click",
|
|
322
|
+
"Click an element on the page",
|
|
323
|
+
buildShape({
|
|
324
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
325
|
+
selector: { type: "string", description: "CSS selector of the element to click" },
|
|
326
|
+
}, ["tabId", "selector"]),
|
|
327
|
+
function (args) {
|
|
328
|
+
var script = "(function() {" +
|
|
329
|
+
"var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
|
|
330
|
+
"if (!el) return 'Element not found: " + args.selector.replace(/'/g, "\\'") + "';" +
|
|
331
|
+
"el.scrollIntoView({ block: 'center', behavior: 'instant' });" +
|
|
332
|
+
"el.click();" +
|
|
333
|
+
"return 'Clicked: " + args.selector.replace(/'/g, "\\'") + "';" +
|
|
334
|
+
"})()";
|
|
335
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
336
|
+
return { content: [{ type: "text", text: result.value || "Click executed" }] };
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
));
|
|
340
|
+
|
|
341
|
+
// --- browser_type ---
|
|
342
|
+
tools.push(tool(
|
|
343
|
+
"browser_type",
|
|
344
|
+
"Type text into an input element (sets value and dispatches input/change events)",
|
|
345
|
+
buildShape({
|
|
346
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
347
|
+
selector: { type: "string", description: "CSS selector of the input element" },
|
|
348
|
+
text: { type: "string", description: "Text to type" },
|
|
349
|
+
}, ["tabId", "selector", "text"]),
|
|
350
|
+
function (args) {
|
|
351
|
+
var script = "(function() {" +
|
|
352
|
+
"var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
|
|
353
|
+
"if (!el) return 'Element not found: " + args.selector.replace(/'/g, "\\'") + "';" +
|
|
354
|
+
"el.focus();" +
|
|
355
|
+
"var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value') || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');" +
|
|
356
|
+
"if (nativeSetter && nativeSetter.set) { nativeSetter.set.call(el, " + JSON.stringify(args.text) + "); }" +
|
|
357
|
+
"else { el.value = " + JSON.stringify(args.text) + "; }" +
|
|
358
|
+
"el.dispatchEvent(new Event('input', { bubbles: true }));" +
|
|
359
|
+
"el.dispatchEvent(new Event('change', { bubbles: true }));" +
|
|
360
|
+
"return 'Typed into: " + args.selector.replace(/'/g, "\\'") + "';" +
|
|
361
|
+
"})()";
|
|
362
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
363
|
+
return { content: [{ type: "text", text: result.value || "Type executed" }] };
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
));
|
|
367
|
+
|
|
368
|
+
// --- browser_scroll ---
|
|
369
|
+
tools.push(tool(
|
|
370
|
+
"browser_scroll",
|
|
371
|
+
"Scroll the page or scroll a specific element into view",
|
|
372
|
+
buildShape({
|
|
373
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
374
|
+
selector: { type: "string", description: "CSS selector to scroll into view (optional)" },
|
|
375
|
+
x: { type: "number", description: "Horizontal scroll position (optional)" },
|
|
376
|
+
y: { type: "number", description: "Vertical scroll position (optional)" },
|
|
377
|
+
}, ["tabId"]),
|
|
378
|
+
function (args) {
|
|
379
|
+
var script;
|
|
380
|
+
if (args.selector) {
|
|
381
|
+
script = "(function() {" +
|
|
382
|
+
"var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
|
|
383
|
+
"if (!el) return 'Element not found';" +
|
|
384
|
+
"el.scrollIntoView({ block: 'center', behavior: 'smooth' });" +
|
|
385
|
+
"return 'Scrolled to: " + args.selector.replace(/'/g, "\\'") + "';" +
|
|
386
|
+
"})()";
|
|
387
|
+
} else {
|
|
388
|
+
var x = args.x || 0;
|
|
389
|
+
var y = args.y || 0;
|
|
390
|
+
script = "(function() { window.scrollTo(" + x + ", " + y + "); return 'Scrolled to (" + x + ", " + y + ")'; })()";
|
|
391
|
+
}
|
|
392
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
393
|
+
return { content: [{ type: "text", text: result.value || "Scroll executed" }] };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
));
|
|
397
|
+
|
|
398
|
+
// --- browser_wait ---
|
|
399
|
+
tools.push(tool(
|
|
400
|
+
"browser_wait",
|
|
401
|
+
"Wait for an element matching a CSS selector to appear in the DOM",
|
|
402
|
+
buildShape({
|
|
403
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
404
|
+
selector: { type: "string", description: "CSS selector to wait for" },
|
|
405
|
+
timeout: { type: "number", description: "Timeout in ms (default 5000)" },
|
|
406
|
+
}, ["tabId", "selector"]),
|
|
407
|
+
function (args) {
|
|
408
|
+
var timeout = args.timeout || 5000;
|
|
409
|
+
var script = "(function() {" +
|
|
410
|
+
"var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
|
|
411
|
+
"if (el) return JSON.stringify({ found: true, tag: el.tagName.toLowerCase() });" +
|
|
412
|
+
"return JSON.stringify({ found: false });" +
|
|
413
|
+
"})()";
|
|
414
|
+
var startTime = Date.now();
|
|
415
|
+
function poll() {
|
|
416
|
+
return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
|
|
417
|
+
var parsed = {};
|
|
418
|
+
try { parsed = JSON.parse(result.value || "{}"); } catch (e) {}
|
|
419
|
+
if (parsed.found) {
|
|
420
|
+
return { content: [{ type: "text", text: "Element found: " + args.selector + " (" + parsed.tag + ")" }] };
|
|
421
|
+
}
|
|
422
|
+
if (Date.now() - startTime >= timeout) {
|
|
423
|
+
throw new Error("Timeout waiting for element: " + args.selector + " (" + timeout + "ms)");
|
|
424
|
+
}
|
|
425
|
+
return new Promise(function (resolve) {
|
|
426
|
+
setTimeout(function () { resolve(poll()); }, 300);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return poll();
|
|
431
|
+
}
|
|
432
|
+
));
|
|
433
|
+
|
|
434
|
+
// --- browser_wait_navigation ---
|
|
435
|
+
tools.push(tool(
|
|
436
|
+
"browser_wait_navigation",
|
|
437
|
+
"Wait for page navigation to complete (URL change + load event)",
|
|
438
|
+
buildShape({
|
|
439
|
+
tabId: { type: "number", description: "Tab ID" },
|
|
440
|
+
timeout: { type: "number", description: "Timeout in ms (default 10000)" },
|
|
441
|
+
}, ["tabId"]),
|
|
442
|
+
function (args) {
|
|
443
|
+
var timeout = args.timeout || 10000;
|
|
444
|
+
return sendCommand("tab_wait_navigation", { tabId: args.tabId, timeout: timeout }, timeout + 3000).then(function (result) {
|
|
445
|
+
if (result.error) throw new Error(result.error);
|
|
446
|
+
return { content: [{ type: "text", text: "Navigation complete: " + (result.url || "unknown URL") }] };
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
));
|
|
450
|
+
|
|
451
|
+
// --- browser_watch_tab ---
|
|
452
|
+
if (contextOps && contextOps.watchTab) {
|
|
453
|
+
tools.push(tool(
|
|
454
|
+
"browser_watch_tab",
|
|
455
|
+
"Add a browser tab as a persistent context source. Its screenshot and text will be automatically included in every subsequent message.",
|
|
456
|
+
buildShape({
|
|
457
|
+
tabId: { type: "number", description: "Tab ID to watch" },
|
|
458
|
+
}, ["tabId"]),
|
|
459
|
+
function (args) {
|
|
460
|
+
var tabs = getTabList ? getTabList() : [];
|
|
461
|
+
var found = null;
|
|
462
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
463
|
+
if (tabs[i].id === args.tabId) { found = tabs[i]; break; }
|
|
464
|
+
}
|
|
465
|
+
if (!found) throw new Error("Tab " + args.tabId + " not found in open tabs");
|
|
466
|
+
var active = contextOps.watchTab(args.tabId);
|
|
467
|
+
return Promise.resolve({
|
|
468
|
+
content: [{ type: "text", text: "Now watching tab " + args.tabId + " (" + (found.title || found.url) + "). Its content will be included as context in every message. Active sources: " + active.join(", ") }],
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
));
|
|
472
|
+
|
|
473
|
+
tools.push(tool(
|
|
474
|
+
"browser_unwatch_tab",
|
|
475
|
+
"Remove a browser tab from persistent context sources. Stops auto-including its content.",
|
|
476
|
+
buildShape({
|
|
477
|
+
tabId: { type: "number", description: "Tab ID to stop watching" },
|
|
478
|
+
}, ["tabId"]),
|
|
479
|
+
function (args) {
|
|
480
|
+
var active = contextOps.unwatchTab(args.tabId);
|
|
481
|
+
return Promise.resolve({
|
|
482
|
+
content: [{ type: "text", text: "Stopped watching tab " + args.tabId + ". Active sources: " + (active.length > 0 ? active.join(", ") : "none") }],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Create the in-process MCP server
|
|
489
|
+
return createSdkMcpServer({
|
|
490
|
+
name: "clay-browser",
|
|
491
|
+
version: "1.0.0",
|
|
492
|
+
tools: tools,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = { create: create };
|
package/lib/daemon.js
CHANGED
|
@@ -807,7 +807,7 @@ var relay = createServer({
|
|
|
807
807
|
var isMateProject = slug.indexOf("mate-") === 0;
|
|
808
808
|
return {
|
|
809
809
|
slug: slug,
|
|
810
|
-
visibility: isMateProject ? "private" : (config.projects[i].visibility || "public"),
|
|
810
|
+
visibility: isMateProject ? "private" : (config.projects[i].visibility || (config.osUsers ? "private" : "public")),
|
|
811
811
|
allowedUsers: config.projects[i].allowedUsers || [],
|
|
812
812
|
ownerId: config.projects[i].ownerId || null,
|
|
813
813
|
};
|
package/lib/os-users.js
CHANGED
|
@@ -254,6 +254,26 @@ function toLinuxUsername(clayUsername) {
|
|
|
254
254
|
return name;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Ensure linger is enabled for a Linux user so systemd creates /run/user/<uid>.
|
|
259
|
+
* Required for CLI tools like gcloud and gh that need XDG_RUNTIME_DIR.
|
|
260
|
+
*/
|
|
261
|
+
function ensureLinger(username) {
|
|
262
|
+
try {
|
|
263
|
+
var uid = execSync("id -u " + username, { encoding: "utf8", timeout: 5000, stdio: "pipe" }).trim();
|
|
264
|
+
var lingerFile = "/var/lib/systemd/linger/" + username;
|
|
265
|
+
if (fs.existsSync(lingerFile)) return;
|
|
266
|
+
execSync("loginctl enable-linger " + username, {
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
timeout: 10000,
|
|
269
|
+
stdio: "pipe",
|
|
270
|
+
});
|
|
271
|
+
console.log("[os-users] Enabled linger for " + username + " (uid " + uid + ")");
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.warn("[os-users] Failed to enable linger for " + username + ": " + (e.stderr || e.message || "").trim());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
/**
|
|
258
278
|
* Check if a Linux user already exists.
|
|
259
279
|
*/
|
|
@@ -353,6 +373,7 @@ function provisionLinuxUser(clayUsername) {
|
|
|
353
373
|
timeout: 15000,
|
|
354
374
|
stdio: "pipe",
|
|
355
375
|
});
|
|
376
|
+
ensureLinger(linuxName);
|
|
356
377
|
console.log("[os-users] Provisioned Linux user: " + linuxName + " (Clay user: " + clayUsername + ")");
|
|
357
378
|
installClaudeCli(linuxName);
|
|
358
379
|
return { ok: true, linuxUser: linuxName };
|
|
@@ -383,6 +404,8 @@ function provisionAllUsers(usersModule) {
|
|
|
383
404
|
console.log("[os-users] Claude CLI missing for " + user.linuxUser + ", installing...");
|
|
384
405
|
installClaudeCli(user.linuxUser);
|
|
385
406
|
}
|
|
407
|
+
// Ensure linger is enabled for existing users
|
|
408
|
+
ensureLinger(user.linuxUser);
|
|
386
409
|
result.skipped.push({ id: user.id, username: user.username, linuxUser: user.linuxUser });
|
|
387
410
|
continue;
|
|
388
411
|
}
|