clay-server 2.26.0-beta.1 → 2.26.0-beta.10

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.
Files changed (42) hide show
  1. package/bin/cli.js +5 -9
  2. package/lib/browser-mcp-server.js +496 -0
  3. package/lib/daemon.js +1 -1
  4. package/lib/os-users.js +23 -0
  5. package/lib/project-debate.js +206 -88
  6. package/lib/project-mate-interaction.js +766 -0
  7. package/lib/project-memory.js +677 -0
  8. package/lib/project.js +546 -1361
  9. package/lib/public/app.js +817 -175
  10. package/lib/public/css/debate.css +224 -2
  11. package/lib/public/css/icon-strip.css +10 -10
  12. package/lib/public/css/input.css +263 -83
  13. package/lib/public/css/mates.css +56 -57
  14. package/lib/public/css/mention.css +7 -4
  15. package/lib/public/css/menus.css +7 -0
  16. package/lib/public/css/messages.css +17 -0
  17. package/lib/public/css/mobile-nav.css +3 -1
  18. package/lib/public/css/overlays.css +181 -0
  19. package/lib/public/css/rewind.css +79 -0
  20. package/lib/public/css/server-settings.css +1 -0
  21. package/lib/public/css/sidebar.css +10 -0
  22. package/lib/public/css/title-bar.css +189 -3
  23. package/lib/public/index.html +53 -16
  24. package/lib/public/modules/context-sources.js +313 -0
  25. package/lib/public/modules/debate.js +184 -97
  26. package/lib/public/modules/input.js +18 -1
  27. package/lib/public/modules/mate-knowledge.js +11 -11
  28. package/lib/public/modules/mate-memory.js +5 -5
  29. package/lib/public/modules/mate-sidebar.js +13 -9
  30. package/lib/public/modules/mention.js +40 -2
  31. package/lib/public/modules/notifications.js +109 -1
  32. package/lib/public/modules/rewind.js +36 -0
  33. package/lib/public/modules/sidebar.js +107 -28
  34. package/lib/public/modules/terminal.js +8 -0
  35. package/lib/public/modules/theme.js +2 -1
  36. package/lib/public/modules/tools.js +69 -24
  37. package/lib/sdk-bridge.js +81 -7
  38. package/lib/sdk-worker.js +13 -1
  39. package/lib/server.js +42 -0
  40. package/lib/sessions.js +39 -7
  41. package/lib/terminal-manager.js +36 -6
  42. 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
- debug: config.debug || false,
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
  }